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


# ====================================================
# Файл №1: Dozor/main.py  
# - запускающий файл, лежит в корне приложения в папке Dozor
# ====================================================

# ---*--- Dozor/main.py ---*---
# -*- coding: utf-8 -*-

import sys
import tkinter as tk
from tkinter import ttk
from gui.main_interface import MainInterface 
from modules.data.database_module import init_database
from modules.utils.app_state import disable_app, enable_app
import ctypes # Импортируем для работы с WinAPI

try:
    from gui.auth_interface import AuthInterface
except ImportError:
    print("[FATAL] Не найден файл gui/auth_interface.py")
    sys.exit(1)

init_database()

def main():
    app = MainInterface()
    
    # Устанавливаем временный базовый заголовок
    app.title("ДОЗОР -- Heri-Hodie-Cras")
    
    disable_app(app)
   
    screen_w = app.winfo_screenwidth()
    screen_h = app.winfo_screenheight()
    win_w, win_h = 458, 360
    pos_x = int((screen_w / 2) - (win_w / 2))
    pos_y = int((screen_h / 2) - (win_h / 2))
    app.geometry(f"{win_w}x{win_h}+{pos_x}+{pos_y}") 

    app.deiconify()
    app.lift()
    app.focus_force()

    current_user = {"login": None}

    def do_login(username, role_from_db):
        if username and role_from_db:
            current_user["login"] = username
            
            enable_app(app)
            auth_win.destroy()
            
            app.current_user = {
                "login": username,
                "role": role_from_db
            }
            
            # Показываем интерфейс главного меню
            app._show_main_screen()
            
            # === WINAPI ХАК: Принудительная смена заголовка ОС ===
            try:
                # Получаем HWND (хэндл/номер окна) нашего приложения
                hwnd = app.winfo_id()
                
                # Формируем правильную строку (важно кодировка!)
                new_title = f"ДОЗОР -- {username} ({role_from_db})"
                
                # Вызываем функцию SetWindowTextA из user32.dll
                ctypes.windll.user32.SetWindowTextA(hwnd, new_title.encode('utf-8'))
                
            except Exception as api_err:
                print(f"[WARNING] Не удалось сменить заголовок через WinAPI: {api_err}")
                # Если WinAPI упал, пробуем стандартный метод (на всякий случай)
                app.title(new_title)

        else:
            from tkinter import messagebox
            messagebox.showwarning("Ошибка", "Данные авторизации некорректны")

    try:
        auth_win = AuthInterface(app, on_login_click=do_login)
        auth_win.transient(app)
        auth_win.grab_set() 
        
        app.wait_window(auth_win)
    except Exception as e:
        print(f"[CRITICAL ERROR] Failed to initialize Auth Window: {e}")
        app.destroy()
        return

    if not current_user["login"]:
        app.destroy()
        return

    app.mainloop()


if __name__ == "__main__":
    main()
	







# ====================================================
# Файл №2: Dozor/gui/main_interface_old.py 
# - старый файл, который подвергается рефакторингу.
# - это файл донор для организации АДМИНИСТРАТОР
# ====================================================

# ---*--- Dozor/gui/main_interface_old.py  ---*---
# -*- coding: utf-8 -*-

import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageTk
import os
from gui.auth_interface import AuthInterface
from modules.administrator.admin_user_inter import AdminUserInter
from modules.utils.window_behavior import LevelOneBehavior
from modules.administrator.admin_db_inter import AdminDBInter
from modules.project.project_create_inter import ProjectCreateInterface
from modules.project.project_edit_inter import ProjectEditInterface
from modules.project.project_create_work import ProjectCreateController
from modules.project.services.project_service import ProjectService


def open_window(title):
    print(f"[DEBUG] Вызван универсальный open_window для: {title}")
    temp_win = tk.Toplevel()
    temp_win.title(title)
    temp_win.geometry("300x150")
    ttk.Label(temp_win, text=f"Интерфейс '{title}'", font=("Arial", 14)).pack(pady=20)
    ttk.Button(temp_win, text="Закрыть", command=temp_win.destroy).pack(pady=10)

class MainInterface(tk.Tk):
    def __init__(self):
        super().__init__()
        
        # Флаг-предохранитель от ошибок winfo при смене пользователя
        self._ui_blocked_by_logout = False
        
        # Текущий пользователь (единый источник истины)
        self.current_user = {"login": None}
        
        behavior = LevelOneBehavior()
        behavior.setup_level_one(self)
        
        self.title("ДОЗОР -- Heri-Hodie-Cras")
        self.geometry("458x360")
        self.resizable(False, False)
        
        bg_color = "#EEEEEE"
        self.configure(bg=bg_color)
        
        main_frame = tk.Frame(self, bg=bg_color)
        main_frame.pack(padx=1, pady=1, fill='both', expand=True)
        
        button_width = 20
        button_height = 2
        button_font = ("Arial", 12, "bold")
        
        for i in range(7):
            main_frame.grid_rowconfigure(i, weight=0)
        main_frame.grid_columnconfigure(0, minsize=210, weight=0)
        main_frame.grid_columnconfigure(1, minsize=235, weight=0)
        
        self.active_popup = None

        try:
            from modules.project.project_create_work import ProjectCreateController
            from modules.data.database_module import get_db_connection
            db_conn = get_db_connection() 
            self.manager = ProjectCreateController(db_conn) 
        except Exception as e:
            print(f"[FATAL] Не удалось инициализировать менеджер проектов: {e}")
            self.manager = None

        self.bind("<Button-1>", self.close_popup_global)

        left_texts = [
            ["Администратор", [("Пользователи", "users"), ("Базы данных", "db")]],
            ["Менеджер базы", [("Менеджер группы", "group"), ("Менеджер показателей", "metrics")]],
            ["Эксперты", [("Создание файлов", "create_files"), ("Агрегирование файлов", "aggregate")]],
            ["Проект", [("Создать", "new"), ("Редактировать", "edit")]],
            ["Прогнозирование", []]
        ]

        # --- ЛЕВАЯ ПАНЕЛЬ С УТОЧНЕНИЕМ ДОСТУПА ---
        for row_index, item in enumerate(left_texts):
            btn_text = item[0]
            
            def execute_button(title=btn_text, index=row_index):
                if title == "Администратор":
                    user_role = getattr(self, 'current_user', {}).get("role")
                    clean_role = str(user_role).strip().lower() if user_role else ""
                    
                    if clean_role != "администратор":
                        from tkinter import messagebox
                        
                        answer = messagebox.askyesno(
                            "Требуются права администратора", 
                            f"У вашей учетной записи ('{clean_role.capitalize()}') недостаточно прав.\n\nХотите сменить пользователя и войти как Администратор?"
                        )
                        
                        if answer:
                            self.switch_user_radical()
                            
                        return 
                
                self.show_dropdown(title, index)

            btn = tk.Button(
                main_frame,
                text=btn_text,
                font=button_font,
                width=button_width,
                height=button_height,
                anchor="w",
                command=execute_button
            )
            btn.grid(row=row_index, column=0, padx=5, pady=5, sticky="ew")

        right_box = tk.Frame(main_frame, bg=bg_color, width=235, height=300)
        right_box.grid(row=0, column=1, rowspan=6, pady=5, sticky="nsew", padx=(0, 5))
        right_box.grid_propagate(False)
        right_box.columnconfigure(0, weight=1)
        
        picture_frame = tk.Frame(right_box, bg="#d9ead3", highlightbackground="gray", highlightthickness=1, width=225, height=160)
        picture_frame.place(relx=0.5, y=5, anchor="n")
        picture_frame.grid_propagate(False)
        
        current_dir = os.path.dirname(os.path.abspath(__file__))
        image_path = os.path.join(current_dir, "images", "Сова1.png")
        try:
            original_img = Image.open(image_path)
            target_w, target_h = 215, 130
            img_ratio = original_img.width / original_img.height
            box_ratio = target_w / target_h
            if img_ratio > box_ratio:
                new_w, new_h = target_w, int(target_w / img_ratio)
            else:
                new_w, new_h = int(target_h * img_ratio), target_h
            resized_img = original_img.resize((new_w, new_h), resample=Image.LANCZOS)
            self.tk_image = ImageTk.PhotoImage(resized_img)
            label = tk.Label(picture_frame, image=self.tk_image, bg="#d9ead3", borderwidth=0)
            label.image = self.tk_image
            label.place(relx=0.5, rely=0.5, anchor='center', width=new_w, height=new_h)
        except Exception as e:
            print(f"[FATAL IMAGE ERROR]: {e}")
            canvas = tk.Canvas(picture_frame, width=225, height=160, bg="red", highlightthickness=0)
            canvas.create_text(112, 80, text="IMG\nFAIL", fill="white")
            canvas.place(x=0, y=0, relwidth=1, relheight=1)
            
        spacer = tk.Frame(right_box, bg=bg_color, height=1)
        spacer.grid(row=0, column=0, pady=(170, 10), sticky="ew")
        
        upper_padding = tk.Frame(right_box, bg=bg_color, height=3)
        upper_padding.grid(row=1, column=0)
        
        btn6 = tk.Button(right_box, text="Руководство", font=button_font, width=button_width, height=button_height)
        btn6.grid(row=2, column=0, ipadx=10, pady=(0, 5), sticky="ew")
        btn7 = tk.Button(right_box, text="О программе", font=button_font, width=button_width, height=button_height)
        btn7.grid(row=3, column=0, ipadx=10, pady=(5, 15), sticky="ew")

        footer_frame = tk.Frame(main_frame, bg="#0078D7")
        footer_frame.grid(row=6, column=0, columnspan=2, sticky="ew", pady=(10, 0))
        footer_label = tk.Label(footer_frame, text="Copyright © Минск, НИИ, 2027 год", fg="#FF8C00", bg="#0078D7", font=("Arial", 12, "bold"))
        footer_label.pack(expand=True, ipady=4)

        self.bind("<B1-Motion>", self._on_drag_main_window)
        self._drag_start_x = None
        self._drag_start_y = None

    def _on_drag_main_window(self, event):
        if self._drag_start_x is None:
            self._drag_start_x = event.x_root
            self._drag_start_y = event.y_root
            return

        delta_x = event.x_root - self._drag_start_x
        delta_y = event.y_root - self._drag_start_y

        if abs(delta_x) > 2 or abs(delta_y) > 2:
            if self.active_popup and self.active_popup.winfo_exists():
                geo = self.active_popup.geometry()
                parts = geo.split('+')
                if len(parts) == 3:
                    new_x = int(parts[1]) + delta_x
                    new_y = int(parts[2]) + delta_y
                    self.active_popup.geometry(f"+{new_x}+{new_y}")
            
            self._update_child_positions(event.x_root - self.winfo_rootx(), event.y_root - self.winfo_rooty())

            self._drag_start_x = event.x_root
            self._drag_start_y = event.y_root

    def show_dropdown(self, menu_name, base_row):
        from modules.utils.app_state import disable_app, enable_app
        
        if self.active_popup is not None:
            if hasattr(self.active_popup, '_child_windows'):
                for child_data in list(self.active_popup._child_windows):
                    win, _, _ = child_data
                    try:
                        win.destroy()
                    except Exception:
                        pass
            self.active_popup.destroy()
        
        self.active_popup = None 
        
        root_win = self.winfo_toplevel()
        
        data = {
            "Администратор": [("Пользователи", "users"), ("Базы данных", "db")],
            "Менеджер базы": [("Менеджер группы", "group"), ("Менеджер показателей", "metrics")],
            "Эксперты": [("Создание файлов", "create_files"), ("Агрегирование файлов", "aggregate")],
            "Проект": [("Создать", "new"), ("Редактировать", "edit")],
        }.get(menu_name, [])
        if not data:
            return

        x = self.winfo_rootx() + 5
        y = self.winfo_rooty() + (base_row * 35) + 50
        
        popup = tk.Toplevel(self)
        popup.overrideredirect(True)
        popup.geometry(f"+{x}+{y}")
        
        self.active_popup = popup

        popup_frame = tk.Frame(popup, bg="#F9F9F9", bd=1, relief="solid")
        popup_frame.pack()

        def make_command(action_title, target_popup=None):
            def execute():
                if target_popup:
                    target_popup.destroy()
                
                disable_app(root_win)

                app_instance = self 

                def handle_logout(restart=False):
                    global current_user_session, current_user
                    current_user_session = {"login": None}
                    current_user = {"login": None} 
                    
                    if hasattr(app_instance, 'current_user'):
                        app_instance.current_user = {"login": None}
                    
                    print("[DEBUG] handle_logout вызван")

                    if restart:
                        # === КРИТИЧЕСКИ ВАЖНО: Ставим блок ПЕРЕД уничтожением окна ===
                        app_instance._ui_blocked_by_logout = True
                        
                        disable_app(root_win)
                        root_win.withdraw() 
                        
                        login_callback = app_instance.do_login
                        new_auth = AuthInterface(root_win, on_login_click=login_callback)
                        
                        root_win.wait_window(new_auth) 
                        
                        print(f"[DEBUG] Авторизация завершена. Текущий юзер: {current_user.get('login')}")
                        
                        if current_user.get("login"):
                            root_win.deiconify()
                            enable_app(root_win)
                            
                            role_display = current_user.get("role", "Без роли")
                            root_win.title(f"ДОЗОР -- {current_user['login']} ({role_display})")
                            
                            # Разблокируем только после успешного входа
                            app_instance._ui_blocked_by_logout = False
                        else:
                            try:
                                root_win.destroy()
                            except Exception:
                                pass
                    else:
                        root_win.destroy()

                if action_title == "Администратор":
                    user_role = getattr(self, 'current_user', {}).get("role")
                    clean_role = str(user_role).strip().lower() if user_role else ""
                    if clean_role != "администратор":
                        enable_app(root_win)
                        from tkinter import messagebox
                        messagebox.showwarning("Доступ запрещен", f"Раздел '{action_title}' доступен только пользователю с ролью Администратор.")
                        return

                if action_title == "Пользователи":
                    users_win = AdminUserInter(root_win)
                    root_win.register_child_window(users_win, offset_x=20, offset_y=20)
                    root_win.wait_window(users_win)
                    
                    # === ГЛАВНАЯ ПРОВЕРКА В ОКНЕ АДМИНКИ ===
                    # Если флаг блокировки стоит — значит, идет смена юзера, просто выходим.
                    if app_instance._ui_blocked_by_logout:
                        return

                    final_login = getattr(app_instance, 'current_user', {}).get("login")
                    if final_login is None:
                        print("[INFO] Пользователь инициировал смену через закрытие окна. Перезапуск...")
                        disable_app(root_win)
                        root_win.withdraw()
                        login_callback = app_instance.do_login
                        new_auth = AuthInterface(root_win, on_login_click=login_callback)
                        root_win.wait_window(new_auth)
                        after_login = getattr(app_instance, 'current_user', {}).get("login")
                        if after_login:
                            root_win.deiconify()
                            enable_app(root_win)
                            role_display = app_instance.current_user.get("role", "Без роли")
                            root_win.title(f"ДОЗОР -- {app_instance.current_user['login']} ({role_display})")
                            app_instance._ui_blocked_by_logout = False
                        else:
                            try:
                                root_win.destroy()
                            except Exception:
                                pass
                        return
                    else:
                        enable_app(root_win)
                        return
                
                elif action_title == "Базы данных":
                    db_win = AdminDBInter(root_win)
                    root_win.register_child_window(db_win, offset_x=20, offset_y=20)
                    root_win.wait_window(db_win)
                
                elif action_title == "Менеджер группы":
                    from modules.manager.manager_group_inter import ManagerGroupInter
                    win = ManagerGroupInter(root_win)
                    root_win.register_child_window(win, offset_x=20, offset_y=20)
                    win.lift()
                    
                elif action_title == "Менеджер показателей":
                    from modules.manager.manager_metrics_inter import ManagerMetricsInter
                    win = ManagerMetricsInter(root_win)
                    root_win.register_child_window(win, offset_x=20, offset_y=20)
                    win.lift()
                    
                elif action_title == "Создание файлов":
                    from modules.excel.excel_export_inter import ExpertExportInterface
                    export_win = ExpertExportInterface(root_win)
                    root_win.register_child_window(export_win, offset_x=20, offset_y=20)
                    export_win.lift()
                    
                elif action_title == "Агрегирование файлов":
                    from modules.excel.excel_aggregate_inter import ExpertAggregateInterface
                    agg_win = ExpertAggregateInterface(root_win)
                    root_win.register_child_window(agg_win, offset_x=20, offset_y=20)
                    agg_win.lift()
                    
                elif action_title == "Создать":
                    from modules.project.project_create_inter import ProjectCreateInterface
                    create_win = ProjectCreateInterface(root_win, self.manager)
                    root_win.register_child_window(create_win, offset_x=20, offset_y=20)
                    create_win.lift()
                    
                elif action_title == "Редактировать":
                    from modules.project.project_edit_inter import ProjectEditInterface
                    def on_project_selected(data_dict):
                        from modules.project.project_event_inter import ProjectEventWindow
                        event_win = ProjectEventWindow(manager=self.manager, selected_data=data_dict, root_window=root_win)
                        root_win.register_child_window(event_win, offset_x=20, offset_y=20)
                        event_win.lift()
                    edit_win = ProjectEditInterface(root_win, manager=self.manager, on_select_callback=on_project_selected)
                    root_win.register_child_window(edit_win, offset_x=20, offset_y=20)
                    edit_win.lift()                    
                    
                else:
                    open_window(action_title)

                enable_app(root_win)
            
            return execute

        # --- ЦИКЛ СОЗДАНИЯ КНОПОК В ОКНЕ ---
        for sub_text, action_key in data:
            btn_command = make_command(sub_text)

            def wrap_execute(cmd=None, pop_ref=popup, r_win=root_win):
                if pop_ref and pop_ref.winfo_exists():
                    if hasattr(pop_ref, '_child_windows') and pop_ref._child_windows:
                         pop_ref.after(10, pop_ref.destroy)
                    else:
                         pop_ref.destroy()
                
                # === ФИНАЛЬНЫЙ БАРЬЕР ДЛЯ ЛЯМБДА-ФУНКЦИИ ===
                # Проверяем флаг перед тем, как вообще пытаться запустить команду cmd()
                if cmd is not None and not r_win._ui_blocked_by_logout:
                    cmd()
                else:
                    enable_app(r_win)

            sub_btn = tk.Button(
                popup_frame,
                text=sub_text,
                font=("Arial", 11),
                width=20,
                height=1,
                anchor="w",
                bd=0,
                relief="flat",
                cursor="hand2",
                command=lambda c=btn_command: wrap_execute(c)
            )
            sub_btn.pack(fill="x", ipadx=5, pady=3)
            sep = tk.Frame(popup_frame, bg="#E0E0E0", height=1)
            sep.pack(fill="x")
        def check_leave(e):
            if not popup.winfo_exists():
                return
            mouse_x = e.x_root
            mouse_y = e.y_root
            pop_x = popup.winfo_rootx()
            pop_y = popup.winfo_rooty()
            pop_w = popup.winfo_width()
            pop_h = popup.winfo_height()
            safe_zone_x1 = pop_x - 30
            safe_zone_y1 = pop_y - 30
            safe_zone_x2 = pop_x + pop_w + 30
            safe_zone_y2 = pop_y + pop_h + 30
            if not (safe_zone_x1 <= mouse_x <= safe_zone_x2 and safe_zone_y1 <= mouse_y <= safe_zone_y2):
                popup.destroy()

        popup.bind("<Motion>", check_leave)

    def switch_user_radical(self):
        """Радикальная смена пользователя: полный выход + немедленный вход."""
        root_win = self.winfo_toplevel()
        
        from modules.utils.app_state import disable_app
        disable_app(root_win)

        if hasattr(self, 'current_user'):
            self.current_user = {"login": None}
            
        if hasattr(self, '_child_windows'):
             self._child_windows.clear()

        try:
            root_win.destroy()
        except Exception:
            pass

        import sys, subprocess, os, shutil
        
        script_path = os.path.abspath("main.py")
        gui_python = shutil.which("pythonw.exe") 
        if not gui_python:
            current_dir = os.path.dirname(sys.executable)
            potential_path = os.path.join(current_dir, "pythonw.exe")
            gui_python = potential_path if os.path.exists(potential_path) else sys.executable

        try:
            subprocess.Popen([gui_python, script_path], shell=True, creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
        except Exception as e:
            print(f"[FATAL] Не удалось запустить новую копию Dozor: {e}")

        sys.exit(0)

    def open_admin_users(self):
        from modules.utils.app_state import disable_app, enable_app
        root = self.winfo_toplevel()
        if not root or not hasattr(root, 'winfo_exists') or not root.winfo_exists():
            return
        disable_app(root)
        users_win = AdminUserInter(self)
        self.wait_window(users_win)
        enable_app(root)

    def open_admin_dbs(self):
        from modules.utils.app_state import disable_app, enable_app
        root = self.winfo_toplevel()
        if not root or not hasattr(root, 'winfo_exists') or not root.winfo_exists():
            return
        disable_app(root)
        db_win = AdminDBInter(root)
        self.wait_window(db_win)
        enable_app(root)

    def close_popup_global(self, event):
        if self.active_popup is not None:
            widget = event.widget
            while widget:
                if widget == self.active_popup:
                    return
                widget = widget.master
            self.active_popup.destroy()
            self.active_popup = None

    def _update_child_positions(self, new_x, new_y):
        if hasattr(self, '_child_windows'):
            for child_data in self._child_windows:
                win, offset_x, offset_y = child_data
                try:
                    win.geometry(f"+{new_x + offset_x}+{new_y + offset_y}")
                except Exception:
                    pass

    def register_child_window(self, window, offset_x=0, offset_y=0):
        if not hasattr(self, '_child_windows'):
            self._child_windows = []
        for item in self._child_windows:
            if item[0] == window:
                return
        self._child_windows.append((window, offset_x, offset_y))

if __name__ == "__main__":
    app = MainInterface()
    app.mainloop()









# ====================================================
# Файл №3: Dozor/modules/administrator/user_logic.py 
# - старый файл, который подвергается рефакторингу.
# - это файл донор для организации АДМИНИСТРАТОР
# ====================================================

# ---*--- Dozor/modules/administrator/user_logic.py   ---*---
# -*- coding: utf-8 -*-

import os
import sqlite3
import hashlib
import secrets

class UserLogic:
    def __init__(self, db_path="db/users.db"):
        self.db_path = db_path
        if not os.path.exists(os.path.dirname(db_path)):
            os.makedirs(os.path.dirname(db_path))
        self._ensure_schema()

    def _hash_password(self, password: str, salt: str) -> str:
        return hashlib.sha256((salt + password).encode()).hexdigest()

    def _generate_salt(self) -> str:
        return secrets.token_hex(16)

    def _ensure_schema(self):
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            
            # Проверяем структуру. Если колонки login нет — таблица старая.
            cursor.execute("PRAGMA table_info(users)")
            columns = [col[1] for col in cursor.fetchall()]
            
            if "login" not in columns or len(columns) < 4:
                print("[INFO] Структура users.db некорректна. Пересоздаем...")
                cursor.execute("DROP TABLE IF EXISTS users")
            
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS users (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    login TEXT UNIQUE NOT NULL,
                    password_hash TEXT NOT NULL,
                    salt TEXT NOT NULL,
                    role TEXT NOT NULL
                )
            ''')
            
            # Создаем дефолтного админа ТОЛЬКО если таблицы пуста
            cursor.execute("SELECT COUNT(*) FROM users")
            if cursor.fetchone()[0] == 0:
                salt = secrets.token_hex(16)
                pwd_hash = hashlib.sha256((salt + "admin").encode()).hexdigest()
                cursor.execute(
                    "INSERT INTO users (login, password_hash, salt, role) VALUES (?, ?, ?, ?)",
                    ("admin", pwd_hash, salt, "Администратор")
                )
                print("[INFO] Создан пользователь 'admin' с паролем 'admin'")
                
            conn.commit()

    def get_all_users(self):
        try:
            with sqlite3.connect(self.db_path) as conn:
                return conn.execute("SELECT id, login, role FROM users ORDER BY id ASC").fetchall()
        except Exception:
            self._ensure_schema()
            return [(1, "admin", "Администратор")]

    def add_user(self, login: str, role: str, password: str):
        valid_roles = ["Администратор", "Эксперт", "Пользователь"]
        if role not in valid_roles:
            raise ValueError(f"Недопустимая роль: {role}")
            
        salt = self._generate_salt()
        pwd_hash = self._hash_password(password, salt)
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                "INSERT INTO users (login, password_hash, salt, role) VALUES (?, ?, ?, ?)",
                (login, pwd_hash, salt, role)
            )
            conn.commit()

    def delete_user(self, user_id: int):
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            cursor.execute("DELETE FROM users WHERE id=?", (user_id,))
            cursor.execute("UPDATE users SET id = id - 1 WHERE id > ?", (user_id,))
            max_id = cursor.execute("SELECT MAX(id) FROM users").fetchone()[0] or 0
            cursor.execute("UPDATE sqlite_sequence SET seq=? WHERE name='users'", (max_id,))
            conn.commit()

    def validate_credentials(self, login: str, password: str) -> bool:
        """
        Проверяет пару логин/пароль по базе данных.
        Возвращает True при успехе, False при неудаче.
        """
        try:
            with sqlite3.connect(self.db_path) as conn:
                cursor = conn.cursor()
                # Ищем пользователя по логину
                row = cursor.execute(
                    "SELECT password_hash, salt FROM users WHERE login=?", (login,)
                ).fetchone()
                
                if not row:
                    return False  # Пользователь не найден

                stored_hash, salt = row
                
                # Хэшируем введенный пароль с той же солью из базы
                test_hash = hashlib.sha256((salt + password).encode()).hexdigest()
                
                # Сравниваем хэши
                return stored_hash == test_hash
        except Exception as e:
            print(f"[DB ERROR] Ошибка проверки пароля: {e}")
            return False










# ====================================================
# Файл №4: Dozor/modules/administrator/login_dialog.py 
# - старый файл, который подвергается рефакторингу.
# - это файл донор для организации АДМИНИСТРАТОР
# ====================================================

# ---*--- Dozor/modules/administrator/login_dialog.py   - это новый адрес 
# -*- coding: utf-8 -*-


import tkinter as tk
from tkinter import ttk, messagebox

class LoginDialog(tk.Toplevel):
    def __init__(self, parent):
        super().__init__(parent)
        self.parent = parent
        self.successful = False  # Этот флаг проверяется в admin_user_inter.py
        self.title("Авторизация")
        self.geometry("300x150")
        self.resizable(False, False)

        main_frame = ttk.Frame(self, padding="15")
        main_frame.pack(expand=True, fill="both")

        ttk.Label(main_frame, text="Логин:").grid(row=0, column=0, sticky="w", pady=5)
        self.login_entry = ttk.Entry(main_frame)
        self.login_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")

        ttk.Label(main_frame, text="Пароль:").grid(row=1, column=0, sticky="w", pady=5)
        self.pass_entry = ttk.Entry(main_frame, show="*")
        self.pass_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew")

        btn_frame = ttk.Frame(main_frame)
        btn_frame.grid(row=2, columnspan=2, pady=15)
        
        ttk.Button(btn_frame, text="Войти", command=self.try_login).pack(side="left", padx=5)
        ttk.Button(btn_frame, text="Отмена", command=self.destroy).pack(side="right", padx=5)

        main_frame.columnconfigure(1, weight=1)

    def try_login(self):
        # TODO: Здесь позже вы добавите проверку логина и пароля по базе данных users.db
        # А пока просто закрываем окно как успешное, чтобы проверить цикл импорта
        self.successful = True
        self.destroy()











# ====================================================
# Файл №5: Dozor/modules/administrator/auth_interface.py - это новый адрес
# - старый файл, который подвергается рефакторингу.
# - это файл донор для организации АДМИНИСТРАТОР
# ====================================================

# ---*--- Dozor/modules/administrator/auth_interface.py   ---*---
# -*- coding: utf-8 -*-


"""
Авторизация vFinal. С силовым выводом окна.
"""
import tkinter as tk
from tkinter import ttk
import ctypes
import sqlite3

class AuthInterface(tk.Toplevel):
    def __init__(self, master, on_login_click=None):
        super().__init__(master)
        
        self.on_login_command = on_login_click
        
        # --- БАЗОВЫЕ ПАРАМЕТРЫ ---
        self.title("Авторизация")
        self.resizable(False, False)
        
        # Принудительно задаем размер ДО любых сеток
        win_w, win_h = 300, 150
        self.geometry(f"{win_w}x{win_h}")
        
        # --- ЖЕСТКОЕ ПОЗИЦИОНИРОВАНИЕ ЧЕРЕЗ WINAPI ---
        user32 = ctypes.windll.user32
        screen_w = user32.GetSystemMetrics(0) # SM_CXSCREEN
        screen_h = user32.GetSystemMetrics(1) # SM_CYSCREEN
        
        pos_x = int((screen_w / 2) - (win_w / 2))
        pos_y = int((screen_h / 2) - (win_h / 2))
        
        # Форсируем позицию минуя логику Tkinter
        self.geometry(f"+{pos_x}+{pos_y}")
        
        # Принудительная отрисовка всех запросов ОС прямо сейчас
        self.update_idletasks() 
        
        # --- ИНТЕРФЕЙС ТОЛЬКО ПОСЛЕ ТОГО, КАК ОКНО НАРИСОВАНО ---
        frame = ttk.Frame(self, padding=20)
        frame.pack(fill="both", expand=True)

        ttk.Label(frame, text="Логин:").grid(row=0, column=0, sticky="w")
        self.login_entry = ttk.Entry(frame, width=25)
        self.login_entry.grid(row=0, column=1, pady=5)

        ttk.Label(frame, text="Пароль:").grid(row=1, column=0, sticky="w")
        self.password_entry = ttk.Entry(frame, show="*", width=25)
        self.password_entry.grid(row=1, column=1, pady=5)

        btn_frame = ttk.Frame(frame)
        btn_frame.grid(row=2, column=0, columnspan=2, pady=15)

        login_btn = ttk.Button(btn_frame, text="Войти", command=self._try_login)
        login_btn.pack(side="left", padx=5)

        exit_btn = ttk.Button(btn_frame, text="Выход", command=self.destroy)
        exit_btn.pack(side="left", padx=5)

        self.login_entry.focus()
        
        # --- ЖЕСТКИЙ ЗАХВАТ ФОКУСА И ПОДНЯТИЕ НАД ВСЕМ ---
        self.lift()           # Поднимаем окно выше других
        self.focus_force()    # Принудительно забираем фокус ввода
        self.grab_set()       # Делаем окно модальным (блокируем всё остальное)
        
        # Еще одна принудительная проверка перед захватом фокуса
        self.update_idletasks()



    def _try_login(self):
        username = self.login_entry.get().strip()
        password = self.password_entry.get().strip()
        
        print(f"[AUTH DEBUG] Логин='{username}', Пароль есть: {len(password) > 0}")
        
        if not username or not password:
            from tkinter import messagebox
            messagebox.showwarning("Ошибка", "Поля не могут быть пустыми")
            return

        try:
            from .user_logic import UserLogic
            db_manager = UserLogic()
            
            # Проверяем путь к файлу физически
            import os
            if not os.path.exists(db_manager.db_path):
                raise FileNotFoundError(f"База users.db не найдена! Путь: {db_manager.db_path}")

            success = db_manager.validate_credentials(username, password)
            print(f"[AUTH DEBUG] validate_credentials вернула: {success}")

            if success:
                role = None
                with sqlite3.connect(db_manager.db_path) as conn:
                    row = conn.execute("SELECT role FROM users WHERE login=?", (username,)).fetchone()
                    if row:
                        role = row[0]
                
                print(f"[AUTH DEBUG] Роль извлечена: '{role}'")
                
                if callable(self.on_login_command):
                    # Вызываем колбэк главного окна
                    self.on_login_command(username, role)
                    print("[AUTH DEBUG] Колбэк выполнен, закрываем окно.")
                    self.destroy()
                else:
                    print("[FATAL ERROR] on_login_command is not callable!")
            else:
                from tkinter import messagebox
                messagebox.showerror("Ошибка", "Неверный логин или пароль")
                self.password_entry.delete(0, tk.END)
                self.password_entry.focus()
                
        except Exception as e:
            from tkinter import messagebox
            messagebox.showerror("Критическая ошибка", str(e))
            print(f"[FATAL LOGIN ERROR]: {e}")












# ====================================================
# Файл №6: Dozor/modules/administrator/admin_user_inter.py 
# - старый файл, который подвергается рефакторингу.
# - это файл донор для организации АДМИНИСТРАТОР
# ====================================================

# ---*--- Dozor/modules/administrator/admin_user_inter.py   ---*---
# -*- coding: utf-8 -*-


import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
from modules.administrator.user_logic import UserLogic


class AdminUserInter(tk.Toplevel):
    def __init__(self, parent, on_logout_callback=None):
        super().__init__(parent)
        self.parent_main = parent
        self.on_logout_callback = on_logout_callback
        
        self.title("Управление пользователями")
        self.geometry("550x450") 
        self.resizable(True, True)
        self.transient(parent)
        self.grab_set()

        container = ttk.Frame(self, padding="20")
        container.pack(expand=True, fill="both")

        left_panel = ttk.Frame(container)
        left_panel.pack(side="left", fill="y", padx=(0, 10))
        right_panel = ttk.Frame(container)
        right_panel.pack(side="right", fill="both", expand=True)

        actions = [
            ("Добавить пользователя", self.action_add),
            ("Сменить пользователя", self.action_change),
            ("Удалить пользователя", self.action_remove)
        ]
        for text, cmd in actions:
            ttk.Button(left_panel, text=text, command=cmd).pack(fill="x", pady=5)

        columns = ("№", "Логин", "Роль")
        self.users_tree = ttk.Treeview(right_panel, columns=columns, show="headings")
        
        self.users_tree.heading("#1", text="№")
        self.users_tree.column("#1", anchor="center", width=50)
        
        self.users_tree.heading("#2", text="Логин")
        self.users_tree.column("#2", anchor="w", width=180)
        
        self.users_tree.heading("#3", text="Роль")
        self.users_tree.column("#3", anchor="center", width=240)
        
        self.users_tree.pack(fill="both", expand=True)

        # Хранилище реального ID для удаления
        self._currently_selected_real_id = None
        
        # Отслеживание выбора строки
        self.users_tree.bind("<<TreeviewSelect>>", self._on_tree_select)
        
        self.user_manager = UserLogic() 
        self.load_data()

        self.update_idletasks()
        p_x = self.parent_main.winfo_rootx()
        p_y = self.parent_main.winfo_rooty()
        x = p_x + 20
        y = p_y + 20
        self.geometry(f"+{x}+{y}")

    def _on_tree_select(self, event):
        selected_item = self.users_tree.selection()
        if selected_item:
            tags = self.users_tree.item(selected_item)['tags']
            if tags:
                try:
                    self._currently_selected_real_id = int(tags[0])
                except ValueError:
                    self._currently_selected_real_id = None

    def load_data(self):
        for i in self.users_tree.get_children():
            self.users_tree.delete(i)
            
        users_from_db = self.user_manager.get_all_users()
        
        for idx, user_row in enumerate(users_from_db, start=1):
            login = user_row[1]
            role = user_row[2]
            real_id = user_row[0]
            self.users_tree.insert("", "end", values=(idx, login, role), tags=(str(real_id),))

    def action_add(self):
        dialog = tk.Toplevel(self)
        dialog.title("Новый пользователь")
        dialog.geometry("300x220")
        dialog.transient(self)
        dialog.grab_set()
        
        main_frame = ttk.Frame(dialog, padding="15")
        main_frame.pack(expand=True, fill="both")

        ttk.Label(main_frame, text="Логин:").grid(row=0, column=0, sticky="w", pady=5)
        login_e = ttk.Entry(main_frame); login_e.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
        
        ttk.Label(main_frame, text="Пароль:").grid(row=1, column=0, sticky="w", pady=5)
        pass_e = ttk.Entry(main_frame, show="*"); pass_e.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
        
        ttk.Label(main_frame, text="Роль:").grid(row=2, column=0, sticky="w", pady=5)
        role_values = ["Администратор", "Эксперт", "Пользователь"]
        role_combo = ttk.Combobox(main_frame, values=role_values, state="readonly")
        role_combo.current(2) 
        role_combo.grid(row=2, column=1, padx=5, pady=5, sticky="ew")

        btn_frame = ttk.Frame(main_frame)
        btn_frame.grid(row=3, column=0, columnspan=2, pady=15)

        def do_add():
            try:
                self.user_manager.add_user(login_e.get(), role_combo.get(), pass_e.get())
                self.load_data()
                dialog.destroy()
                messagebox.showinfo("Успех", "Пользователь добавлен.")
            except Exception as e:
                messagebox.showerror("Ошибка", str(e))

        ttk.Button(btn_frame, text="Создать", command=do_add).pack(side="left", padx=5)
        main_frame.columnconfigure(1, weight=1)
        login_e.focus()
        
    def action_remove(self):
        if self._currently_selected_real_id is None:
            messagebox.showwarning("Ошибка", "Выберите пользователя для удаления!")
            return
        
        answer = messagebox.askyesno(
            "Подтверждение", 
            f"УДАЛИТЬ пользователя с реальным ID={self._currently_selected_real_id}?\nЭто действие нельзя отменить."
        )
        if answer:
            try:
                self.user_manager.delete_user(self._currently_selected_real_id)
                self._currently_selected_real_id = None
                self.load_data()
                messagebox.showinfo("Успех", "Пользователь удален.")
            except Exception as e:
                messagebox.showerror("Ошибка", str(e))

    # === ИСПРАВЛЕННЫЙ МЕТОД СМЕНЫ ПОЛЬЗОВАТЕЛЯ ===
    def action_change(self):
        # 1. Сначала получаем ссылку на ГЛАВНОЕ ОКНО (оно еще живо)
        root = self.parent_main.winfo_toplevel()
        
        # 2. Останавливаем работу главного окна (его внутренний цикл Tcl)
        try:
            if hasattr(root, 'quit'):
                root.quit()
        except Exception:
            pass

        # 3. Закрываем само окно админки
        self.destroy()

        # 4. Теперь пытаемся уничтожить нативное окно ОС
        try:
            if root and root.winfo_exists():
                root.destroy()
        except Exception:
            pass

        # 5. Запускаем новый процесс
        import sys, subprocess, os, shutil
        
        script_path = os.path.abspath("main.py")
        gui_python = shutil.which("pythonw.exe") 

        if not gui_python:
            current_dir = os.path.dirname(sys.executable)
            potential_path = os.path.join(current_dir, "pythonw.exe")
            gui_python = potential_path if os.path.exists(potential_path) else sys.executable

        try:
            subprocess.Popen([gui_python, script_path], shell=True, creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
        except Exception as e:
            print(f"[FATAL] Не удалось запустить новую копию Dozor: {e}")

        sys.exit(0)













# ====================================================
# Файл №7: Dozor/modules/administrator/admin_db_work.py 
# - старый файл, который подвергается рефакторингу.
# - это файл донор для организации АДМИНИСТРАТОР
# ====================================================

# ---*--- Dozor/modules/administrator/admin_db_work.py   ---*---
# -*- coding: utf-8 -*-

import io
import os
import shutil
import sqlite3
from datetime import datetime
from pathlib import Path

class DBFileManager:
    def __init__(self):
        # ЕДИНСТВЕННАЯ точка определения корня проекта
        self.project_root = Path(__file__).resolve().parents[2]
        
        self.databases = {
            "database": str(self.project_root / "db" / "database.db"),
            "masterdata": str(self.project_root / "db" / "masterdata.db")
        }
        self.active_flag_path = str(self.project_root / "db" / "active.flag")
        self.backup_dir = str(self.project_root / "backups")  # Теперь точно сюда
        
        os.makedirs(self.backup_dir, exist_ok=True)
        self._ensure_active_flag()

    def _ensure_active_flag(self):
        try:
            if not os.path.exists(self.active_flag_path):
                with open(self.active_flag_path, 'w') as f:
                    f.write("database")
        except Exception as e:
            print(f"[WARN] Не удалось создать флаг активной БД: {e}")

    def _get_active_name(self):
        try:
            with open(self.active_flag_path, 'r') as f:
                name = f.read().strip()
                return name if name in self.databases else "database"
        except FileNotFoundError:
            return "database"

    def _set_active_name(self, name):
        with open(self.active_flag_path, 'w') as f:
            f.write(name)

    def get_active_path(self):
        return self.databases[self._get_active_name()]

    def create_backup(self):
        src = self.get_active_path()
        ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
        filename = f"{ts}__{os.path.basename(src)}"
        dst = os.path.join(self.backup_dir, filename)
        shutil.copy2(src, dst)
        return filename

    def restore_from_file(self, backup_filename):
        import io

        project_root = Path(__file__).resolve().parents[2]
        
        src_path = str(project_root / "backups" / backup_filename)
        active_name_str = self._get_active_name()
        dest_path = str(project_root / "db" / f"{active_name_str}.db")

        print(f"[DEBUG RESTORE-DUMP] From Backup: {src_path}")
        print(f"[DEBUG RESTORE-DUMP] To Active DB: {dest_path}")

        try:
            # 1. Читаем бэкап в строку
            with sqlite3.connect(src_path) as src_con:
                sql_dump_io = io.StringIO()
                for line in src_con.iterdump():
                    sql_dump_io.write(line + '\n')
                full_sql_script = sql_dump_io.getvalue()
            
            print("[DEBUG RESTORE-DUMP] Бэкап прочитан в RAM.")

            # === ФИЛЬТРАЦИЯ СКРИПТА (Убираем CREATE TABLE) ===
            lines = full_sql_script.split('\n')
            filtered_lines = []
            skip_create_table = False
            
            for line in lines:
                if line.strip().upper().startswith('CREATE TABLE'):
                    skip_create_table = True
                    continue
                
                if skip_create_table:
                    if ');' in line:
                        skip_create_table = False
                    continue
                
                filtered_lines.append(line)
            
            filtered_script = '\n'.join(filtered_lines)
            # ==================================================================================

            # 2. Выполнение БЕЗ ручного BEGIN/COMMIT 
            with sqlite3.connect(dest_path) as dest_con:
                cursor = dest_con.cursor()
                
                # Выключаем ключи для скорости очистки
                cursor.execute("PRAGMA foreign_keys=OFF;")
                
                # ОЧИСТКА ВСЕХ ТАБЛИЦ ОДНИМ ЗАПРОСОМ (вместо цикла FOR)
                # Это быстрее и надежнее. Сбрасываем счетчики AUTOINCREMENT тоже.
                cursor.execute("""
                    DELETE FROM sqlite_sequence;
                """)
                
                tables_query = """
                    SELECT name FROM sqlite_master 
                    WHERE type='table' AND name NOT LIKE 'sqlite_%';
                """
                cursor.execute(tables_query)
                tables = [row[0] for row in cursor.fetchall()]
                
                delete_statements = "; ".join([f"DELETE FROM {t}" for t in tables]) + ";"
                if delete_statements.strip() != ";":
                    cursor.executescript(delete_statements)

                # Вставка данных. executescript сам управляет своими внутренними транзакциями.
                cursor.executescript(filtered_script)
                
                # Включаем ключи обратно
                cursor.execute("PRAGMA foreign_keys=ON;")
                
            print("[DEBUG RESTORE-DUMP] SUCCESS: Восстановление завершено.")

        except Exception as e:
            # Выведем ПЕРВИЧНУЮ ошибку в консоль, чтобы понять, почему скрипт упал
            print(f"[CRITICAL DUMP ERROR] Первичная ошибка дампа: {str(e)}")
            import traceback
            traceback.print_exc() 
            
            raise RuntimeError(f"Восстановление прервано. Смотрите логи в терминале.")

    def migrate_data(self, source_key: str, dest_key: str):
        import io
        import re

        project_root = Path(__file__).resolve().parents[2]
        source_path = str(project_root / "db" / f"{source_key}.db")
        dest_path = str(project_root / "db" / f"{dest_key}.db")

        print(f"[DEBUG MIGRATE-DUMP] From: {source_path}")
        print(f"[DEBUG MIGRATE-DUMP] To: {dest_path} (через SQL Dump)")

        try:
            # 1. Читаем ВСЮ исходную базу в строку
            with sqlite3.connect(source_path) as src_con:
                sql_dump_io = io.StringIO()
                for line in src_con.iterdump():
                    sql_dump_io.write(line + '\n')
                full_sql_script = sql_dump_io.getvalue()
            
            print("[DEBUG DUMP] Данные успешно сериализованы в SQL.")

            # 2. Фильтруем скрипт: убираем CREATE TABLE, оставляем INSERT и прочее
            lines = full_sql_script.split('\n')
            filtered_lines = []
            
            skip_create_table = False
            for line in lines:
                if line.strip().upper().startswith('CREATE TABLE'):
                    skip_create_table = True
                    continue
                
                if skip_create_table:
                    if ');' in line:
                        skip_create_table = False
                    continue
                
                filtered_lines.append(line)
            
            filtered_script = '\n'.join(filtered_lines)

            # 3. Очищаем целевую базу и вставляем данные БЕЗ ручного BEGIN/COMMIT
            with sqlite3.connect(dest_path) as dest_con:
                cursor = dest_con.cursor()
                
                cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
                tables = [row[0] for row in cursor.fetchall() if not row[0].startswith('sqlite_')]
                
                cursor.execute("PRAGMA foreign_keys=OFF;")
                
                for table in tables:
                    cursor.execute(f"DELETE FROM {table};")
                
                cursor.executescript(filtered_script)
                
                cursor.execute("PRAGMA foreign_keys=ON;")
            
            print("[DEBUG MIGRATE-DUMP] SUCCESS: Данные перенесены через фильтрованный SQL-скрипт.")

        except Exception as e:
            # Сюда упала ошибка SyntaxError - этого блока не хватало
            raise RuntimeError(f"Миграция через SQL Dump прервана: {str(e)}")














# ====================================================
# Файл №8: Dozor/modules/administrator/admin_db_inter.py 
# - старый файл, который подвергается рефакторингу.
# - это файл донор для организации АДМИНИСТРАТОР
# ====================================================

# ---*--- Dozor/modules/administrator/admin_db_inter.py   ---*---
# -*- coding: utf-8 -*-

import os
import shutil
import sqlite3
import tkinter as tk
from tkinter import ttk, messagebox
from datetime import datetime
from pathlib import Path

from .admin_db_work import DBFileManager

class AdminDBInter(tk.Toplevel):
    def __init__(self, parent):
        super().__init__(parent)
        self.parent_main = parent
        self.title("Управление базами данных")
        self.geometry("600x400")
        self.resizable(True, True)
        self.transient(parent)
        self.grab_set()
        
        # Здесь тоже используем правильный импорт из пакета administrator
        self.file_manager = DBFileManager() 
        self.build_ui()
        self.refresh_backups_list()
        self.sync_active_flag()

    def build_ui(self):
        container = ttk.Frame(self)
        container.pack(fill="both", expand=True, padx=10, pady=10)

        top_panel = ttk.Frame(container)
        top_panel.pack(fill="x", pady=10)

        self.use_master_db = tk.BooleanVar(value=(self.file_manager._get_active_name() == "masterdata"))
        style = ttk.Style()
        style.configure("RedText.TCheckbutton", foreground="red")
        
        ttk.Checkbutton(
            top_panel,
            text="Использовать основную базу данных (masterdata.db)",
            variable=self.use_master_db,
            style="RedText.TCheckbutton",
            command=self.toggle_active_db
        ).pack(side="left", padx=10)

        tools_panel = ttk.Frame(container)
        tools_panel.pack(fill="x", pady=10)

        ttk.Button(tools_panel, text="Создать резервную копию", command=self.do_backup).pack(side="left", padx=5)
        ttk.Button(tools_panel, text="Восстановить из копии", command=self.do_restore).pack(side="left", padx=5)
        ttk.Button(tools_panel, text="Мигрировать данные", command=self.do_migrate).pack(side="left", padx=5)

        backup_columns = ("Дата", "Файл")
        self.backups_tree = ttk.Treeview(container, columns=backup_columns, show="headings", height=10)
        for col in backup_columns:
            self.backups_tree.heading(col, text=col)
            self.backups_tree.column(col, width=270)
        self.backups_tree.pack(fill="both", expand=True, pady=10)

    def refresh_backups_list(self):
        self.backups_tree.delete(*self.backups_tree.get_children())
        
        backup_dir = Path(self.file_manager.backup_dir)
        
        if not backup_dir.exists():
            return
            
        # Сортируем по дате создания файла на диске, а не по имени
        files = sorted(backup_dir.glob("*.db"), key=os.path.getctime, reverse=True)
        
        for file_path in files:
            # Выводим только имя файла без лишних преобразований
            display_name = file_path.name 
            # Дата модификации файла в читаемом виде
            mod_time = datetime.fromtimestamp(file_path.stat().st_mtime).strftime("%d.%m %H:%M")
            
            self.backups_tree.insert("", "end", values=(mod_time, display_name))

    def sync_active_flag(self):
        is_master = self.file_manager._get_active_name() == "masterdata"
        self.use_master_db.set(is_master)

    def toggle_active_db(self):
        target = "masterdata" if self.use_master_db.get() else "database"
        self.file_manager._set_active_name(target)
        messagebox.showinfo("Готово", f"Теперь активной будет {target}.db\nПерезапустите приложение для полной смены контекста.")

    def do_backup(self):
        try:
            fname = self.file_manager.create_backup()
            self.refresh_backups_list()
            messagebox.showinfo("Бэкап", f"Копия создана:\n{fname}\nПуть: {self.file_manager.backup_dir}")
        except Exception as e:
            messagebox.showerror("Ошибка", str(e))

    def do_restore(self):
        # Игнорируем выделение в таблице. Ищем самый свежий файл сами.
        backup_dir = Path(self.file_manager.backup_dir)
        
        if not backup_dir.exists():
            messagebox.showwarning("Внимание", "Папка с бэкапами пуста.")
            return
            
        files = sorted(backup_dir.glob("*.db"), key=os.path.getctime, reverse=True)
        
        if not files:
            messagebox.showwarning("Внимание", "Не найдено ни одного файла .db в папке backups.")
            return
            
        latest_backup = files[0]
        
        try:
            self.file_manager.restore_from_file(latest_backup.name)
            messagebox.showinfo("Успех", f"База восстановлена из {latest_backup.name}\nПриложение нужно перезапустить.")
        except Exception as e:
            print(f"[CRITICAL RESTORE ERROR] {e}")
            import traceback
            traceback.print_exc() # Выведем полную ошибку в консоль
            messagebox.showerror("Ошибка", f"Критическая ошибка при восстановлении:\n{str(e)}")

    def do_migrate(self):
        answer = messagebox.askyesnocancel(
            "Миграция данных",
            "Перенести ВСЕ данные из НЕАКТИВНОЙ базы в АКТИВНУЮ?\nАктивная база будет ПОЛНОСТЬЮ ЗАТЕРТА данными из другой.\nДействие необратимо."
        )
        if answer is None or not answer:
            return
            
        current = self.file_manager._get_active_name()
        source = "masterdata" if current == "database" else "database"
        try:
            self.file_manager.migrate_data(source, current)
            messagebox.showinfo("Миграция", f"Данные перенесены из {source}.db в {current}.db\nПерезапустите приложение.")
        except Exception as e:
            messagebox.showerror("Ошибка", str(e))
# ---*--- Конец файла ---*---















# ====================================================
# Файл №9: Dozor/gui/main_interface.py 
# - НОВЫЙ ОСНОВНОЙ ФАЙЛ с которым работаем.
# - это основной файл для главного окна интерфейса 
# - в этом файле организовываем пока первую кнопку АДМИНИСТРАТОР
# ====================================================

# ---*--- Dozor/gui/main_interface.py  ---*---
# -*- coding: utf-8 -*-

import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageTk
import os

class MainInterface(tk.Tk):
    def __init__(self):
        super().__init__()
        self.current_user = {"login": None}
        
        # === ТОЛЬКО ГЕОМЕТРИЯ И ВИДЖЕТЫ ===
        self.geometry("458x360")
        self.resizable(False, False)
        
        bg_color = "#EEEEEE"
        self.configure(bg=bg_color)
        
        main_frame = tk.Frame(self, bg=bg_color)
        main_frame.pack(padx=1, pady=1, fill='both', expand=True)
        
        button_width = 20
        button_height = 2
        button_font = ("Arial", 12, "bold")

        for i in range(7):
            main_frame.grid_rowconfigure(i, weight=0)
        main_frame.grid_columnconfigure(0, minsize=210, weight=0)
        main_frame.grid_columnconfigure(1, minsize=235, weight=0)

        # --- ЛЕВАЯ ПАНЕЛЬ ---
        left_buttons = [
            ("Администратор", self._show_admin_menu),
            ("Менеджер базы", self._show_manager_menu),
            ("Эксперты", self._show_experts_menu),
            ("Проект", self._show_project_menu),
            ("Прогнозирование", self._show_forecast_menu)
        ]

        for row_index, (btn_text, command_func) in enumerate(left_buttons):
            btn = tk.Button(
                main_frame,
                text=btn_text,
                font=button_font,
                width=button_width,
                height=button_height,
                anchor="w",
                command=command_func
            )
            btn.grid(row=row_index, column=0, padx=5, pady=5, sticky="ew")

        # --- ПРАВАЯ ПАНЕЛЬ ---
        right_box = tk.Frame(main_frame, bg=bg_color, width=235, height=300)
        right_box.grid(row=0, column=1, rowspan=6, pady=5, sticky="nsew", padx=(0, 5))
        right_box.grid_propagate(False)
        right_box.columnconfigure(0, weight=1)

        picture_frame = tk.Frame(right_box, bg="#d9ead3", highlightbackground="gray", 
                                 highlightthickness=1, width=225, height=160)
        picture_frame.place(relx=0.5, y=5, anchor="n")
        picture_frame.grid_propagate(False)

        current_dir = os.path.dirname(os.path.abspath(__file__))
        image_path = os.path.join(current_dir, "images", "Сова1.png")
        try:
            original_img = Image.open(image_path)
            target_w, target_h = 215, 130
            img_ratio = original_img.width / original_img.height
            box_ratio = target_w / target_h
            if img_ratio > box_ratio:
                new_w, new_h = target_w, int(target_w / img_ratio)
            else:
                new_w, new_h = int(target_h * img_ratio), target_h
            resized_img = original_img.resize((new_w, new_h), resample=Image.LANCZOS)
            self.tk_image = ImageTk.PhotoImage(resized_img)
            label = tk.Label(picture_frame, image=self.tk_image, bg="#d9ead3", borderwidth=0)
            label.image = self.tk_image
            label.place(relx=0.5, rely=0.5, anchor='center', width=new_w, height=new_h)
        except Exception as e:
            print(f"[FATAL IMAGE ERROR]: {e}")
            canvas = tk.Canvas(picture_frame, width=225, height=160, bg="red", highlightthickness=0)
            canvas.create_text(112, 80, text="IMG\nFAIL", fill="white")
            canvas.place(x=0, y=0, relwidth=1, relheight=1)

        spacer = tk.Frame(right_box, bg=bg_color, height=1)
        spacer.grid(row=0, column=0, pady=(170, 10), sticky="ew")
        
        upper_padding = tk.Frame(right_box, bg=bg_color, height=3)
        upper_padding.grid(row=1, column=0)

        btn6 = tk.Button(right_box, text="Руководство", font=button_font, width=button_width, height=button_height)
        btn6.grid(row=2, column=0, ipadx=10, pady=(0, 5), sticky="ew")
        
        btn7 = tk.Button(right_box, text="О программе", font=button_font, width=button_width, height=button_height)
        btn7.grid(row=3, column=0, ipadx=10, pady=(5, 15), sticky="ew")

        footer_frame = tk.Frame(main_frame, bg="#0078D7")
        footer_frame.grid(row=6, column=0, columnspan=2, sticky="ew", pady=(10, 0))
        footer_label = tk.Label(footer_frame, text="Copyright © Минск, НИИ, 2027 год", fg="#FF8C00", 
                                bg="#0078D7", font=("Arial", 12, "bold"))
        footer_label.pack(expand=True, ipady=4)

        # --- КОНТЕЙНЕРЫ ДЛЯ ПОДМЕНЮ ---
        self.menu_container = tk.Frame(main_frame, bg=bg_color)
        self.menu_container.grid_rowconfigure(0, weight=1)
        self.menu_container.grid_columnconfigure(0, weight=1)

        def _create_menu_panel(container, title_text):
            panel = tk.Frame(container, bg="#f0f0f0", bd=2, relief="sunken")
            panel.grid_propagate(False)
            
            header_frame = tk.Frame(panel, bg="#4a4a4a", height=30)
            header_frame.pack(fill='x')
            
            close_btn = tk.Button(
                header_frame, 
                text="← Назад", 
                font=("Arial", 9), 
                fg="white", 
                bg="#888888", 
                borderwidth=0,
                command=self._show_main_screen
            )
            close_btn.pack(side='right', padx=5, pady=2)
            
            content_frame = tk.Frame(panel, bg="#f0f0f0")
            content_frame.pack(fill='both', expand=True, padx=10, pady=10)
            ttk.Label(content_frame, text=f"Здесь будет функционал:\n{title_text}").pack(expand=True)
            
            return panel

        self.admin_panel = _create_menu_panel(self.menu_container, "Подменю Администратор")
        self.manager_panel = _create_menu_panel(self.menu_container, "Подменю Менеджер базы")
        self.experts_panel = _create_menu_panel(self.menu_container, "Подменю Эксперты")
        self.project_panel = _create_menu_panel(self.menu_container, "Подменю Проект")
        self.forecast_panel = _create_menu_panel(self.menu_container, "Подменю Прогнозирование")
        self.guide_panel = _create_menu_panel(self.menu_container, "Подменю Руководство")
        self.about_panel = _create_menu_panel(self.menu_container, "Подменю О программе")

        self._current_menu_visible = None

    # --- МЕТОД ВОЗВРАТА НА ГЛАВНЫЙ ЭКРАН ---
    def _show_main_screen(self):
        self._hide_current_menu()
        self.menu_container.grid_remove()
        
        self.grid_columnconfigure(0, minsize=210, weight=0)
        self.grid_columnconfigure(1, minsize=235, weight=0)

    def _hide_current_menu(self):
        panels = {
            'admin': self.admin_panel,
            'manager': self.manager_panel,
            'experts': self.experts_panel,
            'project': self.project_panel,
            'forecast': self.forecast_panel,
            'guide': self.guide_panel,
            'about': self.about_panel
        }
        if self._current_menu_visible and self._current_menu_visible in panels:
            panels[self._current_menu_visible].grid_remove()
        self._current_menu_visible = None

    # --- ОБРАБОТЧИКИ КНОПОК (БЕЗ .title()) ---
    def _show_admin_menu(self):
        self._show_main_screen()
        self.menu_container.grid(row=0, column=0, rowspan=6, columnspan=2, sticky="nsew")
        self.admin_panel.grid(row=0, column=0, sticky="nsew")
        self._current_menu_visible = 'admin'

    def _show_manager_menu(self):
        self._show_main_screen()
        self.menu_container.grid(row=0, column=0, rowspan=6, columnspan=2, sticky="nsew")
        self.manager_panel.grid(row=0, column=0, sticky="nsew")
        self._current_menu_visible = 'manager'

    def _show_experts_menu(self):
        self._show_main_screen()
        self.menu_container.grid(row=0, column=0, rowspan=6, columnspan=2, sticky="nsew")
        self.experts_panel.grid(row=0, column=0, sticky="nsew")
        self._current_menu_visible = 'experts'

    def _show_project_menu(self):
        self._show_main_screen()
        self.menu_container.grid(row=0, column=0, rowspan=6, columnspan=2, sticky="nsew")
        self.project_panel.grid(row=0, column=0, sticky="nsew")
        self._current_menu_visible = 'project'

    def _show_forecast_menu(self):
        self._show_main_screen()
        self.menu_container.grid(row=0, column=0, rowspan=6, columnspan=2, sticky="nsew")
        self.forecast_panel.grid(row=0, column=0, sticky="nsew")
        self._current_menu_visible = 'forecast'
        
    def _show_guide_menu(self):
        self._show_main_screen()
        self.menu_container.grid(row=0, column=0, rowspan=6, columnspan=2, sticky="nsew")
        self.guide_panel.grid(row=0, column=0, sticky="nsew")
        self._current_menu_visible = 'guide'
        
    def _show_about_menu(self):
        self._show_main_screen()
        self.menu_container.grid(row=0, column=0, rowspan=6, columnspan=2, sticky="nsew")
        self.about_panel.grid(row=0, column=0, sticky="nsew")
        self._current_menu_visible = 'about'
        
    def switch_user_radical(self):
        root_win = self.winfo_toplevel()
        from modules.utils.app_state import disable_app
        disable_app(root_win)
        if hasattr(self, 'current_user'):
            self.current_user = {"login": None}
        if hasattr(self, '_child_windows'):
            self._child_windows.clear()
        try:
            root_win.destroy()
        except Exception:
            pass
        import sys, subprocess, os, shutil
        script_path = os.path.abspath("main.py")
        gui_python = shutil.which("pythonw.exe")
        if not gui_python:
            current_dir = os.path.dirname(sys.executable)
            potential_path = os.path.join(current_dir, "pythonw.exe")
            gui_python = potential_path if os.path.exists(potential_path) else sys.executable
        try:
            subprocess.Popen([gui_python, script_path], shell=True, creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
        except Exception as e:
            print(f"[FATAL] Не удалось запустить новую копию Dozor: {e}")
        sys.exit(0)