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


# -*- coding: utf-8 -*-
# adminmanager.py

import hashlib
import os
import shutil
import sqlite3
import tkinter as tk
import uuid
from datetime import datetime
from tkinter import filedialog, messagebox, ttk


def center_on_parent(child_window):
    """
    Универсальная функция для центрирования дочернего окна.
    Работает корректно как при наличии parent, так и без него (None).
    """
    # Обновляем геометрию окна, чтобы winfo_reqwidth/height были актуальны
    child_window.update_idletasks()
    
    # Получаем размеры и координаты ребенка
    window_width = child_window.winfo_reqwidth()
    window_height = child_window.winfo_reqheight()
    
    # --- НАЧАЛО БЛОКА ИСПРАВЛЕНИЯ ---
    # Проверяем, есть ли у окна родитель (мастер)
    if child_window.master is not None:
        # Режим 1: Родитель есть. Центрирование относительно родителя.
        parent = child_window.master
        
        # Получаем координаты и размеры родителя
        x = parent.winfo_rootx() + (parent.winfo_width() // 2) - (window_width // 2)
        y = parent.winfo_rooty() + (parent.winfo_height() // 2) - (window_height // 2)
    else:
        # Режим 2: Родителя нет. Центрирование относительно всего экрана.
        screen_width = child_window.winfo_screenwidth()
        screen_height = child_window.winfo_screenheight()
        
        x = (screen_width // 2) - (window_width // 2)
        y = (screen_height // 2) - (window_height // 2)
    # --- КОНЕЦ БЛОКА ИСПРАВЛЕНИЯ ---

    # Устанавливаем новую позицию окна
    child_window.geometry(f"+{x}+{y}")


# Класс диалогового окна для добавления пользователя
class AddUserDialog(tk.Toplevel):

    def __init__(self, parent):
        super().__init__(parent)
        self.parent = parent  # Родительское окно
        self.title("Добавление пользователя")
        self.result = None 
        self.protocol("WM_DELETE_WINDOW", self.cancel)

        # Логин
        lbl_username = ttk.Label(self, text="Логин:")
        lbl_username.grid(row=0, column=0, sticky="w", padx=10, pady=5)
        self.entry_username = ttk.Entry(self)
        self.entry_username.grid(row=0, column=1, padx=10, pady=5)

        # Роль
        lbl_role = ttk.Label(self, text="Роль:")
        lbl_role.grid(row=1, column=0, sticky="w", padx=10, pady=5)
        self.combobox_role = ttk.Combobox(
            self, values=["Администратор", "Эксперт", "Пользователь"]
        )
        self.combobox_role.grid(row=1, column=1, padx=10, pady=5)

        # Пароль
        lbl_password = ttk.Label(self, text="Пароль:")
        lbl_password.grid(row=2, column=0, sticky="w", padx=10, pady=5)
        self.entry_password = ttk.Entry(self, show="*")
        self.entry_password.grid(row=2, column=1, padx=10, pady=5)

        # Кнопки
        btn_frame = ttk.Frame(self)
        btn_frame.grid(row=3, columnspan=2, pady=10)

        btn_ok = ttk.Button(btn_frame, text="OK", command=self.ok)
        btn_ok.pack(side="left", padx=10)

        btn_cancel = ttk.Button(btn_frame, text="Отмена", command=self.cancel)
        btn_cancel.pack(side="right", padx=10)

    def ok(self):
        username = self.entry_username.get()
        role = self.combobox_role.get()
        password = self.entry_password.get()

        if username and role and password:
            self.result = (username, role, password)
            self.destroy()
        else:
            messagebox.showwarning("Ошибка", "Все поля обязательны для заполнения!")

    def cancel(self):
        self.result = None
        self.destroy()


# === Окно для работы с пользователями ===
class UsersWindow(tk.Toplevel):
    def __init__(self, parent, main_app_reference=None):
        super().__init__(parent)
        self.main_app = main_app_reference # Сохраняем ссылку
        self.title("Пользователи")
        self.build_ui()
        center_on_parent(self)

    def build_ui(self):
        # Контейнер
        container = ttk.Frame(self)
        container.pack(fill="both", expand=True)

        # Левая панель кнопок
        left_panel = ttk.Frame(container)
        left_panel.pack(side="left", fill="y", padx=10, pady=10)

        # Кнопка добавления пользователя
        btn_add_user = ttk.Button(
            left_panel, text="Добавить пользователя", command=self.add_user
        )
        btn_add_user.pack(fill="x", pady=5)

        # Кнопка смены пользователя
        btn_change_user = ttk.Button(
            left_panel, text="Сменить пользователя", command=self.change_user
        )
        btn_change_user.pack(fill="x", pady=5)

        # Кнопка удаления пользователя
        btn_remove_user = ttk.Button(
            left_panel, text="Удалить пользователя", command=self.remove_user
        )
        btn_remove_user.pack(fill="x", pady=5)

        # Правая панель с таблицей пользователей
        right_panel = ttk.Frame(container)
        right_panel.pack(side="right", fill="both", expand=True)

        # Лист пользователей (Treeview)
        columns = ("ID", "Логин", "Роль")
        self.users_tree = ttk.Treeview(right_panel, columns=columns, show="headings")
        for col in columns:
            self.users_tree.heading(col, text=col)
            self.users_tree.column(
                col, stretch=True, anchor="w", width=100
            )  # Ширина столбца
        self.users_tree.pack(fill="both", expand=True)

        # Загрузка данных
        self.load_users()

    def load_users(self):
        """Загружает данные о пользователях в дерево."""
        conn = sqlite3.connect("users.db")
        cursor = conn.cursor()
        cursor.execute("SELECT id, username, role FROM users ORDER BY id ASC;")
        rows = cursor.fetchall()

        # Очистка таблицы
        for i in self.users_tree.get_children():
            self.users_tree.delete(i)

        # Добавляем новые данные без четвертого столбца
        for row in rows:
            self.users_tree.insert("", "end", values=(row[0], row[1], row[2]))

        conn.close()

    def add_user(self):
        """Метод для добавления пользователя."""
        dialog = AddUserDialog(self)
        self.wait_window(dialog)
        if dialog.result:
            username, role, password = dialog.result

            # Хэшируем пароль
            salt = uuid.uuid4().hex
            hashed_password = hashlib.sha256((password + salt).encode()).hexdigest()

            # Добавляем пользователя в базу данных
            with sqlite3.connect("users.db") as conn:
                cursor = conn.cursor()
                cursor.execute(
                    "INSERT INTO users (username, role, password_hash, salt) VALUES (?,?,?,?)",
                    (username, role, hashed_password, salt),
                )
                conn.commit()

            # Обновляем таблицу
            self.load_users()

    # Внутри класса UsersWindow в adminmanager.py
    def change_user(self):
        """
        Открывает диалоговое окно для смены/выбора пользователя.
        Само это окно не выполняет выход, оно лишь ждет результата от диалога входа.
        """
        # Создаем модальное диалоговое окно для входа
        login_dialog = LoginDialog(parent=self) 
        
        # Ждем, пока диалоговое окно не закроется
        self.wait_window(login_dialog)

        # --- КОД НИЖЕ ВЫПОЛНИТСЯ ТОЛЬКО ПОСЛЕ ЗАКРЫТИЯ ОКНА ---

        # Проверяем, был ли вход успешным (флаг устанавливается в LoginDialog)
        if hasattr(login_dialog, 'login_success') and login_dialog.login_success:
            messagebox.showinfo("Успех", "Пользователь сменен.")
            # После успешной смены можно закрыть окно управления пользователями
            self.destroy()

    def remove_user(self):
        """Метод для удаления пользователя и правильного обновления последовательности ID"""
        selected_item = self.users_tree.selection()
        if not selected_item:
            messagebox.showwarning("Ошибка", "Выберите пользователя для удаления!")
            return

        # Получаем ID выбранного пользователя
        user_id = int(self.users_tree.item(selected_item)["values"][0])

        # Подтверждение удаления
        answer = messagebox.askyesno(
            "Подтверждение удаления", f"Удалить пользователя с ID={user_id}?"
        )
        if not answer:
            return

        # Удаляем пользователя и перестраиваем ID
        with sqlite3.connect("users.db") as conn:
            cursor = conn.cursor()

            # Удаляем пользователя
            cursor.execute("DELETE FROM users WHERE id=?", (user_id,))

            # Перестраиваем последовательность ID
            cursor.execute("SELECT * FROM users ORDER BY id ASC;")
            rows = cursor.fetchall()

            new_id = 1
            for row in rows:
                old_id = row[0]
                cursor.execute("UPDATE users SET id=? WHERE id=?", (new_id, old_id))
                new_id += 1

            # Сбрасываем счётчик AUTO_INCREMENT
            cursor.execute("DELETE FROM sqlite_sequence WHERE name='users';")

            conn.commit()

        # Обновляем таблицу
        self.load_users()


# === Окно для работы с базами данных ===
class DatabaseWindow(tk.Toplevel):

    def __init__(self, parent):
        super().__init__(parent)
        
        # Убираем сохранение собственного атрибута .parent
        # self.parent = parent 

        self.title("Управление базами данных")
        self.geometry("600x400")

        # --- ИСПРАВЛЕНИЕ ЗДЕСЬ ---
        # Вызываем функцию центрирования только по себе.
        # Функция сама найдет главного родителя через self.master
        center_on_parent(self) 
        
        # Основная структура интерфейса
        self.build_ui()

    def build_ui(self):
        # Основной контейнер
        container = ttk.Frame(self)
        container.pack(fill="both", expand=True)

        # Верхняя панель настроек
        top_panel = ttk.Frame(container)
        top_panel.pack(fill="x", pady=10)

        # Переключатель баз данных
        self.use_master_db = tk.BooleanVar(value=False)
        style = ttk.Style()
        style.configure("RedText.TCheckbutton", foreground="red")
        master_db_checkbox = ttk.Checkbutton(
            top_panel,
            text="Использовать основную базу данных",
            variable=self.use_master_db,
            style="RedText.TCheckbutton",
        )
        master_db_checkbox.pack(side="left", padx=10)

        # Панель инструментов
        tools_panel = ttk.Frame(container)
        tools_panel.pack(fill="x", pady=10)

        # Кнопка резервного копирования текущей базы
        btn_current_backup = ttk.Button(
            tools_panel,
            text="Создать резервную копию текущей базы",
            command=self.backup_current_database,
        )
        btn_current_backup.pack(side="left", padx=10)

        # Кнопка восстановления базы данных
        btn_restore = ttk.Button(
            tools_panel, text="Восстановить базу данных", command=self.restore_database
        )
        btn_restore.pack(side="left", padx=10)

        # Кнопка миграции баз данных
        btn_migrate = ttk.Button(
            tools_panel, text="Мигрировать данные", command=self.migrate_databases
        )
        btn_migrate.pack(side="left", padx=10)

        # Таблица резервных копий
        backup_columns = ("Дата", "Файл")
        self.backups_tree = ttk.Treeview(
            container, columns=backup_columns, show="headings"
        )
        for col in backup_columns:
            self.backups_tree.heading(col, text=col)
            self.backups_tree.column(col, width=200)
        self.backups_tree.pack(fill="both", expand=True)

        # Загружаем существующие резервные копии
        self.load_backups()

    def load_backups(self):
        """Загружает список существующих резервных копий в дерево."""
        self.backups_tree.delete(*self.backups_tree.get_children())  # Очищаем дерево

        # Директория резервных копий
        backup_dir = "backups/"
        os.makedirs(backup_dir, exist_ok=True)

        # Список всех резервных копий
        backups = []
        for filename in os.listdir(backup_dir):
            if filename.endswith(".db"):  # Фильтруем только файлы SQLite
                filepath = os.path.join(backup_dir, filename)
                creation_time = datetime.fromtimestamp(
                    os.path.getmtime(filepath)
                ).strftime("%Y-%m-%d %H:%M:%S")
                backups.append({"file": filename, "creation_time": creation_time})

        # Сортируем резервные копии по дате создания
        sorted_backups = sorted(backups, key=lambda b: b["creation_time"], reverse=True)

        # Добавляем записи в TreeView
        for backup in sorted_backups:
            self.backups_tree.insert(
                "", "end", values=(backup["creation_time"], backup["file"])
            )

    def backup_current_database(self):
        """Создает резервную копию текущей базы данных."""
        db_filename = "database.db" if not self.use_master_db.get() else "masterdata.db"

        # Полный путь к файлу резервной копии
        backup_dir = "backups/"
        os.makedirs(backup_dir, exist_ok=True)
        now = datetime.now()
        backup_filename = f"{now.strftime('%Y-%m-%d_%H-%M-%S')}__{db_filename}"
        backup_filepath = os.path.join(backup_dir, backup_filename)

        try:
            shutil.copy(db_filename, backup_filepath)
            messagebox.showinfo(
                "Резервное копирование",
                f"Резервная копия успешно создана:\n{backup_filepath}",
            )
            self.load_backups()  # Обновляем список резервных копий
        except Exception as e:
            messagebox.showerror("Ошибка", str(e))

    def restore_database(self):
        """Восстанавливает выбранную резервную копию."""
        selected_item = self.backups_tree.selection()
        if len(selected_item) > 0:
            backup_filename = self.backups_tree.item(selected_item)[0]["values"][1]
            backup_filepath = os.path.join("backups/", backup_filename)

            active_db = (
                "database.db" if not self.use_master_db.get() else "masterdata.db"
            )

            try:
                shutil.copy(backup_filepath, active_db)
                messagebox.showinfo(
                    "Восстановление",
                    f"База данных успешно восстановлена из резервной копии.\nАктивная база данных обновлена.",
                )
                self.load_backups()  # Обновляем список резервных копий
            except Exception as e:
                messagebox.showerror("Ошибка", str(e))
        else:
            messagebox.showwarning(
                "Предупреждение", "Сначала выберите резервную копию для восстановления."
            )

    def migrate_databases(self):
        """Выполняет миграцию данных между двумя базами данных."""
        if not self.use_master_db.get():
            src_db = "database.db"
            dest_db = "masterdata.db"
        else:
            src_db = "masterdata.db"
            dest_db = "database.db"

        # Создание временной копии исходной базы данных
        temp_src_db = f"{src_db}_temp"
        shutil.copy(src_db, temp_src_db)

        # Перемещение данных
        try:
            # Открытие обеих баз данных
            conn_src = sqlite3.connect(temp_src_db)
            conn_dest = sqlite3.connect(dest_db)

            # Копирование содержимого одной базы в другую
            conn_src.backup(conn_dest)

            # Закрытие соединений
            conn_src.close()
            conn_dest.close()

            # Сообщение об успехе
            messagebox.showinfo("Миграция", "Данные успешно перенесены.")
        except Exception as e:
            messagebox.showerror("Ошибка", str(e))
            
class LoginDialog(tk.Toplevel):
    """Диалоговое окно для авторизации нового пользователя."""
    def __init__(self, parent):
        super().__init__(parent)
        self.title("Вход в систему")
        self.geometry("300x200")
        self.resizable(False, False)
        
        # Флаг успешной авторизации по умолчанию - False
        self.successful = False 

        frame = ttk.Frame(self, padding="20")
        frame.pack(expand=True)

        # --- Поля ввода ---
        ttk.Label(frame, text="Логин:").grid(row=0, column=0, sticky="e", pady=5)
        self.entry_login = ttk.Entry(frame)
        self.entry_login.grid(row=0, column=1, padx=10, pady=5, sticky="we")

        ttk.Label(frame, text="Пароль:").grid(row=1, column=0, sticky="e", pady=5)
        self.entry_pass = ttk.Entry(frame, show="*") # Символы пароля скрыты звездочками
        self.entry_pass.grid(row=1, column=1, padx=10, pady=5, sticky="we")

        # --- Кнопки ---
        btn_frame = ttk.Frame(frame)
        btn_frame.grid(row=2, column=0, columnspan=2, pady=15)

        btn_ok = ttk.Button(btn_frame, text="Войти", command=self.on_login)
        btn_ok.pack(side="left", expand=True, ipadx=10, ipady=5)

        btn_cancel = ttk.Button(btn_frame, text="Отмена", command=self.destroy)
        btn_cancel.pack(side="right", expand=True, ipadx=10, ipady=5)

    def on_login(self):
        """
        Проверяет введенные данные по базе данных SQLite.
        """
        login = self.entry_login.get()
        password = self.entry_pass.get()

        # Если поля пусты - выходим сразу
        if not (login and password):
            messagebox.showerror("Ошибка", "Пожалуйста, заполните все поля!")
            return

        try:
            # Подключаемся к базе данных пользователей
            conn = sqlite3.connect("users.db")
            cursor = conn.cursor()

            # 1. Находим пользователя по логину
            cursor.execute("SELECT id, username, role, password_hash, salt FROM users WHERE username=?", (login,))
            user_data = cursor.fetchone()

            # 2. Проверяем, нашли ли мы такого пользователя
            if user_data is None:
                raise ValueError("Пользователь не найден.")

            # Достаём из базы хеш пароля и соль
            db_password_hash = user_data[3]
            salt = user_data[4]

            # 3. Хешируем введенный пароль с той же солью
            hashed_input = hashlib.sha256((password + salt).encode()).hexdigest()

            # 4. Сравниваем полученный хеш с тем, что лежит в базе
            if hashed_input == db_password_hash:
                # --- УСПЕХ ---
                messagebox.showinfo("Успех", f"Вход выполнен успешно!\nПриветствую, {user_data[1]}!")
                
                # Здесь можно сохранить данные о пользователе куда нужно
                # Например, передать их в главное окно через атрибут
                self.selected_user = {
                    "username": user_data[1],
                    "role": user_data[2]
                }
                self.successful = True
                self.destroy() # Закрываем окно

            else:
                # Пароль неверный
                raise ValueError("Неверный логин или пароль.")

        except Exception as e:
            # Сюда попадём, если пользователь не найден или пароль неверный
            print(f"Ошибка входа:", e) # Для отладки
            messagebox.showerror("Ошибка", "Неверный логин или пароль.")
            
        finally:
            # Обязательно закрываем соединение с БД
            if 'conn' in locals():
                conn.close()