18.03.2026
This commit is contained in:
parent
b4f9488785
commit
99edd3938f
BIN
Catalog_import.xlsx
Normal file
BIN
Catalog_import.xlsx
Normal file
Binary file not shown.
BIN
Categories_import.xlsx
Normal file
BIN
Categories_import.xlsx
Normal file
Binary file not shown.
BIN
Clients_import.xlsx
Normal file
BIN
Clients_import.xlsx
Normal file
Binary file not shown.
BIN
Materials_import.xlsx
Normal file
BIN
Materials_import.xlsx
Normal file
Binary file not shown.
BIN
OrderItems.xlsx
Normal file
BIN
OrderItems.xlsx
Normal file
Binary file not shown.
BIN
Orders_import.xlsx
Normal file
BIN
Orders_import.xlsx
Normal file
Binary file not shown.
BIN
Products_import.xlsx
Normal file
BIN
Products_import.xlsx
Normal file
Binary file not shown.
BIN
Resource/Catalog_import.xlsx
Normal file
BIN
Resource/Catalog_import.xlsx
Normal file
Binary file not shown.
BIN
Resource/Categories_import.xlsx
Normal file
BIN
Resource/Categories_import.xlsx
Normal file
Binary file not shown.
BIN
Resource/Clients_import.xlsx
Normal file
BIN
Resource/Clients_import.xlsx
Normal file
Binary file not shown.
BIN
Resource/Materials_import.xlsx
Normal file
BIN
Resource/Materials_import.xlsx
Normal file
Binary file not shown.
BIN
Resource/OrderItems.xlsx
Normal file
BIN
Resource/OrderItems.xlsx
Normal file
Binary file not shown.
BIN
Resource/Orders_import.xlsx
Normal file
BIN
Resource/Orders_import.xlsx
Normal file
Binary file not shown.
BIN
Resource/Products_import.xlsx
Normal file
BIN
Resource/Products_import.xlsx
Normal file
Binary file not shown.
BIN
Resource/favicon.png
Normal file
BIN
Resource/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
588
main.py
Normal file
588
main.py
Normal file
|
|
@ -0,0 +1,588 @@
|
|||
import openpyxl # Добавь это
|
||||
openpyxl.__version__ = "3.1.5" # И это
|
||||
import sqlite3
|
||||
import pandas as pd
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
RESOURCE_DIR = os.path.join(BASE_DIR, "Resource")
|
||||
# ========== ИЗМЕНЕНО: partners.db -> materials.db ==========
|
||||
DB_PATH = os.path.join(BASE_DIR, "materials.db")
|
||||
|
||||
|
||||
def create_tables(db_connection):
|
||||
"""Создает все необходимые таблицы в базе данных"""
|
||||
cursor = db_connection.cursor()
|
||||
|
||||
cursor.executescript('''
|
||||
-- Типы контрагентов
|
||||
CREATE TABLE IF NOT EXISTS ContractorType (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type_name VARCHAR(100) NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
-- Контрагенты/Клиенты
|
||||
CREATE TABLE IF NOT EXISTS Clients (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
contractor_type_id INTEGER REFERENCES ContractorType(id),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
director VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
phone VARCHAR(50),
|
||||
address TEXT,
|
||||
inn VARCHAR(20) UNIQUE,
|
||||
rating DECIMAL(3,2) DEFAULT 0.0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Категории продукции
|
||||
CREATE TABLE IF NOT EXISTS Categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
category_name VARCHAR(100) NOT NULL UNIQUE,
|
||||
coefficient DECIMAL(5,2) NOT NULL DEFAULT 1.0
|
||||
);
|
||||
|
||||
-- Таблица материалов
|
||||
CREATE TABLE IF NOT EXISTS Materials (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
material_name VARCHAR(100) NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
category VARCHAR(50),
|
||||
color VARCHAR(50),
|
||||
density DECIMAL(10,2),
|
||||
thickness DECIMAL(10,2),
|
||||
supplier VARCHAR(255),
|
||||
country VARCHAR(100),
|
||||
wear_class VARCHAR(20),
|
||||
price_per_unit DECIMAL(15,2),
|
||||
unit VARCHAR(20) DEFAULT 'шт',
|
||||
in_stock INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Каталог продукции
|
||||
CREATE TABLE IF NOT EXISTS Catalog (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
category_id INTEGER REFERENCES Categories(id),
|
||||
material_id INTEGER REFERENCES Materials(id),
|
||||
product_name VARCHAR(255) NOT NULL,
|
||||
article VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
min_price DECIMAL(15,2) NOT NULL,
|
||||
quantity_in_stock INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Таблица корзин
|
||||
CREATE TABLE IF NOT EXISTS Cart (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_id INTEGER REFERENCES Clients(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
status VARCHAR(50) DEFAULT 'активна'
|
||||
);
|
||||
|
||||
-- Таблица товаров в корзине
|
||||
CREATE TABLE IF NOT EXISTS CartItems (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cart_id INTEGER REFERENCES Cart(id),
|
||||
product_id INTEGER REFERENCES Catalog(id),
|
||||
quantity INTEGER NOT NULL CHECK (quantity > 0),
|
||||
price_at_moment DECIMAL(15,2) NOT NULL,
|
||||
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- ========== НОВАЯ СТРУКТУРА ЗАКАЗОВ ==========
|
||||
|
||||
-- Заказы (без product_id и quantity)
|
||||
CREATE TABLE IF NOT EXISTS Orders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_id INTEGER NOT NULL REFERENCES Clients(id),
|
||||
order_date DATE NOT NULL,
|
||||
total_amount DECIMAL(15,2) NOT NULL DEFAULT 0,
|
||||
status VARCHAR(50) DEFAULT 'Новый',
|
||||
discount_percent INTEGER DEFAULT 0,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Позиции заявок
|
||||
CREATE TABLE IF NOT EXISTS OrderItems (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id INTEGER NOT NULL REFERENCES Orders(id) ON DELETE CASCADE,
|
||||
product_id INTEGER NOT NULL REFERENCES Catalog(id),
|
||||
quantity INTEGER NOT NULL CHECK (quantity > 0),
|
||||
price_at_moment DECIMAL(15,2) NOT NULL,
|
||||
transport_cost DECIMAL(15,2) DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Индексы для оптимизации
|
||||
CREATE INDEX IF NOT EXISTS idx_client_type ON Clients(contractor_type_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_client_inn ON Clients(inn);
|
||||
CREATE INDEX IF NOT EXISTS idx_catalog_category ON Catalog(category_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_catalog_material ON Catalog(material_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_catalog_article ON Catalog(article);
|
||||
CREATE INDEX IF NOT EXISTS idx_material_name ON Materials(material_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_cart_client ON Cart(client_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cart_status ON Cart(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_cart_items_cart ON CartItems(cart_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_order_client ON Orders(client_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_order_date ON Orders(order_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_order_status ON Orders(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_orderitems_order ON OrderItems(order_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_orderitems_product ON OrderItems(product_id);
|
||||
''')
|
||||
|
||||
db_connection.commit()
|
||||
print("✅ Таблицы успешно созданы (новая структура с OrderItems)")
|
||||
|
||||
|
||||
def calculate_discount(total_amount):
|
||||
"""Рассчитывает скидку на основе общей суммы заказов"""
|
||||
if total_amount >= 500000:
|
||||
return 20
|
||||
elif total_amount >= 300000:
|
||||
return 15
|
||||
elif total_amount >= 150000:
|
||||
return 10
|
||||
return 0
|
||||
|
||||
|
||||
def get_clients(db_path=DB_PATH, sort_by='rating', ascending=False):
|
||||
"""Получает данные о клиентах с возможностью сортировки"""
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
query = """
|
||||
SELECT
|
||||
ct.type_name AS "Тип клиента",
|
||||
c.name AS "Наименование клиента",
|
||||
c.director AS "Директор",
|
||||
c.email AS "Электронная почта",
|
||||
c.phone AS "Телефон",
|
||||
c.address AS "Адрес",
|
||||
c.inn AS "ИНН",
|
||||
c.rating AS "Рейтинг",
|
||||
c.created_at AS "Дата создания",
|
||||
COALESCE(SUM(o.total_amount), 0) as total_amount,
|
||||
COUNT(DISTINCT o.id) as orders_count
|
||||
FROM Clients c
|
||||
JOIN ContractorType ct ON c.contractor_type_id = ct.id
|
||||
LEFT JOIN Orders o ON c.id = o.client_id
|
||||
GROUP BY c.id
|
||||
"""
|
||||
|
||||
if sort_by == 'discount':
|
||||
query += " ORDER BY total_amount"
|
||||
elif sort_by == 'rating':
|
||||
query += " ORDER BY c.rating"
|
||||
elif sort_by == 'name':
|
||||
query += " ORDER BY c.name"
|
||||
elif sort_by == 'type':
|
||||
query += " ORDER BY ct.type_name, c.name"
|
||||
else:
|
||||
query += " ORDER BY c.rating"
|
||||
|
||||
query += " DESC" if not ascending else " ASC"
|
||||
|
||||
df = pd.read_sql_query(query, conn)
|
||||
conn.close()
|
||||
|
||||
if not df.empty:
|
||||
df['Скидка_процент'] = df['total_amount'].apply(calculate_discount)
|
||||
df['Скидка_текст'] = df['Скидка_процент'].apply(lambda x: f'{x}%')
|
||||
df['Общая_сумма'] = df['total_amount'].apply(lambda x: f"{x:,.2f} руб.")
|
||||
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def get_orders_history(db_path=DB_PATH, limit=50, sort_by='date_desc'):
|
||||
"""Получает историю заказов"""
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
query = """
|
||||
SELECT
|
||||
o.order_date AS "Дата заказа",
|
||||
c.name AS "Клиент",
|
||||
COUNT(oi.id) AS "Кол-во позиций",
|
||||
o.total_amount AS "Сумма заказа",
|
||||
o.status AS "Статус",
|
||||
o.discount_percent AS "Скидка",
|
||||
GROUP_CONCAT(cat.product_name, ', ') AS "Товары"
|
||||
FROM Orders o
|
||||
JOIN Clients c ON o.client_id = c.id
|
||||
LEFT JOIN OrderItems oi ON o.id = oi.order_id
|
||||
LEFT JOIN Catalog cat ON oi.product_id = cat.id
|
||||
GROUP BY o.id
|
||||
"""
|
||||
|
||||
if sort_by == 'date_desc':
|
||||
query += " ORDER BY o.order_date DESC, o.created_at DESC"
|
||||
elif sort_by == 'date_asc':
|
||||
query += " ORDER BY o.order_date ASC, o.created_at ASC"
|
||||
elif sort_by == 'amount_desc':
|
||||
query += " ORDER BY o.total_amount DESC"
|
||||
else:
|
||||
query += " ORDER BY o.order_date DESC, o.created_at DESC"
|
||||
|
||||
query += " LIMIT ?"
|
||||
|
||||
df = pd.read_sql_query(query, conn, params=(limit,))
|
||||
conn.close()
|
||||
return df
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка при получении истории заказов: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def print_client_cards(clients_df):
|
||||
"""Выводит карточки клиентов"""
|
||||
if clients_df is None or clients_df.empty:
|
||||
print("📭 Нет данных о клиентах")
|
||||
return
|
||||
|
||||
for index, row in clients_df.iterrows():
|
||||
print("╔" + "═" * 79 + "╗")
|
||||
print(f"║ {'КАРТОЧКА КЛИЕНТА':^77} ║")
|
||||
print("╠" + "═" * 79 + "╣")
|
||||
|
||||
print(f"║ Тип: {row['Тип клиента']:<70} ║")
|
||||
print(f"║ Наименование: {row['Наименование клиента']:<63} ║")
|
||||
print(f"║ Директор: {row['Директор']:<68} ║")
|
||||
print(f"║ Рейтинг: {row['Рейтинг']:<69} ║")
|
||||
print(f"║ Заказов: {row['orders_count']:<69} ║")
|
||||
print(f"║ Сумма заказов: {row['total_amount']:,.2f} руб. ║")
|
||||
print(f"║ Скидка: {row['Скидка_текст']:<70} ║")
|
||||
print(f"║ {'':<77} ║")
|
||||
print(f"║ Email: {row['Электронная почта']:<70} ║")
|
||||
print(f"║ Телефон: {row['Телефон']:<69} ║")
|
||||
print(f"║ ИНН: {row['ИНН']:<73} ║")
|
||||
|
||||
address = row['Адрес'] if row['Адрес'] else 'Не указан'
|
||||
if len(address) > 70:
|
||||
address = address[:67] + "..."
|
||||
print(f"║ Адрес: {address:<70} ║")
|
||||
print("╚" + "═" * 79 + "╝")
|
||||
print()
|
||||
|
||||
|
||||
def print_orders_cards(orders_df):
|
||||
"""Выводит карточки заказов"""
|
||||
if orders_df is None or orders_df.empty:
|
||||
print("📭 Нет данных о заказах")
|
||||
return
|
||||
|
||||
print("╔" + "═" * 120 + "╗")
|
||||
print(f"║ {'ИСТОРИЯ ЗАКАЗОВ':^118} ║")
|
||||
print("╠" + "═" * 120 + "╣")
|
||||
|
||||
for index, row in orders_df.iterrows():
|
||||
date = str(row['Дата заказа'])[:10]
|
||||
client = row['Клиент'][:20] if len(row['Клиент']) > 20 else row['Клиент']
|
||||
items = row['Кол-во позиций']
|
||||
amount = f"{row['Сумма заказа']:,.2f}"
|
||||
status = row['Статус']
|
||||
discount = f"{row['Скидка']}%"
|
||||
products = row['Товары'][:50] if row['Товары'] else '-'
|
||||
|
||||
print(f"║ {date} | {client:<20} | Позиций: {items:2} | Скидка: {discount:3} | {amount:>15} руб. | {status} ║")
|
||||
print(f"║ Товары: {products:<106} ║")
|
||||
print("╠" + "═" * 120 + "╣")
|
||||
|
||||
total_amount = orders_df['Сумма заказа'].sum()
|
||||
print(f"║ {'ИТОГО:':<20} {len(orders_df)} заказов, общая сумма: {total_amount:,.2f} руб. {'':>30} ║")
|
||||
print("╚" + "═" * 120 + "╝")
|
||||
|
||||
|
||||
def simple_choice(options):
|
||||
"""Упрощенная функция выбора"""
|
||||
print("\n" + "=" * 60)
|
||||
print("ВЫБЕРИТЕ ОПЦИЮ:")
|
||||
print("=" * 60)
|
||||
for i, option in enumerate(options, 1):
|
||||
print(f"{i}. {option}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
choice = input(f"\nВведите номер (1-{len(options)}): ").strip()
|
||||
if choice.isdigit():
|
||||
choice_num = int(choice)
|
||||
if 1 <= choice_num <= len(options):
|
||||
return options[choice_num - 1]
|
||||
print(f"❌ Пожалуйста, введите число от 1 до {len(options)}")
|
||||
except (ValueError, EOFError):
|
||||
print("❌ Неверный ввод. Попробуйте еще раз.")
|
||||
except KeyboardInterrupt:
|
||||
print("\n👋 Программа прервана пользователем.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def check_database():
|
||||
"""Проверяет загруженные данные в базе"""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
print("\n🔍 Проверка загруженных данных:")
|
||||
tables = ['ContractorType', 'Clients', 'Categories', 'Materials', 'Catalog', 'Cart', 'CartItems', 'Orders', 'OrderItems']
|
||||
for table in tables:
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {table}")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f" {table}: {count} записей")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def load_data_from_files():
|
||||
"""Загружает данные из Excel файлов в базу данных"""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
print("\n📁 Введите имена файлов:")
|
||||
clients_file = input("Файл с клиентами (Enter = Clients_import.xlsx): ").strip() or "Clients_import.xlsx"
|
||||
categories_file = input("Файл с категориями (Enter = Categories_import.xlsx): ").strip() or "Categories_import.xlsx"
|
||||
materials_file = input("Файл с материалами (Enter = Materials_import.xlsx): ").strip() or "Materials_import.xlsx"
|
||||
catalog_file = input("Файл с каталогом (Enter = Products_import.xlsx): ").strip() or "Products_import.xlsx"
|
||||
orders_file = input("Файл с заказами (Enter = Orders_import.xlsx): ").strip() or "Orders_import.xlsx"
|
||||
|
||||
# Формируем пути
|
||||
clients_file = os.path.join(RESOURCE_DIR, os.path.basename(clients_file))
|
||||
categories_file = os.path.join(RESOURCE_DIR, os.path.basename(categories_file))
|
||||
materials_file = os.path.join(RESOURCE_DIR, os.path.basename(materials_file))
|
||||
catalog_file = os.path.join(RESOURCE_DIR, os.path.basename(catalog_file))
|
||||
orders_file = os.path.join(RESOURCE_DIR, os.path.basename(orders_file))
|
||||
|
||||
# 1. Загрузка типов контрагентов
|
||||
print("\n1. 📊 Загрузка типов контрагентов...")
|
||||
clients_df = pd.read_excel(clients_file)
|
||||
|
||||
type_column = 'Тип партнера'
|
||||
if type_column in clients_df.columns:
|
||||
contractor_types = clients_df[type_column].unique()
|
||||
for contractor_type in contractor_types:
|
||||
cursor.execute("INSERT OR IGNORE INTO ContractorType (type_name) VALUES (?)", (contractor_type,))
|
||||
print(f" Загружено {len(contractor_types)} типов")
|
||||
|
||||
# 2. Загрузка клиентов
|
||||
print("2. 👥 Загрузка клиентов...")
|
||||
loaded_clients = 0
|
||||
for _, row in clients_df.iterrows():
|
||||
type_value = row.get('Тип партнера', 'Клиент')
|
||||
cursor.execute("SELECT id FROM ContractorType WHERE type_name = ?", (type_value,))
|
||||
result = cursor.fetchone()
|
||||
contractor_type_id = result[0] if result else 1
|
||||
|
||||
cursor.execute("""INSERT OR IGNORE INTO Clients
|
||||
(contractor_type_id, name, director, email, phone, address, inn, rating)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(contractor_type_id,
|
||||
row.get('Наименование партнера', ''),
|
||||
row.get('Директор', ''),
|
||||
row.get('Электронная почта партнера', ''),
|
||||
row.get('Телефон партнера', ''),
|
||||
row.get('Юридический адрес партнера', ''),
|
||||
str(row.get('ИНН', '')),
|
||||
float(row.get('Рейтинг', 0)))
|
||||
)
|
||||
loaded_clients += 1
|
||||
print(f" Загружено {loaded_clients} клиентов")
|
||||
|
||||
# 3. Загрузка категорий
|
||||
print("3. 📦 Загрузка категорий...")
|
||||
categories_df = pd.read_excel(categories_file)
|
||||
loaded_categories = 0
|
||||
for _, row in categories_df.iterrows():
|
||||
cursor.execute("INSERT OR IGNORE INTO Categories (category_name, coefficient) VALUES (?, ?)",
|
||||
(row.get('Тип продукции', ''),
|
||||
float(row.get('Коэффициент типа продукции', 1.0)))
|
||||
)
|
||||
loaded_categories += 1
|
||||
print(f" Загружено {loaded_categories} категорий")
|
||||
|
||||
# 4. Загрузка материалов
|
||||
print("4. 📦 Загрузка материалов...")
|
||||
materials_df = pd.read_excel(materials_file)
|
||||
loaded_materials = 0
|
||||
for _, row in materials_df.iterrows():
|
||||
cursor.execute("""
|
||||
INSERT OR IGNORE INTO Materials
|
||||
(material_name, description, category, color, density, thickness,
|
||||
supplier, country, wear_class, price_per_unit, unit, in_stock)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
row.get('Наименование материала', ''),
|
||||
row.get('Описание', ''),
|
||||
row.get('Категория', ''),
|
||||
row.get('Цвет', ''),
|
||||
row.get('Плотность', None),
|
||||
row.get('Толщина', None),
|
||||
row.get('Поставщик', ''),
|
||||
row.get('Страна', ''),
|
||||
row.get('Класс прочности', ''),
|
||||
float(row.get('Цена за ед.', 0)),
|
||||
row.get('Единица', 'шт'),
|
||||
int(row.get('В наличии', 0))
|
||||
))
|
||||
loaded_materials += 1
|
||||
print(f" ✅ Загружено {loaded_materials} материалов")
|
||||
|
||||
# 5. Загрузка каталога
|
||||
print("5. 🏷️ Загрузка каталога...")
|
||||
catalog_df = pd.read_excel(catalog_file)
|
||||
loaded_products = 0
|
||||
for _, row in catalog_df.iterrows():
|
||||
cursor.execute("""
|
||||
INSERT OR IGNORE INTO Catalog
|
||||
(category_id, material_id, product_name, article, description, min_price, quantity_in_stock)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
int(row.get('category_id', 1)),
|
||||
int(row.get('material_id')) if pd.notna(row.get('material_id')) else None,
|
||||
row.get('product_name', ''),
|
||||
str(row.get('article', '')),
|
||||
row.get('description', ''),
|
||||
float(row.get('min_price', 0)),
|
||||
int(row.get('quantity_in_stock', 0))
|
||||
))
|
||||
loaded_products += 1
|
||||
print(f" ✅ Загружено {loaded_products} товаров")
|
||||
|
||||
# 6. Загрузка заказов
|
||||
print("6. 📝 Загрузка заказов...")
|
||||
|
||||
# Получаем соответствие товаров
|
||||
products_dict = {}
|
||||
cursor.execute("SELECT id, product_name, article, min_price FROM Catalog")
|
||||
for row in cursor.fetchall():
|
||||
products_dict[row[1]] = {'id': row[0], 'price': row[3]}
|
||||
|
||||
# Получаем соответствие поставщиков
|
||||
suppliers_dict = {}
|
||||
cursor.execute("SELECT id, name FROM Clients")
|
||||
for row in cursor.fetchall():
|
||||
suppliers_dict[row[1]] = row[0]
|
||||
|
||||
# Читаем заказы
|
||||
orders_df = pd.read_excel(orders_file)
|
||||
|
||||
# Группируем заказы
|
||||
order_groups = {}
|
||||
for _, row in orders_df.iterrows():
|
||||
supplier_name = row.get('Наименование партнера', '')
|
||||
date = pd.to_datetime(row.get('Дата', datetime.now())).strftime('%Y-%m-%d')
|
||||
key = f"{supplier_name}_{date}"
|
||||
|
||||
if key not in order_groups:
|
||||
order_groups[key] = {
|
||||
'supplier_id': suppliers_dict.get(supplier_name),
|
||||
'date': date,
|
||||
'items': []
|
||||
}
|
||||
|
||||
product_name = row.get('Продукция', '')
|
||||
quantity = int(row.get('Количество', 1))
|
||||
|
||||
if product_name in products_dict:
|
||||
order_groups[key]['items'].append({
|
||||
'product_id': products_dict[product_name]['id'],
|
||||
'quantity': quantity,
|
||||
'price': products_dict[product_name]['price']
|
||||
})
|
||||
|
||||
# Создаем заказы
|
||||
orders_count = 0
|
||||
items_count = 0
|
||||
|
||||
for key, data in order_groups.items():
|
||||
if not data['supplier_id'] or not data['items']:
|
||||
continue
|
||||
|
||||
# Считаем сумму
|
||||
total = 0
|
||||
for item in data['items']:
|
||||
total += item['quantity'] * item['price'] * 1.1
|
||||
|
||||
# Создаем заказ
|
||||
cursor.execute('''
|
||||
INSERT INTO Orders (client_id, order_date, total_amount, status)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (data['supplier_id'], data['date'], total, 'Выполнен'))
|
||||
|
||||
order_id = cursor.lastrowid
|
||||
orders_count += 1
|
||||
|
||||
# Создаем позиции
|
||||
for item in data['items']:
|
||||
transport = item['quantity'] * item['price'] * 0.1
|
||||
cursor.execute('''
|
||||
INSERT INTO OrderItems (order_id, product_id, quantity, price_at_moment, transport_cost)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (order_id, item['product_id'], item['quantity'], item['price'], transport))
|
||||
items_count += 1
|
||||
|
||||
conn.commit()
|
||||
print(f" ✅ Загружено {orders_count} заказов и {items_count} позиций")
|
||||
|
||||
check_database()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка при загрузке данных: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
conn.rollback()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def display_menu():
|
||||
"""Главное меню программы"""
|
||||
menu_options = [
|
||||
'КАРТОЧКИ КЛИЕНТОВ (сортировка по рейтингу)',
|
||||
'КАРТОЧКИ КЛИЕНТОВ (сортировка по скидкам)',
|
||||
'ИСТОРИЯ ЗАКАЗОВ',
|
||||
'ЗАПОЛНЕНИЕ БД из файлов',
|
||||
'ВЫХОД'
|
||||
]
|
||||
|
||||
while True:
|
||||
choice = simple_choice(menu_options)
|
||||
|
||||
if choice == 'КАРТОЧКИ КЛИЕНТОВ (сортировка по рейтингу)':
|
||||
print("\n🎯 КАРТОЧКИ КЛИЕНТОВ (сортировка по рейтингу)")
|
||||
clients = get_clients(sort_by='rating', ascending=False)
|
||||
print_client_cards(clients)
|
||||
|
||||
elif choice == 'КАРТОЧКИ КЛИЕНТОВ (сортировка по скидкам)':
|
||||
print("\n📊 КАРТОЧКИ КЛИЕНТОВ (сортировка по скидкам)")
|
||||
clients = get_clients(sort_by='discount', ascending=False)
|
||||
print_client_cards(clients)
|
||||
|
||||
elif choice == 'ИСТОРИЯ ЗАКАЗОВ':
|
||||
print("\n📈 ИСТОРИЯ ЗАКАЗОВ")
|
||||
orders = get_orders_history(limit=50)
|
||||
print_orders_cards(orders)
|
||||
|
||||
elif choice == 'ЗАПОЛНЕНИЕ БД из файлов':
|
||||
if os.path.exists(DB_PATH):
|
||||
os.remove(DB_PATH)
|
||||
print("🗑️ База данных materials.db удалена")
|
||||
else:
|
||||
print("ℹ️ База данных не найдена, создаем новую")
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
create_tables(conn)
|
||||
conn.close()
|
||||
load_data_from_files()
|
||||
|
||||
elif choice == 'ВЫХОД':
|
||||
print("\n👋 До свидания!")
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
display_menu()
|
||||
999
main_flask.py
Normal file
999
main_flask.py
Normal file
|
|
@ -0,0 +1,999 @@
|
|||
from flask import Flask, render_template, send_from_directory, request, redirect, url_for, flash, session
|
||||
import sqlite3
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = 'my_secret_key_12345'
|
||||
|
||||
# ========== ПОДКЛЮЧЕНИЕ К БАЗЕ ДАННЫХ ==========
|
||||
def get_db_connection():
|
||||
conn = sqlite3.connect('materials.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
# ========== ФУНКЦИИ РАСЧЕТА СКИДОК И СТОИМОСТИ ==========
|
||||
|
||||
def calculate_supplier_discount(supplier_id, year):
|
||||
"""Расчет накопительной скидки поставщика за год (по сумме заказов)"""
|
||||
conn = get_db_connection()
|
||||
|
||||
# Получаем все заказы поставщика за указанный год
|
||||
orders = conn.execute('''
|
||||
SELECT total_amount FROM Orders
|
||||
WHERE client_id = ? AND strftime('%Y', order_date) = ?
|
||||
''', (supplier_id, str(year))).fetchall()
|
||||
|
||||
conn.close()
|
||||
|
||||
total_year = sum(order['total_amount'] for order in orders)
|
||||
|
||||
if total_year >= 500000:
|
||||
return 20
|
||||
elif total_year >= 300000:
|
||||
return 15
|
||||
elif total_year >= 150000:
|
||||
return 10
|
||||
return 0
|
||||
|
||||
def calculate_order_total_with_transport(items):
|
||||
"""Расчет общей суммы заказа с транспортными расходами (10%)"""
|
||||
total = 0
|
||||
for item in items:
|
||||
total += item['quantity'] * item['price'] * 1.1
|
||||
return round(total, 2)
|
||||
|
||||
# ========== ГЛАВНАЯ СТРАНИЦА (ПОСТАВЩИКИ) ==========
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
# Получаем параметры фильтрации из URL
|
||||
search_query = request.args.get('search', '')
|
||||
client_type = request.args.get('type', 'all')
|
||||
min_rating = request.args.get('rating', '0')
|
||||
sort_by = request.args.get('sort', 'name_asc')
|
||||
|
||||
conn = get_db_connection()
|
||||
|
||||
try:
|
||||
# Получаем общее количество всех клиентов (без фильтров)
|
||||
total_count = conn.execute('SELECT COUNT(*) as count FROM Clients').fetchone()['count']
|
||||
|
||||
# Базовый SQL запрос для клиентов
|
||||
sql = '''
|
||||
SELECT c.*, ct.type_name as contractor_type_name,
|
||||
COALESCE(SUM(o.total_amount), 0) as total_amount,
|
||||
COUNT(DISTINCT o.id) as orders_count
|
||||
FROM Clients c
|
||||
LEFT JOIN ContractorType ct ON c.contractor_type_id = ct.id
|
||||
LEFT JOIN Orders o ON c.id = o.client_id
|
||||
WHERE 1=1
|
||||
'''
|
||||
params = []
|
||||
|
||||
# Поиск по названию или ИНН
|
||||
if search_query:
|
||||
sql += " AND (c.name LIKE ? OR c.inn LIKE ?)"
|
||||
params.extend([f'%{search_query}%', f'%{search_query}%'])
|
||||
|
||||
# Фильтр по типу клиента
|
||||
if client_type != 'all':
|
||||
sql += " AND ct.type_name = ?"
|
||||
params.append(client_type)
|
||||
|
||||
# Фильтр по минимальному рейтингу
|
||||
if min_rating != '0':
|
||||
sql += " AND c.rating >= ?"
|
||||
params.append(float(min_rating))
|
||||
|
||||
# Группировка (обязательно после WHERE)
|
||||
sql += " GROUP BY c.id"
|
||||
|
||||
# Сортировка
|
||||
if sort_by == 'name_asc':
|
||||
sql += " ORDER BY c.name ASC"
|
||||
elif sort_by == 'name_desc':
|
||||
sql += " ORDER BY c.name DESC"
|
||||
elif sort_by == 'rating_desc':
|
||||
sql += " ORDER BY c.rating DESC"
|
||||
elif sort_by == 'rating_asc':
|
||||
sql += " ORDER BY c.rating ASC"
|
||||
elif sort_by == 'discount_desc':
|
||||
sql += " ORDER BY total_amount DESC"
|
||||
elif sort_by == 'discount_asc':
|
||||
sql += " ORDER BY total_amount ASC"
|
||||
else:
|
||||
sql += " ORDER BY c.name ASC"
|
||||
|
||||
# Выполняем запрос
|
||||
clients = conn.execute(sql, params).fetchall()
|
||||
|
||||
# Добавляем информацию о скидке на основе суммы
|
||||
clients_with_discount = []
|
||||
for client in clients:
|
||||
total_amount = client['total_amount'] or 0
|
||||
|
||||
# Рассчитываем текущую скидку на основе истории заказов
|
||||
current_discount = calculate_supplier_discount(client['id'], datetime.now().year)
|
||||
|
||||
client_dict = dict(client)
|
||||
client_dict['total_amount'] = total_amount
|
||||
client_dict['total_sales'] = total_amount
|
||||
client_dict['orders_count'] = client['orders_count'] or 0
|
||||
client_dict['discount_percent'] = current_discount
|
||||
client_dict['discount'] = current_discount
|
||||
client_dict['discount_text'] = f'{current_discount}%'
|
||||
|
||||
clients_with_discount.append(client_dict)
|
||||
|
||||
conn.close()
|
||||
|
||||
return render_template('index.html',
|
||||
partners=clients_with_discount,
|
||||
total_count=total_count)
|
||||
|
||||
except sqlite3.OperationalError as e:
|
||||
conn.close()
|
||||
return render_template('index.html',
|
||||
partners=[],
|
||||
total_count=0,
|
||||
error_message=f"Ошибка базы данных: {str(e)}")
|
||||
|
||||
# ========== МАРШРУТЫ ДЛЯ КЛИЕНТОВ ==========
|
||||
|
||||
@app.route('/add_client', methods=['GET', 'POST'])
|
||||
def add_client():
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
name = request.form['name']
|
||||
director = request.form.get('director', '')
|
||||
email = request.form['email']
|
||||
phone = request.form.get('phone', '')
|
||||
address = request.form.get('address', '')
|
||||
inn = request.form.get('inn', '')
|
||||
contractor_type_id = request.form['contractor_type_id']
|
||||
rating = request.form.get('rating', 0.0)
|
||||
|
||||
conn = get_db_connection()
|
||||
|
||||
# Проверяем уникальность ИНН
|
||||
if inn:
|
||||
existing = conn.execute('SELECT id FROM Clients WHERE inn = ?', (inn,)).fetchone()
|
||||
if existing:
|
||||
flash('Клиент с таким ИНН уже существует', 'error')
|
||||
conn.close()
|
||||
return redirect(url_for('add_client'))
|
||||
|
||||
conn.execute('''
|
||||
INSERT INTO Clients (name, director, email, phone, address, inn, contractor_type_id, rating)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (name, director, email, phone, address, inn, contractor_type_id, rating))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
flash('Клиент успешно добавлен!', 'success')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
except Exception as e:
|
||||
flash(f'Ошибка: {str(e)}', 'error')
|
||||
return redirect(url_for('add_client'))
|
||||
|
||||
# GET запрос
|
||||
conn = get_db_connection()
|
||||
contractor_types = conn.execute('SELECT * FROM ContractorType ORDER BY type_name').fetchall()
|
||||
conn.close()
|
||||
return render_template('add_client.html', contractor_types=contractor_types)
|
||||
|
||||
@app.route('/edit_client/<int:client_id>', methods=['GET', 'POST'])
|
||||
def edit_client(client_id):
|
||||
conn = get_db_connection()
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
name = request.form['name']
|
||||
director = request.form.get('director', '')
|
||||
email = request.form['email']
|
||||
phone = request.form.get('phone', '')
|
||||
address = request.form.get('address', '')
|
||||
inn = request.form.get('inn', '')
|
||||
contractor_type_id = request.form['contractor_type_id']
|
||||
rating = request.form.get('rating', 0.0)
|
||||
|
||||
# Проверяем уникальность ИНН
|
||||
if inn:
|
||||
existing = conn.execute(
|
||||
'SELECT id FROM Clients WHERE inn = ? AND id != ?',
|
||||
(inn, client_id)
|
||||
).fetchone()
|
||||
if existing:
|
||||
flash('Клиент с таким ИНН уже существует', 'error')
|
||||
conn.close()
|
||||
return redirect(url_for('edit_client', client_id=client_id))
|
||||
|
||||
conn.execute('''
|
||||
UPDATE Clients
|
||||
SET name = ?, director = ?, email = ?, phone = ?,
|
||||
address = ?, inn = ?, contractor_type_id = ?, rating = ?
|
||||
WHERE id = ?
|
||||
''', (name, director, email, phone, address, inn, contractor_type_id, rating, client_id))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
flash('Данные обновлены!', 'success')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
except Exception as e:
|
||||
flash(f'Ошибка: {str(e)}', 'error')
|
||||
conn.close()
|
||||
return redirect(url_for('edit_client', client_id=client_id))
|
||||
|
||||
# GET запрос
|
||||
client = conn.execute('''
|
||||
SELECT c.*, ct.type_name as contractor_type_name
|
||||
FROM Clients c
|
||||
LEFT JOIN ContractorType ct ON c.contractor_type_id = ct.id
|
||||
WHERE c.id = ?
|
||||
''', (client_id,)).fetchone()
|
||||
|
||||
contractor_types = conn.execute('SELECT * FROM ContractorType ORDER BY type_name').fetchall()
|
||||
conn.close()
|
||||
|
||||
if not client:
|
||||
flash('Клиент не найден', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
return render_template('edit_client.html', client=client, contractor_types=contractor_types)
|
||||
|
||||
# ========== МАРШРУТЫ ДЛЯ ЗАКАЗОВ ==========
|
||||
|
||||
@app.route('/order_history/<int:client_id>')
|
||||
def order_history(client_id):
|
||||
conn = get_db_connection()
|
||||
|
||||
client = conn.execute('''
|
||||
SELECT c.*, ct.type_name as contractor_type_name
|
||||
FROM Clients c
|
||||
LEFT JOIN ContractorType ct ON c.contractor_type_id = ct.id
|
||||
WHERE c.id = ?
|
||||
''', (client_id,)).fetchone()
|
||||
|
||||
if not client:
|
||||
conn.close()
|
||||
flash('Клиент не найден', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# Получаем все заказы
|
||||
orders = conn.execute('''
|
||||
SELECT o.*
|
||||
FROM Orders o
|
||||
WHERE o.client_id = ?
|
||||
ORDER BY o.order_date DESC, o.id DESC
|
||||
''', (client_id,)).fetchall()
|
||||
|
||||
orders_with_items = []
|
||||
|
||||
for order in orders:
|
||||
order_dict = dict(order)
|
||||
|
||||
# Получаем позиции для заказа
|
||||
items = conn.execute('''
|
||||
SELECT oi.*, cat.product_name, cat.article,
|
||||
cat2.category_name, m.material_name
|
||||
FROM OrderItems oi
|
||||
JOIN Catalog cat ON oi.product_id = cat.id
|
||||
LEFT JOIN Categories cat2 ON cat.category_id = cat2.id
|
||||
LEFT JOIN Materials m ON cat.material_id = m.id
|
||||
WHERE oi.order_id = ?
|
||||
''', (order['id'],)).fetchall()
|
||||
|
||||
# Рассчитываем базовую стоимость (сумма quantity * price_at_moment)
|
||||
base_amount = 0
|
||||
items_list = []
|
||||
|
||||
for item in items:
|
||||
item_dict = dict(item)
|
||||
# Рассчитываем стоимость позиции
|
||||
item_total = item_dict['quantity'] * item_dict['price_at_moment']
|
||||
item_dict['item_total'] = item_total
|
||||
base_amount += item_total
|
||||
items_list.append(item_dict)
|
||||
|
||||
# Транспортные расходы (10% от базовой стоимости)
|
||||
shipping_cost = base_amount * 0.1
|
||||
|
||||
# Получаем скидку клиента
|
||||
try:
|
||||
if order_dict['order_date']:
|
||||
order_year = datetime.strptime(order_dict['order_date'], '%Y-%m-%d').year
|
||||
else:
|
||||
order_year = datetime.now().year
|
||||
except:
|
||||
order_year = datetime.now().year
|
||||
|
||||
discount = calculate_supplier_discount(client_id, order_year)
|
||||
|
||||
# Рассчитываем итоговую стоимость с учётом скидки
|
||||
total_with_shipping = base_amount + shipping_cost
|
||||
total_amount_calculated = total_with_shipping * (1 - discount / 100) if discount > 0 else total_with_shipping
|
||||
|
||||
# Добавляем рассчитанные поля в словарь заказа
|
||||
order_dict['base_amount'] = base_amount
|
||||
order_dict['shipping_cost'] = shipping_cost
|
||||
order_dict['discount'] = discount
|
||||
order_dict['total_amount_calculated'] = total_amount_calculated
|
||||
order_dict['display_total'] = order_dict.get('total_amount', total_amount_calculated)
|
||||
order_dict['order_items'] = items_list
|
||||
order_dict['items_count'] = len(items_list)
|
||||
|
||||
orders_with_items.append(order_dict)
|
||||
|
||||
# Считаем общую статистику
|
||||
total_stats = conn.execute('''
|
||||
SELECT
|
||||
COUNT(DISTINCT o.id) as total_orders,
|
||||
SUM(o.total_amount) as total_amount,
|
||||
SUM(CASE WHEN o.status = 'Выполнен' THEN 1 ELSE 0 END) as completed_orders,
|
||||
COALESCE(SUM(oi.quantity), 0) as total_quantity
|
||||
FROM Orders o
|
||||
LEFT JOIN OrderItems oi ON o.id = oi.order_id
|
||||
WHERE o.client_id = ?
|
||||
''', (client_id,)).fetchone()
|
||||
|
||||
conn.close()
|
||||
|
||||
return render_template('order_history.html',
|
||||
client=client,
|
||||
orders=orders_with_items,
|
||||
total_stats=total_stats)
|
||||
|
||||
@app.route('/update_order_status/<int:order_id>', methods=['POST'])
|
||||
def update_order_status(order_id):
|
||||
"""Обновление статуса заказа"""
|
||||
try:
|
||||
new_status = request.form['status']
|
||||
|
||||
conn = get_db_connection()
|
||||
conn.execute('UPDATE Orders SET status = ? WHERE id = ?', (new_status, order_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
flash(f'Статус заказа обновлен на "{new_status}"', 'success')
|
||||
except Exception as e:
|
||||
flash(f'Ошибка при обновлении статуса: {str(e)}', 'error')
|
||||
|
||||
return redirect(request.referrer or url_for('orders_list'))
|
||||
|
||||
@app.route('/add_order/<int:client_id>', methods=['GET', 'POST'])
|
||||
def add_order(client_id):
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
order_date = request.form['order_date']
|
||||
status = request.form.get('status', 'Новый')
|
||||
|
||||
conn = get_db_connection()
|
||||
|
||||
# Проверяем наличие таблицы OrderItems
|
||||
cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='OrderItems'")
|
||||
has_orderitems = cursor.fetchone() is not None
|
||||
|
||||
if has_orderitems:
|
||||
# Новая структура
|
||||
cursor = conn.execute('''
|
||||
INSERT INTO Orders (client_id, order_date, status, total_amount)
|
||||
VALUES (?, ?, ?, 0)
|
||||
''', (client_id, order_date, status))
|
||||
|
||||
order_id = cursor.lastrowid
|
||||
|
||||
# Получаем позиции из формы
|
||||
product_ids = request.form.getlist('product_id[]')
|
||||
quantities = request.form.getlist('quantity[]')
|
||||
|
||||
total = 0
|
||||
|
||||
for i in range(len(product_ids)):
|
||||
if product_ids[i] and quantities[i]:
|
||||
product_id = int(product_ids[i])
|
||||
quantity = int(quantities[i])
|
||||
|
||||
# Получаем цену товара
|
||||
product = conn.execute('SELECT min_price FROM Catalog WHERE id = ?', (product_id,)).fetchone()
|
||||
|
||||
if product:
|
||||
price = product['min_price']
|
||||
|
||||
# Добавляем позицию
|
||||
conn.execute('''
|
||||
INSERT INTO OrderItems (order_id, product_id, quantity, price_at_moment)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (order_id, product_id, quantity, price))
|
||||
|
||||
# Считаем сумму с транспортными расходами
|
||||
total += quantity * price * 1.1
|
||||
|
||||
# Рассчитываем скидку
|
||||
year = datetime.strptime(order_date, '%Y-%m-%d').year
|
||||
discount = calculate_supplier_discount(client_id, year)
|
||||
|
||||
# Применяем скидку
|
||||
total_with_discount = total * (1 - discount/100)
|
||||
|
||||
# Обновляем сумму заказа
|
||||
conn.execute('UPDATE Orders SET total_amount = ? WHERE id = ?', (total_with_discount, order_id))
|
||||
|
||||
discount_msg = f' Применена накопительная скидка {discount}% (экономия {total * discount/100:.2f} руб.)' if discount > 0 else ''
|
||||
|
||||
else:
|
||||
# Старая структура (для совместимости)
|
||||
product_id = request.form.get('product_id')
|
||||
quantity = int(request.form.get('quantity', 1))
|
||||
|
||||
product = conn.execute('SELECT min_price FROM Catalog WHERE id = ?', (product_id,)).fetchone()
|
||||
|
||||
if not product:
|
||||
flash('Товар не найден', 'error')
|
||||
return redirect(url_for('add_order', client_id=client_id))
|
||||
|
||||
base_amount = product['min_price'] * quantity
|
||||
total_amount = base_amount * 1.1 # с транспортом
|
||||
|
||||
cursor = conn.execute('''
|
||||
INSERT INTO Orders (client_id, product_id, quantity, order_date, total_amount, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
''', (client_id, product_id, quantity, order_date, total_amount, status))
|
||||
|
||||
order_id = cursor.lastrowid
|
||||
discount_msg = ''
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
flash(f'Заказ успешно добавлен!{discount_msg}', 'success')
|
||||
return redirect(url_for('order_history', client_id=client_id))
|
||||
|
||||
except Exception as e:
|
||||
flash(f'Ошибка при добавлении заказа: {str(e)}', 'error')
|
||||
return redirect(url_for('add_order', client_id=client_id))
|
||||
|
||||
# GET запрос
|
||||
conn = get_db_connection()
|
||||
|
||||
client = conn.execute('''
|
||||
SELECT c.*, ct.type_name as contractor_type_name
|
||||
FROM Clients c
|
||||
LEFT JOIN ContractorType ct ON c.contractor_type_id = ct.id
|
||||
WHERE c.id = ?
|
||||
''', (client_id,)).fetchone()
|
||||
|
||||
products = conn.execute('''
|
||||
SELECT cat.*, c.category_name, m.material_name
|
||||
FROM Catalog cat
|
||||
LEFT JOIN Categories c ON cat.category_id = c.id
|
||||
LEFT JOIN Materials m ON cat.material_id = m.id
|
||||
ORDER BY c.category_name, cat.product_name
|
||||
''').fetchall()
|
||||
|
||||
conn.close()
|
||||
|
||||
if not client:
|
||||
flash('Клиент не найден', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
return render_template('add_order.html', client=client, products=products, today=today)
|
||||
|
||||
@app.route('/orders_list')
|
||||
def orders_list():
|
||||
"""Список всех заказов"""
|
||||
conn = get_db_connection()
|
||||
|
||||
# Получаем всех клиентов для выпадающего списка
|
||||
clients = conn.execute('''
|
||||
SELECT id, name FROM Clients ORDER BY name
|
||||
''').fetchall()
|
||||
|
||||
# Проверяем наличие таблицы OrderItems
|
||||
cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='OrderItems'")
|
||||
has_orderitems = cursor.fetchone() is not None
|
||||
|
||||
if has_orderitems:
|
||||
# Новая структура
|
||||
orders = conn.execute('''
|
||||
SELECT
|
||||
o.id,
|
||||
o.client_id,
|
||||
o.order_date,
|
||||
o.total_amount,
|
||||
o.status,
|
||||
c.name as client_name,
|
||||
c.inn as client_inn,
|
||||
COUNT(oi.id) as items_count,
|
||||
SUM(oi.quantity) as total_quantity,
|
||||
GROUP_CONCAT(cat.product_name, ', ') as products_list
|
||||
FROM Orders o
|
||||
LEFT JOIN Clients c ON o.client_id = c.id
|
||||
LEFT JOIN OrderItems oi ON o.id = oi.order_id
|
||||
LEFT JOIN Catalog cat ON oi.product_id = cat.id
|
||||
GROUP BY o.id
|
||||
ORDER BY o.order_date DESC, o.id DESC
|
||||
LIMIT 100
|
||||
''').fetchall()
|
||||
|
||||
# Считаем общую статистику
|
||||
total_stats = conn.execute('''
|
||||
SELECT
|
||||
COUNT(*) as total_orders,
|
||||
SUM(o.total_amount) as total_amount,
|
||||
SUM(CASE WHEN o.status = 'Выполнен' THEN 1 ELSE 0 END) as completed_orders
|
||||
FROM Orders o
|
||||
''').fetchone()
|
||||
|
||||
else:
|
||||
# Старая структура (с product_id и quantity в Orders)
|
||||
orders = conn.execute('''
|
||||
SELECT
|
||||
o.id,
|
||||
o.client_id,
|
||||
o.order_date,
|
||||
o.total_amount,
|
||||
o.status,
|
||||
o.quantity,
|
||||
c.name as client_name,
|
||||
c.inn as client_inn,
|
||||
cat.product_name,
|
||||
cat.article,
|
||||
cat2.category_name
|
||||
FROM Orders o
|
||||
LEFT JOIN Clients c ON o.client_id = c.id
|
||||
LEFT JOIN Catalog cat ON o.product_id = cat.id
|
||||
LEFT JOIN Categories cat2 ON cat.category_id = cat2.id
|
||||
ORDER BY o.order_date DESC, o.id DESC
|
||||
LIMIT 100
|
||||
''').fetchall()
|
||||
|
||||
# Считаем общую статистику
|
||||
total_stats = conn.execute('''
|
||||
SELECT
|
||||
COUNT(*) as total_orders,
|
||||
SUM(quantity) as total_quantity,
|
||||
SUM(total_amount) as total_amount,
|
||||
SUM(CASE WHEN status = 'Выполнен' THEN 1 ELSE 0 END) as completed_orders
|
||||
FROM Orders
|
||||
''').fetchone()
|
||||
|
||||
conn.close()
|
||||
|
||||
return render_template('orders_list.html', orders=orders, total_stats=total_stats, clients=clients, has_orderitems=has_orderitems)
|
||||
|
||||
# ========== МАРШРУТ ДЛЯ КАТАЛОГА ==========
|
||||
|
||||
@app.route('/catalog')
|
||||
def catalog():
|
||||
conn = get_db_connection()
|
||||
|
||||
# Получаем список всех категорий для фильтра
|
||||
categories = conn.execute('SELECT * FROM Categories ORDER BY category_name').fetchall()
|
||||
|
||||
# Получаем список всех поставщиков для фильтра
|
||||
suppliers = conn.execute('SELECT id, name FROM Clients ORDER BY name').fetchall()
|
||||
|
||||
# Получаем параметры фильтрации из URL
|
||||
search_query = request.args.get('search', '')
|
||||
category_id = request.args.get('category', 'all')
|
||||
supplier_id = request.args.get('supplier', 'all')
|
||||
sort_by = request.args.get('sort', 'name_asc')
|
||||
|
||||
# Базовый SQL запрос с добавлением поставщика
|
||||
sql = '''
|
||||
SELECT cat.*, c.category_name, m.material_name, m.color,
|
||||
cl.name as supplier_name, cl.id as supplier_id
|
||||
FROM Catalog cat
|
||||
LEFT JOIN Categories c ON cat.category_id = c.id
|
||||
LEFT JOIN Materials m ON cat.material_id = m.id
|
||||
LEFT JOIN Clients cl ON m.supplier = cl.name
|
||||
WHERE 1=1
|
||||
'''
|
||||
params = []
|
||||
|
||||
# Поиск по названию или артикулу
|
||||
if search_query:
|
||||
sql += " AND (cat.product_name LIKE ? OR cat.article LIKE ?)"
|
||||
params.extend([f'%{search_query}%', f'%{search_query}%'])
|
||||
|
||||
# Фильтр по категории
|
||||
if category_id != 'all':
|
||||
sql += " AND cat.category_id = ?"
|
||||
params.append(category_id)
|
||||
|
||||
# Фильтр по поставщику
|
||||
if supplier_id != 'all':
|
||||
sql += " AND cl.id = ?"
|
||||
params.append(supplier_id)
|
||||
|
||||
# Сортировка
|
||||
if sort_by == 'name_asc':
|
||||
sql += " ORDER BY cat.product_name ASC"
|
||||
elif sort_by == 'name_desc':
|
||||
sql += " ORDER BY cat.product_name DESC"
|
||||
elif sort_by == 'price_asc':
|
||||
sql += " ORDER BY cat.min_price ASC"
|
||||
elif sort_by == 'price_desc':
|
||||
sql += " ORDER BY cat.min_price DESC"
|
||||
elif sort_by == 'stock_desc':
|
||||
sql += " ORDER BY cat.quantity_in_stock DESC"
|
||||
else:
|
||||
sql += " ORDER BY cat.product_name ASC"
|
||||
|
||||
# Выполняем запрос
|
||||
products = conn.execute(sql, params).fetchall()
|
||||
|
||||
# Получаем количество товаров в корзине для бейджа
|
||||
cart_count = 0
|
||||
if 'cart' in session:
|
||||
cart_count = len(session['cart'])
|
||||
|
||||
conn.close()
|
||||
|
||||
return render_template('catalog.html',
|
||||
products=products,
|
||||
categories=categories,
|
||||
suppliers=suppliers,
|
||||
cart_count=cart_count)
|
||||
|
||||
# ========== МАРШРУТЫ ДЛЯ УПРАВЛЕНИЯ ТОВАРАМИ ==========
|
||||
|
||||
@app.route('/add_product', methods=['GET', 'POST'])
|
||||
def add_product():
|
||||
"""Добавление нового товара"""
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
product_name = request.form['product_name']
|
||||
article = request.form['article']
|
||||
category_id = request.form['category_id']
|
||||
material_id = request.form.get('material_id') or None
|
||||
description = request.form.get('description', '')
|
||||
min_price = float(request.form['min_price'])
|
||||
quantity_in_stock = int(request.form.get('quantity_in_stock', 0))
|
||||
|
||||
conn = get_db_connection()
|
||||
|
||||
# Проверяем уникальность артикула
|
||||
existing = conn.execute('SELECT id FROM Catalog WHERE article = ?', (article,)).fetchone()
|
||||
if existing:
|
||||
flash('Товар с таким артикулом уже существует', 'error')
|
||||
conn.close()
|
||||
return redirect(url_for('add_product'))
|
||||
|
||||
conn.execute('''
|
||||
INSERT INTO Catalog (product_name, article, category_id, material_id, description, min_price, quantity_in_stock)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
''', (product_name, article, category_id, material_id, description, min_price, quantity_in_stock))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
flash('Товар успешно добавлен!', 'success')
|
||||
return redirect(url_for('catalog'))
|
||||
|
||||
except Exception as e:
|
||||
flash(f'Ошибка: {str(e)}', 'error')
|
||||
return redirect(url_for('add_product'))
|
||||
|
||||
# GET запрос
|
||||
conn = get_db_connection()
|
||||
categories = conn.execute('SELECT * FROM Categories ORDER BY category_name').fetchall()
|
||||
materials = conn.execute('SELECT * FROM Materials ORDER BY material_name').fetchall()
|
||||
conn.close()
|
||||
|
||||
return render_template('add_product.html', categories=categories, materials=materials)
|
||||
|
||||
@app.route('/edit_product/<int:product_id>', methods=['GET', 'POST'])
|
||||
def edit_product(product_id):
|
||||
"""Редактирование товара"""
|
||||
conn = get_db_connection()
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
product_name = request.form['product_name']
|
||||
article = request.form['article']
|
||||
category_id = request.form['category_id']
|
||||
material_id = request.form.get('material_id') or None
|
||||
description = request.form.get('description', '')
|
||||
min_price = float(request.form['min_price'])
|
||||
quantity_in_stock = int(request.form.get('quantity_in_stock', 0))
|
||||
|
||||
# Проверяем уникальность артикула (исключая текущий товар)
|
||||
existing = conn.execute(
|
||||
'SELECT id FROM Catalog WHERE article = ? AND id != ?',
|
||||
(article, product_id)
|
||||
).fetchone()
|
||||
if existing:
|
||||
flash('Товар с таким артикулом уже существует', 'error')
|
||||
conn.close()
|
||||
return redirect(url_for('edit_product', product_id=product_id))
|
||||
|
||||
conn.execute('''
|
||||
UPDATE Catalog
|
||||
SET product_name = ?, article = ?, category_id = ?, material_id = ?,
|
||||
description = ?, min_price = ?, quantity_in_stock = ?
|
||||
WHERE id = ?
|
||||
''', (product_name, article, category_id, material_id, description, min_price, quantity_in_stock, product_id))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
flash('Товар успешно обновлен!', 'success')
|
||||
return redirect(url_for('catalog'))
|
||||
|
||||
except Exception as e:
|
||||
flash(f'Ошибка: {str(e)}', 'error')
|
||||
return redirect(url_for('edit_product', product_id=product_id))
|
||||
|
||||
# GET запрос
|
||||
product = conn.execute('''
|
||||
SELECT cat.*, c.category_name, m.material_name
|
||||
FROM Catalog cat
|
||||
LEFT JOIN Categories c ON cat.category_id = c.id
|
||||
LEFT JOIN Materials m ON cat.material_id = m.id
|
||||
WHERE cat.id = ?
|
||||
''', (product_id,)).fetchone()
|
||||
|
||||
categories = conn.execute('SELECT * FROM Categories ORDER BY category_name').fetchall()
|
||||
materials = conn.execute('SELECT * FROM Materials ORDER BY material_name').fetchall()
|
||||
conn.close()
|
||||
|
||||
if not product:
|
||||
flash('Товар не найден', 'error')
|
||||
return redirect(url_for('catalog'))
|
||||
|
||||
return render_template('edit_product.html', product=product, categories=categories, materials=materials)
|
||||
|
||||
@app.route('/delete_product/<int:product_id>', methods=['POST'])
|
||||
def delete_product(product_id):
|
||||
"""Удаление товара"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
|
||||
# Проверяем наличие таблицы OrderItems
|
||||
cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='OrderItems'")
|
||||
has_orderitems = cursor.fetchone() is not None
|
||||
|
||||
if has_orderitems:
|
||||
# Новая структура
|
||||
items = conn.execute('SELECT COUNT(*) as count FROM OrderItems WHERE product_id = ?', (product_id,)).fetchone()
|
||||
if items['count'] > 0:
|
||||
flash('Нельзя удалить товар, который есть в заказах', 'error')
|
||||
conn.close()
|
||||
return redirect(url_for('catalog'))
|
||||
else:
|
||||
# Старая структура
|
||||
orders = conn.execute('SELECT COUNT(*) as count FROM Orders WHERE product_id = ?', (product_id,)).fetchone()
|
||||
if orders['count'] > 0:
|
||||
flash('Нельзя удалить товар, который есть в заказах', 'error')
|
||||
conn.close()
|
||||
return redirect(url_for('catalog'))
|
||||
|
||||
conn.execute('DELETE FROM Catalog WHERE id = ?', (product_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
flash('Товар успешно удален', 'success')
|
||||
except Exception as e:
|
||||
flash(f'Ошибка при удалении: {str(e)}', 'error')
|
||||
|
||||
return redirect(url_for('catalog'))
|
||||
|
||||
# ========== МАРШРУТЫ ДЛЯ КОРЗИНЫ ==========
|
||||
|
||||
@app.route('/cart')
|
||||
def view_cart():
|
||||
"""Просмотр корзины"""
|
||||
cart = session.get('cart', {})
|
||||
cart_items = []
|
||||
total_amount = 0
|
||||
|
||||
conn = get_db_connection()
|
||||
|
||||
for key, item_data in cart.items():
|
||||
product_id = item_data['product_id']
|
||||
quantity = item_data['quantity']
|
||||
supplier_id = item_data.get('supplier_id')
|
||||
|
||||
product = conn.execute('''
|
||||
SELECT cat.*, c.category_name, m.material_name, cat.quantity_in_stock
|
||||
FROM Catalog cat
|
||||
LEFT JOIN Categories c ON cat.category_id = c.id
|
||||
LEFT JOIN Materials m ON cat.material_id = m.id
|
||||
WHERE cat.id = ?
|
||||
''', (product_id,)).fetchone()
|
||||
|
||||
if product:
|
||||
# Получаем информацию о поставщике
|
||||
supplier = None
|
||||
if supplier_id:
|
||||
supplier = conn.execute('SELECT name FROM Clients WHERE id = ?', (supplier_id,)).fetchone()
|
||||
|
||||
cart_items.append({
|
||||
'key': key,
|
||||
'product_id': product_id,
|
||||
'supplier_id': supplier_id,
|
||||
'supplier_name': supplier['name'] if supplier else 'Не указан',
|
||||
'product_name': product['product_name'],
|
||||
'article': product['article'],
|
||||
'category_name': product['category_name'],
|
||||
'material_name': product['material_name'],
|
||||
'price': product['min_price'],
|
||||
'quantity': quantity,
|
||||
'in_stock': product['quantity_in_stock'] or 0
|
||||
})
|
||||
total_amount += product['min_price'] * quantity
|
||||
|
||||
conn.close()
|
||||
|
||||
return render_template('cart_new.html', cart_items=cart_items, total_amount=total_amount)
|
||||
|
||||
@app.route('/add_to_cart/<int:product_id>', methods=['POST'])
|
||||
def add_to_cart(product_id):
|
||||
"""Добавление товара в корзину с выбором поставщика"""
|
||||
quantity = int(request.form.get('quantity', 1))
|
||||
supplier_id = request.form.get('supplier_id')
|
||||
|
||||
if not supplier_id:
|
||||
flash('Пожалуйста, выберите поставщика', 'error')
|
||||
return redirect(url_for('catalog'))
|
||||
|
||||
# Получаем информацию о товаре для проверки наличия
|
||||
conn = get_db_connection()
|
||||
product = conn.execute('SELECT quantity_in_stock FROM Catalog WHERE id = ?', (product_id,)).fetchone()
|
||||
conn.close()
|
||||
|
||||
if not product or product['quantity_in_stock'] < quantity:
|
||||
flash('Недостаточно товара на складе', 'error')
|
||||
return redirect(url_for('catalog'))
|
||||
|
||||
cart = session.get('cart', {})
|
||||
|
||||
# Создаем уникальный ключ для комбинации товар + поставщик
|
||||
key = f"{product_id}_{supplier_id}"
|
||||
|
||||
if key in cart:
|
||||
cart[key]['quantity'] += quantity
|
||||
else:
|
||||
cart[key] = {
|
||||
'product_id': product_id,
|
||||
'supplier_id': int(supplier_id),
|
||||
'quantity': quantity
|
||||
}
|
||||
|
||||
session['cart'] = cart
|
||||
flash('Товар добавлен в корзину!', 'success')
|
||||
|
||||
return redirect(url_for('catalog'))
|
||||
|
||||
@app.route('/update_cart/<path:item_key>', methods=['POST'])
|
||||
def update_cart(item_key):
|
||||
"""Обновление количества товара в корзине"""
|
||||
quantity = int(request.form.get('quantity', 1))
|
||||
|
||||
cart = session.get('cart', {})
|
||||
|
||||
if item_key in cart:
|
||||
if quantity > 0:
|
||||
cart[item_key]['quantity'] = quantity
|
||||
else:
|
||||
cart.pop(item_key, None)
|
||||
|
||||
session['cart'] = cart
|
||||
|
||||
return redirect(url_for('view_cart'))
|
||||
|
||||
@app.route('/remove_from_cart/<path:item_key>', methods=['POST'])
|
||||
def remove_from_cart(item_key):
|
||||
"""Удаление товара из корзины"""
|
||||
cart = session.get('cart', {})
|
||||
cart.pop(item_key, None)
|
||||
session['cart'] = cart
|
||||
|
||||
return redirect(url_for('view_cart'))
|
||||
|
||||
@app.route('/checkout', methods=['POST'])
|
||||
def checkout():
|
||||
"""Оформление заказа из корзины"""
|
||||
cart = session.get('cart', {})
|
||||
|
||||
if not cart:
|
||||
flash('Корзина пуста', 'error')
|
||||
return redirect(url_for('catalog'))
|
||||
|
||||
# Группируем товары по поставщикам для создания отдельных заказов
|
||||
orders_by_supplier = {}
|
||||
|
||||
conn = get_db_connection()
|
||||
|
||||
for key, item_data in cart.items():
|
||||
supplier_id = item_data['supplier_id']
|
||||
product_id = item_data['product_id']
|
||||
quantity = item_data['quantity']
|
||||
|
||||
if supplier_id not in orders_by_supplier:
|
||||
orders_by_supplier[supplier_id] = []
|
||||
|
||||
# Получаем цену товара
|
||||
product = conn.execute('SELECT min_price FROM Catalog WHERE id = ?', (product_id,)).fetchone()
|
||||
if product:
|
||||
orders_by_supplier[supplier_id].append({
|
||||
'product_id': product_id,
|
||||
'quantity': quantity,
|
||||
'price': product['min_price']
|
||||
})
|
||||
|
||||
# Создаем заказы для каждого поставщика
|
||||
orders_created = 0
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
for supplier_id, items in orders_by_supplier.items():
|
||||
# Считаем общую сумму с транспортом
|
||||
total = 0
|
||||
for item in items:
|
||||
total += item['quantity'] * item['price'] * 1.1
|
||||
|
||||
# Рассчитываем скидку
|
||||
discount = calculate_supplier_discount(supplier_id, datetime.now().year)
|
||||
total_with_discount = total * (1 - discount/100)
|
||||
|
||||
# Создаем заказ
|
||||
cursor = conn.execute('''
|
||||
INSERT INTO Orders (client_id, order_date, total_amount, status)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (supplier_id, today, total_with_discount, 'Новый'))
|
||||
|
||||
order_id = cursor.lastrowid
|
||||
|
||||
# Добавляем позиции
|
||||
for item in items:
|
||||
conn.execute('''
|
||||
INSERT INTO OrderItems (order_id, product_id, quantity, price_at_moment)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (order_id, item['product_id'], item['quantity'], item['price']))
|
||||
|
||||
orders_created += 1
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Очищаем корзину
|
||||
session.pop('cart', None)
|
||||
|
||||
flash(f'Заказ успешно оформлен! Создано {orders_created} заказов.', 'success')
|
||||
return redirect(url_for('orders_list'))
|
||||
|
||||
# ========== МАРШРУТ ДЛЯ МАТЕРИАЛОВ ==========
|
||||
|
||||
@app.route('/materials')
|
||||
def materials():
|
||||
"""Список всех материалов"""
|
||||
conn = get_db_connection()
|
||||
|
||||
materials = conn.execute('''
|
||||
SELECT * FROM Materials
|
||||
ORDER BY material_name
|
||||
''').fetchall()
|
||||
|
||||
conn.close()
|
||||
|
||||
return render_template('materials.html', materials=materials)
|
||||
|
||||
# ========== СЛУЖЕБНЫЕ МАРШРУТЫ ==========
|
||||
|
||||
@app.route('/favicon.ico')
|
||||
def favicon():
|
||||
return send_from_directory(os.path.join(app.root_path, 'static'),
|
||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||
|
||||
@app.route('/static/<path:filename>')
|
||||
def static_files(filename):
|
||||
return send_from_directory(app.root_path + '/static', filename)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
BIN
materials.db
Normal file
BIN
materials.db
Normal file
Binary file not shown.
470
static/css/style.css
Normal file
470
static/css/style.css
Normal file
|
|
@ -0,0 +1,470 @@
|
|||
/* Подключаем шрифт Verdana */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Verdana:wght@400;500;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
--antique-white: #FAEBD7;
|
||||
--light-brown: #DEB887;
|
||||
--brown: #A0522D;
|
||||
--dark-brown: #8B4513;
|
||||
--text-dark: #2c3e50;
|
||||
--text-light: #5D3A1A;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Verdana', sans-serif;
|
||||
background-color: var(--antique-white);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
background-color: #ffffff;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--light-brown) 100%);
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 35px;
|
||||
color: white;
|
||||
box-shadow: 0 6px 20px rgba(160, 82, 45, 0.3);
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 70px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.2));
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 2.5em;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
text-shadow: 2px 2px 6px rgba(0,0,0,0.3);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: var(--brown);
|
||||
font-weight: 700;
|
||||
border-bottom: 3px solid var(--light-brown);
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 25px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
/* Карточки партнеров */
|
||||
.partner-card {
|
||||
border: 3px solid var(--light-brown);
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
margin-bottom: 25px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 6px 20px rgba(222, 184, 135, 0.25);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.partner-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 12px 25px rgba(160, 82, 45, 0.2);
|
||||
border-color: var(--brown);
|
||||
}
|
||||
|
||||
.partner-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 3px solid var(--light-brown);
|
||||
padding-bottom: 18px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.partner-name-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.partner-type {
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
color: var(--brown);
|
||||
background: linear-gradient(135deg, var(--antique-white) 0%, #F5DEB3 100%);
|
||||
padding: 8px 18px;
|
||||
border-radius: 25px;
|
||||
border: 2px solid var(--light-brown);
|
||||
}
|
||||
|
||||
.partner-name {
|
||||
font-size: 1.6em;
|
||||
font-weight: 700;
|
||||
color: var(--dark-brown);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.divider {
|
||||
color: var(--light-brown);
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.discount-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.discount-badge {
|
||||
font-size: 1.2em;
|
||||
padding: 10px 20px;
|
||||
border-radius: 30px;
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 3px 10px rgba(160, 82, 45, 0.3);
|
||||
}
|
||||
|
||||
.sales-amount {
|
||||
font-size: 1.1em;
|
||||
color: var(--brown);
|
||||
font-weight: 600;
|
||||
background-color: var(--antique-white);
|
||||
padding: 8px 15px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.partner-info-row {
|
||||
margin-bottom: 0;
|
||||
padding: 8px 0;
|
||||
line-height: 1.5;
|
||||
border-bottom: 1px dashed var(--light-brown);
|
||||
}
|
||||
|
||||
.partner-info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.partner-label {
|
||||
font-weight: 700;
|
||||
color: var(--brown);
|
||||
min-width: 140px;
|
||||
display: inline-block;
|
||||
font-size: 1.05em;
|
||||
}
|
||||
|
||||
.partner-value {
|
||||
font-size: 1.05em;
|
||||
color: var(--text-dark);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.partner-buttons-container {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: flex-start;
|
||||
margin-top: 25px;
|
||||
padding-top: 20px;
|
||||
border-top: 3px solid var(--light-brown);
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
padding: 12px 25px;
|
||||
font-size: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 200px;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 700;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
letter-spacing: 0.5px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: linear-gradient(135deg, var(--light-brown) 0%, var(--brown) 100%);
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%);
|
||||
}
|
||||
|
||||
.btn-history {
|
||||
background: linear-gradient(135deg, #8B7355 0%, var(--dark-brown) 100%);
|
||||
}
|
||||
|
||||
.btn-action:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 20px rgba(160, 82, 45, 0.25);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-new-partner {
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 14px 28px;
|
||||
font-weight: 700;
|
||||
border-radius: 10px;
|
||||
font-size: 1.1em;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn-new-partner:hover {
|
||||
background: linear-gradient(135deg, var(--dark-brown) 0%, var(--brown) 100%);
|
||||
color: white;
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 20px rgba(160, 82, 45, 0.3);
|
||||
}
|
||||
|
||||
.rating-badge {
|
||||
font-size: 1em;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(135deg, var(--light-brown) 0%, var(--brown) 100%);
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
background: linear-gradient(135deg, var(--antique-white) 0%, #F5DEB3 100%);
|
||||
border-left: 6px solid var(--brown);
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.volume-info {
|
||||
background: linear-gradient(135deg, var(--antique-white) 0%, #F5DEB3 100%);
|
||||
padding: 12px 20px;
|
||||
border-radius: 10px;
|
||||
margin-top: 15px;
|
||||
border: 2px solid var(--light-brown);
|
||||
}
|
||||
|
||||
.volume-label {
|
||||
font-weight: 700;
|
||||
color: var(--brown);
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.volume-value {
|
||||
color: var(--dark-brown);
|
||||
font-weight: 700;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
/* Стили для страницы истории */
|
||||
.table-responsive {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 3px solid var(--light-brown);
|
||||
box-shadow: 0 6px 20px rgba(222, 184, 135, 0.3);
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%);
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
font-size: 1.1em;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: 14px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
border-bottom: 2px solid var(--light-brown);
|
||||
font-size: 1.05em;
|
||||
}
|
||||
|
||||
.table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: rgba(222, 184, 135, 0.08);
|
||||
}
|
||||
|
||||
.table-striped tbody tr:hover {
|
||||
background-color: rgba(160, 82, 45, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.table tfoot {
|
||||
background: linear-gradient(135deg, var(--antique-white) 0%, #F5DEB3 100%);
|
||||
font-weight: 700;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.table tfoot td {
|
||||
border-top: 3px solid var(--brown);
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.text-end {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
/* Стили для иконок */
|
||||
i {
|
||||
color: var(--brown);
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.btn-action i, .btn-new-partner i {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Стили для блока с расчетом стоимости */
|
||||
.calculation-block {
|
||||
background: linear-gradient(135deg, var(--antique-white) 0%, #F5DEB3 100%);
|
||||
border: 3px solid var(--light-brown);
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 6px 20px rgba(222, 184, 135, 0.25);
|
||||
}
|
||||
|
||||
.calculation-title {
|
||||
color: var(--brown);
|
||||
font-weight: 700;
|
||||
border-bottom: 3px solid var(--brown);
|
||||
padding-bottom: 15px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.calculation-result {
|
||||
background-color: white;
|
||||
border: 2px solid var(--light-brown);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px dashed var(--light-brown);
|
||||
}
|
||||
|
||||
.result-item:last-child {
|
||||
border-bottom: none;
|
||||
font-weight: 700;
|
||||
font-size: 1.2em;
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
/* Стили для фильтров и сортировки - ВАЖНО! */
|
||||
.filter-section {
|
||||
background: white !important;
|
||||
border: 2px solid var(--light-brown) !important;
|
||||
border-radius: 12px !important;
|
||||
padding: 20px !important;
|
||||
margin-bottom: 25px !important;
|
||||
box-shadow: 0 2px 8px rgba(160, 82, 45, 0.1) !important;
|
||||
}
|
||||
|
||||
.filter-title {
|
||||
color: var(--brown) !important;
|
||||
font-weight: 700 !important;
|
||||
margin-bottom: 15px !important;
|
||||
border-bottom: 2px solid var(--light-brown) !important;
|
||||
padding-bottom: 10px !important;
|
||||
}
|
||||
|
||||
.form-control, .form-select {
|
||||
border: 2px solid var(--light-brown) !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 10px 15px !important;
|
||||
font-size: 1em !important;
|
||||
transition: all 0.3s ease !important;
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
.form-control:focus, .form-select:focus {
|
||||
border-color: var(--brown) !important;
|
||||
box-shadow: 0 0 0 0.25rem rgba(160, 82, 45, 0.25) !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
color: var(--brown) !important;
|
||||
font-weight: 600 !important;
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn-primary {
|
||||
background: var(--brown) !important;
|
||||
color: white !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--dark-brown) !important;
|
||||
transform: translateY(-1px) !important;
|
||||
box-shadow: 0 4px 8px rgba(160, 82, 45, 0.2) !important;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--antique-white) !important;
|
||||
color: var(--brown) !important;
|
||||
border: 1px solid var(--light-brown) !important;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--light-brown) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.table-responsive {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.table th, .table td {
|
||||
padding: 10px 6px;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
min-width: 150px;
|
||||
padding: 10px 15px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.partner-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Анимации */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.partner-card, .table-responsive, .calculation-block, .filter-section {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
37
static/js/script.js
Normal file
37
static/js/script.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Кнопка нового партнера
|
||||
const newPartnerBtn = document.querySelector('.btn-new-partner');
|
||||
if (newPartnerBtn) {
|
||||
newPartnerBtn.addEventListener('click', function() {
|
||||
// Переход на страницу добавления партнера
|
||||
window.location.href = '/add_partner';
|
||||
});
|
||||
}
|
||||
|
||||
// Кнопка "Редактирование"
|
||||
document.querySelectorAll('.btn-edit').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const partnerId = this.getAttribute('data-partner-id');
|
||||
// Переход на страницу редактирования
|
||||
window.location.href = `/edit_partner/${partnerId}`;
|
||||
});
|
||||
});
|
||||
|
||||
// Кнопка "Добавление продажи"
|
||||
document.querySelectorAll('.btn-add').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const partnerId = this.getAttribute('data-partner-id');
|
||||
// Переход на страницу добавления продажи
|
||||
window.location.href = `/add_sale/${partnerId}`;
|
||||
});
|
||||
});
|
||||
|
||||
// Кнопка "История реализации"
|
||||
document.querySelectorAll('.btn-history').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const partnerId = this.getAttribute('data-partner-id');
|
||||
// Переход на страницу истории реализации
|
||||
window.location.href = `/sales_history/${partnerId}`;
|
||||
});
|
||||
});
|
||||
});
|
||||
18
templates/about.html
Normal file
18
templates/about.html
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>About Flask</title>
|
||||
</head>
|
||||
<body>
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
|
||||
<h1> About Flask </h1>
|
||||
<p> Flask is a micro web framework written in Python.</p>
|
||||
<p> Applications that use the Flask framework include Pinterest,
|
||||
LinkedIn, and the community web page for Flask itself.</p>
|
||||
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
521
templates/add_client.html
Normal file
521
templates/add_client.html
Normal file
|
|
@ -0,0 +1,521 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Добавление поставщика{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<!-- Хлебные крошки -->
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('index') }}">Поставщики</a></li>
|
||||
<li class="breadcrumb-item active">Добавление поставщика</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="form-card">
|
||||
<div class="form-card-header">
|
||||
<h2 class="form-card-title">
|
||||
<i class="fas fa-truck"></i> Добавление нового поставщика
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="form-card-body">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show">
|
||||
<i class="fas fa-{{ 'exclamation-circle' if category == 'error' else 'check-circle' }} me-2"></i>
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" id="supplierForm">
|
||||
<div class="row">
|
||||
<!-- Левая колонка - Основная информация -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="section-title">
|
||||
<i class="fas fa-info-circle"></i> Основная информация
|
||||
</h5>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name" class="form-label">
|
||||
<i class="fas fa-building"></i>
|
||||
Название компании
|
||||
</label>
|
||||
<input type="text" class="form-control" id="name" name="name"
|
||||
required maxlength="255" placeholder="ООО 'СтройМатериалы'"
|
||||
onkeypress="return blockNumbers(event)" onpaste="return blockNumbersPaste(event)">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="director" class="form-label">
|
||||
<i class="fas fa-user-tie"></i>
|
||||
Директор (ФИО)
|
||||
</label>
|
||||
<input type="text" class="form-control" id="director" name="director"
|
||||
maxlength="255" placeholder="Петров Петр Петрович"
|
||||
onkeypress="return blockNumbers(event)" onpaste="return blockNumbersPaste(event)">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="contractor_type_id" class="form-label">
|
||||
<i class="fas fa-tag"></i>
|
||||
Тип поставщика
|
||||
</label>
|
||||
<select class="form-select" id="contractor_type_id" name="contractor_type_id" required>
|
||||
<option value="" selected disabled>Выберите тип поставщика</option>
|
||||
{% for type in contractor_types %}
|
||||
<option value="{{ type.id }}">{{ type.type_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="rating" class="form-label">
|
||||
<i class="fas fa-star"></i>
|
||||
Рейтинг надежности
|
||||
</label>
|
||||
<input type="number" class="form-control" id="rating" name="rating"
|
||||
min="0" max="5" step="0.1" value="5.0">
|
||||
<div class="form-text">От 0 до 5 (чем выше, тем надежнее)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Правая колонка - Контактные данные -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="section-title">
|
||||
<i class="fas fa-address-card"></i> Контактные данные
|
||||
</h5>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email" class="form-label">
|
||||
<i class="fas fa-envelope"></i>
|
||||
Email
|
||||
</label>
|
||||
<input type="email" class="form-control" id="email" name="email"
|
||||
required maxlength="255" placeholder="info@stroymaterial.ru">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="phone" class="form-label">
|
||||
<i class="fas fa-phone"></i>
|
||||
Телефон
|
||||
</label>
|
||||
<input type="tel" class="form-control" id="phone" name="phone"
|
||||
maxlength="50" placeholder="+7 (495) 123-45-67"
|
||||
onkeypress="return allowPhoneNumbers(event)">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="inn" class="form-label">
|
||||
<i class="fas fa-id-card"></i>
|
||||
ИНН
|
||||
</label>
|
||||
<input type="text" class="form-control" id="inn" name="inn"
|
||||
maxlength="12" placeholder="1234567890"
|
||||
onkeypress="return allowOnlyNumbers(event)" onpaste="return allowNumbersPaste(event)">
|
||||
<div class="form-text">10 или 12 цифр</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Адрес на всю ширину -->
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
<label for="address" class="form-label">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
Юридический адрес
|
||||
</label>
|
||||
<textarea class="form-control" id="address" name="address"
|
||||
rows="2" maxlength="500" placeholder="г. Москва, ул. Строителей, д. 1"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки -->
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('index') }}" class="btn btn-outline">
|
||||
<i class="fas fa-times"></i> Отмена
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Добавить поставщика
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* ===== ОСНОВНЫЕ ПЕРЕМЕННЫЕ ===== */
|
||||
:root {
|
||||
--antique-white: #FAEBD7;
|
||||
--light-brown: #DEB887;
|
||||
--brown: #A0522D;
|
||||
--dark-brown: #8B4513;
|
||||
}
|
||||
|
||||
/* ===== ХЛЕБНЫЕ КРОШКИ ===== */
|
||||
.breadcrumb {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.breadcrumb-item a {
|
||||
color: var(--brown);
|
||||
text-decoration: none;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.breadcrumb-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
color: #6c757d;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
color: var(--light-brown);
|
||||
}
|
||||
|
||||
/* ===== КАРТОЧКА ФОРМЫ ===== */
|
||||
.form-card {
|
||||
background: white;
|
||||
border: 1px solid var(--light-brown);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(160, 82, 45, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-card-header {
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%);
|
||||
padding: 20px 25px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-card-title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-card-title i {
|
||||
margin-right: 10px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-card-body {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
/* ===== ЗАГОЛОВКИ СЕКЦИЙ ===== */
|
||||
.section-title {
|
||||
color: var(--brown);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid var(--light-brown);
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.section-title i {
|
||||
color: var(--brown);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* ===== ЭЛЕМЕНТЫ ФОРМЫ ===== */
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--brown);
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 6px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.form-label i {
|
||||
color: var(--brown);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-control, .form-select {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
padding: 0 12px;
|
||||
border: 2px solid var(--light-brown);
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
transition: all 0.2s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.form-control:focus, .form-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--brown);
|
||||
box-shadow: 0 0 0 3px rgba(160, 82, 45, 0.1);
|
||||
}
|
||||
|
||||
.form-control:hover, .form-select:hover {
|
||||
border-color: var(--brown);
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
height: auto;
|
||||
padding: 10px 12px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
margin-top: 4px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
/* ===== КНОПКИ ===== */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 25px;
|
||||
padding-top: 20px;
|
||||
border-top: 2px solid var(--light-brown);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 25px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--brown);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--dark-brown);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(160, 82, 45, 0.3);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: white;
|
||||
border: 2px solid var(--light-brown);
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--antique-white);
|
||||
border-color: var(--brown);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* ===== АЛЕРТЫ ===== */
|
||||
.alert {
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--light-brown);
|
||||
font-family: 'Verdana', sans-serif;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: var(--antique-white);
|
||||
color: var(--brown);
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: var(--antique-white);
|
||||
color: #721c24;
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
|
||||
/* ===== ИКОНКИ ===== */
|
||||
i {
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.btn-primary i, .btn-outline i {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ===== АДАПТИВНОСТЬ ===== */
|
||||
@media (max-width: 768px) {
|
||||
.form-card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column-reverse;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.form-actions .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.col-md-6 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.section-title:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Запрет на ввод цифр в текстовые поля
|
||||
function blockNumbers(event) {
|
||||
const key = event.key;
|
||||
if (key === ' ' || key === 'Backspace' || key === 'Delete' || key === 'Tab' ||
|
||||
key === 'Enter' || key === 'ArrowLeft' || key === 'ArrowRight' ||
|
||||
key === 'ArrowUp' || key === 'ArrowDown' || key === 'Home' || key === 'End') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key >= '0' && key <= '9') {
|
||||
event.preventDefault();
|
||||
alert('Цифры запрещены в этом поле');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function blockNumbersPaste(event) {
|
||||
event.preventDefault();
|
||||
const pastedText = (event.clipboardData || window.clipboardData).getData('text');
|
||||
const cleanedText = pastedText.replace(/[0-9]/g, '');
|
||||
|
||||
const input = event.target;
|
||||
const start = input.selectionStart;
|
||||
const end = input.selectionEnd;
|
||||
const currentValue = input.value;
|
||||
const newValue = currentValue.substring(0, start) + cleanedText + currentValue.substring(end);
|
||||
input.value = newValue;
|
||||
|
||||
if (pastedText.match(/[0-9]/)) {
|
||||
alert('Цифры были удалены из вставленного текста');
|
||||
}
|
||||
}
|
||||
|
||||
// Только цифры для ИНН
|
||||
function allowOnlyNumbers(event) {
|
||||
const key = event.key;
|
||||
if (key === 'Backspace' || key === 'Delete' || key === 'Tab' ||
|
||||
key === 'Enter' || key === 'ArrowLeft' || key === 'ArrowRight' ||
|
||||
key === 'ArrowUp' || key === 'ArrowDown' || key === 'Home' || key === 'End') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key >= '0' && key <= '9') {
|
||||
return true;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
function allowNumbersPaste(event) {
|
||||
event.preventDefault();
|
||||
const pastedText = (event.clipboardData || window.clipboardData).getData('text');
|
||||
const cleanedText = pastedText.replace(/[^0-9]/g, '');
|
||||
|
||||
const input = event.target;
|
||||
const start = input.selectionStart;
|
||||
const end = input.selectionEnd;
|
||||
const currentValue = input.value;
|
||||
const newValue = currentValue.substring(0, start) + cleanedText + currentValue.substring(end);
|
||||
input.value = newValue;
|
||||
}
|
||||
|
||||
// Для телефона
|
||||
function allowPhoneNumbers(event) {
|
||||
const key = event.key;
|
||||
const allowedKeys = ['+', '-', '(', ')', ' '];
|
||||
|
||||
if (key === 'Backspace' || key === 'Delete' || key === 'Tab' ||
|
||||
key === 'Enter' || key === 'ArrowLeft' || key === 'ArrowRight' ||
|
||||
key === 'ArrowUp' || key === 'ArrowDown' || key === 'Home' || key === 'End') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (allowedKeys.includes(key)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key >= '0' && key <= '9') {
|
||||
return true;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Валидация формы
|
||||
document.getElementById('supplierForm').addEventListener('submit', function(e) {
|
||||
const inn = document.getElementById('inn').value;
|
||||
const name = document.getElementById('name').value;
|
||||
|
||||
if (name.match(/[0-9]/)) {
|
||||
e.preventDefault();
|
||||
alert('Название компании не должно содержать цифры');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (inn && !/^\d{10,12}$/.test(inn)) {
|
||||
e.preventDefault();
|
||||
alert('ИНН должен содержать 10 или 12 цифр');
|
||||
return false;
|
||||
}
|
||||
|
||||
const rating = parseFloat(document.getElementById('rating').value);
|
||||
if (rating < 0 || rating > 5) {
|
||||
e.preventDefault();
|
||||
alert('Рейтинг должен быть от 0 до 5');
|
||||
return false;
|
||||
}
|
||||
|
||||
const email = document.getElementById('email').value;
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
e.preventDefault();
|
||||
alert('Введите корректный email адрес');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
737
templates/add_order.html
Normal file
737
templates/add_order.html
Normal file
|
|
@ -0,0 +1,737 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Добавление заказа{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<!-- Хлебные крошки -->
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('index') }}">Поставщики</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('order_history', client_id=client.id) }}">{{ client.name }}</a></li>
|
||||
<li class="breadcrumb-item active">Добавление заказа</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="form-card">
|
||||
<div class="form-card-header">
|
||||
<h2 class="form-card-title">
|
||||
<i class="fas fa-cart-plus"></i> Добавление заказа
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="form-card-body">
|
||||
<!-- Информация о поставщике -->
|
||||
<div class="supplier-info mb-4">
|
||||
<div class="supplier-info-header">
|
||||
<i class="fas fa-truck"></i> Информация о поставщике
|
||||
</div>
|
||||
<div class="supplier-info-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Поставщик:</span>
|
||||
<span class="info-value">{{ client.name }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Тип:</span>
|
||||
<span class="info-value">{{ client.contractor_type_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Email:</span>
|
||||
<span class="info-value">{{ client.email }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Телефон:</span>
|
||||
<span class="info-value">{{ client.phone or 'Не указан' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" id="orderForm">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="product_id" class="form-label">
|
||||
<i class="fas fa-cube"></i>
|
||||
Материал
|
||||
</label>
|
||||
<div class="select-wrapper">
|
||||
<select class="form-select modern-select" id="product_id" name="product_id" required>
|
||||
<option value="" selected disabled>Выберите материал</option>
|
||||
{% for product in products %}
|
||||
<option value="{{ product.id }}"
|
||||
data-price="{{ product.min_price }}"
|
||||
data-name="{{ product.product_name }}"
|
||||
data-stock="{{ product.quantity_in_stock or 0 }}">
|
||||
{{ product.product_name }} ({{ product.article }}) - {{ product.min_price }} руб. (в наличии: {{ product.quantity_in_stock or 0 }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<i class="fas fa-chevron-down select-arrow"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="quantity" class="form-label">
|
||||
<i class="fas fa-cubes"></i>
|
||||
Количество
|
||||
</label>
|
||||
<input type="number" class="form-control modern-input" id="quantity" name="quantity"
|
||||
min="1" max="0" required value="1">
|
||||
<div class="form-text" id="maxQuantityHint">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Максимально доступно: <span id="maxAvailable">0</span> ед.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="order_date" class="form-label">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
Дата заказа
|
||||
</label>
|
||||
<input type="date" class="form-control modern-input" id="order_date" name="order_date"
|
||||
required value="{{ today }}">
|
||||
</div>
|
||||
|
||||
<!-- Скрытое поле для статуса (всегда "Новый") -->
|
||||
<input type="hidden" name="status" value="Новый">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Информация о выбранном товаре -->
|
||||
<div class="material-info-block mt-3">
|
||||
<div class="material-info-row">
|
||||
<div class="material-info-item">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span class="material-info-label">Материал:</span>
|
||||
<span class="material-info-value" id="selectedMaterial">Не выбран</span>
|
||||
</div>
|
||||
<div class="material-info-item text-end">
|
||||
<i class="fas fa-warehouse"></i>
|
||||
<span class="material-info-label">На складе:</span>
|
||||
<span class="material-info-value" id="maxOrder">0</span> ед.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Автоматический расчет суммы со скидкой -->
|
||||
<div class="calculation-block mt-4">
|
||||
<div class="calculation-header">
|
||||
<i class="fas fa-calculator"></i> Расчет стоимости
|
||||
</div>
|
||||
<div class="calculation-body">
|
||||
<div class="calculation-row">
|
||||
<span class="calculation-label">Количество:</span>
|
||||
<span class="calculation-value"><span id="displayQuantity">1</span> ед.</span>
|
||||
</div>
|
||||
<div class="calculation-row">
|
||||
<span class="calculation-label">Цена за единицу:</span>
|
||||
<span class="calculation-value"><span id="displayPrice">0</span> руб.</span>
|
||||
</div>
|
||||
<div class="calculation-row">
|
||||
<span class="calculation-label">Базовая сумма:</span>
|
||||
<span class="calculation-value"><span id="baseAmount">0</span> руб.</span>
|
||||
</div>
|
||||
<div class="calculation-row">
|
||||
<span class="calculation-label">Транспортные расходы (10%):</span>
|
||||
<span class="calculation-value"><span id="transportAmount">0</span> руб.</span>
|
||||
</div>
|
||||
<div class="calculation-row total-row">
|
||||
<span class="calculation-label">
|
||||
<i class="fas fa-percent"></i>
|
||||
Итоговая сумма <span id="discountPercentLabel"></span>:
|
||||
</span>
|
||||
<span class="calculation-value total-value">
|
||||
<span id="totalAmount">0.00</span> руб.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки -->
|
||||
<div class="form-actions">
|
||||
<a href="javascript:history.back()" class="btn btn-outline">
|
||||
<i class="fas fa-times"></i> Отмена
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<i class="fas fa-save"></i> Создать заказ
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* ===== ОСНОВНЫЕ ПЕРЕМЕННЫЕ ===== */
|
||||
:root {
|
||||
--antique-white: #FAEBD7;
|
||||
--light-brown: #DEB887;
|
||||
--brown: #A0522D;
|
||||
--dark-brown: #8B4513;
|
||||
}
|
||||
|
||||
/* ===== ХЛЕБНЫЕ КРОШКИ ===== */
|
||||
.breadcrumb {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.breadcrumb-item a {
|
||||
color: var(--brown);
|
||||
text-decoration: none;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.breadcrumb-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
color: #6c757d;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
color: var(--light-brown);
|
||||
}
|
||||
|
||||
/* ===== КАРТОЧКА ФОРМЫ ===== */
|
||||
.form-card {
|
||||
background: white;
|
||||
border: 1px solid var(--light-brown);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(160, 82, 45, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-card-header {
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%);
|
||||
padding: 20px 25px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-card-title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-card-title i {
|
||||
margin-right: 10px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-card-body {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
/* ===== ИНФОРМАЦИЯ О ПОСТАВЩИКЕ ===== */
|
||||
.supplier-info {
|
||||
border: 1px solid var(--light-brown);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 25px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.supplier-info-header {
|
||||
background: var(--antique-white);
|
||||
padding: 12px 20px;
|
||||
font-weight: 600;
|
||||
color: var(--brown);
|
||||
font-family: 'Verdana', sans-serif;
|
||||
border-bottom: 1px solid var(--light-brown);
|
||||
}
|
||||
|
||||
.supplier-info-header i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.supplier-info-body {
|
||||
padding: 15px 20px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
margin-bottom: 8px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #2c3e50;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== ЭЛЕМЕНТЫ ФОРМЫ ===== */
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--brown);
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 6px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.form-label i {
|
||||
color: var(--brown);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.modern-input {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
padding: 0 12px;
|
||||
border: 2px solid var(--light-brown);
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
transition: all 0.2s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.modern-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--brown);
|
||||
box-shadow: 0 0 0 3px rgba(160, 82, 45, 0.1);
|
||||
}
|
||||
|
||||
.modern-input:hover {
|
||||
border-color: var(--brown);
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modern-select {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 42px;
|
||||
padding: 8px 35px 8px 12px;
|
||||
border: 2px solid var(--light-brown);
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
transition: all 0.2s ease;
|
||||
line-height: 1.4;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.modern-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--brown);
|
||||
box-shadow: 0 0 0 3px rgba(160, 82, 45, 0.1);
|
||||
}
|
||||
|
||||
.modern-select:hover {
|
||||
border-color: var(--brown);
|
||||
}
|
||||
|
||||
.select-arrow {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--brown);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
margin-top: 4px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.form-text i {
|
||||
color: var(--brown);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* ===== ИНФОРМАЦИЯ О МАТЕРИАЛЕ ===== */
|
||||
.material-info-block {
|
||||
border: 1px solid var(--light-brown);
|
||||
border-radius: 10px;
|
||||
background: var(--antique-white);
|
||||
padding: 12px 15px;
|
||||
}
|
||||
|
||||
.material-info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.material-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.material-info-item i {
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.material-info-label {
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.material-info-value {
|
||||
color: var(--brown);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.material-info-item.text-end {
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* ===== БЛОК РАСЧЕТА ===== */
|
||||
.calculation-block {
|
||||
border: 1px solid var(--light-brown);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calculation-header {
|
||||
background: var(--antique-white);
|
||||
padding: 12px 20px;
|
||||
font-weight: 600;
|
||||
color: var(--brown);
|
||||
font-family: 'Verdana', sans-serif;
|
||||
border-bottom: 1px solid var(--light-brown);
|
||||
}
|
||||
|
||||
.calculation-header i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.calculation-body {
|
||||
padding: 15px 20px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.calculation-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px dashed var(--light-brown);
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.calculation-row:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.calculation-row.total-row {
|
||||
margin-top: 5px;
|
||||
padding-top: 12px;
|
||||
border-top: 2px solid var(--light-brown);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.calculation-label {
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.calculation-value {
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.total-value {
|
||||
color: var(--brown) !important;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
#discountPercentLabel {
|
||||
color: var(--brown);
|
||||
font-size: 0.9rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* ===== КНОПКИ ===== */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 25px;
|
||||
padding-top: 20px;
|
||||
border-top: 2px solid var(--light-brown);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 25px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--brown);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--dark-brown);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(160, 82, 45, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background: #6c757d;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: white;
|
||||
border: 2px solid var(--light-brown);
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--antique-white);
|
||||
border-color: var(--brown);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* ===== АДАПТИВНОСТЬ ===== */
|
||||
@media (max-width: 768px) {
|
||||
.form-card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column-reverse;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.form-actions .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.material-info-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.material-info-item.text-end {
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.calculation-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.calculation-label, .calculation-value {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Script loaded!');
|
||||
|
||||
const productSelect = document.getElementById('product_id');
|
||||
const quantityInput = document.getElementById('quantity');
|
||||
const displayQuantitySpan = document.getElementById('displayQuantity');
|
||||
const displayPriceSpan = document.getElementById('displayPrice');
|
||||
const baseAmountSpan = document.getElementById('baseAmount');
|
||||
const transportAmountSpan = document.getElementById('transportAmount');
|
||||
const totalAmountSpan = document.getElementById('totalAmount');
|
||||
const discountPercentLabel = document.getElementById('discountPercentLabel');
|
||||
const selectedMaterialSpan = document.getElementById('selectedMaterial');
|
||||
const maxAvailableSpan = document.getElementById('maxAvailable');
|
||||
const maxOrderSpan = document.getElementById('maxOrder');
|
||||
const maxQuantityHint = document.getElementById('maxQuantityHint');
|
||||
|
||||
// Устанавливаем сегодняшнюю дату по умолчанию
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('order_date').value = today;
|
||||
|
||||
// Обновление информации о товаре и материале
|
||||
productSelect.addEventListener('change', function() {
|
||||
console.log('Product changed');
|
||||
const selectedOption = this.options[this.selectedIndex];
|
||||
const price = selectedOption.getAttribute('data-price');
|
||||
const stock = parseInt(selectedOption.getAttribute('data-stock')) || 0;
|
||||
const productName = selectedOption.getAttribute('data-name');
|
||||
|
||||
console.log('Price:', price);
|
||||
console.log('Stock:', stock);
|
||||
|
||||
if (price) {
|
||||
displayPriceSpan.textContent = parseFloat(price).toFixed(2);
|
||||
selectedMaterialSpan.textContent = productName;
|
||||
maxAvailableSpan.textContent = stock;
|
||||
maxOrderSpan.textContent = stock;
|
||||
|
||||
// Устанавливаем максимальное значение для поля ввода
|
||||
if (stock > 0) {
|
||||
quantityInput.max = stock;
|
||||
quantityInput.value = 1;
|
||||
maxQuantityHint.style.display = 'block';
|
||||
} else {
|
||||
quantityInput.max = 0;
|
||||
quantityInput.value = 0;
|
||||
maxQuantityHint.style.display = 'block';
|
||||
}
|
||||
|
||||
calculateTotal();
|
||||
} else {
|
||||
selectedMaterialSpan.textContent = 'Не выбран';
|
||||
maxAvailableSpan.textContent = '0';
|
||||
maxOrderSpan.textContent = '0';
|
||||
quantityInput.removeAttribute('max');
|
||||
maxQuantityHint.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Расчет общей суммы со скидкой
|
||||
function calculateTotal() {
|
||||
const quantity = parseInt(quantityInput.value) || 0;
|
||||
const price = parseFloat(displayPriceSpan.textContent) || 0;
|
||||
const baseAmount = quantity * price;
|
||||
const transportAmount = baseAmount * 0.1; // 10% транспортные расходы
|
||||
const amountWithTransport = baseAmount + transportAmount;
|
||||
|
||||
// Расчет скидки в зависимости от суммы
|
||||
let discountPercent = 0;
|
||||
let discountLabel = '';
|
||||
|
||||
if (amountWithTransport >= 500000) {
|
||||
discountPercent = 20;
|
||||
discountLabel = 'со скидкой 20%';
|
||||
} else if (amountWithTransport >= 300000) {
|
||||
discountPercent = 15;
|
||||
discountLabel = 'со скидкой 15%';
|
||||
} else if (amountWithTransport >= 150000) {
|
||||
discountPercent = 10;
|
||||
discountLabel = 'со скидкой 10%';
|
||||
}
|
||||
|
||||
const discountAmount = amountWithTransport * discountPercent / 100;
|
||||
const total = amountWithTransport - discountAmount;
|
||||
|
||||
displayQuantitySpan.textContent = quantity;
|
||||
displayPriceSpan.textContent = price.toFixed(2);
|
||||
baseAmountSpan.textContent = baseAmount.toFixed(2);
|
||||
transportAmountSpan.textContent = transportAmount.toFixed(2);
|
||||
totalAmountSpan.textContent = total.toFixed(2);
|
||||
|
||||
// Обновляем текст с информацией о скидке
|
||||
if (discountPercent > 0) {
|
||||
discountPercentLabel.textContent = discountLabel;
|
||||
} else {
|
||||
discountPercentLabel.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Слушатели изменений
|
||||
quantityInput.addEventListener('input', function() {
|
||||
const max = parseInt(this.max);
|
||||
const value = parseInt(this.value);
|
||||
|
||||
if (max && value > max) {
|
||||
this.value = max;
|
||||
alert(`Максимально доступное количество: ${max} ед.`);
|
||||
}
|
||||
|
||||
if (value < 1) {
|
||||
this.value = 1;
|
||||
}
|
||||
|
||||
calculateTotal();
|
||||
});
|
||||
|
||||
productSelect.addEventListener('change', calculateTotal);
|
||||
|
||||
// Проверка перед отправкой формы
|
||||
document.getElementById('orderForm').addEventListener('submit', function(e) {
|
||||
const productId = document.getElementById('product_id').value;
|
||||
const quantity = parseInt(quantityInput.value) || 0;
|
||||
const maxStock = parseInt(maxOrderSpan.textContent) || 0;
|
||||
|
||||
if (!productId) {
|
||||
e.preventDefault();
|
||||
alert('Пожалуйста, выберите материал');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (quantity <= 0) {
|
||||
e.preventDefault();
|
||||
alert('Количество должно быть больше 0');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (quantity > maxStock) {
|
||||
e.preventDefault();
|
||||
alert(`Невозможно создать заказ!\nНедостаточно материалов на складе.\nДоступно: ${maxStock} ед., запрошено: ${quantity} ед.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (maxStock === 0) {
|
||||
e.preventDefault();
|
||||
alert(`Невозможно создать заказ!\nМатериал отсутствует на складе.`);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Инициализация расчета
|
||||
calculateTotal();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
458
templates/add_product.html
Normal file
458
templates/add_product.html
Normal file
|
|
@ -0,0 +1,458 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Добавление материала{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<!-- Хлебные крошки -->
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('catalog') }}">Каталог материалов</a></li>
|
||||
<li class="breadcrumb-item active">Добавление материала</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="form-card">
|
||||
<div class="form-card-header">
|
||||
<h2 class="form-card-title">
|
||||
<i class="fas fa-cube"></i> Добавление нового материала
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="form-card-body">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show">
|
||||
<i class="fas fa-{{ 'exclamation-circle' if category == 'error' else 'check-circle' }} me-2"></i>
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" id="materialForm">
|
||||
<div class="row">
|
||||
<!-- Левая колонка -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="section-title">
|
||||
<i class="fas fa-info-circle"></i> Основная информация
|
||||
</h5>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="product_name" class="form-label">
|
||||
<i class="fas fa-tag"></i>
|
||||
Название материала
|
||||
</label>
|
||||
<input type="text" class="form-control modern-input" id="product_name" name="product_name"
|
||||
required maxlength="255" placeholder="Например: Паркетная доска">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="article" class="form-label">
|
||||
<i class="fas fa-barcode"></i>
|
||||
Артикул
|
||||
</label>
|
||||
<input type="text" class="form-control modern-input" id="article" name="article"
|
||||
required maxlength="50" placeholder="Например: ПРК-001">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="category_id" class="form-label">
|
||||
<i class="fas fa-folder"></i>
|
||||
Категория
|
||||
</label>
|
||||
<div class="select-wrapper">
|
||||
<select class="form-select modern-select" id="category_id" name="category_id" required>
|
||||
<option value="" selected disabled>Выберите категорию</option>
|
||||
{% for category in categories %}
|
||||
<option value="{{ category.id }}">{{ category.category_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<i class="fas fa-chevron-down select-arrow"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Правая колонка -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="section-title">
|
||||
<i class="fas fa-cog"></i> Характеристики
|
||||
</h5>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="min_price" class="form-label">
|
||||
<i class="fas fa-ruble-sign"></i>
|
||||
Цена
|
||||
</label>
|
||||
<input type="number" class="form-control modern-input" id="min_price" name="min_price"
|
||||
required min="0" step="0.01" value="0.00" placeholder="0.00">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="material_id" class="form-label">
|
||||
<i class="fas fa-cube"></i>
|
||||
Тип материала
|
||||
</label>
|
||||
<div class="select-wrapper">
|
||||
<select class="form-select modern-select" id="material_id" name="material_id">
|
||||
<option value="" selected>Не выбран</option>
|
||||
{% for material in materials %}
|
||||
<option value="{{ material.id }}">{{ material.material_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<i class="fas fa-chevron-down select-arrow"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="quantity_in_stock" class="form-label">
|
||||
<i class="fas fa-warehouse"></i>
|
||||
Наличие на складе
|
||||
</label>
|
||||
<input type="number" class="form-control modern-input" id="quantity_in_stock" name="quantity_in_stock"
|
||||
min="0" value="0" placeholder="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Описание на всю ширину -->
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
<label for="description" class="form-label">
|
||||
<i class="fas fa-align-left"></i>
|
||||
Описание материала
|
||||
</label>
|
||||
<textarea class="form-control modern-input" id="description" name="description"
|
||||
rows="4" maxlength="1000" placeholder="Подробное описание материала..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки -->
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('catalog') }}" class="btn btn-outline">
|
||||
<i class="fas fa-times"></i> Отмена
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Добавить материал
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* ===== ОСНОВНЫЕ ПЕРЕМЕННЫЕ ===== */
|
||||
:root {
|
||||
--antique-white: #FAEBD7;
|
||||
--light-brown: #DEB887;
|
||||
--brown: #A0522D;
|
||||
--dark-brown: #8B4513;
|
||||
}
|
||||
|
||||
/* ===== ХЛЕБНЫЕ КРОШКИ ===== */
|
||||
.breadcrumb {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.breadcrumb-item a {
|
||||
color: var(--brown);
|
||||
text-decoration: none;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.breadcrumb-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
color: #6c757d;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
color: var(--light-brown);
|
||||
}
|
||||
|
||||
/* ===== КАРТОЧКА ФОРМЫ ===== */
|
||||
.form-card {
|
||||
background: white;
|
||||
border: 1px solid var(--light-brown);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(160, 82, 45, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-card-header {
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%);
|
||||
padding: 20px 25px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-card-title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-card-title i {
|
||||
margin-right: 10px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-card-body {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
/* ===== ЗАГОЛОВКИ СЕКЦИЙ ===== */
|
||||
.section-title {
|
||||
color: var(--brown);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid var(--light-brown);
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.section-title i {
|
||||
color: var(--brown);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* ===== ЭЛЕМЕНТЫ ФОРМЫ ===== */
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--brown);
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 6px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.form-label i {
|
||||
color: var(--brown);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.modern-input {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
padding: 0 12px;
|
||||
border: 2px solid var(--light-brown);
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
transition: all 0.2s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.modern-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--brown);
|
||||
box-shadow: 0 0 0 3px rgba(160, 82, 45, 0.1);
|
||||
}
|
||||
|
||||
.modern-input:hover {
|
||||
border-color: var(--brown);
|
||||
}
|
||||
|
||||
textarea.modern-input {
|
||||
height: auto;
|
||||
padding: 12px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modern-select {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
padding: 0 35px 0 12px;
|
||||
border: 2px solid var(--light-brown);
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.modern-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--brown);
|
||||
box-shadow: 0 0 0 3px rgba(160, 82, 45, 0.1);
|
||||
}
|
||||
|
||||
.modern-select:hover {
|
||||
border-color: var(--brown);
|
||||
}
|
||||
|
||||
.select-arrow {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--brown);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ===== КНОПКИ ===== */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 25px;
|
||||
padding-top: 20px;
|
||||
border-top: 2px solid var(--light-brown);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 25px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--brown);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--dark-brown);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(160, 82, 45, 0.3);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: white;
|
||||
border: 2px solid var(--light-brown);
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--antique-white);
|
||||
border-color: var(--brown);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* ===== АЛЕРТЫ ===== */
|
||||
.alert {
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--light-brown);
|
||||
font-family: 'Verdana', sans-serif;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: var(--antique-white);
|
||||
color: var(--brown);
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: var(--antique-white);
|
||||
color: #721c24;
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
|
||||
/* ===== ИКОНКИ ===== */
|
||||
i {
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.btn-primary i, .btn-outline i, .form-card-header i {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ===== АДАПТИВНОСТЬ ===== */
|
||||
@media (max-width: 768px) {
|
||||
.form-card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column-reverse;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.form-actions .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.col-md-6 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.section-title:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Валидация формы
|
||||
document.getElementById('materialForm').addEventListener('submit', function(e) {
|
||||
const name = document.getElementById('product_name').value;
|
||||
const article = document.getElementById('article').value;
|
||||
const price = parseFloat(document.getElementById('min_price').value);
|
||||
const quantity = parseInt(document.getElementById('quantity_in_stock').value) || 0;
|
||||
|
||||
if (!name.trim()) {
|
||||
e.preventDefault();
|
||||
alert('Введите название материала');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!article.trim()) {
|
||||
e.preventDefault();
|
||||
alert('Введите артикул материала');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (price < 0) {
|
||||
e.preventDefault();
|
||||
alert('Цена не может быть отрицательной');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (quantity < 0) {
|
||||
e.preventDefault();
|
||||
alert('Количество не может быть отрицательным');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
686
templates/base.html
Normal file
686
templates/base.html
Normal file
|
|
@ -0,0 +1,686 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Строительные материалы{% endblock %}</title>
|
||||
|
||||
<!-- Bootstrap 5 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Font Awesome Icons -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Verdana Font -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Verdana:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--antique-white: #FAEBD7;
|
||||
--light-brown: #DEB887;
|
||||
--brown: #A0522D;
|
||||
--dark-brown: #8B4513;
|
||||
}
|
||||
|
||||
/* ПОЛНОЕ ОТКЛЮЧЕНИЕ ВСЕХ ПОДСВЕТОК */
|
||||
*,
|
||||
*:focus,
|
||||
*:hover,
|
||||
*:active,
|
||||
*:visited {
|
||||
outline: 0 !important;
|
||||
outline-style: none !important;
|
||||
outline-width: 0 !important;
|
||||
outline-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important;
|
||||
-webkit-tap-highlight-color: transparent !important;
|
||||
-webkit-focus-ring-color: transparent !important;
|
||||
}
|
||||
|
||||
a:focus,
|
||||
a:hover,
|
||||
a:active,
|
||||
button:focus,
|
||||
button:hover,
|
||||
button:active,
|
||||
.btn:focus,
|
||||
.btn:hover,
|
||||
.btn:active,
|
||||
.btn.active,
|
||||
.dropdown-toggle:focus,
|
||||
.form-control:focus,
|
||||
.form-select:focus,
|
||||
.page-link:focus,
|
||||
.nav-link:focus {
|
||||
outline: 0 !important;
|
||||
outline-style: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Для Bootstrap кнопок */
|
||||
.btn:focus,
|
||||
.btn:active,
|
||||
.btn.active,
|
||||
.btn.show,
|
||||
.btn.show:focus,
|
||||
.btn.show:active {
|
||||
outline: 0 !important;
|
||||
box-shadow: none !important;
|
||||
border-color: inherit !important;
|
||||
}
|
||||
|
||||
/* Для форм */
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: var(--brown) !important;
|
||||
box-shadow: none !important;
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
/* Исправление синего текста при наведении */
|
||||
a,
|
||||
a:hover,
|
||||
a:focus,
|
||||
a:active,
|
||||
.nav-link,
|
||||
.nav-link:hover,
|
||||
.nav-link:focus,
|
||||
.nav-link:active,
|
||||
.btn,
|
||||
.btn:hover,
|
||||
.btn:focus,
|
||||
.btn:active {
|
||||
color: inherit !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Для карточек поставщиков */
|
||||
.supplier-card a,
|
||||
.supplier-card a:hover,
|
||||
.supplier-card a:focus,
|
||||
.supplier-card a:active {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
/* Для кнопок в карточках */
|
||||
.action-btn,
|
||||
.action-btn:hover,
|
||||
.action-btn:focus,
|
||||
.action-btn:active {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.edit-btn,
|
||||
.edit-btn:hover,
|
||||
.edit-btn:focus,
|
||||
.edit-btn:active {
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
.add-btn,
|
||||
.add-btn:hover,
|
||||
.add-btn:focus,
|
||||
.add-btn:active {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.history-btn,
|
||||
.history-btn:hover,
|
||||
.history-btn:focus,
|
||||
.history-btn:active {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Убираем подсветку при нажатии на мобильных */
|
||||
* {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Разрешаем выделение текста в инпутах */
|
||||
input,
|
||||
textarea {
|
||||
-webkit-user-select: text;
|
||||
-khtml-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Verdana', sans-serif;
|
||||
background-color: var(--antique-white);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
background-color: #ffffff;
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 30px;
|
||||
}
|
||||
|
||||
/* ===== HEADER + НАВИГАЦИЯ В ОДНОЙ ПОЛОСЕ ===== */
|
||||
.header-nav-wrapper {
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%);
|
||||
width: 100%;
|
||||
box-shadow: 0 4px 12px rgba(160, 82, 45, 0.2);
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.header-nav-wrapper .container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 30px;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 60px !important;
|
||||
width: auto !important;
|
||||
object-fit: contain !important;
|
||||
display: block !important;
|
||||
filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.2));
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
||||
font-family: 'Verdana', sans-serif;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Навигация в хедере */
|
||||
.header-nav {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.header-nav .nav-link {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-family: 'Verdana', sans-serif;
|
||||
font-weight: 500;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 10px;
|
||||
padding: 10px 20px;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 1rem;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-nav .nav-link i {
|
||||
font-size: 1.1rem;
|
||||
color: inherit;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-nav .nav-link:hover {
|
||||
color: white;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
border-color: var(--light-brown);
|
||||
}
|
||||
|
||||
.header-nav .nav-link.active {
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border-color: var(--light-brown);
|
||||
}
|
||||
|
||||
/* ===== ЗАГОЛОВКИ СТРАНИЦ ===== */
|
||||
.page-title {
|
||||
color: var(--brown);
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.page-title i {
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
/* ===== КНОПКИ ===== */
|
||||
.btn {
|
||||
font-family: 'Verdana', sans-serif;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 20px;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, var(--dark-brown) 0%, var(--brown) 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 10px rgba(160, 82, 45, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--light-brown);
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--brown);
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 10px rgba(160, 82, 45, 0.2);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: white;
|
||||
border: 2px solid var(--brown);
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--antique-white);
|
||||
border-color: var(--dark-brown);
|
||||
color: var(--dark-brown);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #218838;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c82333;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 10px rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
/* ===== ФОРМЫ ===== */
|
||||
.form-label {
|
||||
color: var(--brown);
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.form-label i {
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.form-control, .form-select {
|
||||
border: 2px solid var(--light-brown);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-control:focus, .form-select:focus {
|
||||
border-color: var(--brown) !important;
|
||||
box-shadow: none !important;
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
.form-control:hover, .form-select:hover {
|
||||
border-color: var(--brown);
|
||||
}
|
||||
|
||||
.form-text {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ===== ТАБЛИЦЫ ===== */
|
||||
.table-container {
|
||||
background: white;
|
||||
border: 2px solid var(--brown);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(160, 82, 45, 0.1);
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 12px 15px;
|
||||
font-size: 0.95rem;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table tbody td {
|
||||
padding: 12px 15px;
|
||||
border-bottom: 2px solid var(--light-brown);
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table tbody tr:hover td {
|
||||
background-color: var(--antique-white);
|
||||
}
|
||||
|
||||
.table tfoot {
|
||||
background: var(--light-brown);
|
||||
}
|
||||
|
||||
.table tfoot td {
|
||||
padding: 12px 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ===== КАРТОЧКИ ===== */
|
||||
.card {
|
||||
background: white;
|
||||
border: 2px solid var(--brown);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(160, 82, 45, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%);
|
||||
color: white;
|
||||
padding: 15px 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-header i {
|
||||
color: white;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* ===== УВЕДОМЛЕНИЯ ===== */
|
||||
.alert {
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--light-brown);
|
||||
padding: 12px 15px;
|
||||
margin-bottom: 20px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: var(--antique-white);
|
||||
color: var(--brown);
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: var(--antique-white);
|
||||
color: #721c24;
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: var(--antique-white);
|
||||
color: var(--brown);
|
||||
border-left: 4px solid var(--brown);
|
||||
}
|
||||
|
||||
/* ===== ХЛЕБНЫЕ КРОШКИ ===== */
|
||||
.breadcrumb {
|
||||
background: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.breadcrumb-item a {
|
||||
color: var(--brown);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
color: var(--light-brown);
|
||||
}
|
||||
|
||||
/* ===== ИКОНКИ ===== */
|
||||
i {
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.btn i, .nav-link i, .alert i, .card-header i, .header-nav-wrapper i {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ===== АДАПТИВНОСТЬ ===== */
|
||||
@media (max-width: 992px) {
|
||||
.header-nav-wrapper .container {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 50px !important;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.header-nav .nav-link {
|
||||
padding: 8px 15px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.d-flex.justify-content-between {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-container">
|
||||
<!-- Header + Навигация в одной полосе -->
|
||||
<div class="header-nav-wrapper">
|
||||
<div class="container">
|
||||
<div class="logo-container">
|
||||
<img src="{{ url_for('static', filename='favicon.png') }}" alt="Стройматериалы" class="logo">
|
||||
<h1 class="header-title">Стройматериалы</h1>
|
||||
</div>
|
||||
|
||||
<!-- Навигационные вкладки -->
|
||||
{% if request.endpoint in ['index', 'orders_list', 'catalog'] %}
|
||||
<div class="header-nav">
|
||||
<a class="nav-link {% if request.endpoint == 'index' %}active{% endif %}" href="{{ url_for('index') }}">
|
||||
<i class="fas fa-truck"></i> Поставщики
|
||||
</a>
|
||||
<a class="nav-link {% if request.endpoint == 'orders_list' %}active{% endif %}" href="{{ url_for('orders_list') }}">
|
||||
<i class="fas fa-clipboard-list"></i> Заказы
|
||||
</a>
|
||||
<a class="nav-link {% if request.endpoint == 'catalog' %}active{% endif %}" href="{{ url_for('catalog') }}">
|
||||
<i class="fas fa-cubes"></i> Материалы
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Основной контент -->
|
||||
<div class="container">
|
||||
<!-- Flash сообщения -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="flashed-messages">
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show">
|
||||
<i class="fas fa-info-circle me-2"></i>{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно с информацией о проекте -->
|
||||
<div class="modal fade" id="projectInfoModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-info-circle"></i> О проекте
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5>Система учета строительных материалов</h5>
|
||||
<p>Управление поставщиками, заказами и каталогом материалов</p>
|
||||
|
||||
<h6>Цветовая палитра:</h6>
|
||||
<div class="d-flex gap-2 mt-2 mb-3">
|
||||
<div style="width: 40px; height: 40px; background-color: #FAEBD7; border: 1px solid #DEB887;"></div>
|
||||
<div style="width: 40px; height: 40px; background-color: #DEB887; border: 1px solid #A0522D;"></div>
|
||||
<div style="width: 40px; height: 40px; background-color: #A0522D; border: 1px solid #8B4513;"></div>
|
||||
<div style="width: 40px; height: 40px; background-color: #8B4513; border: 1px solid #000;"></div>
|
||||
</div>
|
||||
|
||||
<h6>Технологии:</h6>
|
||||
<div>
|
||||
<span class="badge bg-secondary me-1">Flask</span>
|
||||
<span class="badge bg-secondary me-1">SQLite</span>
|
||||
<span class="badge bg-secondary me-1">Bootstrap</span>
|
||||
<span class="badge bg-secondary me-1">Verdana</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Tooltips
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map(function(tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Auto-dismiss alerts after 5 seconds
|
||||
setTimeout(function() {
|
||||
document.querySelectorAll('.alert:not(.alert-permanent)').forEach(function(alert) {
|
||||
var bsAlert = new bootstrap.Alert(alert);
|
||||
bsAlert.close();
|
||||
});
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
function showProjectInfo() {
|
||||
var modal = new bootstrap.Modal(document.getElementById('projectInfoModal'));
|
||||
modal.show();
|
||||
}
|
||||
</script>
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
731
templates/cart_new.html
Normal file
731
templates/cart_new.html
Normal file
|
|
@ -0,0 +1,731 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Моя корзина{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<!-- Хлебные крошки -->
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('catalog') }}">Каталог материалов</a></li>
|
||||
<li class="breadcrumb-item active">Корзина</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="page-title">
|
||||
<i class="fas fa-shopping-cart"></i> Моя корзина
|
||||
</h2>
|
||||
<a href="{{ url_for('catalog') }}" class="btn btn-outline">
|
||||
<i class="fas fa-arrow-left"></i> Продолжить покупки
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if cart_items %}
|
||||
<!-- Информация о транспортных расходах -->
|
||||
<div class="info-card mb-3">
|
||||
<div class="info-card-header">
|
||||
<i class="fas fa-truck"></i> Транспортные расходы
|
||||
</div>
|
||||
<div class="info-card-body">
|
||||
<p><i class="fas fa-info-circle"></i> Транспортные расходы составляют 10% от стоимости материалов</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Таблица материалов в корзине -->
|
||||
<div class="cart-table-container">
|
||||
<table class="cart-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Материал</th>
|
||||
<th>Поставщик</th>
|
||||
<th>Артикул</th>
|
||||
<th>Категория</th>
|
||||
<th>Цена</th>
|
||||
<th style="text-align: center;">Количество</th>
|
||||
<th>Сумма</th>
|
||||
<th style="text-align: center;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="cartBody">
|
||||
{% for item in cart_items %}
|
||||
<tr id="item-{{ item.key }}" data-price="{{ item.price }}" data-max="{{ item.in_stock }}" data-key="{{ item.key }}" data-supplier-id="{{ item.supplier_id }}" data-supplier-name="{{ item.supplier_name }}">
|
||||
<td data-label="Материал" class="product-name">{{ item.product_name }}</td>
|
||||
<td data-label="Поставщик">{{ item.supplier_name }}</td>
|
||||
<td data-label="Артикул">{{ item.article }}</td>
|
||||
<td data-label="Категория">{{ item.category_name }}</td>
|
||||
<td data-label="Цена" class="item-price">{{ item.price|int }} ₽</td>
|
||||
<td data-label="Количество" style="text-align: center;">
|
||||
<div class="quantity-control">
|
||||
<input type="number" class="quantity-input"
|
||||
value="{{ item.quantity }}" min="1" max="{{ item.in_stock }}"
|
||||
data-item-key="{{ item.key }}"
|
||||
id="quantity-{{ item.key }}"
|
||||
placeholder="Кол-во">
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Сумма" class="item-total amount-value">{{ (item.price * item.quantity)|int }} ₽</td>
|
||||
<td data-label="Действия" style="text-align: center;">
|
||||
<button class="btn-remove remove-item"
|
||||
data-item-key="{{ item.key }}"
|
||||
data-item-name="{{ item.product_name }}"
|
||||
title="Удалить материал">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cart-summary">
|
||||
<td colspan="5" class="text-end"><strong>Базовая сумма:</strong></td>
|
||||
<td colspan="2" class="total-amount" id="base-total">{{ total_amount|int }} ₽</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr class="cart-summary">
|
||||
<td colspan="5" class="text-end"><strong>Транспортные расходы (10%):</strong></td>
|
||||
<td colspan="2" class="total-amount" id="transport-cost">{{ (total_amount * 0.1)|int }} ₽</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr class="cart-total">
|
||||
<td colspan="5" class="text-end"><strong>ИТОГО к оплате:</strong></td>
|
||||
<td colspan="2" class="total-amount" id="cart-total">{{ (total_amount * 1.1)|int }} ₽</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Информация о заказе по поставщикам -->
|
||||
{% if cart_items|groupby('supplier_id') %}
|
||||
<div class="supplier-orders mt-4">
|
||||
<h4 class="section-title">Заказы по поставщикам</h4>
|
||||
<div id="supplier-orders-container">
|
||||
{% for supplier_id, items in cart_items|groupby('supplier_id') %}
|
||||
{% set supplier_total = 0 %}
|
||||
{% for item in items %}
|
||||
{% set supplier_total = supplier_total + (item.price * item.quantity) %}
|
||||
{% endfor %}
|
||||
{% set supplier_total_with_transport = supplier_total * 1.1 %}
|
||||
|
||||
<div class="supplier-order-card mb-3" id="supplier-{{ supplier_id }}">
|
||||
<div class="supplier-order-header">
|
||||
<span><i class="fas fa-truck"></i> Поставщик: {{ items[0].supplier_name }}</span>
|
||||
<span class="supplier-order-total" id="supplier-total-{{ supplier_id }}">{{ supplier_total_with_transport|int }} ₽ (с транспортными расходами)</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Кнопка оформления заказа -->
|
||||
<div class="cart-actions">
|
||||
<form method="POST" action="{{ url_for('checkout') }}" id="checkoutForm">
|
||||
<button type="submit" class="btn btn-primary btn-lg"
|
||||
onclick="return confirm('Оформить заказ? Все материалы будут перемещены в заказы.')">
|
||||
<i class="fas fa-check"></i> Оформить заказ
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- Пустая корзина -->
|
||||
<div class="empty-cart">
|
||||
<div class="empty-icon">
|
||||
<i class="fas fa-shopping-cart"></i>
|
||||
</div>
|
||||
<h4 class="empty-title">Корзина пуста</h4>
|
||||
<p class="empty-text">Добавьте материалы в корзину, чтобы оформить заказ</p>
|
||||
<a href="{{ url_for('catalog') }}" class="btn btn-primary btn-lg">
|
||||
<i class="fas fa-arrow-left"></i> Перейти в каталог
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* ===== ОСНОВНЫЕ ПЕРЕМЕННЫЕ ===== */
|
||||
:root {
|
||||
--antique-white: #FAEBD7;
|
||||
--light-brown: #DEB887;
|
||||
--brown: #A0522D;
|
||||
--dark-brown: #8B4513;
|
||||
}
|
||||
|
||||
/* ===== ХЛЕБНЫЕ КРОШКИ ===== */
|
||||
.breadcrumb {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.breadcrumb-item a {
|
||||
color: var(--brown);
|
||||
text-decoration: none;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.breadcrumb-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
color: #6c757d;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
color: var(--light-brown);
|
||||
}
|
||||
|
||||
/* ===== ЗАГОЛОВОК ===== */
|
||||
.page-title {
|
||||
color: var(--brown);
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.page-title i {
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
/* ===== КНОПКИ ===== */
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--brown);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--dark-brown);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: white;
|
||||
border: 2px solid var(--brown);
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--antique-white);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 12px 30px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* ===== ИНФОРМАЦИОННАЯ КАРТОЧКА ===== */
|
||||
.info-card {
|
||||
background: white;
|
||||
border: 1px solid var(--light-brown);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.info-card-header {
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%);
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
font-weight: 600;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.info-card-header i {
|
||||
color: white;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.info-card-body {
|
||||
padding: 15px 20px;
|
||||
background: var(--antique-white);
|
||||
}
|
||||
|
||||
.info-card-body p {
|
||||
margin: 8px 0;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.info-card-body i {
|
||||
color: var(--brown);
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
/* ===== ТАБЛИЦА КОРЗИНЫ ===== */
|
||||
.cart-table-container {
|
||||
background: white;
|
||||
border: 2px solid var(--brown);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cart-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.cart-table thead th {
|
||||
background: var(--brown);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 15px;
|
||||
font-size: 1rem;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
border-bottom: 2px solid var(--light-brown);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cart-table tbody td {
|
||||
padding: 15px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
color: #2c3e50;
|
||||
border-bottom: 2px solid var(--light-brown);
|
||||
vertical-align: middle;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.cart-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.cart-table tbody tr:hover td {
|
||||
background-color: var(--antique-white);
|
||||
}
|
||||
|
||||
.cart-table tfoot tr {
|
||||
background: var(--antique-white);
|
||||
border-top: 2px solid var(--brown);
|
||||
}
|
||||
|
||||
.cart-table tfoot td {
|
||||
padding: 12px 15px !important;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.cart-summary td {
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.cart-total td {
|
||||
background: var(--light-brown);
|
||||
color: var(--dark-brown);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.total-amount {
|
||||
font-weight: 700;
|
||||
color: var(--dark-brown);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-weight: 600;
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: var(--brown);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid var(--light-brown);
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
/* ===== УПРАВЛЕНИЕ КОЛИЧЕСТВОМ ===== */
|
||||
.quantity-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.quantity-input {
|
||||
width: 80px;
|
||||
height: 40px;
|
||||
border: 2px solid var(--light-brown);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
padding: 0 8px;
|
||||
margin: 0;
|
||||
background-color: white;
|
||||
color: #2c3e50;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Убираем стрелки в поле ввода */
|
||||
.quantity-input::-webkit-inner-spin-button,
|
||||
.quantity-input::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.quantity-input {
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
}
|
||||
|
||||
.quantity-input:focus {
|
||||
border-color: var(--brown);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* ===== КНОПКА УДАЛЕНИЯ ===== */
|
||||
.btn-remove {
|
||||
background: white;
|
||||
border: 2px solid #dc3545;
|
||||
color: #dc3545;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.btn-remove:hover {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-remove i {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-weight: 700;
|
||||
color: var(--brown);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* ===== КАРТОЧКИ ЗАКАЗОВ ПО ПОСТАВЩИКАМ ===== */
|
||||
.supplier-order-card {
|
||||
border: 1px solid var(--light-brown);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.supplier-order-header {
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%);
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.supplier-order-header i {
|
||||
color: white;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.supplier-order-total {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ===== КНОПКА ОФОРМЛЕНИЯ ===== */
|
||||
.cart-actions {
|
||||
text-align: right;
|
||||
margin-top: 25px;
|
||||
padding-top: 20px;
|
||||
border-top: 2px solid var(--light-brown);
|
||||
}
|
||||
|
||||
/* ===== ПУСТАЯ КОРЗИНА ===== */
|
||||
.empty-cart {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
background: white;
|
||||
border: 2px solid var(--brown);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 5rem;
|
||||
color: var(--light-brown);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-icon i {
|
||||
color: var(--light-brown);
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
color: var(--brown);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #6c757d;
|
||||
margin-bottom: 30px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
/* ===== АДАПТИВНОСТЬ ===== */
|
||||
@media (max-width: 992px) {
|
||||
.cart-table {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cart-table thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cart-table tbody tr {
|
||||
display: block;
|
||||
margin-bottom: 15px;
|
||||
border: 2px solid var(--brown);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cart-table tbody td {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border-bottom: 2px solid var(--light-brown);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.cart-table tbody td:before {
|
||||
content: attr(data-label);
|
||||
font-weight: 600;
|
||||
color: var(--brown);
|
||||
margin-right: 10px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.cart-table tbody tr:last-child td:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.cart-table tfoot {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cart-total td,
|
||||
.cart-summary td {
|
||||
display: block;
|
||||
text-align: left !important;
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.quantity-control {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.quantity-input {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.cart-actions {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.d-flex.justify-content-between {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.supplier-order-header {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const quantityInputs = document.querySelectorAll('.quantity-input');
|
||||
const removeButtons = document.querySelectorAll('.remove-item');
|
||||
const updateTimers = {};
|
||||
|
||||
function updateQuantity(itemKey, newQuantity) {
|
||||
const row = document.getElementById(`item-${itemKey}`);
|
||||
const price = parseFloat(row.dataset.price);
|
||||
const maxStock = parseInt(row.dataset.max);
|
||||
|
||||
if (newQuantity > maxStock) {
|
||||
alert(`Максимально доступное количество: ${maxStock} ед.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newQuantity < 1) newQuantity = 1;
|
||||
|
||||
const totalCell = row.querySelector('.item-total');
|
||||
totalCell.textContent = (price * newQuantity).toFixed(0) + ' ₽';
|
||||
|
||||
updateCartTotal();
|
||||
updateSupplierTotals();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateCartOnServer(itemKey, quantity) {
|
||||
if (updateTimers[itemKey]) clearTimeout(updateTimers[itemKey]);
|
||||
updateTimers[itemKey] = setTimeout(() => {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = "{{ url_for('update_cart', item_key=0) }}".replace('0', itemKey);
|
||||
form.style.display = 'none';
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'quantity';
|
||||
input.value = quantity;
|
||||
form.appendChild(input);
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}, 800);
|
||||
}
|
||||
|
||||
function updateCartTotal() {
|
||||
let totalBase = 0;
|
||||
document.querySelectorAll('.item-total').forEach(cell => {
|
||||
const value = parseFloat(cell.textContent) || 0;
|
||||
totalBase += value;
|
||||
});
|
||||
|
||||
const transport = totalBase * 0.1;
|
||||
const totalWithTransport = totalBase + transport;
|
||||
|
||||
document.getElementById('base-total').textContent = totalBase.toFixed(0) + ' ₽';
|
||||
document.getElementById('transport-cost').textContent = transport.toFixed(0) + ' ₽';
|
||||
document.getElementById('cart-total').textContent = totalWithTransport.toFixed(0) + ' ₽';
|
||||
}
|
||||
|
||||
function updateSupplierTotals() {
|
||||
const supplierTotals = {};
|
||||
|
||||
document.querySelectorAll('#cartBody tr').forEach(row => {
|
||||
const supplierId = row.dataset.supplierId;
|
||||
const supplierName = row.dataset.supplierName;
|
||||
const totalCell = row.querySelector('.item-total');
|
||||
const value = parseFloat(totalCell.textContent) || 0;
|
||||
|
||||
if (!supplierTotals[supplierId]) {
|
||||
supplierTotals[supplierId] = {
|
||||
name: supplierName,
|
||||
total: 0
|
||||
};
|
||||
}
|
||||
supplierTotals[supplierId].total += value;
|
||||
});
|
||||
|
||||
// Обновляем отображение для каждого поставщика
|
||||
for (const [supplierId, data] of Object.entries(supplierTotals)) {
|
||||
const supplierTotalElement = document.getElementById(`supplier-total-${supplierId}`);
|
||||
if (supplierTotalElement) {
|
||||
const totalWithTransport = data.total * 1.1;
|
||||
supplierTotalElement.textContent = totalWithTransport.toFixed(0) + ' ₽ (с транспортными расходами)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quantityInputs.forEach(input => {
|
||||
// Запрещаем изменение стрелками на клавиатуре
|
||||
input.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('input', function() {
|
||||
const itemKey = this.dataset.itemKey;
|
||||
const row = document.getElementById(`item-${itemKey}`);
|
||||
const maxStock = parseInt(row.dataset.max);
|
||||
let newQuantity = parseInt(this.value);
|
||||
|
||||
if (isNaN(newQuantity)) return;
|
||||
|
||||
if (newQuantity > maxStock) {
|
||||
this.value = maxStock;
|
||||
newQuantity = maxStock;
|
||||
alert(`Максимально доступное количество: ${maxStock} ед.`);
|
||||
}
|
||||
|
||||
if (newQuantity < 1) {
|
||||
this.value = 1;
|
||||
newQuantity = 1;
|
||||
}
|
||||
|
||||
if (updateQuantity(itemKey, newQuantity)) {
|
||||
updateCartOnServer(itemKey, newQuantity);
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function() {
|
||||
const itemKey = this.dataset.itemKey;
|
||||
let value = parseInt(this.value);
|
||||
if (isNaN(value) || value < 1) {
|
||||
this.value = 1;
|
||||
value = 1;
|
||||
updateQuantity(itemKey, 1);
|
||||
updateCartOnServer(itemKey, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
removeButtons.forEach(button => {
|
||||
button.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const itemKey = this.dataset.itemKey;
|
||||
const itemName = this.dataset.itemName;
|
||||
|
||||
if (confirm(`Удалить материал "${itemName}" из корзины?`)) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = "{{ url_for('remove_from_cart', item_key=0) }}".replace('0', itemKey);
|
||||
form.style.display = 'none';
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Инициализация
|
||||
updateCartTotal();
|
||||
updateSupplierTotals();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
868
templates/catalog.html
Normal file
868
templates/catalog.html
Normal file
|
|
@ -0,0 +1,868 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Каталог материалов{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Заголовок с корзиной и кнопкой добавления -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="page-title">Каталог материалов</h2>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ url_for('add_product') }}" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Добавить материал
|
||||
</a>
|
||||
<a href="{{ url_for('view_cart') }}" class="btn btn-primary">
|
||||
<i class="fas fa-shopping-cart"></i> Корзина
|
||||
{% if cart_count > 0 %}
|
||||
<span class="cart-badge">{{ cart_count }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Поиск и сортировка - УЛУЧШЕННЫЙ UX -->
|
||||
<div class="search-section mb-4">
|
||||
<div class="search-header">
|
||||
<h4 class="search-title">
|
||||
<i class="fas fa-sliders-h"></i> Фильтры и поиск
|
||||
</h4>
|
||||
<a href="{{ url_for('catalog') }}" class="btn-clear">
|
||||
<i class="fas fa-times"></i> Сбросить все
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="GET" action="{{ url_for('catalog') }}" id="filterForm" class="search-form">
|
||||
<!-- Строка поиска -->
|
||||
<div class="search-row">
|
||||
<div class="search-input-wrapper">
|
||||
<i class="fas fa-search search-icon"></i>
|
||||
<input type="text" class="search-input" name="search" id="search"
|
||||
placeholder="Поиск по названию или артикулу..."
|
||||
value="{{ request.args.get('search', '') }}" autocomplete="off">
|
||||
{% if request.args.get('search') %}
|
||||
<a href="{{ url_for('catalog', category=request.args.get('category'), sort=request.args.get('sort'), supplier=request.args.get('supplier')) }}" class="search-clear">
|
||||
<i class="fas fa-times-circle"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Фильтры -->
|
||||
<div class="filters-row">
|
||||
<div class="filter-group">
|
||||
<label for="supplier" class="filter-label">
|
||||
<i class="fas fa-truck"></i> Поставщик
|
||||
</label>
|
||||
<select class="filter-select" name="supplier" id="supplier">
|
||||
<option value="all" {% if request.args.get('supplier') == 'all' or not request.args.get('supplier') %}selected{% endif %}>Все поставщики</option>
|
||||
{% for supplier in suppliers %}
|
||||
<option value="{{ supplier.id }}" {% if request.args.get('supplier')|int == supplier.id %}selected{% endif %}>
|
||||
{{ supplier.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="category" class="filter-label">
|
||||
<i class="fas fa-tag"></i> Категория
|
||||
</label>
|
||||
<select class="filter-select" name="category" id="category">
|
||||
<option value="all" {% if request.args.get('category') == 'all' or not request.args.get('category') %}selected{% endif %}>Все категории</option>
|
||||
{% for category in categories %}
|
||||
<option value="{{ category.id }}" {% if request.args.get('category')|int == category.id %}selected{% endif %}>
|
||||
{{ category.category_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="sort" class="filter-label">
|
||||
<i class="fas fa-sort-amount-down"></i> Сортировка
|
||||
</label>
|
||||
<select class="filter-select" name="sort" id="sort">
|
||||
<option value="name_asc" {% if request.args.get('sort') == 'name_asc' or not request.args.get('sort') %}selected{% endif %}>Название (А-Я)</option>
|
||||
<option value="name_desc" {% if request.args.get('sort') == 'name_desc' %}selected{% endif %}>Название (Я-А)</option>
|
||||
<option value="price_asc" {% if request.args.get('sort') == 'price_asc' %}selected{% endif %}>Сначала дешевые</option>
|
||||
<option value="price_desc" {% if request.args.get('sort') == 'price_desc' %}selected{% endif %}>Сначала дорогие</option>
|
||||
<option value="stock_desc" {% if request.args.get('sort') == 'stock_desc' %}selected{% endif %}>По наличию</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-apply">
|
||||
<i class="fas fa-check"></i> Применить
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Активные фильтры -->
|
||||
{% if request.args.get('search') or (request.args.get('category') and request.args.get('category') != 'all') or (request.args.get('supplier') and request.args.get('supplier') != 'all') %}
|
||||
<div class="active-filters mb-3">
|
||||
<span class="active-filters-label">Активные фильтры:</span>
|
||||
<div class="filter-tags">
|
||||
{% if request.args.get('search') %}
|
||||
<span class="filter-tag">
|
||||
Поиск: "{{ request.args.get('search') }}"
|
||||
<a href="{{ url_for('catalog', category=request.args.get('category'), sort=request.args.get('sort'), supplier=request.args.get('supplier')) }}" class="remove-tag">
|
||||
<i class="fas fa-times"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if request.args.get('category') and request.args.get('category') != 'all' %}
|
||||
<span class="filter-tag">
|
||||
{% for category in categories %}
|
||||
{% if category.id == request.args.get('category')|int %}
|
||||
Категория: {{ category.category_name }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<a href="{{ url_for('catalog', search=request.args.get('search'), sort=request.args.get('sort'), supplier=request.args.get('supplier')) }}" class="remove-tag">
|
||||
<i class="fas fa-times"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if request.args.get('supplier') and request.args.get('supplier') != 'all' %}
|
||||
<span class="filter-tag">
|
||||
{% for supplier in suppliers %}
|
||||
{% if supplier.id == request.args.get('supplier')|int %}
|
||||
Поставщик: {{ supplier.name }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<a href="{{ url_for('catalog', search=request.args.get('search'), category=request.args.get('category'), sort=request.args.get('sort')) }}" class="remove-tag">
|
||||
<i class="fas fa-times"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- КАТАЛОГ МАТЕРИАЛОВ -->
|
||||
{% if products %}
|
||||
<div class="row">
|
||||
{% for product in products %}
|
||||
<div class="col-12">
|
||||
<div class="product-card">
|
||||
<!-- Первая строка: Категория | Название -->
|
||||
<div class="product-header">
|
||||
<div class="product-name-line">
|
||||
<span class="product-category">
|
||||
{{ product.category_name }}
|
||||
</span>
|
||||
<span class="divider">|</span>
|
||||
<span class="product-name">{{ product.product_name }}</span>
|
||||
</div>
|
||||
<div class="stock-info">
|
||||
<span class="stock-amount">
|
||||
{% if product.quantity_in_stock and product.quantity_in_stock > 0 %}
|
||||
{{ product.quantity_in_stock }} ед.
|
||||
{% else %}
|
||||
Нет в наличии
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Информация о материале -->
|
||||
<div class="product-info-row">
|
||||
<span class="info-label">Артикул:</span>
|
||||
<span class="info-value">{{ product.article }}</span>
|
||||
</div>
|
||||
|
||||
<div class="product-info-row">
|
||||
<span class="info-label">Материал:</span>
|
||||
<span class="info-value">
|
||||
{% if product.material_name %}
|
||||
{{ product.material_name }}
|
||||
{% else %}
|
||||
Не указан
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="product-info-row">
|
||||
<span class="info-label">Поставщик:</span>
|
||||
<span class="info-value">
|
||||
{% if product.supplier_name %}
|
||||
{{ product.supplier_name }}
|
||||
{% else %}
|
||||
Не указан
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if product.description %}
|
||||
<div class="product-info-row">
|
||||
<span class="info-label">Описание:</span>
|
||||
<span class="info-value">{{ product.description }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="product-info-row">
|
||||
<span class="info-label">Цена:</span>
|
||||
<span class="info-value price-value">{{ product.min_price|int }} руб.</span>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки действий с выбором поставщика -->
|
||||
<div class="product-buttons-container">
|
||||
<a href="{{ url_for('edit_product', product_id=product.id) }}" class="btn-action btn-edit">
|
||||
<i class="fas fa-edit"></i> Редактирование
|
||||
</a>
|
||||
|
||||
<!-- Форма добавления в корзину с выбором поставщика -->
|
||||
<form method="POST" action="{{ url_for('add_to_cart', product_id=product.id) }}" style="flex: 1;" class="add-to-cart-form">
|
||||
<div class="supplier-select-wrapper" style="display: flex; gap: 5px;">
|
||||
<select name="supplier_id" class="supplier-select" required style="flex: 1; padding: 8px; border: 2px solid #DEB887; border-radius: 8px; font-family: 'Verdana', sans-serif;">
|
||||
<option value="" disabled selected>Выберите поставщика</option>
|
||||
{% for supplier in suppliers %}
|
||||
<option value="{{ supplier.id }}">{{ supplier.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="hidden" name="quantity" value="1">
|
||||
<button type="submit" class="btn-action btn-add" style="width: auto; padding: 8px 15px;"
|
||||
{% if not product.quantity_in_stock or product.quantity_in_stock == 0 %}disabled{% endif %}>
|
||||
<i class="fas fa-cart-plus"></i> В корзину
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Информация о результатах -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<p class="text-muted">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Найдено материалов: <strong>{{ products|length }}</strong>
|
||||
{% if request.args.get('search') %}
|
||||
по запросу "{{ request.args.get('search') }}"
|
||||
{% endif %}
|
||||
{% if request.args.get('category') and request.args.get('category') != 'all' %}
|
||||
{% for category in categories %}
|
||||
{% if category.id == request.args.get('category')|int %}
|
||||
, категория: {{ category.category_name }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if request.args.get('supplier') and request.args.get('supplier') != 'all' %}
|
||||
{% for supplier in suppliers %}
|
||||
{% if supplier.id == request.args.get('supplier')|int %}
|
||||
, поставщик: {{ supplier.name }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
{% if request.args.get('search') or request.args.get('category') != 'all' or request.args.get('supplier') != 'all' %}
|
||||
<strong>Ничего не найдено</strong> по вашему запросу.
|
||||
<br><a href="{{ url_for('catalog') }}" class="alert-link">Сбросить все фильтры</a>
|
||||
{% else %}
|
||||
В каталоге нет материалов. <a href="{{ url_for('add_product') }}" class="alert-link">Добавьте первый материал</a>.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script>
|
||||
let filterTimeout;
|
||||
|
||||
document.getElementById('supplier').addEventListener('change', function() {
|
||||
clearTimeout(filterTimeout);
|
||||
filterTimeout = setTimeout(() => {
|
||||
document.getElementById('filterForm').submit();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
document.getElementById('category').addEventListener('change', function() {
|
||||
clearTimeout(filterTimeout);
|
||||
filterTimeout = setTimeout(() => {
|
||||
document.getElementById('filterForm').submit();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
document.getElementById('sort').addEventListener('change', function() {
|
||||
clearTimeout(filterTimeout);
|
||||
filterTimeout = setTimeout(() => {
|
||||
document.getElementById('filterForm').submit();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
let searchTimeout;
|
||||
document.getElementById('search').addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
if (this.value.length >= 2 || this.value.length === 0) {
|
||||
document.getElementById('filterForm').submit();
|
||||
}
|
||||
}, 800);
|
||||
});
|
||||
|
||||
document.getElementById('search').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
document.getElementById('filterForm').submit();
|
||||
}
|
||||
});
|
||||
|
||||
// Валидация выбора поставщика перед отправкой
|
||||
document.querySelectorAll('.add-to-cart-form').forEach(form => {
|
||||
form.addEventListener('submit', function(e) {
|
||||
const select = this.querySelector('.supplier-select');
|
||||
if (!select.value) {
|
||||
e.preventDefault();
|
||||
alert('Пожалуйста, выберите поставщика');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page-title {
|
||||
color: #A0522D;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.d-flex.gap-2 {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #A0522D;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 20px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #8B4513;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(160, 82, 45, 0.2);
|
||||
}
|
||||
|
||||
.cart-badge {
|
||||
background-color: #8B4513;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
padding: 2px 6px;
|
||||
font-size: 0.7rem;
|
||||
margin-left: 5px;
|
||||
min-width: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ===== УЛУЧШЕННЫЙ ПОИСК И ФИЛЬТРЫ ===== */
|
||||
.search-section {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
padding: 20px;
|
||||
margin-bottom: 25px;
|
||||
border: 1px solid #DEB887;
|
||||
}
|
||||
|
||||
.search-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-title i {
|
||||
color: #A0522D;
|
||||
}
|
||||
|
||||
.btn-clear {
|
||||
background: none;
|
||||
border: 1px solid #DEB887;
|
||||
padding: 8px 16px;
|
||||
border-radius: 30px;
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.btn-clear:hover {
|
||||
background: #FAEBD7;
|
||||
border-color: #A0522D;
|
||||
color: #A0522D;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.search-row {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #adb5bd;
|
||||
font-size: 1rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
padding: 0 45px 0 45px;
|
||||
border: 2px solid #DEB887;
|
||||
border-radius: 30px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #A0522D;
|
||||
box-shadow: 0 0 0 4px rgba(160, 82, 45, 0.1);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.search-clear {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #adb5bd;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-clear:hover {
|
||||
color: #A0522D;
|
||||
}
|
||||
|
||||
.filters-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 6px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.filter-label i {
|
||||
color: #A0522D;
|
||||
margin-right: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
padding: 0 35px 0 12px;
|
||||
border: 2px solid #DEB887;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
color: #2c3e50;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23A0522D' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
background-size: 16px;
|
||||
}
|
||||
|
||||
.filter-select:hover {
|
||||
border-color: #A0522D;
|
||||
}
|
||||
|
||||
.filter-select:focus {
|
||||
outline: none;
|
||||
border-color: #A0522D;
|
||||
box-shadow: 0 0 0 3px rgba(160, 82, 45, 0.1);
|
||||
}
|
||||
|
||||
.btn-apply {
|
||||
background: #A0522D;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0 25px;
|
||||
height: 42px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-apply:hover {
|
||||
background: #8B4513;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(160, 82, 45, 0.3);
|
||||
}
|
||||
|
||||
/* Активные фильтры */
|
||||
.active-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px 15px;
|
||||
background: #FAEBD7;
|
||||
border-radius: 30px;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #DEB887;
|
||||
}
|
||||
|
||||
.active-filters-label {
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-tag {
|
||||
background: white;
|
||||
border: 1px solid #DEB887;
|
||||
padding: 6px 12px;
|
||||
border-radius: 30px;
|
||||
font-size: 0.9rem;
|
||||
color: #2c3e50;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.remove-tag {
|
||||
color: #adb5bd;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.remove-tag:hover {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
/* ===== КАРТОЧКИ МАТЕРИАЛОВ ===== */
|
||||
.product-card {
|
||||
background: white;
|
||||
border: 1px solid #DEB887;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 4px 8px rgba(160, 82, 45, 0.1);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
box-shadow: 0 8px 16px rgba(160, 82, 45, 0.2);
|
||||
border-color: #A0522D;
|
||||
}
|
||||
|
||||
.product-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #DEB887;
|
||||
}
|
||||
|
||||
.product-name-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.product-category {
|
||||
background-color: transparent;
|
||||
color: #A0522D;
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
border: 2px solid #DEB887;
|
||||
}
|
||||
|
||||
.divider {
|
||||
color: #A0522D;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
color: #8B4513;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.stock-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stock-amount {
|
||||
color: #A0522D;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
background-color: #FAEBD7;
|
||||
padding: 5px 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.product-info-row {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px dashed #DEB887;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.product-info-row:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
width: 100px;
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
color: #2c3e50;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
color: #A0522D;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.product-buttons-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 2px solid #DEB887;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
text-align: center;
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: linear-gradient(135deg, #DEB887 0%, #C17B4A 100%);
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.btn-edit:hover {
|
||||
background: linear-gradient(135deg, #C17B4A 0%, #DEB887 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(160, 82, 45, 0.1);
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
background: linear-gradient(135deg, #A0522D 0%, #8B4513 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-add:hover {
|
||||
background: linear-gradient(135deg, #8B4513 0%, #A0522D 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(160, 82, 45, 0.3);
|
||||
}
|
||||
|
||||
.btn-add:disabled {
|
||||
background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.supplier-select {
|
||||
border: 2px solid #DEB887;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.supplier-select:focus {
|
||||
border-color: #A0522D;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* ===== ДОПОЛНИТЕЛЬНЫЕ ЭЛЕМЕНТЫ ===== */
|
||||
.text-muted {
|
||||
font-family: 'Verdana', sans-serif;
|
||||
margin: 0;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #DEB887;
|
||||
background: #FAEBD7;
|
||||
border-left: 4px solid #A0522D;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.alert-link {
|
||||
color: #A0522D;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.alert-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
i {
|
||||
color: #A0522D;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.btn-primary i, .btn-clear i, .btn-apply i, .btn-action i {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ===== АДАПТИВНОСТЬ ===== */
|
||||
@media (max-width: 768px) {
|
||||
.filters-row {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-apply {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.active-filters {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.product-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stock-info {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.product-info-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
width: 100%;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.product-buttons-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.supplier-select-wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.d-flex.gap-2 {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.d-flex.gap-2 .btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
8
templates/components/header.html
Normal file
8
templates/components/header.html
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<div class="header">
|
||||
<div class="container">
|
||||
<div class="logo-container">
|
||||
<img src="{{ url_for('static', filename='favicon.png') }}" alt="Стройматериалы" class="logo">
|
||||
<h1 class="header-title">Строительные материалы</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
249
templates/components/partner_card.html
Normal file
249
templates/components/partner_card.html
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
<div class="col-12">
|
||||
<div class="supplier-card">
|
||||
<!-- Верхняя часть с типом и названием -->
|
||||
<div class="supplier-header">
|
||||
<div class="supplier-type-badge">
|
||||
{% if partner.contractor_type_name %}
|
||||
{{ partner.contractor_type_name }}
|
||||
{% else %}
|
||||
Тип не указан
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="supplier-name">{{ partner.name }}</div>
|
||||
<!-- Удален блок supplier-orders -->
|
||||
</div>
|
||||
|
||||
<!-- Основная информация в две колонки -->
|
||||
<div class="supplier-info-grid">
|
||||
<div class="info-column">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Директор:</span>
|
||||
<span class="info-value">{{ partner.director or 'Не указан' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Телефон:</span>
|
||||
<span class="info-value">{{ partner.phone or 'Не указан' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-column">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Рейтинг:</span>
|
||||
<span class="info-value">
|
||||
<span class="rating-badge" style="background-color: {% if partner.rating >= 4 %}#A0522D{% elif partner.rating >= 3 %}#DEB887{% else %}#8B4513{% endif %}; color: white;">
|
||||
{{ partner.rating }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Сумма заказов:</span>
|
||||
<span class="info-value">{{ "%.2f"|format(partner.total_sales) }} руб.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки действий в ряд -->
|
||||
<div class="supplier-actions">
|
||||
<a href="{{ url_for('edit_client', client_id=partner.id) }}" class="action-btn edit-btn">
|
||||
Редактировать
|
||||
</a>
|
||||
<a href="{{ url_for('add_order', client_id=partner.id) }}" class="action-btn add-btn">
|
||||
Добавить заказ
|
||||
</a>
|
||||
<a href="{{ url_for('order_history', client_id=partner.id) }}" class="action-btn history-btn">
|
||||
История
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Убираем стандартные outline у всех элементов */
|
||||
* {
|
||||
outline: none !important;
|
||||
-webkit-tap-highlight-color: transparent !important;
|
||||
}
|
||||
|
||||
.supplier-card {
|
||||
background: linear-gradient(135deg, #FAEBD7 0%, #F5DEB3 100%);
|
||||
border: 1px solid #DEB887;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 12px rgba(160, 82, 45, 0.1);
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.supplier-card:hover {
|
||||
box-shadow: 0 8px 20px rgba(160, 82, 45, 0.15);
|
||||
border-color: #A0522D;
|
||||
}
|
||||
|
||||
/* Заголовок карточки */
|
||||
.supplier-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #DEB887;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.supplier-type-badge {
|
||||
background: white;
|
||||
border: 2px solid #A0522D;
|
||||
color: #A0522D;
|
||||
padding: 6px 15px;
|
||||
border-radius: 30px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.supplier-name {
|
||||
color: #A0522D;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Сетка информации */
|
||||
.supplier-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #8B7355;
|
||||
font-weight: 500;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #5D3A1A;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.rating-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Кнопки действий */
|
||||
.supplier-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 2px solid #DEB887;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 15px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s ease, transform 0.2s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Убираем outline при фокусе */
|
||||
.action-btn:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
background: linear-gradient(135deg, #DEB887 0%, #C17B4A 100%);
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.edit-btn:hover {
|
||||
background: linear-gradient(135deg, #C17B4A 0%, #DEB887 100%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
background: linear-gradient(135deg, #A0522D 0%, #8B4513 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
background: linear-gradient(135deg, #8B4513 0%, #A0522D 100%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.history-btn {
|
||||
background: linear-gradient(135deg, #8B7355 0%, #5D3A1A 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.history-btn:hover {
|
||||
background: linear-gradient(135deg, #5D3A1A 0%, #8B7355 100%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 992px) {
|
||||
.supplier-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.supplier-name {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.supplier-info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.supplier-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.info-item {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
815
templates/edit_client.html
Normal file
815
templates/edit_client.html
Normal file
|
|
@ -0,0 +1,815 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Редактирование поставщика{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<!-- Хлебные крошки -->
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('index') }}">Поставщики</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('order_history', client_id=client.id) }}">{{ client.name }}</a></li>
|
||||
<li class="breadcrumb-item active">Редактирование</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="form-card">
|
||||
<div class="form-card-header">
|
||||
<h2 class="form-card-title">
|
||||
<i class="fas fa-truck"></i> Редактирование поставщика
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="form-card-body">
|
||||
<!-- Flash сообщения -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show">
|
||||
<i class="fas fa-{{ 'exclamation-circle' if category == 'error' else 'check-circle' }} me-2"></i>
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" id="supplierForm">
|
||||
<div class="row">
|
||||
<!-- Основная информация -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="section-title">
|
||||
<i class="fas fa-info-circle"></i> Основная информация
|
||||
</h5>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name" class="form-label">
|
||||
<i class="fas fa-building"></i>
|
||||
Название компании
|
||||
</label>
|
||||
<input type="text" class="form-control modern-input" id="name" name="name"
|
||||
value="{{ client.name }}" required maxlength="255"
|
||||
onkeypress="return blockNumbers(event)" onpaste="return blockNumbersPaste(event)">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="director" class="form-label">
|
||||
<i class="fas fa-user-tie"></i>
|
||||
Директор (ФИО)
|
||||
</label>
|
||||
<input type="text" class="form-control modern-input" id="director" name="director"
|
||||
value="{{ client.director or '' }}" maxlength="255"
|
||||
onkeypress="return blockNumbers(event)" onpaste="return blockNumbersPaste(event)">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="contractor_type_id" class="form-label">
|
||||
<i class="fas fa-tag"></i>
|
||||
Тип поставщика
|
||||
</label>
|
||||
<div class="select-wrapper">
|
||||
<select class="form-select modern-select" id="contractor_type_id" name="contractor_type_id" required>
|
||||
<option value="" disabled>Выберите тип поставщика</option>
|
||||
{% for type in contractor_types %}
|
||||
<option value="{{ type.id }}"
|
||||
{% if type.id == client.contractor_type_id %}selected{% endif %}>
|
||||
{{ type.type_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<i class="fas fa-chevron-down select-arrow"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="rating" class="form-label">
|
||||
<i class="fas fa-star"></i>
|
||||
Рейтинг надежности
|
||||
</label>
|
||||
<input type="number" class="form-control modern-input" id="rating" name="rating"
|
||||
min="0" max="5" step="0.1" value="{{ client.rating or 0.0 }}">
|
||||
<div class="form-text">От 0.0 до 5.0 (чем выше, тем надежнее)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Контактная информация -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="section-title">
|
||||
<i class="fas fa-address-card"></i> Контактные данные
|
||||
</h5>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email" class="form-label">
|
||||
<i class="fas fa-envelope"></i>
|
||||
Email
|
||||
</label>
|
||||
<input type="email" class="form-control modern-input" id="email" name="email"
|
||||
value="{{ client.email }}" required maxlength="255">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="phone" class="form-label">
|
||||
<i class="fas fa-phone"></i>
|
||||
Телефон
|
||||
</label>
|
||||
<div class="phone-input-container">
|
||||
<input type="tel" class="form-control modern-input" id="phone" name="phone"
|
||||
value="{{ client.phone or '' }}" maxlength="18"
|
||||
onkeypress="return allowPhoneCharacters(event)"
|
||||
onkeyup="formatPhoneNumber(this)"
|
||||
onblur="validatePhoneNumber(this)"
|
||||
placeholder="+7 (999) 999-99-99">
|
||||
<i class="fas fa-phone phone-icon"></i>
|
||||
</div>
|
||||
<div class="phone-hint" id="phoneHint">
|
||||
<i class="fas fa-info-circle"></i> Формат: +7 (XXX) XXX-XX-XX
|
||||
</div>
|
||||
<div class="phone-error" id="phoneError" style="display: none;">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<span id="phoneErrorMessage"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="inn" class="form-label">
|
||||
<i class="fas fa-id-card"></i>
|
||||
ИНН
|
||||
</label>
|
||||
<input type="text" class="form-control modern-input" id="inn" name="inn"
|
||||
value="{{ client.inn or '' }}" maxlength="12"
|
||||
onkeypress="return allowOnlyNumbers(event)" onpaste="return allowNumbersPaste(event)">
|
||||
<div class="form-text">10 или 12 цифр</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Адрес на всю ширину -->
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
<label for="address" class="form-label">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
Юридический адрес
|
||||
</label>
|
||||
<textarea class="form-control modern-input" id="address" name="address"
|
||||
rows="3" maxlength="500">{{ client.address or '' }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки -->
|
||||
<div class="form-actions">
|
||||
<div>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-outline">
|
||||
<i class="fas fa-times"></i> Отмена
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ url_for('order_history', client_id=client.id) }}"
|
||||
class="btn btn-info">
|
||||
<i class="fas fa-history"></i> История заказов
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* ===== ОСНОВНЫЕ ПЕРЕМЕННЫЕ ===== */
|
||||
:root {
|
||||
--antique-white: #FAEBD7;
|
||||
--light-brown: #DEB887;
|
||||
--brown: #A0522D;
|
||||
--dark-brown: #8B4513;
|
||||
--error-red: #dc3545;
|
||||
--success-green: #28a745;
|
||||
}
|
||||
|
||||
/* ===== ХЛЕБНЫЕ КРОШКИ ===== */
|
||||
.breadcrumb {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.breadcrumb-item a {
|
||||
color: var(--brown);
|
||||
text-decoration: none;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.breadcrumb-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
color: #6c757d;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
color: var(--light-brown);
|
||||
}
|
||||
|
||||
/* ===== КАРТОЧКА ФОРМЫ ===== */
|
||||
.form-card {
|
||||
background: white;
|
||||
border: 1px solid var(--light-brown);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(160, 82, 45, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-card-header {
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%);
|
||||
padding: 20px 25px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-card-title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-card-title i {
|
||||
margin-right: 10px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-card-body {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
/* ===== ЗАГОЛОВКИ СЕКЦИЙ ===== */
|
||||
.section-title {
|
||||
color: var(--brown);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid var(--light-brown);
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.section-title i {
|
||||
color: var(--brown);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* ===== ЭЛЕМЕНТЫ ФОРМЫ ===== */
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--brown);
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 6px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.form-label i {
|
||||
color: var(--brown);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.modern-input {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
padding: 0 12px;
|
||||
border: 2px solid var(--light-brown);
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
transition: all 0.2s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.modern-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--brown);
|
||||
box-shadow: 0 0 0 3px rgba(160, 82, 45, 0.1);
|
||||
}
|
||||
|
||||
.modern-input:hover {
|
||||
border-color: var(--brown);
|
||||
}
|
||||
|
||||
textarea.modern-input {
|
||||
height: auto;
|
||||
padding: 10px 12px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* Специальные стили для поля телефона */
|
||||
.phone-input-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.phone-input-container .modern-input {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.phone-icon {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--light-brown);
|
||||
pointer-events: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.phone-input-container .modern-input:focus + .phone-icon {
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.phone-hint {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
margin-top: 4px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.phone-hint i {
|
||||
color: var(--light-brown);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.phone-error {
|
||||
font-size: 0.8rem;
|
||||
color: var(--error-red);
|
||||
margin-top: 4px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.phone-error i {
|
||||
color: var(--error-red);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modern-select {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
padding: 0 35px 0 12px;
|
||||
border: 2px solid var(--light-brown);
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.modern-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--brown);
|
||||
box-shadow: 0 0 0 3px rgba(160, 82, 45, 0.1);
|
||||
}
|
||||
|
||||
.modern-select:hover {
|
||||
border-color: var(--brown);
|
||||
}
|
||||
|
||||
.select-arrow {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--brown);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
margin-top: 4px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
/* ===== КНОПКИ ===== */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 25px;
|
||||
padding-top: 20px;
|
||||
border-top: 2px solid var(--light-brown);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--brown);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--dark-brown);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(160, 82, 45, 0.3);
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: var(--light-brown);
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.btn-info:hover {
|
||||
background: var(--brown);
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(160, 82, 45, 0.3);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: white;
|
||||
border: 2px solid var(--light-brown);
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--antique-white);
|
||||
border-color: var(--brown);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.d-flex.gap-2 {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* ===== АЛЕРТЫ ===== */
|
||||
.alert {
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--light-brown);
|
||||
font-family: 'Verdana', sans-serif;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: var(--antique-white);
|
||||
color: var(--brown);
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: var(--antique-white);
|
||||
color: #721c24;
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
|
||||
/* ===== ИКОНКИ ===== */
|
||||
i {
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.btn-primary i, .btn-info i, .btn-outline i {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ===== АДАПТИВНОСТЬ ===== */
|
||||
@media (max-width: 768px) {
|
||||
.form-card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column-reverse;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.form-actions > div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.d-flex.gap-2 {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.d-flex.gap-2 .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.col-md-6 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.section-title:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Запрет на ввод цифр в текстовые поля (название компании, ФИО)
|
||||
function blockNumbers(event) {
|
||||
const key = event.key;
|
||||
// Разрешаем: буквы, пробел, backspace, delete, tab, enter, стрелки
|
||||
if (key === ' ' || key === 'Backspace' || key === 'Delete' || key === 'Tab' ||
|
||||
key === 'Enter' || key === 'ArrowLeft' || key === 'ArrowRight' ||
|
||||
key === 'ArrowUp' || key === 'ArrowDown' || key === 'Home' || key === 'End') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Запрещаем цифры
|
||||
if (key >= '0' && key <= '9') {
|
||||
event.preventDefault();
|
||||
alert('Цифры запрещены в этом поле');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Разрешаем буквы (любые)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Запрет на вставку цифр через Ctrl+V
|
||||
function blockNumbersPaste(event) {
|
||||
event.preventDefault();
|
||||
const pastedText = (event.clipboardData || window.clipboardData).getData('text');
|
||||
// Удаляем все цифры из вставляемого текста
|
||||
const cleanedText = pastedText.replace(/[0-9]/g, '');
|
||||
|
||||
// Вставляем очищенный текст
|
||||
const input = event.target;
|
||||
const start = input.selectionStart;
|
||||
const end = input.selectionEnd;
|
||||
const currentValue = input.value;
|
||||
const newValue = currentValue.substring(0, start) + cleanedText + currentValue.substring(end);
|
||||
input.value = newValue;
|
||||
|
||||
// Возвращаем курсор в конец вставленного текста
|
||||
input.selectionStart = input.selectionEnd = start + cleanedText.length;
|
||||
|
||||
if (pastedText.match(/[0-9]/)) {
|
||||
alert('Цифры были удалены из вставленного текста');
|
||||
}
|
||||
}
|
||||
|
||||
// Разрешаем только цифры (для ИНН)
|
||||
function allowOnlyNumbers(event) {
|
||||
const key = event.key;
|
||||
// Разрешаем: цифры, backspace, delete, tab, enter, стрелки
|
||||
if (key === 'Backspace' || key === 'Delete' || key === 'Tab' ||
|
||||
key === 'Enter' || key === 'ArrowLeft' || key === 'ArrowRight' ||
|
||||
key === 'ArrowUp' || key === 'ArrowDown' || key === 'Home' || key === 'End') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Разрешаем только цифры
|
||||
if (key >= '0' && key <= '9') {
|
||||
return true;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
alert('Только цифры разрешены в этом поле');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Разрешаем вставку только цифр (для ИНН)
|
||||
function allowNumbersPaste(event) {
|
||||
event.preventDefault();
|
||||
const pastedText = (event.clipboardData || window.clipboardData).getData('text');
|
||||
// Оставляем только цифры
|
||||
const cleanedText = pastedText.replace(/[^0-9]/g, '');
|
||||
|
||||
// Вставляем очищенный текст
|
||||
const input = event.target;
|
||||
const start = input.selectionStart;
|
||||
const end = input.selectionEnd;
|
||||
const currentValue = input.value;
|
||||
const newValue = currentValue.substring(0, start) + cleanedText + currentValue.substring(end);
|
||||
input.value = newValue;
|
||||
|
||||
// Возвращаем курсор в конец вставленного текста
|
||||
input.selectionStart = input.selectionEnd = start + cleanedText.length;
|
||||
|
||||
if (pastedText.match(/[^0-9]/)) {
|
||||
alert('Были вставлены только цифры, остальные символы удалены');
|
||||
}
|
||||
}
|
||||
|
||||
// Для поля телефона разрешаем цифры и необходимые символы
|
||||
function allowPhoneCharacters(event) {
|
||||
const key = event.key;
|
||||
// Разрешаем: цифры, +, -, (, ), пробел, backspace и т.д.
|
||||
const allowedKeys = ['+', '-', '(', ')', ' '];
|
||||
|
||||
if (key === 'Backspace' || key === 'Delete' || key === 'Tab' ||
|
||||
key === 'Enter' || key === 'ArrowLeft' || key === 'ArrowRight' ||
|
||||
key === 'ArrowUp' || key === 'ArrowDown' || key === 'Home' || key === 'End') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (allowedKeys.includes(key)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key >= '0' && key <= '9') {
|
||||
return true;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Форматирование номера телефона
|
||||
function formatPhoneNumber(input) {
|
||||
let value = input.value.replace(/\D/g, '');
|
||||
|
||||
// Если номер начинается с 8 или 7, заменяем на +7
|
||||
if (value.length > 0) {
|
||||
if (value[0] === '8') {
|
||||
value = '7' + value.substring(1);
|
||||
}
|
||||
|
||||
// Форматируем номер
|
||||
if (value.length <= 11) {
|
||||
let formattedValue = '';
|
||||
|
||||
if (value.length > 0) {
|
||||
formattedValue = '+7';
|
||||
|
||||
if (value.length > 1) {
|
||||
formattedValue += ' (' + value.substring(1, Math.min(4, value.length));
|
||||
|
||||
if (value.length >= 4) {
|
||||
formattedValue += ') ';
|
||||
|
||||
if (value.length >= 7) {
|
||||
formattedValue += value.substring(4, 7);
|
||||
|
||||
if (value.length >= 9) {
|
||||
formattedValue += '-' + value.substring(7, 9);
|
||||
|
||||
if (value.length >= 11) {
|
||||
formattedValue += '-' + value.substring(9, 11);
|
||||
}
|
||||
}
|
||||
} else if (value.length > 4) {
|
||||
formattedValue += value.substring(4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input.value = formattedValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем валидность номера
|
||||
validatePhoneNumber(input);
|
||||
}
|
||||
|
||||
// Валидация номера телефона
|
||||
function validatePhoneNumber(input) {
|
||||
const phoneValue = input.value.replace(/\D/g, '');
|
||||
const phoneError = document.getElementById('phoneError');
|
||||
const phoneErrorMessage = document.getElementById('phoneErrorMessage');
|
||||
const phoneHint = document.getElementById('phoneHint');
|
||||
|
||||
if (phoneValue.length === 0) {
|
||||
// Пустое поле - допустимо
|
||||
phoneError.style.display = 'none';
|
||||
phoneHint.style.display = 'flex';
|
||||
input.style.borderColor = 'var(--light-brown)';
|
||||
return true;
|
||||
}
|
||||
|
||||
if (phoneValue.length === 11 && phoneValue[0] === '7') {
|
||||
// Валидный номер
|
||||
phoneError.style.display = 'none';
|
||||
phoneHint.style.display = 'flex';
|
||||
input.style.borderColor = 'var(--success-green)';
|
||||
return true;
|
||||
} else {
|
||||
// Невалидный номер
|
||||
phoneError.style.display = 'flex';
|
||||
phoneHint.style.display = 'none';
|
||||
input.style.borderColor = 'var(--error-red)';
|
||||
|
||||
if (phoneValue.length !== 11) {
|
||||
phoneErrorMessage.textContent = 'Номер должен содержать 11 цифр (включая код страны)';
|
||||
} else if (phoneValue[0] !== '7') {
|
||||
phoneErrorMessage.textContent = 'Номер должен начинаться с 7 или 8';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Валидация формы
|
||||
document.getElementById('supplierForm').addEventListener('submit', function(e) {
|
||||
const inn = document.getElementById('inn').value;
|
||||
const name = document.getElementById('name').value;
|
||||
const director = document.getElementById('director').value;
|
||||
const phone = document.getElementById('phone').value;
|
||||
|
||||
// Проверка на наличие цифр в названии компании
|
||||
if (name.match(/[0-9]/)) {
|
||||
e.preventDefault();
|
||||
alert('Название компании не должно содержать цифры');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверка на наличие цифр в ФИО директора
|
||||
if (director && director.match(/[0-9]/)) {
|
||||
e.preventDefault();
|
||||
alert('ФИО директора не должно содержать цифры');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверка ИНН
|
||||
if (inn && !/^\d{10,12}$/.test(inn)) {
|
||||
e.preventDefault();
|
||||
alert('ИНН должен содержать 10 или 12 цифр');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверка телефона
|
||||
if (phone) {
|
||||
const phoneDigits = phone.replace(/\D/g, '');
|
||||
if (phoneDigits.length > 0) {
|
||||
if (phoneDigits.length !== 11 || phoneDigits[0] !== '7') {
|
||||
e.preventDefault();
|
||||
alert('Пожалуйста, введите корректный номер телефона в формате: +7 (XXX) XXX-XX-XX');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка рейтинга
|
||||
const rating = parseFloat(document.getElementById('rating').value);
|
||||
if (rating < 0 || rating > 5) {
|
||||
e.preventDefault();
|
||||
alert('Рейтинг должен быть от 0.0 до 5.0');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверка email
|
||||
const email = document.getElementById('email').value;
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
e.preventDefault();
|
||||
alert('Введите корректный email адрес');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Форматирование телефона при загрузке страницы
|
||||
window.addEventListener('load', function() {
|
||||
const phoneInput = document.getElementById('phone');
|
||||
if (phoneInput.value) {
|
||||
formatPhoneNumber(phoneInput);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
454
templates/edit_product.html
Normal file
454
templates/edit_product.html
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Редактирование материала{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<!-- Хлебные крошки -->
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('catalog') }}">Каталог материалов</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('catalog') }}?search={{ product.product_name }}">{{ product.product_name }}</a></li>
|
||||
<li class="breadcrumb-item active">Редактирование</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="form-card">
|
||||
<div class="form-card-header">
|
||||
<h2 class="form-card-title">
|
||||
<i class="fas fa-edit"></i> Редактирование материала
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="form-card-body">
|
||||
<!-- Flash сообщения -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show">
|
||||
<i class="fas fa-{{ 'exclamation-circle' if category == 'error' else 'check-circle' }} me-2"></i>
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" id="materialForm">
|
||||
<div class="row">
|
||||
<!-- Левая колонка -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="section-title">
|
||||
<i class="fas fa-info-circle"></i> Основная информация
|
||||
</h5>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="product_name" class="form-label">
|
||||
<i class="fas fa-tag"></i>
|
||||
Название материала
|
||||
</label>
|
||||
<input type="text" class="form-control modern-input" id="product_name" name="product_name"
|
||||
value="{{ product.product_name }}" required maxlength="255">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="article" class="form-label">
|
||||
<i class="fas fa-barcode"></i>
|
||||
Артикул
|
||||
</label>
|
||||
<input type="text" class="form-control modern-input" id="article" name="article"
|
||||
value="{{ product.article }}" required maxlength="100">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="category_id" class="form-label">
|
||||
<i class="fas fa-folder"></i>
|
||||
Категория
|
||||
</label>
|
||||
<div class="select-wrapper">
|
||||
<select class="form-select modern-select" id="category_id" name="category_id" required>
|
||||
<option value="" disabled>Выберите категорию</option>
|
||||
{% for category in categories %}
|
||||
<option value="{{ category.id }}" {% if category.id == product.category_id %}selected{% endif %}>
|
||||
{{ category.category_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<i class="fas fa-chevron-down select-arrow"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Правая колонка -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="section-title">
|
||||
<i class="fas fa-cog"></i> Характеристики
|
||||
</h5>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="material_id" class="form-label">
|
||||
<i class="fas fa-cube"></i>
|
||||
Материал
|
||||
</label>
|
||||
<div class="select-wrapper">
|
||||
<select class="form-select modern-select" id="material_id" name="material_id">
|
||||
<option value="">Не выбран</option>
|
||||
{% for material in materials %}
|
||||
<option value="{{ material.id }}" {% if material.id == product.material_id %}selected{% endif %}>
|
||||
{{ material.material_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<i class="fas fa-chevron-down select-arrow"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="min_price" class="form-label">
|
||||
<i class="fas fa-ruble-sign"></i>
|
||||
Цена
|
||||
</label>
|
||||
<input type="number" class="form-control modern-input" id="min_price" name="min_price"
|
||||
min="0" step="0.01" required value="{{ product.min_price }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="in_stock" class="form-label">
|
||||
<i class="fas fa-warehouse"></i>
|
||||
Наличие на складе
|
||||
</label>
|
||||
<input type="number" class="form-control modern-input" id="in_stock" name="in_stock"
|
||||
min="0" value="{{ product.in_stock or 0 }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Описание на всю ширину -->
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
<label for="description" class="form-label">
|
||||
<i class="fas fa-align-left"></i>
|
||||
Описание материала
|
||||
</label>
|
||||
<textarea class="form-control modern-input" id="description" name="description"
|
||||
rows="4" maxlength="1000">{{ product.description or '' }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки -->
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('catalog') }}" class="btn btn-outline">
|
||||
<i class="fas fa-times"></i> Отмена
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Сохранить изменения
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* ===== ОСНОВНЫЕ ПЕРЕМЕННЫЕ ===== */
|
||||
:root {
|
||||
--antique-white: #FAEBD7;
|
||||
--light-brown: #DEB887;
|
||||
--brown: #A0522D;
|
||||
--dark-brown: #8B4513;
|
||||
}
|
||||
|
||||
/* ===== ХЛЕБНЫЕ КРОШКИ ===== */
|
||||
.breadcrumb {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.breadcrumb-item a {
|
||||
color: var(--brown);
|
||||
text-decoration: none;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.breadcrumb-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
color: #6c757d;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
color: var(--light-brown);
|
||||
}
|
||||
|
||||
/* ===== КАРТОЧКА ФОРМЫ ===== */
|
||||
.form-card {
|
||||
background: white;
|
||||
border: 1px solid var(--light-brown);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(160, 82, 45, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-card-header {
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%);
|
||||
padding: 20px 25px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-card-title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-card-title i {
|
||||
margin-right: 10px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-card-body {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
/* ===== ЗАГОЛОВКИ СЕКЦИЙ ===== */
|
||||
.section-title {
|
||||
color: var(--brown);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid var(--light-brown);
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.section-title i {
|
||||
color: var(--brown);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* ===== ЭЛЕМЕНТЫ ФОРМЫ ===== */
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--brown);
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 6px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.form-label i {
|
||||
color: var(--brown);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.modern-input {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
padding: 0 12px;
|
||||
border: 2px solid var(--light-brown);
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
transition: all 0.2s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.modern-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--brown);
|
||||
box-shadow: 0 0 0 3px rgba(160, 82, 45, 0.1);
|
||||
}
|
||||
|
||||
.modern-input:hover {
|
||||
border-color: var(--brown);
|
||||
}
|
||||
|
||||
textarea.modern-input {
|
||||
height: auto;
|
||||
padding: 12px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modern-select {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
padding: 0 35px 0 12px;
|
||||
border: 2px solid var(--light-brown);
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.modern-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--brown);
|
||||
box-shadow: 0 0 0 3px rgba(160, 82, 45, 0.1);
|
||||
}
|
||||
|
||||
.modern-select:hover {
|
||||
border-color: var(--brown);
|
||||
}
|
||||
|
||||
.select-arrow {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--brown);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ===== КНОПКИ ===== */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 25px;
|
||||
padding-top: 20px;
|
||||
border-top: 2px solid var(--light-brown);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 25px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--brown);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--dark-brown);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(160, 82, 45, 0.3);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: white;
|
||||
border: 2px solid var(--light-brown);
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--antique-white);
|
||||
border-color: var(--brown);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* ===== АЛЕРТЫ ===== */
|
||||
.alert {
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--light-brown);
|
||||
font-family: 'Verdana', sans-serif;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: var(--antique-white);
|
||||
color: var(--brown);
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: var(--antique-white);
|
||||
color: #721c24;
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
|
||||
/* ===== ИКОНКИ ===== */
|
||||
i {
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.btn-primary i, .btn-outline i, .form-card-header i {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ===== АДАПТИВНОСТЬ ===== */
|
||||
@media (max-width: 768px) {
|
||||
.form-card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column-reverse;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.form-actions .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.col-md-6 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.section-title:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.getElementById('materialForm').addEventListener('submit', function(e) {
|
||||
const price = parseFloat(document.getElementById('min_price').value);
|
||||
const name = document.getElementById('product_name').value.trim();
|
||||
const article = document.getElementById('article').value.trim();
|
||||
|
||||
if (!name) {
|
||||
e.preventDefault();
|
||||
alert('Введите название материала');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!article) {
|
||||
e.preventDefault();
|
||||
alert('Введите артикул материала');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (price <= 0) {
|
||||
e.preventDefault();
|
||||
alert('Цена должна быть больше 0');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
16
templates/home.html
Normal file
16
templates/home.html
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Flask Tutorial</title>
|
||||
</head>
|
||||
<body>
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
|
||||
<h1> My First Try Using Flask </h1>
|
||||
<p> Flask is Fun </p>
|
||||
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
547
templates/index.html
Normal file
547
templates/index.html
Normal file
|
|
@ -0,0 +1,547 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Поставщики строительных материалов{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Заголовок и кнопка добавления -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="page-title">Поставщики материалов</h2>
|
||||
<a href="{{ url_for('add_client') }}" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Новый поставщик
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Поиск и сортировка - УЛУЧШЕННЫЙ СТИЛЬ -->
|
||||
<div class="search-section mb-4">
|
||||
<div class="search-header">
|
||||
<h4 class="search-title">
|
||||
<i class="fas fa-sliders-h"></i> Поиск и сортировка
|
||||
</h4>
|
||||
<a href="{{ url_for('index') }}" class="btn-clear">
|
||||
<i class="fas fa-times"></i> Сбросить все
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="GET" action="{{ url_for('index') }}" id="filterForm" class="search-form">
|
||||
<!-- Строка поиска -->
|
||||
<div class="search-row">
|
||||
<div class="search-input-wrapper">
|
||||
<i class="fas fa-search search-icon"></i>
|
||||
<input type="text" class="search-input" name="search" id="search"
|
||||
placeholder="Поиск по названию или ИНН..."
|
||||
value="{{ request.args.get('search', '') }}" autocomplete="off">
|
||||
{% if request.args.get('search') %}
|
||||
<a href="{{ url_for('index', sort=request.args.get('sort'), type=request.args.get('type'), rating=request.args.get('rating')) }}" class="search-clear">
|
||||
<i class="fas fa-times-circle"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Фильтры -->
|
||||
<div class="filters-row">
|
||||
<div class="filter-group">
|
||||
<label for="type" class="filter-label">
|
||||
<i class="fas fa-building"></i> Тип поставщика
|
||||
</label>
|
||||
<select class="filter-select" name="type" id="type">
|
||||
<option value="all" {% if request.args.get('type') == 'all' or not request.args.get('type') %}selected{% endif %}>Все типы</option>
|
||||
<option value="ЗАО" {% if request.args.get('type') == 'ЗАО' %}selected{% endif %}>ЗАО</option>
|
||||
<option value="ОАО" {% if request.args.get('type') == 'ОАО' %}selected{% endif %}>ОАО</option>
|
||||
<option value="ООО" {% if request.args.get('type') == 'ООО' %}selected{% endif %}>ООО</option>
|
||||
<option value="ПАО" {% if request.args.get('type') == 'ПАО' %}selected{% endif %}>ПАО</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="sort" class="filter-label">
|
||||
<i class="fas fa-sort-amount-down"></i> Сортировка
|
||||
</label>
|
||||
<select class="filter-select" name="sort" id="sort">
|
||||
<option value="name_asc" {% if request.args.get('sort') == 'name_asc' or not request.args.get('sort') %}selected{% endif %}>Название (А-Я)</option>
|
||||
<option value="name_desc" {% if request.args.get('sort') == 'name_desc' %}selected{% endif %}>Название (Я-А)</option>
|
||||
<option value="rating_desc" {% if request.args.get('sort') == 'rating_desc' %}selected{% endif %}>Сначала высокий рейтинг</option>
|
||||
<option value="rating_asc" {% if request.args.get('sort') == 'rating_asc' %}selected{% endif %}>Сначала низкий рейтинг</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="rating" class="filter-label">
|
||||
<i class="fas fa-star"></i> Мин. рейтинг
|
||||
</label>
|
||||
<select class="filter-select" name="rating" id="rating">
|
||||
<option value="0" {% if request.args.get('rating') == '0' or not request.args.get('rating') %}selected{% endif %}>Любой</option>
|
||||
<option value="4" {% if request.args.get('rating') == '4' %}selected{% endif %}>4+</option>
|
||||
<option value="4.5" {% if request.args.get('rating') == '4.5' %}selected{% endif %}>4.5+</option>
|
||||
<option value="4.8" {% if request.args.get('rating') == '4.8' %}selected{% endif %}>4.8+</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-apply">
|
||||
<i class="fas fa-check"></i> Применить
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Активные фильтры -->
|
||||
{% if request.args.get('search') or (request.args.get('type') and request.args.get('type') != 'all') or (request.args.get('rating') and request.args.get('rating') != '0') %}
|
||||
<div class="active-filters mb-3">
|
||||
<span class="active-filters-label">Активные фильтры:</span>
|
||||
<div class="filter-tags">
|
||||
{% if request.args.get('search') %}
|
||||
<span class="filter-tag">
|
||||
Поиск: "{{ request.args.get('search') }}"
|
||||
<a href="{{ url_for('index', sort=request.args.get('sort'), type=request.args.get('type'), rating=request.args.get('rating')) }}" class="remove-tag">
|
||||
<i class="fas fa-times"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if request.args.get('type') and request.args.get('type') != 'all' %}
|
||||
<span class="filter-tag">
|
||||
Тип: {{ request.args.get('type') }}
|
||||
<a href="{{ url_for('index', search=request.args.get('search'), sort=request.args.get('sort'), rating=request.args.get('rating')) }}" class="remove-tag">
|
||||
<i class="fas fa-times"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if request.args.get('rating') and request.args.get('rating') != '0' %}
|
||||
<span class="filter-tag">
|
||||
Рейтинг ≥ {{ request.args.get('rating') }}
|
||||
<a href="{{ url_for('index', search=request.args.get('search'), sort=request.args.get('sort'), type=request.args.get('type')) }}" class="remove-tag">
|
||||
<i class="fas fa-times"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Список поставщиков -->
|
||||
{% if partners %}
|
||||
<div class="row">
|
||||
{% for partner in partners %}
|
||||
{% include 'components/partner_card.html' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Информация о результатах поиска -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<p class="text-muted">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Найдено поставщиков: <strong>{{ partners|length }}</strong>
|
||||
{% if request.args.get('search') %}
|
||||
по запросу "{{ request.args.get('search') }}"
|
||||
{% endif %}
|
||||
{% if request.args.get('type') and request.args.get('type') != 'all' %}
|
||||
, тип: {{ request.args.get('type') }}
|
||||
{% endif %}
|
||||
{% if request.args.get('rating') and request.args.get('rating') != '0' %}
|
||||
, рейтинг ≥ {{ request.args.get('rating') }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
{% if request.args.get('search') or request.args.get('type') != 'all' or request.args.get('rating') != '0' %}
|
||||
<strong>Ничего не найдено</strong> по вашему запросу.
|
||||
<br><a href="{{ url_for('index') }}" class="alert-link">Сбросить все фильтры</a>
|
||||
{% else %}
|
||||
Нет данных о поставщиках. <a href="{{ url_for('add_client') }}" class="alert-link">Добавьте первого поставщика</a>.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script>
|
||||
// Автоматическое применение при изменении фильтров
|
||||
let filterTimeout;
|
||||
|
||||
document.getElementById('sort').addEventListener('change', function() {
|
||||
clearTimeout(filterTimeout);
|
||||
filterTimeout = setTimeout(() => {
|
||||
document.getElementById('filterForm').submit();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
document.getElementById('type').addEventListener('change', function() {
|
||||
clearTimeout(filterTimeout);
|
||||
filterTimeout = setTimeout(() => {
|
||||
document.getElementById('filterForm').submit();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
document.getElementById('rating').addEventListener('change', function() {
|
||||
clearTimeout(filterTimeout);
|
||||
filterTimeout = setTimeout(() => {
|
||||
document.getElementById('filterForm').submit();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Поиск с debounce
|
||||
let searchTimeout;
|
||||
document.getElementById('search').addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
if (this.value.length >= 2 || this.value.length === 0) {
|
||||
document.getElementById('filterForm').submit();
|
||||
}
|
||||
}, 800);
|
||||
});
|
||||
|
||||
document.getElementById('search').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
document.getElementById('filterForm').submit();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page-title {
|
||||
color: #A0522D;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #A0522D;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 20px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #8B4513;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(160, 82, 45, 0.2);
|
||||
}
|
||||
|
||||
/* ===== УЛУЧШЕННЫЙ ПОИСК И ФИЛЬТРЫ (КАК В КАТАЛОГЕ) ===== */
|
||||
.search-section {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
padding: 20px;
|
||||
margin-bottom: 25px;
|
||||
border: 1px solid #DEB887;
|
||||
}
|
||||
|
||||
.search-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-title i {
|
||||
color: #A0522D;
|
||||
}
|
||||
|
||||
.btn-clear {
|
||||
background: none;
|
||||
border: 1px solid #DEB887;
|
||||
padding: 8px 16px;
|
||||
border-radius: 30px;
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.btn-clear:hover {
|
||||
background: #FAEBD7;
|
||||
border-color: #A0522D;
|
||||
color: #A0522D;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.search-row {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #adb5bd;
|
||||
font-size: 1rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
padding: 0 45px 0 45px;
|
||||
border: 2px solid #DEB887;
|
||||
border-radius: 30px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #A0522D;
|
||||
box-shadow: 0 0 0 4px rgba(160, 82, 45, 0.1);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.search-clear {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #adb5bd;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-clear:hover {
|
||||
color: #A0522D;
|
||||
}
|
||||
|
||||
.filters-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 6px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.filter-label i {
|
||||
color: #A0522D;
|
||||
margin-right: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
padding: 0 35px 0 12px;
|
||||
border: 2px solid #DEB887;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
color: #2c3e50;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23A0522D' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
background-size: 16px;
|
||||
}
|
||||
|
||||
.filter-select:hover {
|
||||
border-color: #A0522D;
|
||||
}
|
||||
|
||||
.filter-select:focus {
|
||||
outline: none;
|
||||
border-color: #A0522D;
|
||||
box-shadow: 0 0 0 3px rgba(160, 82, 45, 0.1);
|
||||
}
|
||||
|
||||
.btn-apply {
|
||||
background: #A0522D;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0 25px;
|
||||
height: 42px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-apply:hover {
|
||||
background: #8B4513;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(160, 82, 45, 0.3);
|
||||
}
|
||||
|
||||
/* Активные фильтры */
|
||||
.active-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px 15px;
|
||||
background: #FAEBD7;
|
||||
border-radius: 30px;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #DEB887;
|
||||
}
|
||||
|
||||
.active-filters-label {
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-tag {
|
||||
background: white;
|
||||
border: 1px solid #DEB887;
|
||||
padding: 6px 12px;
|
||||
border-radius: 30px;
|
||||
font-size: 0.9rem;
|
||||
color: #2c3e50;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.remove-tag {
|
||||
color: #adb5bd;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.remove-tag:hover {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
/* ===== СТИЛИ ДЛЯ КАРТОЧЕК ПОСТАВЩИКОВ ===== */
|
||||
.card {
|
||||
border: 1px solid #DEB887;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
font-family: 'Verdana', sans-serif;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #DEB887;
|
||||
background: #FAEBD7;
|
||||
border-left: 4px solid #A0522D;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.alert-link {
|
||||
color: #A0522D;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.alert-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
i {
|
||||
color: #A0522D;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.btn-primary i, .btn-clear i, .btn-apply i {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Стили для карточек поставщиков */
|
||||
.partner-card {
|
||||
background: white;
|
||||
border: 1px solid #DEB887;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.partner-card:hover {
|
||||
box-shadow: 0 8px 16px rgba(160, 82, 45, 0.1);
|
||||
border-color: #A0522D;
|
||||
}
|
||||
|
||||
/* ===== АДАПТИВНОСТЬ ===== */
|
||||
@media (max-width: 768px) {
|
||||
.filters-row {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-apply {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.active-filters {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
266
templates/materials.html
Normal file
266
templates/materials.html
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Материалы{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Заголовок -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="page-title">Материалы</h2>
|
||||
<span class="badge" style="background: linear-gradient(135deg, var(--steel-blue) 0%, #3a6d99 100%); color: white; padding: 8px 15px; border-radius: 20px; font-size: 1rem;">
|
||||
<i class="fas fa-cube me-1"></i> Всего: {{ materials|length }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Список материалов -->
|
||||
{% if materials %}
|
||||
<div class="row">
|
||||
{% for material in materials %}
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="material-card">
|
||||
<div class="material-card-header">
|
||||
<h5 class="material-title">
|
||||
<i class="fas fa-cube"></i> {{ material.material_name }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="material-card-body">
|
||||
{% if material.description %}
|
||||
<p class="material-description">{{ material.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="material-properties">
|
||||
{% if material.category %}
|
||||
<div class="property-item">
|
||||
<span class="property-label">Категория:</span>
|
||||
<span class="property-value">{{ material.category }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if material.color %}
|
||||
<div class="property-item">
|
||||
<span class="property-label">Цвет:</span>
|
||||
<span class="property-value">{{ material.color }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if material.density %}
|
||||
<div class="property-item">
|
||||
<span class="property-label">Плотность:</span>
|
||||
<span class="property-value">{{ material.density }} кг/м³</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if material.thickness %}
|
||||
<div class="property-item">
|
||||
<span class="property-label">Толщина:</span>
|
||||
<span class="property-value">{{ material.thickness }} мм</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if material.supplier %}
|
||||
<div class="property-item">
|
||||
<span class="property-label">Поставщик:</span>
|
||||
<span class="property-value">{{ material.supplier }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if material.country %}
|
||||
<div class="property-item">
|
||||
<span class="property-label">Страна:</span>
|
||||
<span class="property-value">{{ material.country }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if material.wear_class %}
|
||||
<div class="property-item">
|
||||
<span class="property-label">Класс износа:</span>
|
||||
<span class="property-value">{{ material.wear_class }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="material-price-stock">
|
||||
{% if material.price_per_unit %}
|
||||
<div class="price-item">
|
||||
<span class="price-label">Цена:</span>
|
||||
<span class="price-value">{{ material.price_per_unit|int }} руб./{{ material.unit or 'шт' }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="stock-item">
|
||||
<span class="stock-label">В наличии:</span>
|
||||
<span class="stock-value {% if material.in_stock and material.in_stock > 0 %}text-success{% else %}text-danger{% endif %}">
|
||||
{{ material.in_stock or 0 }} {{ material.unit or 'шт' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="material-footer">
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt"></i> Добавлено: {{ material.created_at[:10] if material.created_at else '-' }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info text-center py-5">
|
||||
<i class="fas fa-cube fa-3x mb-3" style="color: var(--light-steel-blue);"></i>
|
||||
<h4 class="mb-3">Нет материалов</h4>
|
||||
<p class="mb-0">В базе данных пока нет материалов. Добавьте материалы через импорт данных.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<style>
|
||||
.page-title {
|
||||
color: var(--steel-blue);
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Franklin Gothic', 'Arial Narrow', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.material-card {
|
||||
background: linear-gradient(135deg, var(--alice-blue) 0%, #E6F2FF 100%);
|
||||
border: 1px solid var(--light-steel-blue);
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
height: 100%;
|
||||
box-shadow: 0 4px 8px rgba(70, 130, 180, 0.1);
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.material-card:hover {
|
||||
box-shadow: 0 8px 16px rgba(70, 130, 180, 0.2);
|
||||
border-color: var(--steel-blue);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.material-card-header {
|
||||
border-bottom: 1px solid var(--light-steel-blue);
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.material-title {
|
||||
color: var(--steel-blue);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
font-family: 'Franklin Gothic', 'Arial Narrow', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.material-title i {
|
||||
margin-right: 8px;
|
||||
color: var(--light-steel-blue);
|
||||
}
|
||||
|
||||
.material-description {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 12px;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px dashed var(--light-steel-blue);
|
||||
}
|
||||
|
||||
.material-properties {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.property-item {
|
||||
margin-bottom: 5px;
|
||||
font-size: 0.9rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.property-label {
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.property-value {
|
||||
color: #2c3e50;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.material-price-stock {
|
||||
background-color: rgba(70, 130, 180, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.price-item, .stock-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.price-label, .stock-label {
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
color: var(--steel-blue);
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.stock-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #28a745 !important;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #dc3545 !important;
|
||||
}
|
||||
|
||||
.material-footer {
|
||||
margin-top: auto;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--light-steel-blue);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
border-radius: 12px;
|
||||
border: 2px dashed var(--light-steel-blue);
|
||||
background: linear-gradient(135deg, var(--alice-blue) 0%, #E6F2FF 100%);
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.alert-info i {
|
||||
color: var(--steel-blue);
|
||||
}
|
||||
|
||||
.alert-info h4 {
|
||||
color: var(--steel-blue);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-family: 'Franklin Gothic', 'Arial Narrow', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.col-md-4 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.property-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
522
templates/order_history.html
Normal file
522
templates/order_history.html
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}История заказов - {{ client.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<!-- Хлебные крошки -->
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('index') }}">Поставщики</a></li>
|
||||
<li class="breadcrumb-item active">{{ client.name }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Заголовок с информацией о клиенте -->
|
||||
<div class="mb-4">
|
||||
<h2 class="page-title">История заказов - {{ client.name }}</h2>
|
||||
<div class="mt-2">
|
||||
{% if client.rating %}
|
||||
<span class="rating-badge">Рейтинг: {{ client.rating }}</span>
|
||||
{% endif %}
|
||||
{% if total_stats %}
|
||||
<span class="stats-badge">Всего заказов: {{ total_stats.total_orders }}</span>
|
||||
<span class="stats-badge">Выполнено: {{ total_stats.completed_orders }}</span>
|
||||
<span class="stats-badge">Общая сумма: {{ "%.2f"|format(total_stats.total_amount or 0) }} руб.</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопка добавления заказа -->
|
||||
<div class="mb-3">
|
||||
<a href="{{ url_for('add_order', client_id=client.id) }}" class="btn-custom">
|
||||
<i class="fas fa-plus"></i> Добавить заказ
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Детальная таблица заказов -->
|
||||
<div class="table-responsive">
|
||||
<table class="custom-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID заказа</th>
|
||||
<th>Дата</th>
|
||||
<th>Товары</th>
|
||||
<th>Базовая стоимость</th>
|
||||
<th>Транспортные расходы (10%)</th>
|
||||
<th>Скидка</th>
|
||||
<th>Итоговая стоимость</th>
|
||||
<th>Статус</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for order in orders %}
|
||||
<tr>
|
||||
<td>{{ order.id }}</td>
|
||||
<td>{{ order.order_date }}</td>
|
||||
<td>
|
||||
<button class="btn-toggle" type="button"
|
||||
onclick="toggleDetails('products-{{ order.id }}')">
|
||||
<i class="fas fa-eye"></i> {{ order.items_count }} товар(ов)
|
||||
</button>
|
||||
</td>
|
||||
<td>{{ "%.2f"|format(order.base_amount) }} руб.</td>
|
||||
<td>{{ "%.2f"|format(order.shipping_cost) }} руб.</td>
|
||||
<td>
|
||||
{% if order.discount and order.discount > 0 %}
|
||||
<span class="discount-badge">{{ order.discount }}%</span>
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><strong>{{ "%.2f"|format(order.display_total) }} руб.</strong></td>
|
||||
<td>
|
||||
<span class="status-badge
|
||||
{% if order.status == 'Выполнен' %}status-success
|
||||
{% elif order.status == 'Отменен' %}status-danger
|
||||
{% elif order.status == 'В обработке' %}status-warning
|
||||
{% elif order.status == 'Новый' %}status-new
|
||||
{% else %}status-default{% endif %}">
|
||||
{{ order.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<form action="{{ url_for('update_order_status', order_id=order.id) }}" method="POST" class="d-inline">
|
||||
<select name="status" class="status-select" onchange="this.form.submit()">
|
||||
<option value="Новый" {% if order.status == 'Новый' %}selected{% endif %}>Новый</option>
|
||||
<option value="В обработке" {% if order.status == 'В обработке' %}selected{% endif %}>В обработке</option>
|
||||
<option value="Выполнен" {% if order.status == 'Выполнен' %}selected{% endif %}>Выполнен</option>
|
||||
<option value="Отменен" {% if order.status == 'Отменен' %}selected{% endif %}>Отменен</option>
|
||||
</select>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="products-{{ order.id }}" class="details-row" style="display: none;">
|
||||
<td colspan="9">
|
||||
<div class="details-container">
|
||||
<div class="details-header">
|
||||
<h6>Детали заказа №{{ order.id }}</h6>
|
||||
<button class="btn-close" onclick="toggleDetails('products-{{ order.id }}')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<table class="details-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Товар</th>
|
||||
<th>Артикул</th>
|
||||
<th>Категория</th>
|
||||
<th>Количество</th>
|
||||
<th>Цена за ед.</th>
|
||||
<th>Стоимость</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if order.order_items and order.order_items|length > 0 %}
|
||||
{% for item in order.order_items %}
|
||||
<tr>
|
||||
<td>{{ item.product_name }}</td>
|
||||
<td>{{ item.article or '—' }}</td>
|
||||
<td>{{ item.category_name or '—' }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td>{{ "%.2f"|format(item.price_at_moment) }} руб.</td>
|
||||
<td>{{ "%.2f"|format(item.item_total) }} руб.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">Нет данных о товарах</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="5" class="text-end"><strong>Базовая стоимость:</strong></td>
|
||||
<td><strong>{{ "%.2f"|format(order.base_amount) }} руб.</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="5" class="text-end">Транспортные расходы (10%):</td>
|
||||
<td>+ {{ "%.2f"|format(order.shipping_cost) }} руб.</td>
|
||||
</tr>
|
||||
{% if order.discount and order.discount > 0 %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-end">Скидка ({{ order.discount }}%):</td>
|
||||
<td>- {{ "%.2f"|format(((order.base_amount + order.shipping_cost) * order.discount / 100)) }} руб.</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr class="total-row">
|
||||
<td colspan="5" class="text-end"><strong>ИТОГО к оплате:</strong></td>
|
||||
<td><strong class="total-amount">{{ "%.2f"|format(order.display_total) }} руб.</strong></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="9" class="text-center py-4">
|
||||
<p class="text-muted mb-2">У этого клиента пока нет заказов</p>
|
||||
<a href="{{ url_for('add_order', client_id=client.id) }}" class="btn-custom btn-sm">
|
||||
<i class="fas fa-plus"></i> Создать первый заказ
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<a href="{{ url_for('index') }}" class="btn-custom">
|
||||
<i class="fas fa-arrow-left"></i> Назад к поставщикам
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Основные цвета */
|
||||
:root {
|
||||
--antique-white: #FAEBD7;
|
||||
--light-brown: #DEB887;
|
||||
--brown: #A0522D;
|
||||
--white: #ffffff;
|
||||
--text-dark: #333333;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--antique-white);
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: var(--antique-white);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* Хлебные крошки */
|
||||
.breadcrumb {
|
||||
background-color: var(--white);
|
||||
border: 2px solid var(--light-brown);
|
||||
border-radius: 8px;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.breadcrumb-item a {
|
||||
color: var(--brown);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
color: var(--light-brown);
|
||||
}
|
||||
|
||||
/* Заголовок */
|
||||
.page-title {
|
||||
color: var(--brown);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
border-bottom: 2px solid var(--light-brown);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Бейджи */
|
||||
.rating-badge, .stats-badge {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
margin-right: 8px;
|
||||
border: 1px solid var(--light-brown);
|
||||
background-color: var(--white);
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn-custom {
|
||||
background: var(--white);
|
||||
border: 2px solid var(--light-brown);
|
||||
color: var(--brown);
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-custom:hover {
|
||||
background: var(--light-brown);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.btn-custom i {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.btn-custom.btn-sm {
|
||||
padding: 5px 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-toggle {
|
||||
background: none;
|
||||
border: 1px solid var(--light-brown);
|
||||
color: var(--brown);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-toggle:hover {
|
||||
background-color: var(--light-brown);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: none;
|
||||
border: 1px solid var(--light-brown);
|
||||
color: var(--brown);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-close:hover {
|
||||
background-color: var(--light-brown);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
/* Таблица */
|
||||
.custom-table {
|
||||
width: 100%;
|
||||
background: var(--white);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 2px solid var(--light-brown);
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.custom-table thead th {
|
||||
background: var(--brown);
|
||||
color: var(--white);
|
||||
padding: 12px;
|
||||
font-weight: 500;
|
||||
border-bottom: 2px solid var(--light-brown);
|
||||
}
|
||||
|
||||
.custom-table tbody td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--light-brown);
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.custom-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.custom-table tbody tr:hover {
|
||||
background-color: var(--antique-white);
|
||||
}
|
||||
|
||||
/* Бейджи статусов */
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border-color: #c3e6cb;
|
||||
}
|
||||
|
||||
.status-danger {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
border-color: #ffeeba;
|
||||
}
|
||||
|
||||
.status-new {
|
||||
background-color: #cce5ff;
|
||||
color: #004085;
|
||||
border-color: #b8daff;
|
||||
}
|
||||
|
||||
.status-default {
|
||||
background-color: #e2e3e5;
|
||||
color: #383d41;
|
||||
border-color: #d6d8db;
|
||||
}
|
||||
|
||||
/* Бейдж скидки */
|
||||
.discount-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--light-brown);
|
||||
color: var(--white);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Селект статуса */
|
||||
.status-select {
|
||||
border: 1px solid var(--light-brown);
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
color: var(--brown);
|
||||
background-color: var(--white);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status-select:hover {
|
||||
background-color: var(--antique-white);
|
||||
}
|
||||
|
||||
/* Детали заказа */
|
||||
.details-row {
|
||||
background-color: var(--antique-white);
|
||||
}
|
||||
|
||||
.details-container {
|
||||
padding: 20px;
|
||||
border: 2px solid var(--light-brown);
|
||||
border-radius: 8px;
|
||||
margin: 10px;
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.details-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid var(--light-brown);
|
||||
}
|
||||
|
||||
.details-header h6 {
|
||||
color: var(--brown);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Детальная таблица */
|
||||
.details-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.details-table th {
|
||||
background-color: var(--antique-white);
|
||||
color: var(--brown);
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
border-bottom: 2px solid var(--light-brown);
|
||||
}
|
||||
|
||||
.details-table td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--light-brown);
|
||||
}
|
||||
|
||||
.details-table tfoot td {
|
||||
background-color: var(--antique-white);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.details-table .total-row {
|
||||
border-top: 2px solid var(--light-brown);
|
||||
}
|
||||
|
||||
.details-table .total-amount {
|
||||
color: var(--brown);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Иконки */
|
||||
i {
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.btn-custom i, .btn-toggle i, .btn-close i {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.custom-table {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.custom-table th, .custom-table td {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.status-select {
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Функция для открытия/закрытия деталей заказа
|
||||
function toggleDetails(elementId) {
|
||||
var element = document.getElementById(elementId);
|
||||
if (element.style.display === "none" || element.style.display === "") {
|
||||
element.style.display = "table-row";
|
||||
// Плавный скролл к открытому элементу
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
} else {
|
||||
element.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Закрытие по клику вне области (опционально)
|
||||
document.addEventListener('click', function(event) {
|
||||
// Можно добавить логику для закрытия при клике вне
|
||||
});
|
||||
|
||||
// Инициализация - скрываем все детали при загрузке
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var detailsRows = document.querySelectorAll('.details-row');
|
||||
detailsRows.forEach(function(row) {
|
||||
row.style.display = 'none';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Добавляем Font Awesome для иконок -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
{% endblock %}
|
||||
410
templates/orders_list.html
Normal file
410
templates/orders_list.html
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Список заказов{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Заголовок и кнопка добавления -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="page-title">Список заказов</h2>
|
||||
<div class="btn-group">
|
||||
<select class="form-select me-2" id="supplierSelect" style="width: 250px;">
|
||||
<option value="">Выберите поставщика</option>
|
||||
{% for client in clients %}
|
||||
<option value="{{ client.id }}">{{ client.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-primary" id="addOrderBtn" onclick="addOrder()">
|
||||
<i class="fas fa-plus"></i> Добавить заказ
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Статистика -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="stats-card">
|
||||
<div class="stat-item">
|
||||
<h5 class="stat-label">Всего заказов</h5>
|
||||
<p class="stat-number">{{ total_stats.total_orders or 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="stats-card">
|
||||
<div class="stat-item">
|
||||
<h5 class="stat-label">Общая сумма</h5>
|
||||
<p class="stat-number">{{ (total_stats.total_amount or 0)|int }} руб.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Таблица заказов -->
|
||||
{% if orders %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Дата</th>
|
||||
<th>Поставщик</th>
|
||||
<th>ИНН</th>
|
||||
<th>Товары</th>
|
||||
<th>Сумма</th>
|
||||
<th>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for order in orders %}
|
||||
<tr>
|
||||
<td>{{ order.id }}</td>
|
||||
<td>{{ order.order_date }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('order_history', client_id=order.client_id) }}">
|
||||
{{ order.client_name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ order.client_inn or '-' }}</td>
|
||||
<td>
|
||||
{% if order.product_name %}
|
||||
<!-- Старая структура - один товар -->
|
||||
<small>{{ order.product_name }}</small>
|
||||
{% elif order.products_list %}
|
||||
<!-- Новая структура - несколько товаров -->
|
||||
<small>{{ order.products_list|truncate(50) }}</small>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="fw-bold">{{ order.total_amount|int }} руб.</td>
|
||||
<td>
|
||||
<span class="badge
|
||||
{% if order.status == 'Выполнен' %}bg-success
|
||||
{% elif order.status == 'Отменен' %}bg-danger
|
||||
{% elif order.status == 'В обработке' %}bg-warning
|
||||
{% else %}bg-secondary{% endif %}">
|
||||
{{ order.status }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Нет данных о заказах. <a href="{{ url_for('index') }}" class="alert-link">Вернуться к поставщикам</a>.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function addOrder() {
|
||||
const supplierId = document.getElementById('supplierSelect').value;
|
||||
if (!supplierId) {
|
||||
alert('Пожалуйста, выберите поставщика');
|
||||
return;
|
||||
}
|
||||
window.location.href = "{{ url_for('add_order', client_id=0) }}".replace('0', supplierId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--antique-white: #FAEBD7;
|
||||
--light-brown: #DEB887;
|
||||
--brown: #A0522D;
|
||||
--dark-brown: #8B4513;
|
||||
--text-dark: #2c3e50;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: var(--brown);
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
border: 2px solid var(--light-brown);
|
||||
border-radius: 8px;
|
||||
padding: 10px 15px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
border-color: var(--brown);
|
||||
box-shadow: 0 0 0 0.2rem rgba(160, 82, 45, 0.25);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 20px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, var(--dark-brown) 0%, var(--brown) 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 10px rgba(160, 82, 45, 0.3);
|
||||
}
|
||||
|
||||
/* Статистика */
|
||||
.stats-card {
|
||||
background: linear-gradient(135deg, var(--antique-white) 0%, #F5DEB3 100%);
|
||||
border: 1px solid var(--light-brown);
|
||||
border-radius: 15px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 4px 10px rgba(160, 82, 45, 0.1);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, white 0%, #f8f9fa 100%);
|
||||
border: 2px solid var(--light-brown);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 8px rgba(160, 82, 45, 0.15);
|
||||
transition: all 0.2s ease;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stat-item:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 15px rgba(160, 82, 45, 0.25);
|
||||
border-color: var(--brown);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
color: var(--dark-brown);
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 5px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--brown);
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Таблица */
|
||||
.table {
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
padding: 12px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background-color: rgba(222, 184, 135, 0.2);
|
||||
}
|
||||
|
||||
.table tbody td {
|
||||
font-family: 'Verdana', sans-serif;
|
||||
color: #2c3e50;
|
||||
vertical-align: middle;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table tbody td:nth-child(3) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table tbody td:nth-child(5) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table a {
|
||||
color: var(--brown);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Бейджи */
|
||||
.badge {
|
||||
padding: 8px 12px;
|
||||
font-size: 0.85rem;
|
||||
border-radius: 20px;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge.bg-success {
|
||||
background: linear-gradient(135deg, #28a745 0%, #218838 100%) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.badge.bg-warning {
|
||||
background: linear-gradient(135deg, var(--light-brown) 0%, var(--brown) 100%) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.badge.bg-danger {
|
||||
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.badge.bg-secondary {
|
||||
background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Карточки статистики */
|
||||
.bg-primary, .bg-success, .bg-info {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.display-6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/* Уведомления */
|
||||
.alert {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--light-brown);
|
||||
background: linear-gradient(135deg, var(--antique-white) 0%, #F5DEB3 100%);
|
||||
border-left: 6px solid var(--brown);
|
||||
font-family: 'Verdana', sans-serif;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.alert-link {
|
||||
color: var(--brown);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.alert-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Иконки */
|
||||
i {
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.btn-primary i {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Адаптивность для мобильных */
|
||||
@media (max-width: 768px) {
|
||||
.table thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table tbody tr {
|
||||
display: block;
|
||||
margin-bottom: 15px;
|
||||
border: 2px solid var(--light-brown);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table tbody td {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px dashed var(--light-brown);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table tbody td:before {
|
||||
content: attr(data-label);
|
||||
font-weight: 600;
|
||||
color: var(--brown);
|
||||
margin-right: 10px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.table tbody td:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
width: 100% !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Добавляем data-label для адаптивности
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const table = document.querySelector('.table tbody');
|
||||
if (table) {
|
||||
const headers = document.querySelectorAll('.table thead th');
|
||||
const rows = table.querySelectorAll('tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const cells = row.querySelectorAll('td');
|
||||
cells.forEach((cell, index) => {
|
||||
if (headers[index]) {
|
||||
cell.setAttribute('data-label', headers[index].textContent.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function addOrder() {
|
||||
const supplierId = document.getElementById('supplierSelect').value;
|
||||
if (!supplierId) {
|
||||
alert('Пожалуйста, выберите поставщика');
|
||||
return;
|
||||
}
|
||||
window.location.href = "{{ url_for('add_order', client_id=0) }}".replace('0', supplierId);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
Reference in a new issue