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


'''
День добрый!
Я, вайб кодер - ты, профессионалный программист. Ты делаешь - я вставляю, заменяю. 
Кусками не работаем - только целыми фрагментами - метод на метод, файл на файл, от комментария до комментария, от комментария до конца - по такому принципу. 
- Файл main_interface.ру принципиально не менять, это только витрина и кнопки.
- Программа будет собираться руинсталлером, поэтому это надо иметь ввиду.
- В программе уже реализованы авторизация и первая кнопка главного меню АДМИНИСТРАТОР. На сейчас - компиляция проходит без ошибок.
- Необходимо подключить в "аккордеоне" вторую кнопку главного меню МЕНЕДЖЕР БАЗЫ. Ниже будут представлены файлы-доноры для этой кнопки. Они реализованы в ткинкере. Мы делаем рефакторинг и все должно стать в кастомткинкере. Логика в файлах рабочая. По образцу этих файлов и надо организовать - один заголовок Аккордеона - 2 файла, из которых один для интерфейса, а второй с функционалом.
- все файлы для работы МЕНЕДЖЕРА БАЗЫ должны лежать по адресу dukora\modules\manager\.
- база расположена по адресу dukora\db\ - с адресом базы будь осторожен, она должна быть прописана как в файле users_tab.ру база users.db. Баз будет две - основная - masterdata, и рабочая пользовательская database.
- файл для работы с базами database_module.ру расположен по адресу dukora\modules\data\.
- если какие то вопросы есть - задавай.

Ниже представляю файлы, которые могут пригодиться как доноры, либо уже присутствующие как реалные файлы приложения.
dukora\main.py 
dukora\gui\main_interface.py 
dukora\modules\admin\users_tab.py 
dukora\modules\admin\admin_accordion_window.py
dukora\modules\manager\manager_window.py
dukora\modules\data\database_module.py

manager_group_inter.ру - файл – донор
manager_group_work.ру - файл – донор
manager_metrics_inter.py - файл – донор
manager_metrics_work.py - файл - донор

==================================================================

Итогом работы должна стать полностью рабочая кнопка МЕНЕДЖЕР БАЗЫ - открываться в аккордеоне два окна оформленных в новом интерфейсе кастомткинкера с рабочим функционалом.
Новый интерфейс это и кастомткинкер и расположение элементов управления.

===================================================================

МЕНЕДЖЕР ГРУППЫ: 
Вверху как бы в таблице из двух колонок расположить
Слева:
Название:    поле для названия
Код:         поле для кода
Усиление:    поле для усиления

Справа 3 кнопки, прижатые к правой стороне окна все одного размера:
Добавить (синего цвета)
Редактировать (зеленого цвета)
Удалить (красного цвета)

Ниже расположена таблица с тремя столбцами:
Код (фиксированной ширины в 12 знаков)
Усиление (фиксированной ширины в 7 знаков)
Название (резиновая)

При клике на сохраненную группу она выделяется в строке и имеется возможность ее удалить, редактировать.
===================================================================

МЕНЕДЖЕР ПОКАЗАТЕЛЕЙ:
Новый интерфейс это и кастомткинкер и расположение элементов управления.
 
Вверху как бы в таблице из двух колонок расположить
Слева в "таблице":
- Группа:    выпадающий список
- Приблизительно на пол окна заголовок Название показателя: и кнопка Вставить (черного цвета),а ниже текстовое поле на 4 переносимые строки и скрол для вставки текста из буфера обмена сочетанием клавишь и кнопкой Вставить

Справа в "таблице" кнопки, прижатые к правой стороне окна все одного размера:
Добавить (синего цвета)
Редактировать (зеленого цвета)
Удалить (красного цвета)

Ниже расположена таблица с тремя столбцами:
№ п/п (фиксированной ширины в 5 знаков), в таблице это для красоты, при удалении пересчитывается по порядку
Код (фиксированной ширины в 15 знаков) не меняется
Название (резиновая).

При клике на сохраненный показатель он выделяется в строке и имеется возможность его удалить, редактировать.

Таблица должна быть не высокая по высоте, строк на 10. Но каждый добавляемый новый показатель должен добавляться и сразу быть на виду в окне.

Ниже таблицы расположить кнопку Экспорт в WORD (черного цвета)
======================================================================
'''
#================================================









#================================================
# 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\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

        try:
            from modules.admin.admin_accordion_window import AdminAccordionWindow
            self._admin_win = AdminAccordionWindow(self)
            self.wait_window(self._admin_win)
        except Exception as e:
            messagebox.showerror("Фатальный сбой", f"Не удалось открыть раздел Администратора.\n\n{e}")
            import traceback
            traceback.print_exc()

    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()
    
#===================================================









#====================================================
# 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

# Импорт нашей утилиты путей
from modules.utils.resources import resource_path

class AdminUsersPanel(ctk.CTkFrame):
    def __init__(self, master):
        super().__init__(master)
        
        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: Path = resource_path('db/users.db')
        
        try:
            conn = sqlite3.connect(str(db_path))
            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}\nПуть: {db_path}")
        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: Path = resource_path('db/users.db')
        
        try:
            conn = sqlite3.connect(str(db_path))
            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: Path = resource_path('db/users.db')
        
        try:
            conn = sqlite3.connect(str(db_path))
            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: Path = resource_path('db/users.db')
        
        try:
            conn = sqlite3.connect(str(db_path))
            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.login_entry.configure(state="disabled")
                
                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")  
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось прочитать данные:\n{e}")
        finally:
            if 'conn' in locals():
                conn.close()
         
#===================================================









#====================================================
# dukora\modules\admin\admin_accordion_window.py - реальный файл,
# для примера 
#====================================================
import customtkinter as ctk
from tkinter import ttk, messagebox
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\modules\manager\manager_window.py - реальный файл
#====================================================
import customtkinter as ctk
from tkinter import ttk

class ManagerWindow(ctk.CTkToplevel):
    def __init__(self, master):
        super().__init__(master)
        
        self.title("Менеджер базы")
        
        window_width = 650
        initial_height = 355 
        
        x_cord = int((self.winfo_screenwidth() / 2) - (window_width / 2))
        y_cord = int((self.winfo_screenheight() / 2) - (initial_height / 2))
        
        self.geometry(f"{window_width}x{initial_height}+{x_cord}+{y_cord}")
        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 = ctk.CTkLabel(main_container, text="Управление базой данных", font=ctk.CTkFont(size=20, weight="bold"))
        title.pack(pady=(0, 15))
        
        accordion_frame = ctk.CTkFrame(main_container)
        accordion_frame.pack(fill="both", expand=True, pady=(10, 0))
        
        # ФРЕЙМ ДЛЯ КНОПКИ (теперь он внутри общей структуры четко)
        button_frame = ctk.CTkFrame(main_container, fg_color="transparent")
        
        self.accordion_data = {}
        self.currently_open_button = None
        
        group_item = self._create_accordion_header(accordion_frame, "Менеджер группы")
        kpi_item = self._create_accordion_header(accordion_frame, "Менеджер показателей")

        self._fill_group_section(group_item["content"])
        self._fill_kpi_section(kpi_item["content"])
        
        for item in [group_item, kpi_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))
            
        # ВАЖНО: Сначала пакуем кнопку, потом задаем её параметры
        button_frame.pack(fill="x", side="bottom") # Привязка к низу
        
        close_btn = ctk.CTkButton(button_frame, text="Закрыть", width=120, command=self.destroy)
        close_btn.pack(side="right", padx=10, pady=10) # Отступы внутри фрейма кнопки

    # === МЕТОДЫ АККОРДЕОНА ===
    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.after_idle(lambda g=new_geo: self.geometry(g))

    def _calculate_window_geometry(self, collapsed=False):
        current_width = self.winfo_width()
        
        # Высота заголовков + разделителей между ними
        header_height = (50 * len(self.accordion_data)) + 30 
        
        BOTTOM_RESERVED_SPACE = 70 
        
        if collapsed or not self.currently_open_button:
            # ЖЕСТКАЯ ФИКСАЦИЯ: если всё закрыто - всегда стартовый размер
            target_height = 355 
        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 + 120 + BOTTOM_RESERVED_SPACE
        
        max_h = self.winfo_screenheight() - 50
        final_height = min(target_height, max_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_group_section(self, frame):
        placeholder = ctk.CTkLabel(frame, text="Здесь будет управление группами.", font=("Arial", 12))
        placeholder.pack(expand=True)

    def _fill_kpi_section(self, frame):
        placeholder = ctk.CTkLabel(frame, text="Здесь будут показатели эффективности.", font=("Arial", 12))
        placeholder.pack(expand=True)
        
#===================================================









#====================================================
# dukora\modules\data\database_module.py - реальный файл,
# для работы с базой данных
#====================================================        
# -*- coding: utf-8 -*-
import sys
import sqlite3
from pathlib import Path

try:
    potential_base = Path.cwd()
    if "Dozor" not in str(potential_base):
        BASE_DIR = Path(__file__).resolve().parents[4]
    else:
        BASE_DIR = potential_base
    
    DB_FOLDER = BASE_DIR / "db"
    ACTIVE_FLAG_PATH = DB_FOLDER / "active.flag"
    
    DEFAULT_DB_PATH = DB_FOLDER / "database.db"
    MASTERDATA_PATH = DB_FOLDER / "masterdata.db"
except Exception as e:
    print(f"[FATAL] Не удалось вычислить путь к базе данных: {e}")
    sys.exit(1)

def _determine_active_path():
    try:
        if ACTIVE_FLAG_PATH.exists():
            with open(ACTIVE_FLAG_PATH, 'r') as f:
                name = f.read().strip()
                if name == "masterdata":
                    return MASTERDATA_PATH
    except Exception:
        pass
    return DEFAULT_DB_PATH

DB_PATH = _determine_active_path()

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session as SqlAlchemySession, declarative_base

engine = create_engine(f"sqlite:///{DB_PATH}", echo=False, connect_args={"check_same_thread": False})
Base = declarative_base()
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def get_db():
    db_session = SessionLocal()
    try:
        yield db_session
    finally:
        db_session.close()

def get_db_connection():
    active_path = _determine_active_path()
    global DB_PATH
    DB_PATH = active_path
    
    if not DB_FOLDER.exists():
        print(f"[ACTION] Создаю папку для базы: {DB_FOLDER}")
        DB_FOLDER.mkdir(parents=True, exist_ok=True)
        
    conn = sqlite3.connect(active_path)
    conn.execute("PRAGMA foreign_keys = ON;")
    return conn

def _create_tables(cursor):
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS groups (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL UNIQUE,
    code TEXT NOT NULL UNIQUE,
    gain_group REAL DEFAULT 0.0
    )
    ''')
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS indicators (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL UNIQUE,
    code TEXT NOT NULL UNIQUE,
    group_code TEXT NOT NULL,
    FOREIGN KEY (group_code) REFERENCES groups(code) ON DELETE CASCADE
    )
    ''')

def _populate_test_data(conn):
    cursor = conn.cursor()
    cursor.execute("SELECT COUNT(*) FROM groups")
    count = cursor.fetchone()[0]
    if count == 0:
        print("[INFO] База данных пуста. Заполняю тестовыми данными...")
        groups_data = [("Клубы", "KB", 1.5), ("Фанаты", "FAN", 2.0)]
        cursor.executemany("INSERT INTO groups (name, code, gain_group) VALUES (?, ?, ?)", groups_data)
        indicators_data = [
            ("ЦСКА", "KB-1", "KB"), ("Спартак", "KB-2", "KB"), ("Зенит", "KB-3", "KB"),
            ("Мужчины", "FAN-1", "FAN"), ("Женщины", "FAN-2", "FAN"), ("Дети", "FAN-3", "FAN")
        ]
        cursor.executemany("INSERT INTO indicators (name, code, group_code) VALUES (?, ?, ?)", indicators_data)
        conn.commit()
        print("[SUCCESS] Тестовые данные успешно записаны.")
    else:
        print("[INFO] База данных уже содержит данные. Пропускаю инициализацию.")

def init_database():
    print("[INIT] Проверка наличия рабочих баз данных...")
    try:
        conn = get_db_connection()
        cursor = conn.cursor()
        _create_tables(cursor)
        _populate_test_data(conn)
    except sqlite3.Error as e:
        print(f"[ERROR] Ошибка при инициализации DATABASE.DB: {e}")
        if conn:
            conn.rollback()
    finally:
        if conn:
            conn.close()

    master_conn = None
    try:
        if not MASTERDATA_PATH.exists():
            print(f"[ACTION] База мастер-данных не найдена. Создаю новую: {MASTERDATA_PATH.name}")
            master_conn = sqlite3.connect(MASTERDATA_PATH)
            master_conn.execute("PRAGMA foreign_keys = ON;")
            master_cursor = master_conn.cursor()
            _create_tables(master_cursor)
            _populate_test_data(master_conn)
    except sqlite3.Error as e:
        print(f"[ERROR] Ошибка при инициализации MASTERDATA.DB: {e}")
        if master_conn:
            master_conn.rollback()
    finally:
        if master_conn:
            master_conn.close()

class Session:
    def __init__(self):
        pass

    # Эти функции объявлены СТАТИЧЕСКИМИ и НА УРОВНЕ МОДУЛЯ ниже
    pass 

# --- ФУНКЦИИ ДЛЯ ИМПОРТА (лежат строго вне класса Session) ---
def list_groups():
    """Функция-заглушка для прямого импорта"""
    return [(1, "Клубы"), (2, "Фанаты")]

def get_all_indicators_by_group(group_code):
    """Функция-заглушка для прямого импорта"""
    mock_data = {
        "KB": [(1, "ЦСКА"), (2, "Спартак"), (3, "Зенит")],
        "FAN": [(4, "Мужчины"), (5, "Женщины"), (6, "Дети")]
    }
    return mock_data.get(group_code, [])

if __name__ == "__main__":
    init_database()       
        
        
        


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

Ниже идут файлы - доноры







#====================================================
# manager_group_inter.ру - файл - донор
#====================================================
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, messagebox
from modules.manager.manager_group_work import list_groups, add_group, delete_group
from modules.utils.window_behavior import LevelThreeBehavior
from modules.utils.app_state import disable_app, enable_app


class ManagerGroupInter(tk.Toplevel):
    def __init__(self, parent):
        super().__init__(parent)
        self.master = parent
        self.transient(parent) 
        self.title("Менеджер групп")
        self.geometry("600x450")

        behavior = LevelThreeBehavior()
        behavior.setup_level_three(self)

        header_frame = tk.Frame(self, bg="#2c3e50", height=30)
        header_frame.pack(fill="x")
        header_frame._name = "title_bar"
        
        close_btn = ttk.Button(header_frame, text="X", width=3, command=self.destroy)
        close_btn.pack(side="right", padx=5, pady=2)
        
        ttk.Label(header_frame, text="Менеджер групп", foreground="white", background="#2c3e50").pack(side="left", padx=10)
        
        ttk.Separator(self, orient='horizontal').pack(fill='x')

        top_frame = tk.Frame(self)
        top_frame.pack(fill="x", padx=10, pady=(5, 10))
        
        # --- НАЗВАНИЕ ---
        ttk.Label(top_frame, text="Название:").grid(row=0, column=0, sticky="w")
        self.name_entry = ttk.Entry(top_frame, width=30)
        self.name_entry.grid(row=0, column=1, padx=5)
        
        # --- КОД (Только чтение для редактирования) ---
        ttk.Label(top_frame, text="Код:").grid(row=1, column=0, sticky="w")
        self.code_var = tk.StringVar() 
        self.code_label = ttk.Label(top_frame, textvariable=self.code_var, relief="sunken", width=30)
        self.code_label.grid(row=1, column=1, padx=5, pady=5)
        
        frame_gain = tk.Frame(top_frame)
        frame_gain.grid(row=2, column=1, sticky="w", padx=5)
        
        ttk.Label(frame_gain, text="Усиление:").pack(side="left")
        
        btn_minus = ttk.Button(frame_gain, text="-", command=lambda: self._change_gain(-1), width=3)
        btn_minus.pack(side="left", padx=2)
        
        self.gain_var = tk.StringVar(value="1")
        gain_entry = ttk.Entry(frame_gain, textvariable=self.gain_var, width=5, justify="center")
        gain_entry.pack(side="left", padx=2)
        
        btn_plus = ttk.Button(frame_gain, text="+", command=lambda: self._change_gain(1), width=3)
        btn_plus.pack(side="left", padx=2)

        action_frame = tk.Frame(top_frame)
        action_frame.grid(row=3, column=0, columnspan=2, pady=10)
        
        # КНОПКА ДОБАВЛЕНИЯ БУДЕТ ВЫЗЫВАТЬ ЭТОТ МЕТОД
        ttk.Button(action_frame, text="Добавить", command=self.add_group).pack(side="left", padx=5)
        ttk.Button(action_frame, text="Редактировать", command=self.edit_group).pack(side="left", padx=5)
        ttk.Button(action_frame, text="Удалить", command=self.delete_group).pack(side="left", padx=5)
        
        tree_frame = tk.Frame(self)
        tree_frame.pack(fill="both", expand=True, padx=10, pady=5)
        
        columns = ("code", "name", "gain")
        self.tree = ttk.Treeview(tree_frame, columns=columns, show="headings")
        self.tree.heading("code", text="Код"); self.tree.column("code", width=100)
        self.tree.heading("name", text="Название"); self.tree.column("name", width=200)
        self.tree.heading("gain", text="Усиление"); self.tree.column("gain", width=80, anchor="center")
        self.tree.pack(fill="both", expand=True, side="left")
        
        scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
        scrollbar.pack(side="right", fill="y")
        self.tree.configure(yscrollcommand=scrollbar.set)
        
        self.update_tree()
        
        self.update_idletasks() 
        screen_width = self.winfo_screenwidth()
        screen_height = self.winfo_screenheight()
        size = tuple(int(_) for _ in self.geometry().split('+')[0].split('x'))
        x = screen_width // 2 - size[0] // 2
        y = screen_height // 2 - size[1] // 2
        self.geometry(f"+{x}+{y}")

    def update_tree(self):
        for item in self.tree.get_children():
            self.tree.delete(item)
            
        groups = list_groups()
        for group in groups:
            gain_text = f"{group.gain_group:.1f}" if group.gain_group is not None else ""
            self.tree.insert("", "end", iid=str(group.id), values=(group.code, group.name, gain_text))

    def get_selected(self):
        selected = self.tree.selection()
        if not selected:
            return None
        
        code, name, gain = self.tree.item(selected[0])["values"]
        db_id_str = selected[0]
        try:
            db_id = int(db_id_str)
        except ValueError:
            return None
            
        return {"id": db_id, "code": code, "name": name}

    # === ВОТ ЭТОТ МЕТОД БЫЛ ПРОПУЩЕН РАНЬШЕ ===
    def add_group(self):
        name = self.name_entry.get().strip()
        # Для добавления временно используем поле ввода кода, если оно есть, иначе генерируем заглушку
        code = "" 
        
        # Проверим, не является ли интерфейс сейчас режимом "редактирование" по наличию лейбла
        if hasattr(self, 'code_label') and self.code_label.cget("text"):
             code = self.code_label.cget("text") 
        
        gain_raw = self.gain_var.get().strip()
        
        if not name or not code:
            messagebox.showwarning("Ошибка", "Заполните название и код.")
            return
            
        try:
            gain_val = float(gain_raw.replace(',', '.')) if gain_raw else 0.0
        except ValueError:
            messagebox.showwarning("Ошибка", "Усиление должно быть числом.")
            return
            
        existing = list_groups()
        for g in existing:
            if g.name == name or g.code == code:
                messagebox.showerror("Ошибка", "Группа с таким названием или кодом уже существует.")
                return
                
        try:
            add_group(name, code, gain_val)
            messagebox.showinfo("Успех", "Группа добавлена.")
            self.update_tree()
            self._clear_form()
        except Exception as e:
            messagebox.showerror("Ошибка БД", str(e))

    def edit_group(self):
        data = self.get_selected()
        if not data:
            messagebox.showwarning("Ошибка", "Выберите группу.")
            return
        
        old_code = data["code"]
        old_name = data["name"]
        
        # Берем текущее усиление прямо сейчас (то, которое выбрал юзер)
        current_gain_str = self.gain_var.get().strip()
        
        # Заполняем форму для наглядности
        self.name_entry.delete(0, tk.END)
        self.name_entry.insert(0, old_name)
        
        self.code_var.set(old_code) 
        
        disable_app(self.master)
        
        edit_win = tk.Toplevel(self.master)
        edit_win.title("Подтверждение")
        edit_win.geometry("+{}+{}".format(self.winfo_pointerx(), self.winfo_pointery()))
        
        msg = f"Изменить группу '{old_code}'?"
        ttk.Label(edit_win, text=msg, wraplength=300).pack(pady=20)
        
        btn_frame = tk.Frame(edit_win)
        btn_frame.pack(pady=10)
        
        # ПЕРЕДАЕМ ЗНАЧЕНИЕ НАПРЯМУЮ В ЛЯМБДУ
        ttk.Button(btn_frame, text="Да", 
                   command=lambda db_id=data["id"], name=old_name, gain=current_gain_str: 
                        self._apply_edit(db_id, name, gain, edit_win)).pack(side="left", padx=10)
                       
        ttk.Button(btn_frame, text="Отмена", command=edit_win.destroy).pack(side="left", padx=10)


    def _apply_edit(self, db_id, name, gain_str, win):
        try:
            final_gain = int(float(gain_str)) 
            
            from modules.data.database_module import get_db_connection
            conn = get_db_connection()
            cursor = conn.cursor()
            
            print(f"[DEBUG] SQL Update -> ID:{db_id} | Name:'{name}' | Gain:{final_gain}")
            
            cursor.execute(
                "UPDATE groups SET name = ?, gain_group = ? WHERE id = ?", 
                (name, final_gain, db_id)
            )
            
            rows_affected = cursor.rowcount
            
            conn.commit()
            conn.close()
            
            if rows_affected > 0:
                messagebox.showinfo("Готово", "Группа обновлена.")
                win.destroy() 
                self.after_idle(self.update_tree) 
                enable_app(self.master)
            else:
                 messagebox.showwarning("Внимание", "Запрос прошел, но строка не изменилась.")
                 win.destroy()
                 enable_app(self.master)
                
        except Exception as e:
            print(f"[FATAL DB ERROR RAW SQL] {e}") 
            messagebox.showerror("Ошибка БД", str(e))
            enable_app(self.master)


    def delete_group(self):
        data = self.get_selected()
        if not data:
            messagebox.showwarning("Ошибка", "Выберите группу.")
            return
        if messagebox.askyesno("Подтверждение", f"Удалить группу {data['name']}?"):
            try:
                delete_group(data["id"]) 
                messagebox.showinfo("Успех", "Группа удалена.")
                self.update_tree()
                self._clear_form()
            except Exception as e:
                messagebox.showerror("Ошибка БД", str(e))

    def _clear_form(self):
        if hasattr(self, 'name_entry'):
            self.name_entry.delete(0, tk.END)
        if hasattr(self, 'code_var'):
            self.code_var.set("")
        self.gain_var.set("1.0")

    def on_close(self):
        self.destroy()

    def _change_gain(self, delta):
        try:
            val = int(self.gain_var.get())
            new_val = max(0, val + delta)
            self.gain_var.set(str(new_val))
        except ValueError:
            self.gain_var.set("1")
            
#===================================================

Ниже идут файлы - доноры







#====================================================
# manager_group_work.ру - файл - донор
#====================================================
# -*- coding: utf-8 -*-
import sqlite3
from typing import List
from contextlib import contextmanager
from modules.data.database_module import get_db_connection

class GroupDTO:
    def __init__(self, id: int, code: str, name: str, gain_group: float = None):
        self.id = id
        self.code = code
        self.name = name
        self.gain_group = gain_group

@contextmanager
def _get_cursor():
    conn = get_db_connection()
    try:
        cursor = conn.cursor()
        yield cursor
        # Принудительная фиксация СРАЗУ ЖЕ
        conn.commit() 
    except Exception:
        conn.rollback()
        raise
    finally:
        # Принудительное закрытие соединения, чтобы сбросить все локи
        conn.close()

def list_groups() -> List[GroupDTO]:
    with _get_cursor() as cursor:
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS groups (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL UNIQUE,
                code TEXT NOT NULL UNIQUE,
                gain_group REAL DEFAULT 0.0
            )
        ''')
        cursor.execute("SELECT id, code, name, gain_group FROM groups ORDER BY name")
        rows = cursor.fetchall()
        return [GroupDTO(id=row[0], code=row[1], name=row[2], gain_group=row[3]) for row in rows]

def add_group(name: str, code: str, gain_group: float = 0.0) -> GroupDTO:
    with _get_cursor() as cursor:
        cursor.execute(
            "INSERT INTO groups (name, code, gain_group) VALUES (?, ?, ?)", 
            (name, code, gain_group)
        )
        new_id = cursor.lastrowid
        return GroupDTO(id=new_id, code=code, name=name, gain_group=gain_group)

def edit_group(group_id: int, new_name: str, new_code: str, new_gain: float):
    conn = get_db_connection()
    cursor = conn.cursor()
    try:
        cursor.execute(
            "UPDATE groups SET name = ?, code = ?, gain_group = ? WHERE id = ?", 
            (new_name, new_code, new_gain, group_id)
        )
        
        # Проверяем, затронута ли хоть одна строка
        if cursor.rowcount == 0:
            raise Exception("Запись не обновлена. Проверьте уникальность кода/названия.")
            
        # Принудительная фиксация прямо здесь
        conn.commit()
        print("[WORK LOGIC] Группа успешно обновлена в БД.")
    except sqlite3.IntegrityError as e:
        conn.rollback()
        raise Exception(f"Нарушение целостности данных: {e}")
    except Exception as e:
        conn.rollback()
        raise e
    finally:
        conn.close()

def delete_group(group_id: int):
    with _get_cursor() as cursor:
        cursor.execute("DELETE FROM groups WHERE id = ?", (group_id,))
        
#===================================================

Ниже идут файлы - доноры







#====================================================
# manager_metrics_inter.py - файл - донор
#====================================================
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import pyperclip  # <-- ДОБАВИЛИ БИБЛИОТЕКУ
from docx import Document
from modules.manager.manager_metrics_work import (
    get_all_indicators_by_group,
    insert_indicator,
    delete_indicator_by_code,
    IndicatorDTO,
    generate_unique_code,
    save_docx_template
)
from modules.manager.manager_group_work import list_groups as group_list
from modules.utils.window_behavior import LevelTwoBehavior

class ManagerMetricsInter(tk.Toplevel):
    def __init__(self, parent):
        super().__init__(parent)
        
        self.parent_main = parent
        self.group_names_to_codes = {}

        behavior = LevelTwoBehavior()
        behavior.setup_level_two(self)

        self.title("Менеджер показателей")
        self.geometry("600x450")

        if not hasattr(self.parent_main, 'current_user') or not self.parent_main.current_user.get("login"):
            messagebox.showwarning("Ошибка", "Сессия пользователя не найдена.")
            self.destroy()
            return

        self.create_widgets()
        self.update_group_combobox()
        self.update_indicators_list()
        
        self.update_idletasks()
        screen_width = self.winfo_screenwidth()
        screen_height = self.winfo_screenheight()
        size = tuple(int(_) for _ in self.geometry().split('+')[0].split('x'))
        x = screen_width // 2 - size[0] // 2
        y = screen_height // 2 - size[1] // 2
        self.geometry(f"+{x}+{y}")
        
        # === СТРОКУ НИЖЕ МЫ УДАЛИЛИ! Теперь окно не крадёт фокус само ===
        # self.name_entry.focus_set()  <-- ЭТОЙ СТРОКИ БОЛЬШЕ НЕТ
        
        # Оставляем глобальный бинд как страховку, но он теперь не мешает
        self._setup_global_paste_hack()


    def create_widgets(self):
        top_panel = tk.Frame(self)
        top_panel.pack(padx=10, pady=10, fill="x")

        ttk.Label(top_panel, text="Группа:").pack(anchor="w")
        self.group_var = tk.StringVar()
        self.group_combobox = ttk.Combobox(top_panel, textvariable=self.group_var, state="readonly", width=30)
        self.group_combobox.pack(fill="x", pady=5)
        self.group_combobox.bind("<<ComboboxSelected>>", self.update_indicators_list)

        ttk.Label(top_panel, text="Название показателя:").pack(anchor="w")
        self.name_entry = tk.Text(top_panel, height=3, width=40)
        self.name_entry.pack(fill="x", pady=5)
        
         
        paste_button = tk.Button(top_panel, text="Вставить", command=self.paste_from_clipboard)
        paste_button.pack()
        # --- ДОБАВЬ ЭТУ СТРОКУ, ЕСЛИ ЕЁ НЕТ ---
        self.paste_button = paste_button 


        buttons_frame = tk.Frame(top_panel)
        buttons_frame.pack(fill="x", pady=5)

        ttk.Button(buttons_frame, text="Добавить", command=self.add_indicator).pack(side="left", padx=2)
        ttk.Button(buttons_frame, text="Удалить", command=self.delete_indicator).pack(side="left", padx=2)
        ttk.Button(buttons_frame, text="Экспорт в Word", command=self.print_to_docx).pack(side="left", padx=5, pady=5)

        middle_panel = tk.Frame(self)
        middle_panel.pack(fill="both", expand=True, padx=10, pady=5)

        scrollbar = ttk.Scrollbar(middle_panel)
        scrollbar.pack(side="right", fill="y")

        columns = ("number", "name", "code")
        self.indicators_tree = ttk.Treeview(
            middle_panel, columns=columns, show="headings", yscrollcommand=scrollbar.set, selectmode="browse"
        )
        self.indicators_tree.heading("number", text="№"); self.indicators_tree.column("number", width=50, anchor="center")
        self.indicators_tree.heading("name", text="Название"); self.indicators_tree.column("name", width=300)
        self.indicators_tree.heading("code", text="Код"); self.indicators_tree.column("code", width=150)
        self.indicators_tree.pack(fill="both", expand=True, side="left")
        scrollbar.config(command=self.indicators_tree.yview)

    def update_group_combobox(self):
        groups = group_list()
        self.group_names_to_codes.clear()
        for group in groups:
            self.group_names_to_codes[group.name] = getattr(group, 'code', '')
        
        group_names = [g.name for g in groups]
        self.group_combobox["values"] = group_names
        if group_names:
            self.group_combobox.current(0)
        self.update_indicators_list()

    def update_indicators_list(self, event=None):
        selected_group_name = self.group_var.get()
        selected_group_code = self.group_names_to_codes.get(selected_group_name)

        for item in self.indicators_tree.get_children():
            self.indicators_tree.delete(item)
            
        indicators = []
        if selected_group_code:
            try:
                indicators = get_all_indicators_by_group(selected_group_code)
            except Exception as e:
                print(f"[METRICS WORK] Заглушка данных: {e}")
                indicators = [IndicatorDTO(code=f"{selected_group_code}-TEST", name="Тестовый показатель")]
                
        for idx, ind in enumerate(indicators, start=1):
            val_name = getattr(ind, 'name', '--')
            val_code = getattr(ind, 'code', '--')
            self.indicators_tree.insert("", "end", iid=val_code, values=(f"{idx}.", val_name, val_code))
        
        self._keep_on_top()

    # --- СТАРЫЙ МЕТОД КНОПКИ ОСТАВЛЯЕМ ДЛЯ ЗАПАСА ---
    def paste_from_clipboard(self):
        try:
            clipboard_text = self.clipboard_get()
            self.name_entry.insert(tk.END, clipboard_text)
        except tk.TclError:
            pass

    # --- НОВЫЙ ГЛАВНЫЙ МЕТОД ВСТАВКИ ЧЕРЕЗ PYPЕРCLIP ---
    def _paste_via_pyperclip(self):
        """Универсальный метод вставки текста из буфера."""
        try:
            text = pyperclip.paste()
            if text:
                self.name_entry.insert(tk.END, text)
        except pyperclip.PyperclipException:
            messagebox.showerror("Ошибка доступа", "Не удалось получить доступ к буферу обмена.\nПопробуйте закрыть другие программы, использующие его.")
        except Exception as e:
             print(f"[DEBUG] Unexpected error in paste: {e}")

    def add_indicator(self):
        group_name = self.group_var.get()
        group_code = self.group_names_to_codes.get(group_name)
        indicator_name = self.name_entry.get("1.0", tk.END).rstrip("\n").strip()

        if not group_code or not indicator_name:
            messagebox.showwarning("Ошибка", "Выберите группу и введите название!")
            return
            
        new_code = generate_unique_code(group_code)

        try:
            insert_indicator(new_code, indicator_name, group_code)
            messagebox.showinfo("Готово", "Индикатор добавлен.")
            self.update_indicators_list() 
            self.name_entry.delete("1.0", tk.END)
        except Exception as e:
            messagebox.showerror("Ошибка", str(e))
            
        self._keep_on_top()

    def edit_indicator(self, event):
        selected_items = self.indicators_tree.selection()
        if not selected_items:
            return
        code = selected_items[0]
        
        current_name = ""
        item_values = self.indicators_tree.item(code)["values"]
        if len(item_values) > 1:
            current_name = item_values[1]
            
        edit_win = tk.Toplevel(self)
        edit_win.title("Редактировать")
        edit_win.geometry("350x150")
        
        x = self.winfo_pointerx() 
        y = self.winfo_pointery()
        edit_win.geometry(f"+{x}+{y}")
        
        ttk.Label(edit_win, text="Новое название:").pack(pady=5)
        new_name_entry = tk.Text(edit_win, height=2, width=40)
        new_name_entry.insert(tk.END, current_name)
        new_name_entry.pack(pady=5)
        
        ttk.Button(edit_win, text="Сохранить",
                   command=lambda: self.save_changes(code, new_name_entry.get("1.0", tk.END), edit_win)).pack(pady=5)

    def save_changes(self, code, new_name_raw, edit_win):
        new_name = new_name_raw.rstrip("\n").strip()
        if not new_name:
            messagebox.showwarning("Ошибка", "Название не может быть пустым.")
            return
        
        messagebox.showinfo("Готово", "Изменения сохранены (заглушка).")
        self.update_indicators_list()
        edit_win.destroy()
        
        self._keep_on_top()

    def delete_indicator(self):
        selected_items = self.indicators_tree.selection()
        if not selected_items:
            messagebox.showwarning("Ошибка", "Выберите индикатор для удаления.")
            return
        code = selected_items[0]
        
        if messagebox.askyesno("Подтверждение", f"Удалить индикатор?"):
            try:
                delete_indicator_by_code(code)
                messagebox.showinfo("Готово", "Удалено успешно.")
                self.update_indicators_list()
            except Exception as e:
                messagebox.showerror("Ошибка", str(e))
                
        self._keep_on_top()

    def print_to_docx(self):
        """
        Экспортирует показатели в Word с автоматическим именем файла:
        [НазваниеГруппы]_[Год-Месяц-День].docx
        """
        group_name = self.group_var.get()
        if not group_name:
            messagebox.showwarning("Ошибка", "Выберите группу.")
            return

        group_code = self.group_names_to_codes.get(group_name)
        indicators = get_all_indicators_by_group(group_code)
        if not indicators:
            messagebox.showwarning("Ошибка", "В группе нет показателей для экспорта.")
            return

        # --- ФОРМИРОВАНИЕ ИМЕНИ ФАЙЛА ---
        from datetime import date
        
        # Получаем текущую дату
        today = date.today()
        
        # Форматируем дату как ГГГГ-ММ-ДД (например, 2026-07-25)
        date_str = today.strftime("%Y-%m-%d")
        
        # Убираем пробелы из названия группы, чтобы не было проблем с путями
        safe_group_name = group_name.replace(' ', '_')
        
        # Собираем финальное имя файла
        filename = f"{safe_group_name}_{date_str}.docx"

        path = None
        try:
            # Открываем диалог сохранения С АВТОЗАПОЛНЕННЫМ именем
            path = filedialog.asksaveasfilename(
                initialfile=filename, 
                defaultextension=".docx",
                filetypes=[("Word files", "*.docx"), ("All files", "*.*")]
            )
            
            # Если пользователь нажал "Отмена" — путь будет пустым ('')
            if not path:
                return

            save_docx_template(group_name, indicators, path)
            messagebox.showinfo("Успех", f"Файл успешно сохранён:\\n{path}")

        except Exception as e:
            messagebox.showerror("Ошибка записи", f"Не удалось сохранить файл:\\n{e}")


    def on_close(self):
        self.destroy()

    def _change_gain(self, delta):
        try:
            val = int(self.gain_var.get())
            new_val = max(0, val + delta)
            self.gain_var.set(str(new_val))
        except ValueError:
            self.gain_var.set("1.0")
            
    def _keep_on_top(self):
        self.lift()
        self.focus_force()
        
    def _setup_global_paste_hack(self):
        """
        Метод-сторож: постоянно следит за буфером обмена.
        Если текст появился, пока активно наше поле — вставляем его.
        """
        self._clipboard_last_check = "" # Храним последнее значение

        def check_clipboard():
            try:
                current_text = pyperclip.paste()
                
                # Проверяем три условия:
                # 1. Текст в буфере изменился (мы что-то скопировали)
                # 2. Текст непустой
                # 3. Наше поле name_entry СЕЙЧАС имеет фокус
                if (current_text != self._clipboard_last_check and 
                    current_text and 
                    self.focus_get() == self.name_entry):
                    
                    # Вставляем текст напрямую
                    self.name_entry.insert(tk.END, current_text)
                    
                    # Обновляем "память", чтобы не вставлять одно и то же дважды
                    self._clipboard_last_check = current_text

            except Exception:
                pass # Игнорируем ошибки доступа к буферу
            
            # === ВАЖНО: запускаем проверку снова через 100 миллисекунд ===
            self.after(100, check_clipboard)
        
        # Запускаем первую итерацию цикла при создании окна
        check_clipboard()
        
#===================================================

Ниже идут файлы - доноры







#====================================================
# manager_metrics_work.py - файл - донор
#====================================================
# -*- coding: utf-8 -*-
import os
import sqlite3
from typing import List
from contextlib import contextmanager
from modules.data.database_module import get_db_connection

class IndicatorDTO:
    def __init__(self, id: int, code: str, name: str, group_code: str):
        self.id = id
        self.code = code
        self.name = name
        self.group_code = group_code

@contextmanager
def _get_cursor():
    conn = get_db_connection()
    try:
        cursor = conn.cursor()
        yield cursor
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()

def insert_indicator(code: str, name: str, group_code: str) -> IndicatorDTO:
    print(f"[WORK LOGIC] INSERT | Код: {code}, Имя: {name}")
    with _get_cursor() as cursor:
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS indicators (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL UNIQUE,
                code TEXT NOT NULL UNIQUE,
                group_code TEXT NOT NULL,
                FOREIGN KEY (group_code) REFERENCES groups(code) ON DELETE CASCADE
            )
        ''')
        
        cursor.execute("INSERT INTO indicators (code, name, group_code) VALUES (?, ?, ?)", 
                       (code, name, group_code))
        new_id = cursor.lastrowid
        return IndicatorDTO(id=new_id, code=code, name=name, group_code=group_code)

def delete_indicator_by_code(code: str):
    print(f"[WORK LOGIC] DELETE | Код: {code}")
    with _get_cursor() as cursor:
        cursor.execute("DELETE FROM indicators WHERE code = ?", (code,))

def get_all_indicators_by_group(group_code: str) -> List[IndicatorDTO]:
    print(f"[WORK LOGIC] SELECT | Группа: {group_code}")
    with _get_cursor() as cursor:
        cursor.execute("SELECT id, code, name, group_code FROM indicators WHERE group_code = ?", (group_code,))
        rows = cursor.fetchall()
        return [IndicatorDTO(id=row[0], code=row[1], name=row[2], group_code=row[3]) for row in rows]

def generate_unique_code(group_code: str) -> str:
    """
    Генерирует уникальный код показателя внутри группы по шаблону ГРУППА-N.
    Например: Эко-1, Эко-2, ИТ-1.
    """
    with _get_cursor() as cursor:
        cursor.execute("""
            SELECT MAX(CAST(substr(code, instr(code, '-') + 1) AS INTEGER)) 
            FROM indicators WHERE code LIKE ?
        """, (f"{group_code}-%",))
        
        row = cursor.fetchone()[0]
        
        if row is None:
            next_num = 1
        else:
            next_num = row + 1
            
        return f"{group_code}-{next_num}"

def save_docx_template(group_name: str, indicators: List[IndicatorDTO], path: str) -> bool:
    """
    Альбомный лист (А4), поля 3см | 1см.
    Шрифт таблицы: Times New Roman, 11пт, обычный.
    Ширина колонок: 3см | 3см | 19.7см.
    """
    print(f"[WORK LOGIC] Вызов: save_docx_template | Путь: {path} | Группа: {group_name}")

    if not indicators:
        return False

    try:
        import win32com.client as win32
        
        word = win32.gencache.EnsureDispatch('Word.Application')
        word.Visible = False
        word.DisplayAlerts = False

        doc = word.Documents.Add()
        
        # === 1. АЛЬБОМНЫЙ ФОРМАТ И ПОЛЯ ===
        section = doc.Sections(1)
        
        # Меняем ориентацию на 1 (wdOrientLandscape)
        section.PageSetup.Orientation = 1 
        
        # Поля: Лево 3см, Право 1см (~28.35 пунктов)
        section.PageSetup.LeftMargin = 85   
        section.PageSetup.RightMargin = 28  
        section.PageSetup.TopMargin = 57    
        section.PageSetup.BottomMargin = 57 

        # Заголовок документа
        para = doc.Paragraphs.Add()
        para.Range.Text = f'Показатели группы: {group_name}'
        para.Range.Font.Name = 'Times New Roman'
        para.Range.Font.Size = 16
        para.Range.Bold = 1
        para.Alignment = 1 # По центру
        para.Range.InsertParagraphAfter()

        # Таблица
        table = doc.Tables.Add(para.Range, len(indicators) + 1, 3)
        
        # === 2. НОВЫЕ РАЗМЕРЫ КОЛОНОК ДЛЯ АЛЬБОМА ===
        table.Columns(1).SetWidth(CentimetersToPoints(3), 0)   
        table.Columns(2).SetWidth(CentimetersToPoints(3), 0)   
        table.Columns(3).SetWidth(CentimetersToPoints(19.7), 0) # ~19.7 см остаток

        # === 3. СТИЛЬ ТАБЛИЦЫ (Шрифт 11, без Жирного) ===
        hdr_cells = table.Rows(1)
        
        # Заголовки (оставим их жирными для акцента, если нужно убрать везде - поставь Bold=0)
        titles = ['№ п/п', 'Код', 'Наименование']
        for i in range(1, 4):
            cell = hdr_cells.Cells(i).Range
            cell.Text = titles[i-1]
            cell.Font.Name = 'Times New Roman'
            cell.Font.Size = 11
            cell.Font.Bold = 1 # Оставил 1 для заголовков, поменяй на 0, если не надо
            cell.ParagraphFormat.Alignment = 1 # Центр

        # Данные (строго 11pt, НЕ жирные)
        for idx, ind in enumerate(indicators, start=1):
            row = table.Rows(idx + 1)
            
            # Колонка №
            c1 = row.Cells(1).Range
            c1.Text = str(idx)
            c1.Font.Name = 'Times New Roman'
            c1.Font.Size = 11
            c1.Font.Bold = 0 # Обычный шрифт
            c1.ParagraphFormat.Alignment = 1 # Центр
            
            # Колонка Код
            c2 = row.Cells(2).Range
            c2.Text = ind.code
            c2.Font.Name = 'Times New Roman'
            c2.Font.Size = 11
            c2.Font.Bold = 0 # Обычный шрифт
            c2.ParagraphFormat.Alignment = 1 # Центр
            
            # Колонка Наименование
            c3 = row.Cells(3).Range
            c3.Text = ind.name
            c3.Font.Name = 'Times New Roman'
            c3.Font.Size = 11
            c3.Font.Bold = 0 # Обычный шрифт
            c3.ParagraphFormat.Alignment = 0 # Слева

        doc.SaveAs(path)
        doc.Close(SaveChanges=False)
        word.Quit()
        
        return True

    except Exception as e:
        print(f"[WORK LOGIC FATAL] Ошибка при работе с живым Word: {repr(e)}")
        return False


# Вспомогательная функция-конвертер (остаётся прежней)
def CentimetersToPoints(cm):
    """Конвертирует сантиметры в пункты (единицы измерения Word)."""
    return cm * 28.35