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


# -*- 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(parent, child):
    """
    Функция для размещения дочернего окна по центру родительского окна.
    """
    x = parent.winfo_x() + (parent.winfo_width() // 2) - (child.winfo_reqwidth() // 2)
    y = parent.winfo_y() + (parent.winfo_height() // 2) - (child.winfo_reqheight() // 2)
    child.geometry("+{}+{}".format(x, y))


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

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

        # Логин
        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):
        super().__init__(parent)
        self.parent = parent  # Родительское окно
        self.title("Пользователи")

        # Основное содержимое окна
        self.build_ui()

        # Центрирование окна на экране родителя
        center_on_parent(self.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()

    def change_user(self):
        """Метод для полного выхода из системы и возвращения в форму авторизации."""
        self.destroy()
        self.parent.logout()

    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)
        self.parent = parent  # Родительское окно
        self.title("Управление базами данных")
        self.geometry("600x400")

        # Центрируем окно на экране родителя
        center_on_parent(self.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))