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


'''
День добрый!
Необходимо решить:

Правильно организовать адресацию в коде.

я вайб-кодер, ты программист.
При нажатии на Администратор - появляется красная табличка, что база не найдена, хотя авторизация проходит в штатном режиме. На другом компьютере проблемм нет.
'''
#================================================








#================================================
# dukora\modules\admin\\users_tab.py
#================================================
import customtkinter as ctk
import tkinter as tk
from tkinter import ttk, messagebox
import sqlite3
from hashlib import sha256
from pathlib import Path

# ВАЖНО: Убедитесь, что в папке admin есть пустой файл __init__.py
from modules.utils.resources import resource_path 

class AdminUsersPanel(ctk.CTkFrame):
    def __init__(self, master):
        super().__init__(master)
        
        self.project_root = Path(__file__).resolve().parents[2] 
        self.db_path = self.project_root / "db" / "users.db"
        
        container = ctk.CTkFrame(self)
        container.pack(fill="both", expand=True, padx=10, pady=10)
        
        style = ttk.Style()
        style.theme_use("default")
        style.configure("mystyle.Treeview", background="#2b2b2b", fieldbackground="#2b2b2b", foreground="white", borderwidth=0, font=("Arial", 10))
        style.map('mystyle.Treeview', background=[('selected', '#4a4a4a')])
        
        table_frame = ctk.CTkFrame(container)
        table_frame.pack(fill="both", expand=True, side="top")
        
        self.tree = ttk.Treeview(table_frame, columns=("ID", "Логин", "ФИО", "Роль"), show="headings", style="mystyle.Treeview")
        self.tree.heading("ID", text="ID")
        self.tree.heading("Логин", text="Логин")
        self.tree.heading("ФИО", text="ФИО")
        self.tree.heading("Роль", text="Роль")
        self.tree.column("ID", width=40)
        self.tree.column("Логин", width=100)
        self.tree.column("ФИО", width=180)
        self.tree.column("Роль", width=80)
        self.tree.pack(side="left", fill="both", expand=True)
        
        scrollbar = ctk.CTkScrollbar(table_frame, command=self.tree.yview)
        scrollbar.pack(side="right", fill="y")
        self.tree.configure(yscrollcommand=scrollbar.set)
        
        form_frame = ctk.CTkFrame(container)
        form_frame.pack(fill="x", pady=(10, 0))
        
        self.login_entry = ctk.CTkEntry(form_frame, placeholder_text="Логин", width=100)
        self.login_entry.grid(row=0, column=0, padx=5, pady=5)
        
        self.fio_entry = ctk.CTkEntry(form_frame, placeholder_text="ФИО", width=180)
        self.fio_entry.grid(row=0, column=1, padx=5, pady=5)
        
        self.pass_entry = ctk.CTkEntry(form_frame, placeholder_text="Пароль", width=120, show="*")
        self.pass_entry.grid(row=0, column=2, padx=5, pady=5)
        
        self.role_cb = ctk.CTkComboBox(form_frame, values=["Админ", "Эксперт", "Пользователь"], width=100)
        self.role_cb.grid(row=0, column=3, padx=5, pady=5)
        self.role_cb.set("Эксперт")
        
        btn_frame = ctk.CTkFrame(container)
        btn_frame.pack(pady=10)
        
        add_btn = ctk.CTkButton(btn_frame, text="Добавить", command=self.add_user)
        add_btn.pack(side="left", padx=5)
        
        edit_btn = ctk.CTkButton(btn_frame, text="Изменить", command=self.edit_user)
        edit_btn.pack(side="left", padx=5)
        
        del_btn = ctk.CTkButton(btn_frame, text="Удалить", command=self.delete_user, fg_color="#d9534f", hover_color="#c9302c")
        del_btn.pack(side="left", padx=5)
        
        self.load_users()

    def load_users(self):
        self.tree.delete(*self.tree.get_children())
        db_path_str = resource_path('db/users.db')
        try:
            conn = sqlite3.connect(db_path_str)
            cursor = conn.cursor()
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS users (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    login TEXT UNIQUE NOT NULL,
                    password_hash TEXT NOT NULL,
                    fio TEXT NOT NULL,
                    role TEXT NOT NULL
                )
            ''')
            conn.commit()
            cursor.execute('SELECT id, login, fio, role FROM users ORDER BY id')
            rows = cursor.fetchall()
            for index, row in enumerate(rows, start=1):
                self.tree.insert("", "end", values=(index, row[1], row[2], row[3]), tags=(str(row[0]),))
        except Exception as e:
            messagebox.showerror("Ошибка БД", f"Не удалось загрузить пользователей:\n{e}")
        finally:
            if 'conn' in locals():
                conn.close()

    def get_form_data(self):
        return {
            "login": self.login_entry.get(),
            "fio": self.fio_entry.get(),
            "password": self.pass_entry.get(),
            "role": self.role_cb.get()
        }
        
    def clear_form(self):
        self.login_entry.delete(0, "end")
        self.fio_entry.delete(0, "end")
        self.pass_entry.delete(0, "end")
        self.role_cb.set("Эксперт")
        
    def add_user(self):
        data = self.get_form_data()
        if not all([data["login"], data["fio"], data["password"]]):
            messagebox.showwarning("Ошибка", "Заполните Логин, ФИО и Пароль")
            return
            
        password_hash = sha256(data["password"].encode()).hexdigest()
        db_path_str = resource_path('db/users.db')
        try:
            conn = sqlite3.connect(db_path_str)
            cursor = conn.cursor()
            cursor.execute('INSERT INTO users (login, password_hash, fio, role) VALUES (?, ?, ?, ?)', (data["login"], password_hash, data["fio"], data["role"]))
            conn.commit()
            messagebox.showinfo("Успех", f"Пользователь {data['login']} добавлен")
            self.clear_form()
            self.load_users()
        except sqlite3.IntegrityError:
            messagebox.showwarning("Ошибка", "Пользователь с таким логином уже существует.")
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось сохранить:\n{e}")
        finally:
            if 'conn' in locals():
                conn.close()
                
    def _get_selected_db_id(self):
        selected = self.tree.selection()
        if not selected:
            return None
        item = self.tree.item(selected)
        db_id_str = item['tags'][0]
        return int(db_id_str)

    def delete_user(self):
        db_id = self._get_selected_db_id()
        login = self.tree.item(self.tree.selection())['values'][1]
        if not db_id:
            messagebox.showwarning("Ошибка", "Выберите строку")
            return
        db_path_str = resource_path('db/users.db')
        try:
            conn = sqlite3.connect(db_path_str)
            cursor = conn.cursor()
            cursor.execute("DELETE FROM users WHERE id=?", (db_id,))
            conn.commit()
            messagebox.showinfo("Успех", f"Пользователь '{login}' удален")
            self.clear_form()
            self.load_users()
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось удалить:\n{e}")
        finally:
            if 'conn' in locals():
                conn.close()

    def edit_user(self):
        db_id = self._get_selected_db_id()
        if not db_id:
            messagebox.showwarning("Ошибка", "Выберите строку")
            return
        
        db_path_str = resource_path('db/users.db')
        
        try:
            conn = sqlite3.connect(db_path_str)
            cursor = conn.cursor()
            cursor.execute('SELECT login, fio, role FROM users WHERE id=?', (db_id,))
            user_data = cursor.fetchone()
            
            if user_data:
                self.login_entry.delete(0, "end")
                self.login_entry.insert(0, user_data[0])
                
                self.fio_entry.delete(0, "end")
                self.fio_entry.insert(0, user_data[1])
                
                self.role_cb.set(user_data[2])
                self.pass_entry.delete(0, "end")  
                self.login_entry.configure(state="disabled")
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось прочитать данные:\n{e}")
        finally:
            if 'conn' in locals():
                conn.close()

#================================================








#================================================
# dukora\main.py
#================================================
import customtkinter as ctk
from gui.main_interface import MainApp
from auth_window import AuthWindow  # Теперь Питон легко найдет этот файл в той же папке

def start_application():
    app = MainApp()
    
    # Создаем модальное окно поверх замороженного главного
    auth_win = AuthWindow(app)
    app.wait_window(auth_win)
    
    # Запускаем основной интерфейс только после успешной авторизации
    app.mainloop()

if __name__ == "__main__":
    try:
        ctk.set_appearance_mode("System")
        ctk.set_default_color_theme("blue")
    except Exception:
        pass
        
    start_application()

#================================================








#================================================
# dukora\modules\admin\\admin_accordion_window.py
#================================================
import customtkinter as ctk
from tkinter import ttk, messagebox

# modules/admin/admin_accordion_window.py
from .users_tab import AdminUsersPanel 
from .db_manager_panel import DBManagerPanel
from .migration_panel import MigrationPanel


class AdminAccordionWindow(ctk.CTkToplevel):
    def __init__(self, master):
        super().__init__(master)
        
        self.parent_main = master
        self.title("Администрирование")
        
        # --- 1. БАЗОВЫЕ ГАБАРИТЫ ОКНА ---
        window_width = 650
        initial_height = 380 
        
        screen_w = self.winfo_screenwidth()
        screen_h = self.winfo_screenheight()
        
        x_cord = int((screen_w - window_width) / 2)
        y_cord = int((screen_h - initial_height) / 2)
        
        self.geometry(f"{window_width}x{initial_height}+{x_cord}+{y_cord}")
        
        # === 2. ЖЕСТКИЕ ОГРАНИЧЕНИЯ РАЗМЕРА ===
        self.minsize(width=window_width, height=250)
        self.maxsize(width=900, height=screen_h - 50)
        
        self.resizable(True, False)
        self.grab_set() 
        self.transient(master)
        
        # --- КОНТЕЙНЕРЫ ---
        main_container = ctk.CTkFrame(self, corner_radius=10)
        main_container.pack(padx=10, pady=10, fill="both", expand=True)
        
        title_lbl = ctk.CTkLabel(main_container, text="Панель администратора", font=ctk.CTkFont(size=20, weight="bold"))
        title_lbl.pack(pady=(0, 15))
        
        accordion_frame = ctk.CTkFrame(main_container)
        accordion_frame.pack(fill="both", expand=True, pady=(10, 0))
        
        self.accordion_data = {}
        self.currently_open_button = None
        
        # === ЗАГОЛОВКИ АККОРДЕОНА ===
        users_item = self._create_accordion_header(accordion_frame, "Управление пользователями")
        security_item = self._create_accordion_header(accordion_frame, "Безопасность")
        migration_item = self._create_accordion_header(accordion_frame, "Миграция")
        
        # Наполняем секции контентом
        self._fill_users_section(users_item["content"])
        self._fill_security_section(security_item["content"]) 
        self._fill_migration_section(migration_item["content"]) 
        
        # Привязка команд переключения
        for item in [users_item, security_item, migration_item]:
            btn = item["button"]
            content = item["content"]
            self.accordion_data[btn] = {"content": content}
            btn.configure(command=lambda b=btn, c=content: self.toggle_section(b, c))
            
        # Открываем первую секцию
        self.toggle_section(users_item["button"], users_item["content"])
            
        # Кнопки внизу
        button_frame = ctk.CTkFrame(main_container)
        button_frame.pack(fill="x", pady=(10, 0))
        
        close_btn = ctk.CTkButton(button_frame, text="Закрыть", width=120, command=self.destroy)
        close_btn.pack(side="right", padx=10, pady=10)

        self.protocol("WM_DELETE_WINDOW", self.on_closing)

        # === 3. ЦЕНТРИРОВАНИЕ ПОСЛЕ ПОЛНОЙ ОТРИСТОВКИ ===
        self.after(100, self._center_over_parent)

    def _center_over_parent(self):
        if not self.parent_main or not self.parent_main.winfo_exists():
            return
            
        self.update_idletasks()
        
        w = self.winfo_width()
        h = self.winfo_height()
        
        root_win = self.parent_main
        p_x = root_win.winfo_rootx()
        p_y = root_win.winfo_rooty()
        
        final_x = int(p_x + (root_win.winfo_width() - w) / 2)
        final_y = int(p_y + (root_win.winfo_height() - h) / 2)
        
        self.geometry(f"+{final_x}+{final_y}")

    # === МЕТОД toggle_section (твой оригинальный) ===
    def toggle_section(self, button, content):
        is_currently_open = (self.currently_open_button == button)
        if self.currently_open_button and not is_currently_open:
            prev_content = self.accordion_data[self.currently_open_button]["content"]
            prev_content.pack_forget()
            self.currently_open_button.configure(fg_color="#34495e")
        if is_currently_open:
            content.pack_forget()
            button.configure(fg_color="#34495e")
            self.currently_open_button = None
            new_geo = self._calculate_window_geometry(collapsed=True)
        else:
            content.pack(fill="x", padx=5, pady=(0, 5), anchor="center")
            button.configure(fg_color="#2c3e50")
            self.currently_open_button = button
            new_geo = self._calculate_window_geometry(collapsed=False)
        
        self.geometry(new_geo)

    # === РАСЧЕТ ГЕОМЕТРИИ (Исправлено!) ===
    def _calculate_window_geometry(self, collapsed=False):
        current_width = self.winfo_width()
        
        # Если ширина еще не посчиталась (например, при первом запуске)
        if current_width == 1:
            current_width = 650

        header_height = 50 * 3 + 30 

        # === ФИКС ДЛЯ КНОПОК: ДОБАВИЛИ ОТСТУП ПОД НИЖНЮЮ ПАНЕЛЬ ===
        button_panel_height = 70  # Высота рамки с кнопкой "Закрыть" + её отступы

        if collapsed or not self.currently_open_button:
            target_height = header_height + 80 + button_panel_height
        else:
            data = self.accordion_data[self.currently_open_button]
            content_widget = data["content"]
            
            # Обязательно дожидаемся перерисовки виджета перед замером высоты!
            self.update_idletasks() 
            open_content_height = content_widget.winfo_reqheight()
            
            # Считаем общую высоту: шапка + контент + нижняя панель
            target_height = header_height + open_content_height + button_panel_height

        max_h = self.winfo_screenheight() - 50
        min_h = 250 

        final_height = min(target_height, max_h)  # Не выше экрана
        final_height = max(final_height, min_h)   # Не ниже порога для кнопок

        return f"{current_width}x{final_height}"


    # === СОЗДАНИЕ ХЕДЕРА ===
    def _create_accordion_header(self, parent, title):
        item_container = ctk.CTkFrame(parent, fg_color="#f0f2f5", border_color="#d1d8e0", border_width=1, corner_radius=8)
        item_container.pack(fill="x", pady=5)
        header_btn = ctk.CTkButton(
            item_container,
            text=title,
            height=45,
            font=ctk.CTkFont(weight="bold", size=16),
            anchor="w",
            fg_color="#34495e",
            hover_color="#3c5a72",
        )
        header_btn.pack(fill="x")
        content_frame = ctk.CTkFrame(item_container, fg_color="transparent", corner_radius=0)
        return {"button": header_btn, "content": content_frame}

    # === НАПОЛНЕНИЕ СЕКЦИЙ ===
    def _fill_users_section(self, frame):
        panel = AdminUsersPanel(frame)
        panel.pack(fill="both", expand=True, padx=10, pady=10)

    def _fill_security_section(self, frame):
        panel = DBManagerPanel(frame)
        panel.pack(fill="both", expand=True, padx=10, pady=10)

    def _fill_migration_section(self, frame):
        panel = MigrationPanel(frame)
        panel.pack(fill="both", expand=True, padx=10, pady=10)

    def on_closing(self):
        try:
            self.after_cancel("all")
        except Exception:
            pass
        try:
            self.destroy()
        except Exception:
            pass

#================================================









#================================================
# dukora\gui\main_interface.py
#================================================
import os
import customtkinter as ctk
from PIL import Image
import sys
from modules.manager.manager_window import ManagerWindow
from modules.experts.experts_window import ExpertsWindow
from modules.project.project_window import ProjectWindow
from modules.forecasting.forecast_window import ForecastWindow
# Добавляем импорт нашей утилиты путей
from modules.utils.resources import resource_path 

class MainApp(ctk.CTk):
    def __init__(self):
        super().__init__()
        
        self.title("Dozor System")
        
        try:
            if sys.platform == "win32":
                from ctypes import windll
                windll.shcore.SetProcessDpiAwareness(1) 
        except Exception:
            pass
            
        target_w, target_h = 422, 335
        self.geometry(f"{target_w}x{target_h}") 
        self.resizable(False, False)
        bg_color = "#F0F0F0"
        self.configure(fg_color=bg_color)
        
        main_bg = ctk.CTkFrame(self, fg_color=bg_color)
        main_bg.pack(fill="both", expand=True, padx=1, pady=1)
        
        main_bg.grid_columnconfigure(0, minsize=210, weight=0) 
        main_bg.grid_columnconfigure(1, weight=1) 
        
        for i in range(6): 
            main_bg.grid_rowconfigure(i, weight=0)
        main_bg.grid_rowconfigure(6, weight=1) 
        
        button_font = ("Arial", 13, "bold")
        btn_height = 45
        
        # === ЛЕВАЯ ПАНЕЛЬ МЕНЮ ===
        sidebar = ctk.CTkFrame(main_bg, fg_color="#EEEEEE", width=210)
        sidebar.grid(row=0, column=0, rowspan=7, sticky="nsew", padx=(5, 5), pady=5)
        sidebar.grid_columnconfigure(0, weight=1)
        
        actions = [
            ("Администратор", self.open_admin_module),
            ("Менеджер базы", self.open_manager_module), 
            ("Эксперты", self.open_experts_module),
            ("Проект", self.open_project_module),
            ("Прогнозирование", self.open_forecasting_module)
        ]
        
        for i, (text, command_func) in enumerate(actions):
            btn = ctk.CTkButton(
                master=sidebar,
                text=text,
                font=button_font,
                height=btn_height,
                command=command_func
            )
            btn.grid(row=i, column=0, padx=10, pady=5, sticky="ew")
            
        # === ПРАВАЯ ЧАСТЬ ВИТРИНЫ ===
        content_area = ctk.CTkFrame(main_bg, fg_color=bg_color)
        content_area.grid(row=0, column=1, rowspan=7, sticky="nsew", padx=(0, 10), pady=10)
        
        content_area.grid_columnconfigure(0, weight=1)
        content_area.grid_rowconfigure(0, weight=0) 
        content_area.grid_rowconfigure(1, minsize=2, weight=0) 
        content_area.grid_rowconfigure(2, minsize=27, weight=0) 
        content_area.grid_rowconfigure(3, weight=0) 
        
        pic_container = ctk.CTkFrame(content_area, fg_color="#d9ead3", width=220, height=130)
        pic_container.grid(row=0, column=0, pady=(5, 0)) 
        pic_container.grid_propagate(False)
        
        img_loaded = False
        try:
            # Используем централизованный путь
            img_path = resource_path('images/logo1.png')
            pil_img = Image.open(img_path)
            
            target_w_i, target_h_i = 210, 110
            ratio = pil_img.width / pil_img.height
            box_ratio = target_w_i / target_h_i
            
            if ratio > box_ratio:
                new_w, new_h = target_w_i, int(target_w_i / ratio)
            else:
                new_w, new_h = int(target_h_i * ratio), target_h_i
                
            resized_img = pil_img.resize((new_w, new_h), resample=Image.LANCZOS)
            tk_image = ctk.CTkImage(light_image=resized_img, size=(new_w, new_h))
            
            lbl_pic = ctk.CTkLabel(pic_container, image=tk_image, text="", fg_color="#d9ead3")
            lbl_pic.place(relx=0.5, rely=0.5, anchor='center', x=0, y=-3)
            img_loaded = True
            
        except Exception as e:
            fail_lbl = ctk.CTkLabel(pic_container, text="Сова\nне найдена", font=("Arial", 14), fg_color="#d9ead3")
            fail_lbl.place(relx=0.5, rely=0.5, anchor="center")
            
        bottom_btns_frame = ctk.CTkFrame(content_area, fg_color=bg_color)
        bottom_btns_frame.grid(row=3, column=0, sticky="ew", padx=0)
        
        bottom_btns_frame.grid_columnconfigure(0, minsize=195, weight=0) 
        ctk.CTkButton(
            bottom_btns_frame, 
            text="Руководство", 
            font=button_font, 
            height=btn_height,
            command=self.open_guide
        ).grid(row=0, column=0, sticky="ew", pady=(0, 8))
        
        ctk.CTkButton(
            bottom_btns_frame, 
            text="О программе", 
            font=button_font, 
            height=btn_height,
            command=self.show_about
        ).grid(row=1, column=0, sticky="ew")
        
        footer_frame = ctk.CTkFrame(main_bg, fg_color="#0078D7", height=30)
        footer_frame.grid(row=7, column=0, columnspan=2, sticky="ew", pady=(10, 0))
        
        footer_label = ctk.CTkLabel(footer_frame, 
                                    text="Copyright © Минск, НИИ, 2027 год", 
                                    text_color="#FF8C00",
                                    fg_color="#0078D7", 
                                    font=("Arial", 13, "bold"))
        footer_label.pack(expand=True, ipady=4)
        
        self.update_idletasks()
        self.tk.call('wm', 'geometry', self._w, f"{target_w}x{target_h}")
        
        screen_w = self.winfo_screenwidth()
        screen_h = self.winfo_screenheight()
        pos_x = (screen_w - target_w) // 2
        pos_y = (screen_h - target_h) // 2
        self.geometry(f"+{pos_x}+{pos_y}")

    def open_admin_module(self):
        if hasattr(self, '_admin_win') and self._admin_win.winfo_exists():
            self._admin_win.lift()
            return
        
        from modules.admin.admin_accordion_window import AdminAccordionWindow
        self._admin_win = AdminAccordionWindow(self)
        self.wait_window(self._admin_win)
        
    def open_manager_module(self):
        if hasattr(self, '_manager_win') and self._manager_win.winfo_exists():
            self._manager_win.lift()
            return
        self._manager_win = ManagerWindow(master=self)
        self.wait_window(self._manager_win)
        
    def open_experts_module(self):
        if hasattr(self, '_experts_win') and self._experts_win.winfo_exists():
            self._experts_win.lift()
            return
        self._experts_win = ExpertsWindow(master=self)
        self.wait_window(self._experts_win)
        
    def open_project_module(self):
        if hasattr(self, '_project_win') and self._project_win.winfo_exists():
            self._project_win.lift()
            return
        self._project_win = ProjectWindow(master=self)
        self.wait_window(self._project_win)
        
    def open_forecasting_module(self):
        if hasattr(self, '_forecast_win') and self._forecast_win.winfo_exists():
            self._forecast_win.lift()
            return
        self._forecast_win = ForecastWindow(master=self)
        self.wait_window(self._forecast_win)
        
    def open_guide(self):
        file_name = "manual.chm" 
        full_path = resource_path(file_name)
        print(f"Пытаюсь открыть: {full_path}") 
        if os.path.exists(full_path):
            try:
                os.startfile(full_path)
            except Exception as e:
                print(f"Система отказалась открывать файл: {e}")
        else:
            print("Ошибка: ФАЙЛА НЕТ в этой папке!")
            
    def show_about(self):
        from tkinter import messagebox
        info_text = (
            "Система Dozor V1.0\n"
            "Разработано: Минск, НИИ\n"
            "Версия сборки: 31.07.2026\n\n"
            "(c) Все права защищены."
        )
        messagebox.showinfo(title="О программе", message=info_text)

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