18.03.2026

This commit is contained in:
parent b4f9488785
commit 99edd3938f
37 changed files with 9400 additions and 0 deletions

BIN
Catalog_import.xlsx Normal file

Binary file not shown.

BIN
Categories_import.xlsx Normal file

Binary file not shown.

BIN
Clients_import.xlsx Normal file

Binary file not shown.

BIN
Materials_import.xlsx Normal file

Binary file not shown.

BIN
OrderItems.xlsx Normal file

Binary file not shown.

BIN
Orders_import.xlsx Normal file

Binary file not shown.

BIN
Products_import.xlsx Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Resource/OrderItems.xlsx Normal file

Binary file not shown.

BIN
Resource/Orders_import.xlsx Normal file

Binary file not shown.

Binary file not shown.

BIN
Resource/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

588
main.py Normal file
View 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
View 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

Binary file not shown.

470
static/css/style.css Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

37
static/js/script.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 %}

View 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>

View 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
View 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
View 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
View 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
View 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
View 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 %}

View 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
View 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 %}