Загрузка данных


Блок 1. Удаление старых таблиц

DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS pickup_points;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS categories;
DROP TABLE IF EXISTS manufacturers;
DROP TABLE IF EXISTS suppliers;

Блок 2. Создание таблиц

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    login VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(50) NOT NULL,
    role VARCHAR(30) NOT NULL,
    full_name VARCHAR(100) NOT NULL
);

CREATE TABLE categories (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE manufacturers (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE suppliers (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE products (
    id INT AUTO_INCREMENT PRIMARY KEY,
    artikul VARCHAR(50) NOT NULL UNIQUE,
    name VARCHAR(200) NOT NULL,
    category_id INT NOT NULL,
    manufacturer_id INT NOT NULL,
    supplier_id INT NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    discount DECIMAL(5,2) DEFAULT 0,
    quantity INT DEFAULT 0,
    description TEXT,
    photo_path VARCHAR(500),
    FOREIGN KEY (category_id) REFERENCES categories(id),
    FOREIGN KEY (manufacturer_id) REFERENCES manufacturers(id),
    FOREIGN KEY (supplier_id) REFERENCES suppliers(id)
);

CREATE TABLE pickup_points (
    id INT AUTO_INCREMENT PRIMARY KEY,
    address VARCHAR(300) NOT NULL
);

CREATE TABLE orders (
    id INT AUTO_INCREMENT PRIMARY KEY,
    order_artikul VARCHAR(200) NOT NULL,
    order_date DATE,
    delivery_date DATE,
    pickup_point_id INT NOT NULL,
    user_id INT NOT NULL,
    pickup_code VARCHAR(20),
    status VARCHAR(30) DEFAULT 'Новый',
    FOREIGN KEY (pickup_point_id) REFERENCES pickup_points(id),
    FOREIGN KEY (user_id) REFERENCES users(id)
);

Блок 3. Пользователи

INSERT INTO users (login, password, role, full_name) VALUES
('94d5ous@gmail.com', 'uzWC67', 'Администратор', 'Никифорова Анна Семеновна'),
('uth4iz@mail.com', '2L6KZG', 'Администратор', 'Стелина Евгения Петровна'),
('5d4zbu@tutanota.com', 'rwVDh9', 'Администратор', 'Никифорова Весения Николаевна'),
('ptec8ym@yahoo.com', 'LdNyos', 'Менеджер', 'Сазонов Руслан Германович'),
('1qz4kw@mail.com', 'gynQMT', 'Менеджер', 'Одинцов Серафим Артёмович'),
('4np6se@mail.com', 'AtnDjr', 'Менеджер', 'Старикова Елена Павловна'),
('yzls62@outlook.com', 'JlFRCZ', 'Авторизированный клиент', 'Степанов Михаил Артёмович'),
('1diph5e@tutanota.com', '8ntwUp', 'Авторизированный клиент', 'Михайлюк Анна Вячеславовна'),
('tjde7c@yahoo.com', 'YOyhfR', 'Авторизированный клиент', 'Ситдикова Елена Анатольевна'),
('wpmrc3do@tutanota.com', 'RSbvHv', 'Авторизированный клиент', 'Ворсин Петр Евгеньевич');

Блок 4. Категории

INSERT INTO categories (name) VALUES
('Прихожая'),
('Диван'),
('Обувница'),
('Пуф'),
('Полка'),
('Стул');

Блок 5. Производители

INSERT INTO manufacturers (name) VALUES
('SVМЕБЕЛЬ'),
('Мебелони'),
('Инвуд'),
('RIDBERG'),
('KRYLOVMANUFACTURA');

Блок 6. Поставщики

INSERT INTO suppliers (name) VALUES
('Стройландия'),
('Кромма'),
('ЗолотоеРуно'),
('KRYLOVMANUFACTURA');

Блок 7. Пункты выдачи

INSERT INTO pickup_points (address) VALUES
('420151, г. Лесной, ул. Вишневая, 32'),
('125061, г. Лесной, ул. Подгорная, 8'),
('630370, г. Лесной, ул. Шоссейная, 24'),
('400562, г. Лесной, ул. Зеленая, 32'),
('614510, г. Лесной, ул. Маяковского, 47'),
('410542, г. Лесной, ул. Светлая, 46'),
('620839, г. Лесной, ул. Цветочная, 8'),
('443890, г. Лесной, ул. Коммунистическая, 1'),
('603379, г. Лесной, ул. Спортивная, 46'),
('603721, г. Лесной, ул. Гоголя, 41'),
('410172, г. Лесной, ул. Северная, 13'),
('614611, г. Лесной, ул. Молодежная, 50'),
('454311, г. Лесной, ул. Новая, 19'),
('660007, г. Лесной, ул. Октябрьская, 19'),
('603036, г. Лесной, ул. Садовая, 4'),
('394060, г. Лесной, ул. Фрунзе, 43'),
('410661, г. Лесной, ул. Школьная, 50'),
('625590, г. Лесной, ул. Коммунистическая, 20'),
('625683, г. Лесной, ул. 8 Марта'),
('450983, г. Лесной, ул. Комсомольская, 26'),
('394782, г. Лесной, ул. Чехова, 3'),
('603002, г. Лесной, ул. Дзержинского, 28'),
('450558, г. Лесной, ул. Набережная, 30'),
('344288, г. Лесной, ул. Чехова, 1'),
('614164, г. Лесной, ул. Степная, 30'),
('394242, г. Лесной, ул. Коммунистическая, 43'),
('660540, г. Лесной, ул. Солнечная, 25'),
('125837, г. Лесной, ул. Шоссейная, 40'),
('125703, г. Лесной, ул. Партизанская, 49'),
('625283, г. Лесной, ул. Победы, 46'),
('614753, г. Лесной, ул. Полевая, 35'),
('426030, г. Лесной, ул. Маяковского, 44'),
('450375, г. Лесной, ул. Клубная, 44'),
('625560, г. Лесной, ул. Некрасова, 12'),
('630201, г. Лесной, ул. Комсомольская, 17'),
('190949, г. Лесной, ул. Мичурина, 26');

Блок 8. Товары

INSERT INTO products (artikul, name, category_id, manufacturer_id, supplier_id, price, discount, quantity, description, photo_path) VALUES
('А112Т4', 'Прихожая Фаворит 1 1420х2056х352мм Дуб Делано/Цемент Светлый SV-М 1 шт', 1, 1, 1, 9577, 10, 0, 'Удивительно функциональная и практичная прихожая Фаворит 1, обладая характерными чертами Скандинавского стиля, выглядит эффектно и способна задать тон интерьеру дома, встречая вас и ваших гостей.', 'product_photos/1.jpg'),
('G843H5', 'Прихожая в коридор Твист с зеркалом мебель со шкафами, 120х37х202 см', 1, 2, 1, 8803, 25, 9, 'Этот стеллаж со шкафом в прихожую комнату станет отличным элементом для вашего интерьера. Мебель для дома обеспечивает удобное хранение перчаток, шапок, зонтов, сумок и других аксессуаров.', 'product_photos/2.jpg'),
('D325D4', 'Угловой диван Кромма Инвуд Лайт, серый двухместный диван, Velutto 32', 2, 3, 2, 29125, 5, 12, 'Угловой диван Инвуд Лайт 2 - стильный и комфортный диван подойдет для комнаты любого размера.', 'product_photos/3.jpg'),
('S432T5', 'Обувница RIDBERG, с вешалкой, стальная, 170x60x26 см, 5 полок, вместимость до 15 пар', 3, 4, 2, 885, 15, 15, 'Обувница Ridberg с 5 полками и вешалкой - идеальное решение для организации хранения обуви в прихожей или гардеробной.', 'product_photos/4.jpg'),
('F325D4', 'Диван, Прямой диван, Диван-кровать Сити темно-коричневый. Квест-33', 2, 3, 3, 14322, 18, 3, 'Прямой диван-кровать Сити - это современное и функциональное решение для вашего дома.', 'product_photos/5.jpg'),
('G432G6', 'Пуф трансформер кровать раскладушка светло-коричневый велюр', 4, 3, 4, 6149, 22, 3, 'Пуф трансформер 5в1 представляет собой уникальное сочетание функций, выступая в качестве пуфика, столика, кресла, шезлонга и дополнительного спального места.', 'product_photos/6.jpg'),
('H542F5', 'Диван, Прямой диван, диван кровать, Рио симпл механизм Пантограф. Симпл-16', 2, 3, 3, 20708, 4, 5, 'Диван Рио симпл от "Золотое Руно" - это сочетание комфорта, функциональности и стильного дизайна.', 'product_photos/7.jpg'),
('C346F5', 'Полка настенная ромб Лофт, черная, 40 см', 5, 4, 4, 2843, 5, 4, 'Полочки для цветов в стиле лофт. Подойдут как для цветов, так и в качестве декоративного элемента. Полки подойдут для дома, офиса, кафе, ресторана.', 'product_photos/8.jpg'),
('F256G6', 'Стулья для кухни', 6, 4, 4, 4760, 6, 2, 'Набор из четырех стульев в лофт-дизайне станет любимой мебелью для отдыха и подойдет для взрослых и детей.', 'product_photos/9.jpg'),
('J532V5', 'Магнитная полка, для холодильника, металл, 3шт, универсальная, чёрная', 5, 4, 4, 1387, 8, 6, 'Магнитная полка для холодильника - это удобный и практичный аксессуар, который поможет организовать пространство в вашем доме.', 'product_photos/10.jpg');

Блок 9. Заказы

INSERT INTO orders (order_artikul, order_date, delivery_date, pickup_point_id, user_id, pickup_code, status) VALUES
('А112Т4, 2, G843H5, 2', '2024-02-27', '2024-04-20', 1, 7, '901', 'Новый'),
('G843H5, 1, А112Т4, 1', '2024-09-28', '2024-04-21', 11, 8, '902', 'Новый'),
('D325D4, 10, S432T5, 10', '2024-03-21', '2024-04-22', 2, 9, '903', 'Новый'),
('F325D4, 5, D325D4, 4', '2024-02-20', '2024-04-23', 11, 10, '904', 'Завершен'),
('G432G6, 20, H542F5, 20', '2024-03-17', '2024-04-24', 2, 7, '905', 'Завершен'),
('А112Т4, 2, G843H5, 2', '2024-03-01', '2024-04-25', 15, 8, '906', 'Завершен'),
('G843H5, 1, А112Т4, 1', '2024-02-28', '2024-04-26', 3, 9, '907', 'Завершен'),
('D325D4, 10, S432T5, 10', '2024-03-31', '2024-04-27', 19, 10, '908', 'Новый'),
('F325D4, 5, D325D4, 4', '2024-04-02', '2024-04-28', 5, 9, '909', 'Новый'),
('G432G6, 20, H542F5, 20', '2024-04-03', '2024-04-29', 19, 10, '910', 'Новый');

Блок 10. Проверка

SELECT * FROM users;
SELECT * FROM categories;
SELECT * FROM manufacturers;
SELECT * FROM suppliers;
SELECT * FROM pickup_points;
SELECT * FROM products;
SELECT * FROM orders;



import mysql.connector
from mysql.connector import Error
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from PIL import Image, ImageTk
import os
import shutil
from datetime import datetime

DB_CONFIG = {
    'host': 'localhost',
    'user': 'root',
    'password': '14881488',
    'database': 'furniture_store',
    'ssl_disabled': True,
    'use_pure': True,
    'auth_plugin': 'mysql_native_password'
}

def db_query(query, params=None, fetch=False):
    conn = mysql.connector.connect(**DB_CONFIG)
    cursor = conn.cursor(dictionary=True)
    try:
        cursor.execute(query, params or ())
        if fetch:
            result = cursor.fetchall()
            conn.commit()
            return result
        conn.commit()
        return cursor.lastrowid if query.strip().upper().startswith('INSERT') else None
    except Error as e:
        conn.rollback()
        raise e
    finally:
        cursor.close()
        conn.close()


def set_icon(window):
    try:
        if os.path.exists("icon.ico"):
            window.iconbitmap("icon.ico")
    except:
        pass


class User:
    def __init__(self, user_data):
        self.id = user_data.get('id', 0)
        self.role = user_data.get('role', 'Гость')
        self.full_name = user_data.get('full_name', 'Гость')
        self.login = user_data.get('login', '')

    @property
    def can_edit_products(self):
        return self.role == 'Администратор'

    @property
    def can_edit_orders(self):
        return self.role == 'Администратор'

    @property
    def can_view_orders(self):
        return self.role in ['Менеджер', 'Администратор']

    @property
    def can_filter_products(self):
        return self.role in ['Менеджер', 'Администратор']


class LoginForm(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Авторизация - МебельОрг")
        self.geometry("400x500")
        self.resizable(False, False)
        self.configure(bg="#FFFFFF")
        set_icon(self)
        self.center_window()

        frame = tk.Frame(self, bg="#FFFFFF")
        frame.pack(expand=True)

        try:
            logo_img = Image.open("logo.png").resize((100, 100), Image.Resampling.LANCZOS)
            self.logo = ImageTk.PhotoImage(logo_img)
            tk.Label(frame, image=self.logo, bg="#FFFFFF").pack(pady=10)
        except:
            pass

        tk.Label(frame, text="Добро пожаловать!", font=("Calibri", 24, "bold"),
                 fg="#0000FF", bg="#FFFFFF").pack(pady=10)

        self.entry_login = tk.Entry(frame, font=("Calibri", 14), width=25)
        self.entry_login.pack(pady=10)
        self.entry_login.insert(0, "Логин")
        self.entry_login.bind("<FocusIn>", lambda e: self.clear_placeholder(self.entry_login, "Логин"))

        self.entry_password = tk.Entry(frame, font=("Calibri", 14), width=25, show="*")
        self.entry_password.pack(pady=10)
        self.entry_password.insert(0, "Пароль")
        self.entry_password.bind("<FocusIn>", lambda e: self.clear_placeholder(self.entry_password, "Пароль"))

        tk.Button(frame, text="Войти", command=self.login,
                  bg="#0000FF", fg="white", font=("Calibri", 14, "bold"),
                  width=20, height=1).pack(pady=10)

        tk.Button(frame, text="Продолжить как гость", command=self.guest_login,
                  bg="gray", fg="white", font=("Calibri", 12),
                  width=20, height=1).pack(pady=5)

        self.bind("<Return>", lambda e: self.login())
        self.protocol("WM_DELETE_WINDOW", self.destroy)

    def center_window(self):
        self.update_idletasks()
        w, h = 400, 500
        x = (self.winfo_screenwidth() // 2) - (w // 2)
        y = (self.winfo_screenheight() // 2) - (h // 2)
        self.geometry(f"{w}x{h}+{x}+{y}")

    def clear_placeholder(self, entry, placeholder):
        if entry.get() == placeholder:
            entry.delete(0, tk.END)

    def login(self):
        login = self.entry_login.get()
        password = self.entry_password.get()
        if not login or login == "Логин" or not password or password == "Пароль":
            messagebox.showwarning("Ошибка", "Введите логин и пароль")
            return
        try:
            result = db_query("SELECT * FROM users WHERE login = %s AND password = %s",
                              (login, password), fetch=True)
            if result:
                user = User(result[0])
                self.withdraw()
                ProductListForm(user, self)
            else:
                messagebox.showerror("Ошибка", "Неверный логин или пароль")
        except Error as e:
            messagebox.showerror("Ошибка БД", f"Ошибка подключения: {e}")

    def guest_login(self):
        guest = User({'id': 0, 'role': 'Гость', 'full_name': 'Гость'})
        self.withdraw()
        ProductListForm(guest, self)


class ProductListForm(tk.Toplevel):
    def __init__(self, user, login_form):
        super().__init__()
        self.user = user
        self.login_form = login_form
        self.title("Список товаров - МебельОрг")
        self.geometry("1400x800")
        self.configure(bg="#F0F0F0")
        set_icon(self)
        self.center_window()

        header_frame = tk.Frame(self, bg="#FFFFFF", height=60)
        header_frame.pack(fill="x", pady=(0, 10))
        header_frame.pack_propagate(False)

        try:
            logo_img = Image.open("logo.png").resize((40, 40), Image.Resampling.LANCZOS)
            self.logo_photo = ImageTk.PhotoImage(logo_img)
            tk.Label(header_frame, image=self.logo_photo, bg="#FFFFFF").pack(side="left", padx=15)
        except:
            pass

        tk.Label(header_frame, text="МебельОрг", font=("Calibri", 20, "bold"),
                 fg="#0000FF", bg="#FFFFFF").pack(side="left", padx=5)

        user_frame = tk.Frame(header_frame, bg="#FFFFFF")
        user_frame.pack(side="right", padx=15)
        tk.Label(user_frame, text=self.user.full_name, font=("Calibri", 12),
                 bg="#FFFFFF", fg="black").pack(side="left", padx=5)
        tk.Button(user_frame, text="Выход", command=self.logout,
                  bg="#FF4444", fg="white", font=("Calibri", 11, "bold"), width=8).pack(side="left", padx=5)

        if user.can_filter_products:
            self.create_filter_panel()

        action_frame = tk.Frame(self, bg="#F0F0F0")
        action_frame.pack(fill="x", padx=10, pady=5)

        if user.can_edit_products:
            tk.Button(action_frame, text="Добавить товар", command=self.add_product,
                      bg="#00C853", fg="white", font=("Calibri", 12, "bold"), width=15).pack(side="left", padx=5)
            tk.Button(action_frame, text="Удалить товар", command=self.delete_product,
                      bg="#FF4444", fg="white", font=("Calibri", 12, "bold"), width=15).pack(side="left", padx=5)

        if user.can_view_orders:
            tk.Button(action_frame, text="Заказы", command=self.open_orders,
                      bg="#FF9800", fg="white", font=("Calibri", 12, "bold"), width=15).pack(side="left", padx=5)

        self.canvas = tk.Canvas(self, bg="#F0F0F0", highlightthickness=0)
        self.scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
        self.cards_frame = tk.Frame(self.canvas, bg="#F0F0F0")

        self.cards_frame.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")))
        self.canvas_window = self.canvas.create_window((0, 0), window=self.cards_frame, anchor="nw")
        self.canvas.configure(yscrollcommand=self.scrollbar.set)

        self.canvas.pack(side="left", fill="both", expand=True, padx=10, pady=10)
        self.scrollbar.pack(side="right", fill="y", pady=10)

        self.canvas.bind("<Configure>", lambda e: self.canvas.itemconfig(self.canvas_window, width=e.width))
        self.bind_mousewheel()

        self.image_cache = {}
        self.default_image = None
        try:
            if os.path.exists("picture.png"):
                img = Image.open("picture.png").resize((200, 150), Image.Resampling.LANCZOS)
                self.default_image = ImageTk.PhotoImage(img)
        except:
            pass

        self.load_products()
        self.protocol("WM_DELETE_WINDOW", self.logout)

    def bind_mousewheel(self):
        def on_mousewheel(event):
            self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")
        self.canvas.bind("<Enter>", lambda e: self.canvas.bind_all("<MouseWheel>", on_mousewheel))
        self.canvas.bind("<Leave>", lambda e: self.canvas.unbind_all("<MouseWheel>"))

    def center_window(self):
        self.update_idletasks()
        w, h = 1400, 800
        x = (self.winfo_screenwidth() // 2) - (w // 2)
        y = (self.winfo_screenheight() // 2) - (h // 2)
        self.geometry(f"{w}x{h}+{x}+{y}")

    def create_filter_panel(self):
        filter_frame = tk.Frame(self, bg="#FFFFFF")
        filter_frame.pack(fill="x", padx=10, pady=5)

        # Поиск
        tk.Label(filter_frame, text="Поиск:", bg="#FFFFFF", font=("Calibri", 12)).grid(row=0, column=0, padx=5)
        self.search_entry = tk.Entry(filter_frame, width=25, font=("Calibri", 12))
        self.search_entry.grid(row=0, column=1, padx=5)
        self.search_entry.bind('<KeyRelease>', lambda e: self.load_products())

        # Сортировка по цене
        tk.Label(filter_frame, text="Цена:", bg="#FFFFFF", font=("Calibri", 12)).grid(row=0, column=2, padx=5)
        self.sort_price = ttk.Combobox(filter_frame, values=["Нет", "По возрастанию", "По убыванию"], width=15, font=("Calibri", 12))
        self.sort_price.set("Нет")
        self.sort_price.grid(row=0, column=3, padx=5)
        self.sort_price.bind('<<ComboboxSelected>>', lambda e: self.load_products())

        # Сортировка по количеству
        tk.Label(filter_frame, text="Кол-во:", bg="#FFFFFF", font=("Calibri", 12)).grid(row=0, column=4, padx=5)
        self.sort_quantity = ttk.Combobox(filter_frame, values=["Нет", "По возрастанию", "По убыванию"], width=15, font=("Calibri", 12))
        self.sort_quantity.set("Нет")
        self.sort_quantity.grid(row=0, column=5, padx=5)
        self.sort_quantity.bind('<<ComboboxSelected>>', lambda e: self.load_products())

        # Фильтр по скидке (Вариант 5: 0-10,99%, 11-14,99%, 15%+)
        tk.Label(filter_frame, text="Скидка:", bg="#FFFFFF", font=("Calibri", 12)).grid(row=1, column=0, padx=5, pady=5)
        self.filter_discount = ttk.Combobox(filter_frame,
                                            values=["Все диапазоны", "0-10,99%", "11-14,99%", "15% и более"],
                                            width=15, font=("Calibri", 12))
        self.filter_discount.set("Все диапазоны")
        self.filter_discount.grid(row=1, column=1, padx=5)
        self.filter_discount.bind('<<ComboboxSelected>>', lambda e: self.load_products())

    def get_photo(self, photo_path):
        if not photo_path:
            return self.default_image
        if photo_path in self.image_cache:
            return self.image_cache[photo_path]
        real_path = photo_path
        if not os.path.exists(real_path):
            base = os.path.splitext(real_path)[0]
            for ext in ['.jpg', '.jpeg', '.png', '.JPG', '.JPEG', '.PNG']:
                test = base + ext
                if os.path.exists(test):
                    real_path = test
                    break
            else:
                return self.default_image
        try:
            img = Image.open(real_path).resize((200, 150), Image.Resampling.LANCZOS)
            photo = ImageTk.PhotoImage(img)
            self.image_cache[photo_path] = photo
            return photo
        except:
            return self.default_image

    def load_products(self):
        query = """
            SELECT p.*, c.name as category_name, m.name as manufacturer_name, s.name as supplier_name
            FROM products p
            JOIN categories c ON p.category_id = c.id
            JOIN manufacturers m ON p.manufacturer_id = m.id
            JOIN suppliers s ON p.supplier_id = s.id
            WHERE 1=1
        """
        params = []

        if hasattr(self, 'search_entry') and self.search_entry.get():
            t = f"%{self.search_entry.get()}%"
            query += " AND (p.name LIKE %s OR p.description LIKE %s OR p.artikul LIKE %s)"
            params.extend([t]*3)

        if hasattr(self, 'filter_discount') and self.filter_discount.get() != "Все диапазоны":
            fv = self.filter_discount.get()
            if fv == "0-10,99%":
                query += " AND p.discount BETWEEN 0 AND 10.99"
            elif fv == "11-14,99%":
                query += " AND p.discount BETWEEN 11 AND 14.99"
            elif fv == "15% и более":
                query += " AND p.discount >= 15"

        order = []
        if hasattr(self, 'sort_price') and self.sort_price.get() != "Нет":
            d = 'ASC' if self.sort_price.get() == 'По возрастанию' else 'DESC'
            order.append(f"p.price {d}")
        if hasattr(self, 'sort_quantity') and self.sort_quantity.get() != "Нет":
            d = 'ASC' if self.sort_quantity.get() == 'По возрастанию' else 'DESC'
            order.append(f"p.quantity {d}")
        if order:
            query += " ORDER BY " + ", ".join(order)

        try:
            products = db_query(query, params, fetch=True)
        except Error as e:
            messagebox.showerror("Ошибка", f"Ошибка загрузки товаров: {e}")
            return

        for w in self.cards_frame.winfo_children():
            w.destroy()
        self.image_cache = {}

        for idx, p in enumerate(products):
            final_price = p['price'] * (1 - p['discount'] / 100)

            # Цвета карточки (Вариант 5: скидка >15% = #008080, 0 остаток = серый)
            if p['quantity'] == 0:
                card_bg = "#D3D3D3"
                fg_color = "black"
            elif p['discount'] > 15:
                card_bg = "#008080"
                fg_color = "white"
            else:
                card_bg = "#FFFFFF" if idx % 2 == 0 else "#F5F5F5"
                fg_color = "black"

            card = tk.Frame(self.cards_frame, bg=card_bg, bd=1, relief="solid")
            card.pack(fill="x", pady=3, padx=5)

            # Фото
            photo_frame = tk.Frame(card, bg=card_bg, width=210, height=160)
            photo_frame.pack(side="left", padx=10, pady=10)
            photo_frame.pack_propagate(False)

            img = self.get_photo(p.get('photo_path', ''))
            if img:
                photo_lbl = tk.Label(photo_frame, image=img, bg=card_bg)
                photo_lbl.image = img
                photo_lbl.pack(expand=True)

            # Информация
            info_frame = tk.Frame(card, bg=card_bg)
            info_frame.pack(side="left", fill="both", expand=True, padx=10, pady=10)

            tk.Label(info_frame, text=f"{p['category_name']} | {p['name']}",
                     font=("Calibri", 14, "bold"), bg=card_bg, fg=fg_color,
                     anchor="w", justify="left").pack(anchor="w")

            desc = p['description'] if p['description'] else "—"
            tk.Label(info_frame, text=f"Описание: {desc}", font=("Calibri", 11),
                     bg=card_bg, fg=fg_color, anchor="w", justify="left",
                     wraplength=700).pack(anchor="w", pady=(5, 0))

            tk.Label(info_frame, text=f"Производитель: {p['manufacturer_name']}",
                     font=("Calibri", 11), bg=card_bg, fg=fg_color, anchor="w").pack(anchor="w")

            tk.Label(info_frame, text=f"Поставщик: {p['supplier_name']}",
                     font=("Calibri", 11), bg=card_bg, fg=fg_color, anchor="w").pack(anchor="w")

            if p['discount'] > 0:
                price_text = f"Цена: {p['price']:.2f} руб. → {final_price:.2f} руб."
            else:
                price_text = f"Цена: {p['price']:.2f} руб."
            tk.Label(info_frame, text=price_text, font=("Calibri", 11),
                     bg=card_bg, fg=fg_color, anchor="w").pack(anchor="w")

            tk.Label(info_frame, text="Единица измерения: шт.", font=("Calibri", 11),
                     bg=card_bg, fg=fg_color, anchor="w").pack(anchor="w")

            tk.Label(info_frame, text=f"Количество на складе: {p['quantity']}",
                     font=("Calibri", 11), bg=card_bg, fg=fg_color, anchor="w").pack(anchor="w")

            tk.Label(info_frame, text=f"Артикул: {p['artikul']}", font=("Calibri", 10),
                     bg=card_bg, fg="gray", anchor="w").pack(anchor="w", pady=(5, 0))

            # Скидка
            discount_frame = tk.Frame(card, bg=card_bg, width=200, height=160)
            discount_frame.pack(side="right", padx=20, pady=10)
            discount_frame.pack_propagate(False)

            tk.Label(discount_frame, text=f"{p['discount']:.0f}%",
                     font=("Calibri", 36, "bold"), bg=card_bg, fg=fg_color).pack(expand=True)
            tk.Label(discount_frame, text="скидка", font=("Calibri", 12),
                     bg=card_bg, fg=fg_color).pack()

            if self.user.can_edit_products:
                card.bind("<Double-1>", lambda e, a=p['artikul']: self.edit_product_by_artikul(a))
                for child in card.winfo_children():
                    child.bind("<Double-1>", lambda e, a=p['artikul']: self.edit_product_by_artikul(a))
                    for sub in child.winfo_children():
                        sub.bind("<Double-1>", lambda e, a=p['artikul']: self.edit_product_by_artikul(a))

    def edit_product_by_artikul(self, artikul):
        ProductEditForm(self.user, self, artikul)

    def add_product(self):
        ProductEditForm(self.user, self)

    def delete_product(self):
        dlg = tk.Toplevel(self)
        dlg.title("Удаление товара")
        dlg.geometry("400x200")
        dlg.configure(bg="#FFFFFF")
        set_icon(dlg)
        dlg.grab_set()
        tk.Label(dlg, text="Артикул для удаления:", font=("Calibri", 12), bg="#FFFFFF").pack(pady=10)
        e = tk.Entry(dlg, font=("Calibri", 12), width=30)
        e.pack(pady=5)
        e.focus()

        def do():
            a = e.get().strip()
            if not a:
                messagebox.showwarning("Ошибка", "Введите артикул")
                return
            try:
                if db_query("SELECT id FROM orders WHERE order_artikul LIKE %s", (f"%{a}%",), fetch=True):
                    messagebox.showerror("Ошибка", "Товар в заказах — удалить нельзя")
                    return
            except Error as ex:
                messagebox.showerror("Ошибка", str(ex))
                return
            if messagebox.askyesno("Подтверждение", f"Удалить {a}?"):
                try:
                    old = db_query("SELECT photo_path FROM products WHERE artikul=%s", (a,), fetch=True)
                    if old and old[0]['photo_path'] and os.path.exists(old[0]['photo_path']):
                        os.remove(old[0]['photo_path'])
                    db_query("DELETE FROM products WHERE artikul=%s", (a,))
                    messagebox.showinfo("Успех", "Удалено")
                    dlg.destroy()
                    self.load_products()
                except Error as ex:
                    messagebox.showerror("Ошибка", str(ex))

        tk.Button(dlg, text="Удалить", command=do, bg="#FF4444", fg="white",
                  font=("Calibri", 12, "bold"), width=15).pack(pady=10)
        tk.Button(dlg, text="Отмена", command=dlg.destroy, bg="gray", fg="white",
                  font=("Calibri", 12), width=15).pack()

    def open_orders(self):
        self.withdraw()
        OrderListForm(self.user, self)

    def logout(self):
        self.destroy()
        self.login_form.deiconify()


class ProductEditForm(tk.Toplevel):
    def __init__(self, user, parent, artikul=None):
        super().__init__()
        self.user = user
        self.parent = parent
        self.artikul = artikul
        self.is_edit = artikul is not None
        self.photo_path = None
        self.old_photo_path = None

        self.title(f"{'Редактирование' if self.is_edit else 'Добавление'} товара")
        self.geometry("550x650")
        self.configure(bg="#F0F0F0")
        self.resizable(False, False)
        set_icon(self)
        self.center_window()
        self.load_references()
        self.create_ui()
        if self.is_edit:
            self.load_product()
        self.grab_set()

    def center_window(self):
        self.update_idletasks()
        w, h = 550, 650
        x = (self.winfo_screenwidth() // 2) - (w // 2)
        y = (self.winfo_screenheight() // 2) - (h // 2)
        self.geometry(f"{w}x{h}+{x}+{y}")

    def load_references(self):
        # Вариант 5: все три — выпадающие списки
        self.refs = {'category_id': {}, 'manufacturer_id': {}, 'supplier_id': {}}
        for table, key in [('categories', 'category_id'), ('manufacturers', 'manufacturer_id'), ('suppliers', 'supplier_id')]:
            for row in db_query(f"SELECT id, name FROM {table}", fetch=True):
                self.refs[key][row['id']] = row['name']

    def create_ui(self):
        frame = tk.Frame(self, bg="#FFFFFF")
        frame.pack(fill="both", expand=True, padx=20, pady=20)

        tk.Label(frame, text=f"{'Редактирование' if self.is_edit else 'Добавление'} товара",
                 font=("Calibri", 20, "bold"), fg="#0000FF", bg="#FFFFFF").pack(pady=15)

        self.photo_label = tk.Label(frame, text="Нет фото", bg="#E0E0E0", width=40, height=8, relief="sunken")
        self.photo_label.pack(pady=5)
        tk.Button(frame, text="Выбрать изображение", command=self.select_photo,
                  bg="#2196F3", fg="white", font=("Calibri", 11)).pack(pady=5)

        fields_frame = tk.Frame(frame, bg="#FFFFFF")
        fields_frame.pack(fill="both", expand=True, pady=10)

        self.entries = {}
        # Вариант 5: все три — выпадающие
        fields_list = [
            ("artikul", "Артикул *"),
            ("name", "Наименование *"),
            ("category_id", "Категория *", True),
            ("manufacturer_id", "Производитель *", True),
            ("supplier_id", "Поставщик *", True),
            ("price", "Цена *"),
            ("discount", "Скидка (%)"),
            ("quantity", "Количество *"),
            ("description", "Описание")
        ]

        for field_info in fields_list:
            if len(field_info) == 3:
                field, label, is_combo = field_info
            else:
                field, label = field_info
                is_combo = False

            row_frame = tk.Frame(fields_frame, bg="#FFFFFF")
            row_frame.pack(fill="x", pady=3)
            tk.Label(row_frame, text=label, width=20, anchor="w", bg="#FFFFFF", font=("Calibri", 11)).pack(side="left", padx=5)

            if is_combo and field in self.refs:
                cb = ttk.Combobox(row_frame, values=list(self.refs[field].values()), width=35, font=("Calibri", 11))
                cb.pack(side="left")
                self.entries[field] = cb
            elif field == 'description':
                t = tk.Text(row_frame, width=35, height=3, font=("Calibri", 11))
                t.pack(side="left")
                self.entries[field] = t
            else:
                en = tk.Entry(row_frame, width=37, font=("Calibri", 11))
                en.pack(side="left")
                self.entries[field] = en

        btn_frame = tk.Frame(frame, bg="#FFFFFF")
        btn_frame.pack(fill="x", pady=15)
        tk.Button(btn_frame, text="Сохранить", command=self.save, bg="#00C853", fg="white",
                  font=("Calibri", 12, "bold"), width=15).pack(side="left", padx=10, expand=True)
        tk.Button(btn_frame, text="Отмена", command=self.destroy, bg="#FF4444", fg="white",
                  font=("Calibri", 12, "bold"), width=15).pack(side="right", padx=10, expand=True)

    def select_photo(self):
        fp = filedialog.askopenfilename(filetypes=[("Изображения", "*.jpg *.jpeg *.png *.bmp")])
        if fp:
            self.photo_path = fp
            try:
                img = Image.open(fp).resize((150, 100), Image.Resampling.LANCZOS)
                ph = ImageTk.PhotoImage(img)
                self.photo_label.configure(image=ph, text="")
                self.photo_label.image = ph
            except:
                pass

    def load_product(self):
        p = db_query("SELECT * FROM products WHERE artikul = %s", (self.artikul,), fetch=True)
        if not p:
            return
        p = p[0]
        self.entries['artikul'].insert(0, p['artikul'])
        self.entries['artikul'].config(state='readonly')
        self.entries['name'].insert(0, p['name'])
        self.entries['category_id'].set(self.refs['category_id'].get(p['category_id'], ''))
        self.entries['manufacturer_id'].set(self.refs['manufacturer_id'].get(p['manufacturer_id'], ''))
        self.entries['supplier_id'].set(self.refs['supplier_id'].get(p['supplier_id'], ''))
        self.entries['price'].insert(0, str(p['price']))
        self.entries['discount'].insert(0, str(p['discount']))
        self.entries['quantity'].insert(0, str(p['quantity']))
        self.entries['description'].insert("1.0", p['description'] or '')
        self.old_photo_path = p.get('photo_path')
        if self.old_photo_path and os.path.exists(self.old_photo_path):
            try:
                img = Image.open(self.old_photo_path).resize((150, 100), Image.Resampling.LANCZOS)
                ph = ImageTk.PhotoImage(img)
                self.photo_label.configure(image=ph, text="")
                self.photo_label.image = ph
            except:
                pass

    def save(self):
        try:
            artikul = self.artikul if self.is_edit else self.entries['artikul'].get()
            name = self.entries['name'].get()
            cat_id = [k for k, v in self.refs['category_id'].items() if v == self.entries['category_id'].get()][0]
            man_id = [k for k, v in self.refs['manufacturer_id'].items() if v == self.entries['manufacturer_id'].get()][0]
            sup_id = [k for k, v in self.refs['supplier_id'].items() if v == self.entries['supplier_id'].get()][0]
            price = float(self.entries['price'].get())
            discount = float(self.entries['discount'].get() or 0)
            quantity = int(self.entries['quantity'].get())
            desc = self.entries['description'].get("1.0", "end").strip()

            if price < 0 or quantity < 0 or discount < 0 or discount > 100:
                raise ValueError("Некорректные значения")

            saved = None
            if self.photo_path and os.path.exists(self.photo_path):
                os.makedirs("product_photos", exist_ok=True)
                ext = os.path.splitext(self.photo_path)[1]
                saved = os.path.join("product_photos", f"{artikul}{ext}")
                if self.is_edit and self.old_photo_path and os.path.exists(self.old_photo_path):
                    try:
                        os.remove(self.old_photo_path)
                    except:
                        pass
                shutil.copy2(self.photo_path, saved)
                Image.open(saved).resize((300, 200), Image.Resampling.LANCZOS).save(saved)

            if self.is_edit:
                if saved:
                    db_query("UPDATE products SET name=%s, category_id=%s, manufacturer_id=%s, supplier_id=%s, price=%s, discount=%s, quantity=%s, description=%s, photo_path=%s WHERE artikul=%s",
                             (name, cat_id, man_id, sup_id, price, discount, quantity, desc, saved, artikul))
                else:
                    db_query("UPDATE products SET name=%s, category_id=%s, manufacturer_id=%s, supplier_id=%s, price=%s, discount=%s, quantity=%s, description=%s WHERE artikul=%s",
                             (name, cat_id, man_id, sup_id, price, discount, quantity, desc, artikul))
                messagebox.showinfo("Успех", "Товар обновлен")
            else:
                db_query("INSERT INTO products (artikul, name, category_id, manufacturer_id, supplier_id, price, discount, quantity, description, photo_path) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)",
                         (artikul, name, cat_id, man_id, sup_id, price, discount, quantity, desc, saved))
                messagebox.showinfo("Успех", "Товар добавлен")
            self.parent.load_products()
            self.destroy()
        except ValueError as e:
            messagebox.showerror("Ошибка", str(e))
        except Exception as e:
            messagebox.showerror("Ошибка", f"Ошибка сохранения: {e}")


class OrderListForm(tk.Toplevel):
    def __init__(self, user, products_form):
        super().__init__()
        self.user = user
        self.products_form = products_form
        self.title("Список заказов - МебельОрг")
        self.geometry("1200x600")
        self.configure(bg="#F0F0F0")
        set_icon(self)
        self.center_window()

        hf = tk.Frame(self, bg="#FFFFFF", height=50)
        hf.pack(fill="x", pady=(0, 10))
        hf.pack_propagate(False)
        tk.Label(hf, text="Управление заказами", font=("Calibri", 18, "bold"),
                 fg="#0000FF", bg="#FFFFFF").pack(side="left", padx=15)

        uf = tk.Frame(hf, bg="#FFFFFF")
        uf.pack(side="right", padx=15)
        tk.Label(uf, text=self.user.full_name, font=("Calibri", 12),
                 bg="#FFFFFF", fg="black").pack(side="left", padx=5)
        tk.Button(uf, text="Назад", command=self.go_back,
                  bg="#FF9800", fg="white", font=("Calibri", 11, "bold"), width=8).pack(side="left", padx=5)
        tk.Button(uf, text="Выход", command=self.logout,
                  bg="#FF4444", fg="white", font=("Calibri", 11, "bold"), width=8).pack(side="left", padx=5)

        af = tk.Frame(self, bg="#F0F0F0")
        af.pack(fill="x", padx=10, pady=5)
        if self.user.can_edit_orders:
            tk.Button(af, text="Добавить заказ", command=self.add_order,
                      bg="#00C853", fg="white", font=("Calibri", 12, "bold"), width=15).pack(side="left", padx=5)
            tk.Button(af, text="Удалить заказ", command=self.delete_order,
                      bg="#FF4444", fg="white", font=("Calibri", 12, "bold"), width=15).pack(side="left", padx=5)
        tk.Button(af, text="Товары", command=self.go_back,
                  bg="#2196F3", fg="white", font=("Calibri", 12, "bold"), width=15).pack(side="left", padx=5)

        self.canvas = tk.Canvas(self, bg="#F0F0F0", highlightthickness=0)
        self.scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
        self.cards_frame = tk.Frame(self.canvas, bg="#F0F0F0")
        self.cards_frame.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")))
        self.canvas_window = self.canvas.create_window((0, 0), window=self.cards_frame, anchor="nw")
        self.canvas.configure(yscrollcommand=self.scrollbar.set)
        self.canvas.pack(side="left", fill="both", expand=True, padx=10, pady=10)
        self.scrollbar.pack(side="right", fill="y", pady=10)
        self.canvas.bind("<Configure>", lambda e: self.canvas.itemconfig(self.canvas_window, width=e.width))
        self.bind_mousewheel()

        self.orders = []
        self.load_orders()
        self.protocol("WM_DELETE_WINDOW", self.logout)

    def bind_mousewheel(self):
        def on_mousewheel(event):
            self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")
        self.canvas.bind("<Enter>", lambda e: self.canvas.bind_all("<MouseWheel>", on_mousewheel))
        self.canvas.bind("<Leave>", lambda e: self.canvas.unbind_all("<MouseWheel>"))

    def center_window(self):
        self.update_idletasks()
        w, h = 1200, 600
        x = (self.winfo_screenwidth() // 2) - (w // 2)
        y = (self.winfo_screenheight() // 2) - (h // 2)
        self.geometry(f"{w}x{h}+{x}+{y}")

    def fmt(self, d):
        if not d:
            return "—"
        if isinstance(d, datetime):
            return d.strftime("%d.%m.%Y")
        if isinstance(d, str):
            try:
                return datetime.strptime(d, "%Y-%m-%d").strftime("%d.%m.%Y")
            except:
                return d
        return str(d)

    def load_orders(self):
        try:
            data = db_query("""
                SELECT o.*, pp.address as pa, u.full_name as un
                FROM orders o
                JOIN pickup_points pp ON o.pickup_point_id = pp.id
                JOIN users u ON o.user_id = u.id
                ORDER BY o.order_date DESC
            """, fetch=True)
        except Error as e:
            messagebox.showerror("Ошибка", str(e))
            return

        for w in self.cards_frame.winfo_children():
            w.destroy()
        self.orders = data

        for idx, o in enumerate(data):
            if o['status'] == 'Новый':
                card_bg = "#FFFFFF" if idx % 2 == 0 else "#F5F5F5"
                status_color = "#0000FF"
            else:
                card_bg = "#E8F5E9"
                status_color = "#2E7D32"

            card = tk.Frame(self.cards_frame, bg=card_bg, bd=1, relief="solid")
            card.pack(fill="x", pady=3, padx=5)

            info_frame = tk.Frame(card, bg=card_bg)
            info_frame.pack(side="left", fill="both", expand=True, padx=20, pady=15)

            tk.Label(info_frame, text=f"Артикул заказа: {o['order_artikul']}",
                     font=("Calibri", 13, "bold"), bg=card_bg, fg="black", anchor="w").pack(anchor="w")

            sf = tk.Frame(info_frame, bg=card_bg)
            sf.pack(anchor="w", pady=(5, 0))
            tk.Label(sf, text="Статус заказа: ", font=("Calibri", 12), bg=card_bg, fg="black").pack(side="left")
            tk.Label(sf, text=o['status'], font=("Calibri", 12, "bold"), bg=card_bg, fg=status_color).pack(side="left")

            tk.Label(info_frame, text=f"Адрес пункта выдачи: {o['pa']}",
                     font=("Calibri", 12), bg=card_bg, fg="black", anchor="w", wraplength=800).pack(anchor="w", pady=(5, 0))

            tk.Label(info_frame, text=f"Дата заказа: {self.fmt(o['order_date'])}",
                     font=("Calibri", 12), bg=card_bg, fg="black", anchor="w").pack(anchor="w", pady=(5, 0))

            tk.Label(info_frame, text=f"Клиент: {o['un']}",
                     font=("Calibri", 11), bg=card_bg, fg="gray", anchor="w").pack(anchor="w", pady=(5, 0))

            tk.Label(info_frame, text=f"Код получения: {o['pickup_code']}",
                     font=("Calibri", 11), bg=card_bg, fg="gray", anchor="w").pack(anchor="w")

            delivery_frame = tk.Frame(card, bg=card_bg, width=220, height=140)
            delivery_frame.pack(side="right", padx=30, pady=15)
            delivery_frame.pack_propagate(False)

            tk.Label(delivery_frame, text="Дата доставки", font=("Calibri", 12), bg=card_bg, fg="gray").pack()
            tk.Label(delivery_frame, text=self.fmt(o['delivery_date']),
                     font=("Calibri", 22, "bold"), bg=card_bg, fg="#0000FF").pack(pady=(5, 0))
            tk.Label(delivery_frame, text=f"Заказ №{o['id']}", font=("Calibri", 10),
                     bg=card_bg, fg="gray").pack(pady=(10, 0))

            if self.user.can_edit_orders:
                card.bind("<Double-1>", lambda e, oid=o['id']: self.edit_order_by_id(oid))
                for child in card.winfo_children():
                    child.bind("<Double-1>", lambda e, oid=o['id']: self.edit_order_by_id(oid))
                    for sub in child.winfo_children():
                        sub.bind("<Double-1>", lambda e, oid=o['id']: self.edit_order_by_id(oid))

    def edit_order_by_id(self, order_id):
        OrderEditForm(self.user, self, order_id)

    def add_order(self):
        OrderEditForm(self.user, self)

    def delete_order(self):
        dlg = tk.Toplevel(self)
        dlg.title("Удаление заказа")
        dlg.geometry("400x200")
        dlg.configure(bg="#FFFFFF")
        set_icon(dlg)
        dlg.grab_set()
        tk.Label(dlg, text="Номер заказа для удаления:", font=("Calibri", 12), bg="#FFFFFF").pack(pady=10)
        e = tk.Entry(dlg, font=("Calibri", 12), width=30)
        e.pack(pady=5)
        e.focus()

        def do():
            try:
                oid = int(e.get().strip())
            except:
                messagebox.showwarning("Ошибка", "Введите номер заказа")
                return
            if messagebox.askyesno("Подтверждение", f"Удалить заказ №{oid}?"):
                try:
                    db_query("DELETE FROM orders WHERE id=%s", (oid,))
                    messagebox.showinfo("Успех", "Заказ удалён")
                    dlg.destroy()
                    self.load_orders()
                except Error as ex:
                    messagebox.showerror("Ошибка", str(ex))

        tk.Button(dlg, text="Удалить", command=do, bg="#FF4444", fg="white",
                  font=("Calibri", 12, "bold"), width=15).pack(pady=10)
        tk.Button(dlg, text="Отмена", command=dlg.destroy, bg="gray", fg="white",
                  font=("Calibri", 12), width=15).pack()

    def go_back(self):
        self.destroy()
        self.products_form.deiconify()
        self.products_form.load_products()

    def logout(self):
        self.destroy()
        self.products_form.destroy()
        self.products_form.login_form.deiconify()


class OrderEditForm(tk.Toplevel):
    def __init__(self, user, parent, order_id=None):
        super().__init__()
        self.user = user
        self.parent = parent
        self.order_id = order_id
        self.is_edit = order_id is not None
        self.title(f"{'Редактирование' if self.is_edit else 'Добавление'} заказа")
        self.geometry("550x550")
        self.configure(bg="#F0F0F0")
        self.resizable(False, False)
        set_icon(self)
        self.center_window()
        self.load_refs()
        self.create_ui()
        if self.is_edit:
            self.load_order()
        self.grab_set()

    def center_window(self):
        self.update_idletasks()
        w, h = 550, 550
        x = (self.winfo_screenwidth() // 2) - (w // 2)
        y = (self.winfo_screenheight() // 2) - (h // 2)
        self.geometry(f"{w}x{h}+{x}+{y}")

    def load_refs(self):
        self.pp = {r['id']: r['address'] for r in db_query("SELECT id, address FROM pickup_points", fetch=True)}
        self.us = {r['id']: r['full_name'] for r in db_query("SELECT id, full_name FROM users WHERE role='Авторизированный клиент'", fetch=True)}

    def create_ui(self):
        m = tk.Frame(self, bg="#FFFFFF")
        m.pack(fill="both", expand=True, padx=20, pady=20)
        tk.Label(m, text=f"{'Редактирование' if self.is_edit else 'Добавление'} заказа",
                 font=("Calibri", 22, "bold"), fg="#0000FF", bg="#FFFFFF").pack(pady=20)
        ff = tk.Frame(m, bg="#FFFFFF")
        ff.pack(fill="both", expand=True, padx=20, pady=10)
        self.ent = {}
        for fld, lbl in [("order_artikul","Артикул заказа *"), ("order_date","Дата заказа *"), ("delivery_date","Дата выдачи *"), ("pickup_point_id","Адрес выдачи *"), ("user_id","Клиент *"), ("pickup_code","Код получения *"), ("status","Статус *")]:
            fr = tk.Frame(ff, bg="#FFFFFF")
            fr.pack(fill="x", pady=5)
            tk.Label(fr, text=lbl, width=25, anchor="w", font=("Calibri", 12), bg="#FFFFFF").pack(side="left", padx=10)
            if fld == 'pickup_point_id':
                cb = ttk.Combobox(fr, values=list(self.pp.values()), width=40)
                cb.pack(side="left")
                self.ent[fld] = cb
            elif fld == 'user_id':
                cb = ttk.Combobox(fr, values=list(self.us.values()), width=40)
                cb.pack(side="left")
                self.ent[fld] = cb
            elif fld == 'status':
                cb = ttk.Combobox(fr, values=['Новый','Завершен'], width=40)
                cb.pack(side="left")
                self.ent[fld] = cb
            else:
                en = tk.Entry(fr, width=42, font=("Calibri", 12))
                en.pack(side="left")
                self.ent[fld] = en
        bf = tk.Frame(m, bg="#FFFFFF")
        bf.pack(fill="x", pady=20)
        tk.Button(bf, text="Сохранить", command=self.save, bg="#00C853", fg="white",
                  font=("Calibri", 14, "bold"), width=15).pack(side="left", padx=10, expand=True)
        tk.Button(bf, text="Отмена", command=self.destroy, bg="#FF4444", fg="white",
                  font=("Calibri", 14, "bold"), width=15).pack(side="right", padx=10, expand=True)

    def load_order(self):
        o = db_query("SELECT * FROM orders WHERE id=%s", (self.order_id,), fetch=True)
        if o:
            o = o[0]
            self.ent['order_artikul'].insert(0, o['order_artikul'])
            for df in ['order_date','delivery_date']:
                v = o[df]
                if v:
                    self.ent[df].insert(0, v.strftime("%Y-%m-%d") if isinstance(v, datetime) else str(v))
            self.ent['pickup_code'].insert(0, o['pickup_code'])
            self.ent['pickup_point_id'].set(self.pp.get(o['pickup_point_id'], ''))
            self.ent['user_id'].set(self.us.get(o['user_id'], ''))
            self.ent['status'].set(o['status'])

    def save(self):
        try:
            oa = self.ent['order_artikul'].get()
            od = self.ent['order_date'].get()
            dd = self.ent['delivery_date'].get()
            pc = self.ent['pickup_code'].get()
            st = self.ent['status'].get()
            pid = [k for k,v in self.pp.items() if v==self.ent['pickup_point_id'].get()][0]
            uid = [k for k,v in self.us.items() if v==self.ent['user_id'].get()][0]
            if self.is_edit:
                db_query("UPDATE orders SET order_artikul=%s, order_date=%s, delivery_date=%s, pickup_point_id=%s, user_id=%s, pickup_code=%s, status=%s WHERE id=%s",
                         (oa, od, dd, pid, uid, pc, st, self.order_id))
            else:
                db_query("INSERT INTO orders (order_artikul, order_date, delivery_date, pickup_point_id, user_id, pickup_code, status) VALUES (%s,%s,%s,%s,%s,%s,%s)",
                         (oa, od, dd, pid, uid, pc, st))
            messagebox.showinfo("Успех", "Сохранено")
            self.parent.load_orders()
            self.destroy()
        except Exception as e:
            messagebox.showerror("Ошибка", str(e))


if __name__ == "__main__":
    os.makedirs("product_photos", exist_ok=True)
    LoginForm().mainloop()



Вариант 5 (МебельОрг)
Шрифт: Calibri
Акцент: #0000FF
Скидка >15%: #008080
0 остаток: серый #D3D3D3
Фильтр: диапазоны скидки (0-10,99%, 11-14,99%, 15%+)
Сортировка: цена, количество (без скидки!)
Выпадающие списки: категория, производитель, поставщик (все три)



Правой кнопкой по подключению MySQL → Создать → База данных. Введите имя (например bike_store).
Нажмите SQL (F3), убедитесь что база выбрана в выпадающем списке сверху.
Если ошибка "Public Key Retrieval": в настройках подключения → Driver Properties → allowPublicKeyRetrieval=TRUE, useSSL=FALSE.
Вставьте скрипт создания таблиц (см. ниже). Нажмите Alt+X.
Правой кнопкой по базе → Обновить → разверните Таблицы. Должно быть 7 таблиц.

ER-диаграмма: правой кнопкой по базе → Просмотр схемы → принтер → PDF.
Дамп: правой кнопкой по базе → Инструменты → Создать дамп → SQL, галочки "Структура" и "Данные".

Блок-схема в Word
Вставка → Фигуры:
Овал — начало/конец
Прямоугольник — действие
Ромб — условие
Стрелки — связи
Начало - Загрузка приложения - Отображение окна входа - Выбор действия: Вход/Гость - Ввод логина/пароля - Проверка в БД - Определение роли пользователья - Клиент, Менеджер, Администратор - Доступ: просмотр товара, товары (фильтр/сортировка/поиск) + заказы (просмотр), полный CRUD товаров и заказов - Отображение ФИО в правом верхнем углу - Кнопка "Выход" -> возврат к окну входа - Конец
                                                                  Гость - Открыть список товаров (только просмотр)


Создайте report.docx. Сделайте скриншоты (Alt+Print Screen):
Окно авторизации
Ошибка при неверном пароле
Гость: товары без фильтров
Клиент: ФИО в углу
Менеджер: фильтры, сортировка, поиск, заказы
Админ: добавление, редактирование, удаление товара
Админ: ошибка при удалении товара из заказа
Админ: добавление, редактирование, удаление заказа

Загрузка в Gogs
Создайте папку с файлами (НЕ АРХИВ):

text
├── main.py
├── database_dump.sql
├── ER_diagram.pdf
├── block_scheme.pdf
├── report.docx
├── logo.png
├── picture.png
├── icon.ico
└── product_photos/
    └── (фото товаров)
Загрузите через кнопку "Загрузить файл" в Gogs.


БД создана, 7 таблиц, данные заполнены
ER-диаграмма, дамп, блок-схема в PDF
Приложение запускается без ошибок
Авторизация для гостя, клиента, менеджера, админа
ФИО в правом верхнем углу
Подсветка карточек: скидка > N% и остаток 0
Поиск/фильтр/сортировка в реальном времени
CRUD товаров: фото 300x200, удаление старого, запрет удаления если в заказе
CRUD заказов: статус выпадающим списком
Кнопки "Назад" и "Выход" работают
Сообщения об ошибках информативные
Файлы загружены папкой, не архивом