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


'''
Напоминаю!
Я, вайб кодер - ты, профессиональный программист. Ты делаешь - я вставляю, заменяю. 
Кусками не работаем - только целыми фрагментами - метод на метод, файл на файл, от комментария до комментария, от комментария до конца и т.д. - по такому принципу. 

- Файл main_interface.ру принципиально не менять, это только витрина и кнопки.
- Программа будет собираться руинсталлером, поэтому это надо иметь ввиду.
- В программе уже реализованы авторизация, первая кнопка главного меню АДМИНИСТРАТОР, вторая кнопка главного меню МЕНЕДЖЕР БАЗЫ.
- Необходимо подключить в "аккордеоне" третью кнопку главного меню ЭКСПЕРТЫ. Ниже будут представлены файлы-доноры для этой кнопки. Они реализованы в ткинкере. Мы делаем рефакторинг и все должно стать в кастомткинкере. Логика в файлах рабочая. По образцу этих файлов и надо организовать по принципу - один заголовок Аккордеона - 2 файла, из которых один для интерфейса, а второй с функционалом. Раньше в старом формате этот  функционал был в двух окнах, теперь решили разделить на 3 окна.

- все файлы для работы ЭКСПЕРТЫ  должны лежать по адресу dukora\modules\experts\.
- файл для работы с базами 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\experts\experts_window.py

dukora\modules\data\database_module.py

excel_aggregate_inter.ру - файл – донор
excel_aggregate_work.ру - файл – донор
excel_export_inter.ру - файл – донор
excel_export_work.ру - файл - донор

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

Итогом работы должна стать полностью рабочая кнопка ЭКСПЕРТЫ - при нажатии на нее должны открываться в аккордеоне три заголовка а в них окна, оформленных в новом интерфейсе кастомткинкера с рабочим функционалом.
Новый интерфейс это и кастомткинкер и расположение элементов управления.

===================================================================
Заголовки и окна организовать так.

СОЗДАНИЕ ФАЙЛОВ ДЛЯ ЭКСПЕРТОВ: 

Вверху 

- Выбрать группу:       поле с выпадающим списком групп
- Выбранные группы:     поле для названия выбранных групп в 5 строк
- Количество экспертов: поле для указания числа экспертов
Заголовки выровнять по левому краю окна. Поля сделать одного размера по ширине, кроме числа экспертов - его поле на 3 знака. Поля должны быть выровнены по вертикальной линии.

Внизу в одну строчку равномерно на всю ширину окна 3 кнопки:

- Добавить (синего цвета)
- Удалить (красного цвета)
- Экспортировать в Excel (черного цвета)

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

АГРЕГИРОВАНИЕ ФАЙЛОВ:

Создать файл:        поле для отображения адреса           кнопка Создать (синего цвета)
Указать путь:        поле для отображения адреса           кнопка Обзор...(зеленого цвета)
Кнопка Объединить информацию  на всю ширину (черного цвета)

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

ПРОВЕРКА ФАЙЛОВ:

Выбрать файл:                         поле для отображения адреса           кнопка Обзор (синего цвета)
Выбрать группу:                       выпадающий список для выбора группы
Кнопка Проверить на транзитивность и  поле на 5 знаков

Две кнопки в одну строку Исправить транзитивность (зеленого цвета) и Нормализовать (черного цвета) 
 
'''
#================================================









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

# 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\modules\experts\experts_window.py - реальный файл
#====================================================
import customtkinter as ctk
from tkinter import ttk

class ExpertsWindow(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
        
        # ТВОИ ЗАГОЛОВКИ (3 штуки)
        create_item = self._create_accordion_header(accordion_frame, "Создание файлов для экспертов")
        aggregate_item = self._create_accordion_header(accordion_frame, "Агрегирование файлов")
        check_item = self._create_accordion_header(accordion_frame, "Проверка файлов")

        # Заглушки
        self._fill_create_section(create_item["content"])
        self._fill_aggregate_section(aggregate_item["content"])
        self._fill_check_section(check_item["content"])
        
        # Привязка команд
        for item in [create_item, aggregate_item, check_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()
        
        # Динамический расчет высоты заголовков (их здесь 3)
        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_create_section(self, frame):
        placeholder = ctk.CTkLabel(frame, text="Инструменты генерации экспертных файлов.", font=("Arial", 12))
        placeholder.pack(expand=True)

    def _fill_aggregate_section(self, frame):
        placeholder = ctk.CTkLabel(frame, text="Объединение данных из разных источников.", font=("Arial", 12))
        placeholder.pack(expand=True)

    def _fill_check_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:
    # Определяем корень проекта надежно:
    # Если мы находимся внутри подпапок dukora, идем вверх до корня репозитория.
    # __file__ дает путь к текущему .py файлу.
    current_file_path = Path(__file__).resolve()
    
    # Ищем папку 'dukora' в родителях
    try:
        dukora_parent = current_file_path.parents[[p.name for p in current_file_path.parents].index('dukora')]
    except ValueError:
        # Если по какой-то причине не нашли (запуск из другой директории), берем CWD
        dukora_parent = Path.cwd()
        
    BASE_DIR = dukora_parent
    
    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', encoding='utf-8') 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}")
        try:
            DB_FOLDER.mkdir(parents=True, exist_ok=True)
        except PermissionError as pe:
            print(f"[FATAL] Нет прав на создание папки '{DB_FOLDER}'. Запустите терминал от имени Администратора.")
            raise SystemExit(1)
            
    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] Проверка наличия рабочих баз данных...")
    conn = None
    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

# --- ФУНКЦИИ ДЛЯ ИМПОРТА (лежат строго вне класса 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()


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

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







#====================================================
# excel_aggregate_inter.py - файл - донор
#====================================================
from openpyxl import load_workbook
import os
from tkinter import filedialog, messagebox, ttk
import tkinter as tk
from pathlib import Path 
from modules.excel.excel_aggregate_work import AggregateWorkController
from modules.utils.window_behavior import LevelTwoBehavior

class ExpertAggregateInterface(tk.Toplevel):
    def __init__(self, parent):
        super().__init__(parent)
        
        behavior = LevelTwoBehavior()
        behavior.setup_level_two(self.winfo_toplevel())
        
        self.title("Агрегирование файлов")
        self.geometry("400x340")  
        self.resizable(False, False) 
        
        self.controller = AggregateWorkController()
        main_frame = ttk.Frame(self)
        main_frame.pack(padx=10, pady=10, fill="both", expand=True, anchor="center")
        
        label_width = 15
        unified_button_width = 25 
        field_input_width = 34
        combobox_width = 30
        
        file_create_frame = ttk.Frame(main_frame)
        file_create_frame.pack(fill="x", pady=(0, 5))
        
        ttk.Label(file_create_frame, text="Создать файл:", width=label_width, anchor="w").pack(side="left")
        self.new_file_name_entry = tk.Entry(file_create_frame, width=field_input_width + 1)
        self.new_file_name_entry.pack(side="left", padx=(5, 0), ipady=2)
        
        create_file_button = ttk.Button(
            file_create_frame, text="Создать", command=self.handle_create_file_request
        )
        create_file_button.pack(side="right", padx=(5, 0))
        
        target_file_frame = ttk.Frame(main_frame)
        target_file_frame.pack(fill="x", pady=(0, 5))
        
        ttk.Label(target_file_frame, text="Указать путь:", width=label_width, anchor="w").pack(side="left")
        self.target_file_entry = tk.Entry(target_file_frame, width=field_input_width + 1)
        self.target_file_entry.pack(side="left", padx=(5, 2), ipady=2)
        
        browse_target_button = ttk.Button(
            target_file_frame, text="Обзор...", command=self.select_target_file
        )
        browse_target_button.pack(side="right", padx=(5, 0))
        
        select_frame = ttk.Frame(main_frame)
        select_frame.pack(fill="x", pady=(0, 15))
        
        ttk.Label(select_frame, text="Выбрать файлы:", width=label_width, anchor="w").pack(side="left")
        self.file_path_entry = tk.Entry(select_frame, width=field_input_width + 1)
        self.file_path_entry.pack(side="left", padx=(5, 2), ipady=2)
        
        browse_source_button = ttk.Button(
            select_frame, text="Обзор...", command=self.select_files_for_aggregation
        )
        browse_source_button.pack(side="right", padx=(5, 0))
        
        aggregate_button = ttk.Button(
            main_frame,
            text="Объединить информацию",
            command=self.aggregate_data,
            style="Big.TButton"
        )
        aggregate_button.pack(fill="x", pady=(10, 15)) 
        
        style = ttk.Style(self)
        style.configure("Big.TButton", font=('TkDefaultFont', 10, 'bold'))
        
        self.opened_file_path_var = tk.StringVar()
        self.transitivity_check_percent_var = tk.StringVar(value="")
        self.transitivity_fix_percent_var = tk.StringVar(value="")
        self.current_sheet_name = ""

        row_5_frame = ttk.Frame(main_frame)
        row_5_frame.pack(fill="x", pady=(0, 5), anchor="w")
        
        open_file_button = ttk.Button(
            row_5_frame, text="Открыть файл", command=self.select_open_target_file, width=unified_button_width
        )
        open_file_button.pack(side="left", padx=(0, 5))
        
        opened_file_entry = tk.Entry(row_5_frame, textvariable=self.opened_file_path_var, state='readonly', width=field_input_width)
        opened_file_entry.pack(side="left", ipady=2)
        
        row_6_frame = ttk.Frame(main_frame)
        row_6_frame.pack(fill="x", pady=(0, 5), anchor="w")
        
        group_label_button = ttk.Button(
            row_6_frame, 
            text="Выбрать группу:", 
            width=unified_button_width,
            state="disabled" 
        )
        group_label_button.pack(side="left", padx=(0, 5))
        
        self.sheet_combobox = ttk.Combobox(row_6_frame, state="readonly", width=combobox_width)
        self.sheet_combobox.pack(side="left", ipady=2)
        self.sheet_combobox.bind("<<ComboboxSelected>>", self._on_sheet_select)
        
        row_7_frame = ttk.Frame(main_frame)
        row_7_frame.pack(fill="x", pady=(0, 5), anchor="w")
        
        check_button = ttk.Button(row_7_frame, text="Проверить транзитивность", command=self.check_transitivity, width=unified_button_width)
        check_button.pack(side="left", padx=(0, 10))
        
        self.transitivity_percentage_label = tk.Label(
            row_7_frame,
            textvariable=self.transitivity_check_percent_var,
            relief="groove",
            width=8, bg="white", anchor="center"
        )
        self.transitivity_percentage_label.pack(side="left")
        
        row_8_frame = ttk.Frame(main_frame)
        row_8_frame.pack(fill="x", pady=(0, 5), anchor="w")
        
        fix_button = ttk.Button(row_8_frame, text="Исправить транзитивность", command=self.apply_transitivity_corrections, width=unified_button_width)
        fix_button.pack(side="left", padx=(0, 10))
        
        self.fix_percentage_label = tk.Label(
            row_8_frame,
            textvariable=self.transitivity_fix_percent_var,
            relief="groove",
            width=8, bg="white", anchor="center"
        )
        self.fix_percentage_label.pack(side="left")
        
        row_9_frame = ttk.Frame(main_frame)
        row_9_frame.pack(fill="x", pady=(0, 5), anchor="w")
        
        send_to_db_button = ttk.Button(
            row_9_frame,
            text="Нормализовать данные",
            command=self.send_to_database,
            width=unified_button_width
        )
        send_to_db_button.pack(side="left", padx=(0, 0))
    
    ### Методы ###
    
    def handle_create_file_request(self):
        filename = self.new_file_name_entry.get().strip()
        result_path = self.controller.create_empty_excel_file(filename)
        if result_path:
             messagebox.showinfo("Готово", f"Файл создан по пути:\n{result_path}")

    def select_target_file(self):
        initial_dir = str(Path(__file__).resolve().parents[2] / "experts")
        file_path = filedialog.askopenfilename(title="Укажите целевой Excel-файл", initialdir=initial_dir, filetypes=[("Excel Files", "*.xlsx")])
        if file_path:
            self.target_file_entry.delete(0, tk.END)
            self.target_file_entry.insert(0, file_path)
            
    def select_files_for_aggregation(self):
        print("[GUI SELECT] Нажат Обзор...")
        try:
            file_paths = filedialog.askopenfilenames(title="Выберите файлы экспертов", filetypes=[("Excel Files", "*.xlsx")])
            if not file_paths: return
            self.controller.input_files = list(file_paths)
            first_fp = file_paths[0]
            sheet_names = []
            try:
                temp_wb = load_workbook(first_fp, read_only=True, data_only=True)
                sheet_names = temp_wb.sheetnames
                temp_wb.close()
                self.sheet_combobox['values'] = sheet_names
                if sheet_names:
                    self.sheet_combobox.current(0)
                    self.current_sheet_name = sheet_names[0]
            except Exception as e:
                messagebox.showerror("Ошибка Excel", f"Не удалось прочитать листы:\n{e}")
                return
            display_text = f"... ({len(file_paths)} files)"
            self.file_path_entry.delete(0, tk.END)
            self.file_path_entry.insert(0, display_text)
        except Exception as e:
            print(f"[GUI FATAL ERROR]: Не удалось открыть диалоговое окно!")
            print(f"Тип ошибки: {type(e).__name__}")
            print(f"Описание: {e}")
            messagebox.showerror("Критическая ошибка", f"Не удалось открыть проводник.\n{e}")

    def _on_sheet_select(self, event):
        selected = event.widget.get()
        self.current_sheet_name = selected
        self.reset_transitivity_field()
		
    def reset_transitivity_field(self):
        self.transitivity_check_percent_var.set("")
        self.transitivity_fix_percent_var.set("")
		
    def reset_check_field(self):
        self.transitivity_check_percent_var.set("")
		
    def reset_fix_field(self):
        self.transitivity_fix_percent_var.set("")
		
    def check_transitivity(self):
        """
        ЛОГИКА ПРОВЕРКИ НАХОДИТСЯ ЗДЕСЬ.
        При нажатии очищает поле ИСПРАВИТЬ.
        """
        self.reset_fix_field()
        selected_sheet = self.current_sheet_name
        file_path = self.opened_file_path_var.get()
        
        if not selected_sheet or not file_path:
            messagebox.showwarning("Внимание", "Сначала выберите файл и лист!")
            return
            
        try:
            wb = load_workbook(file_path, data_only=True)
            if selected_sheet not in wb.sheetnames:
                messagebox.showerror("Ошибка", "Лист не найден.")
                return
                
            ws = wb[selected_sheet]
            
            n = 0
            while True:
                cell_val = ws.cell(row=2, column=4 + n).value
                norm_val = ""
                if cell_val is not None and str(cell_val).strip() != "":
                    s = str(cell_val).replace(',', '.').strip().upper()
                    try:
                        num = float(s)
                        norm_val = int(num) if abs(num - int(num)) < 1e-9 else round(num, 3)
                    except ValueError:
                        norm_val = s
                
                if isinstance(norm_val, (int, float)) or str(norm_val).strip() != "":
                    n += 1
                else:
                    break
                    
            if n < 3:
                self.transitivity_check_percent_var.set("100%")
                wb.close()
                return

            values = [[None] * n for _ in range(n)]
            for i in range(n):
                for j in range(n):
                    cell = ws.cell(row=2 + i, column=4 + j).value
                    v = None
                    if cell is not None and str(cell).strip() != "":
                        s = str(cell).replace(',', '.').strip().upper()
                        try:
                            num = float(s)
                            v = int(num) if abs(num - int(num)) < 1e-9 else round(num, 3)
                        except ValueError:
                            v = s
                    values[i][j] = v
            
            total_triads = 0
            consistent_triads = 0
            
            for i in range(n):
                for j in range(n):
                    if i == j:
                        continue
                    for k in range(n):
                        if k == i or k == j:
                            continue
                            
                        a, b, c = values[i][j], values[j][k], values[i][k]
                        
                        if all(isinstance(x, (int, float)) for x in [a, b, c]):
                            total_triads += 1
                            is_consistent = False
                            
                            if a > b and b > c:
                                if a > c: is_consistent = True
                            elif a == b and b > c:
                                if a > c: is_consistent = True
                            elif a > b and b == c:
                                if a > c: is_consistent = True
                            elif a == b and b == c:
                                if a == c: is_consistent = True
                                    
                            if is_consistent:
                                consistent_triads += 1
                                    
            wb.close()
            
            if total_triads == 0:
                percent_str = "100%"
            else:
                percent = int((consistent_triads / total_triads) * 100)
                percent_str = f"{percent}%"
                
            self.transitivity_check_percent_var.set(percent_str)
            
        except Exception as e:
            print(f"[GUI TRANS CHECK CRITICAL FAIL]: {e}")
            self.transitivity_check_percent_var.set("Ошибка")

    def apply_transitivity_corrections(self):
        """
        Вызывает контроллер для правки данных.
        При нажатии очищает поле ПРОВЕРИТЬ.
        Берет "Старое" значение напрямую из файла перед правкой.
        Если процент не вырос — сообщает эксперту о необходимости ручной переоценки.
        """
        self.reset_check_field()
        
        selected_sheet = self.current_sheet_name
        file_path = self.opened_file_path_var.get()
        
        if not selected_sheet or not file_path:
            messagebox.showwarning("Внимание", "Сначала выберите файл и лист!")
            return

        # === 1. СЧИТЫВАЕМ РЕАЛЬНЫЙ СТАТУС "ДО" НАПРЯМУЮ ИЗ ФАЙЛА ===
        old_status_raw = "Ошибка"
        try:
            wb_before = load_workbook(file_path, data_only=True)
            ws_before = wb_before[selected_sheet]
            
            n = 0
            while True:
                cell_val = ws_before.cell(row=2, column=4 + n).value
                if cell_val is not None and str(cell_val).strip() != "":
                    n += 1
                else:
                    break
                    
            if n < 3:
                old_status_raw = "100%"
                total_triads = 0
            else:
                values = [[None] * n for _ in range(n)]
                for i in range(n):
                    for j in range(n):
                        cell = ws_before.cell(row=2 + i, column=4 + j).value
                        v = None
                        if cell is not None and str(cell).strip() != "":
                            s = str(cell).replace(',', '.').strip().upper()
                            try:
                                num = float(s)
                                v = int(num) if abs(num - int(num)) < 1e-9 else round(num, 3)
                            except ValueError:
                                v = s
                        values[i][j] = v
                
                total_triads = 0
                consistent_triads = 0
                
                for i in range(n):
                    for j in range(n):
                        if i == j:
                            continue
                        for k in range(n):
                            if k == i or k == j:
                                continue
                                
                            a, b, c = values[i][j], values[j][k], values[i][k]
                            
                            if all(isinstance(x, (int, float)) for x in [a, b, c]):
                                total_triads += 1
                                is_consistent = False
                                
                                if a > b and b > c:
                                    if a > c: is_consistent = True
                                elif a == b and b > c:
                                    if a > c: is_consistent = True
                                elif a > b and b == c:
                                    if a > c: is_consistent = True
                                elif a == b and b == c:
                                    if a == c: is_consistent = True
                                        
                                if is_consistent:
                                    consistent_triads += 1
                
                if total_triads == 0:
                    old_status_raw = "100%"
                else:
                    percent = int((consistent_triads / total_triads) * 100)
                    old_status_raw = f"{percent}%"
            
            wb_before.close()
        except Exception as e:
            print(f"[GUI PRE-CHECK ERR]: {e}")
            old_status_raw = "Ошибка"

        # Готовим числа для сравнения
        try:
            old_percent = int(old_status_raw.strip('%')) if old_status_raw else 0
        except ValueError:
            old_percent = 0

        # === 2. ЗАПУСКАЕМ ИСПРАВЛЕНИЕ ===
        self.controller.current_file_path = file_path
        self.controller.current_sheet_name = selected_sheet
        
        success = self.controller.apply_transitivity_corrections()
        
        if success:
            # === 3. ПРОВЕРЯЕМ ПОСЛЕ (ЛОКАЛЬНО, БЕЗ КОНТРОЛЛЕРА) ===
            current_status_raw = "Ошибка"
            try:
                wb_after = load_workbook(file_path, data_only=True)
                ws_after = wb_after[selected_sheet]
                
                n = 0
                while True:
                    cell_val = ws_after.cell(row=2, column=4 + n).value
                    if cell_val is not None and str(cell_val).strip() != "":
                        n += 1
                    else:
                        break
                        
                if n < 3:
                    current_status_raw = "100%"
                    total_triads = 0
                else:
                    values = [[None] * n for _ in range(n)]
                    for i in range(n):
                        for j in range(n):
                            cell = ws_after.cell(row=2 + i, column=4 + j).value
                            v = None
                            if cell is not None and str(cell).strip() != "":
                                s = str(cell).replace(',', '.').strip().upper()
                                try:
                                    num = float(s)
                                    v = int(num) if abs(num - int(num)) < 1e-9 else round(num, 3)
                                except ValueError:
                                    v = s
                            values[i][j] = v
                    
                    total_triads = 0
                    consistent_triads = 0
                    
                    for i in range(n):
                        for j in range(n):
                            if i == j:
                                continue
                            for k in range(n):
                                if k == i or k == j:
                                    continue
                                    
                                a, b, c = values[i][j], values[j][k], values[i][k]
                                
                                if all(isinstance(x, (int, float)) for x in [a, b, c]):
                                    total_triads += 1
                                    is_consistent = False
                                    
                                    if a > b and b > c:
                                        if a > c: is_consistent = True
                                    elif a == b and b > c:
                                        if a > c: is_consistent = True
                                    elif a > b and b == c:
                                        if a > c: is_consistent = True
                                    elif a == b and b == c:
                                        if a == c: is_consistent = True
                                            
                                    if is_consistent:
                                        consistent_triads += 1
                    
                    if total_triads == 0:
                        current_status_raw = "100%"
                    else:
                        percent = int((consistent_triads / total_triads) * 100)
                        current_status_raw = f"{percent}%"
                
                wb_after.close()
            except Exception as e:
                print(f"[GUI POST-CHECK ERR]: {e}")
                current_status_raw = "Ошибка"

            # Извлекаем число для логики окон
            try:
                current_percent = int(current_status_raw.strip('%')) if current_status_raw else 0
            except ValueError:
                current_percent = 0

            self.transitivity_fix_percent_var.set("OK") 

            # ЛОГИКА СООБЩЕНИЙ (ТЕПЕРЬ OLD_PERCENT ЧЕСТНЫЙ)
            if current_status_raw == "Ошибка":
                messagebox.showerror("Ошибка", "Не удалось перепроверить данные после исправления.")
            elif current_percent == old_percent and current_percent < 100:
                # Теперь это сработает корректно
                messagebox.showwarning(
                    "Требуется внимание эксперта", 
                    f"Автоматическое исправление завершено.\nБыло: {old_status_raw}\nСтало: {current_status_raw}\n\n"
                    "Оценки экспертов содержат логические противоречия, которые нельзя исправить алгоритмически "
                    "без изменения смысла оценок.\nРекомендуется перепроверить матрицу вручную."
                )
            elif current_percent >= 95:
                messagebox.showinfo(
                    "Отлично!", 
                    f"Транзитивность восстановлена до максимума.\nБыло: {old_status_raw}\nСтало: {current_status_raw}"
                )
            else:
                messagebox.showinfo(
                    "Готово", 
                    f"Транзитивность исправлена.\nБыло: {old_status_raw}\nСтало: {current_status_raw}"
                )

            # Обновляем визуальное поле проверки актуальным значением
            self.transitivity_check_percent_var.set(current_status_raw)
			
        else:
            messagebox.showerror("Ошибка", "Не удалось записать данные в файл.")
			
    def select_open_target_file(self):
        initial_dir = str(Path(__file__).resolve().parents[2] / "experts")
        file_path = filedialog.askopenfilename(title="Откройте Excel-файл для обработки", initialdir=initial_dir, filetypes=[("Excel Files", "*.xlsx")])
        if file_path:
            self.current_file_path = file_path 
            self.controller.current_file_path = file_path
            self.opened_file_path_var.set(file_path)
            self.target_file_entry.delete(0, tk.END)
            self.target_file_entry.insert(0, file_path)
            try:
                sheet_names = self.controller.get_sheet_names(file_path)
                self.sheet_combobox['values'] = sheet_names
                if sheet_names:
                    self.sheet_combobox.current(0)
                    self.current_sheet_name = sheet_names[0]
                else:
                    self.sheet_combobox.set('')
                    self.current_sheet_name = ""
            except Exception as e:
                messagebox.showerror("Ошибка Excel", f"Не удалось прочитать структуру файла:\n{e}")
                self.sheet_combobox.set('')
                self.current_sheet_name = ""

    def aggregate_data(self):
        output_file_path = self.target_file_entry.get().strip()
        if not output_file_path:
            messagebox.showwarning("Предупреждение", "Сначала укажите целевой файл!")
            return
        input_files = getattr(self.controller, 'input_files', [])
        if not input_files or len(input_files) == 0:
            messagebox.showwarning("Внимание", "Сначала выберите исходные файлы кнопкой 'Обзор...'.")
            return
        workbook = self.controller.aggregate_data(output_file_path, input_files)
        if workbook is None:
            messagebox.showerror("Ошибка данных", "Не удалось собрать данные.\nСмотрите консоль.")
            return
        messagebox.showinfo("Готово", f"Сводный отчет успешно сохранен в:\n{output_file_path}")
        
    def send_to_database(self):
        """
        КНОПКА: Нормализовать данные.
        """
        file_path = self.opened_file_path_var.get()
        selected_sheet = self.current_sheet_name

        if not file_path or not selected_sheet:
            messagebox.showwarning("Внимание", "Сначала выберите файл и лист!")
            return

        self.controller.current_file_path = file_path
        self.controller.current_sheet_name = selected_sheet

        success, message = self.controller.normalize_group_data()

        if success:
            messagebox.showinfo("Готово", message)
        else:
            msg_type = messagebox.showerror
            if "уже проведена" in message or "очистить" in message or "квадратн" in message:
                msg_type = messagebox.showwarning
            msg_type("Статус операции", message)
            
#===================================================

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







#====================================================
# excel_aggregate_work.py - файл - донор
#====================================================
from openpyxl import load_workbook, Workbook
import os
import re
from statistics import mean, mode, StatisticsError
from collections import defaultdict
from copy import copy
import tkinter.messagebox as messagebox
from pathlib import Path

class AggregateWorkController:
    def __init__(self):
        self.input_files = []
        self._loaded_wbs = {}
        self.current_file_path = ""
        self.current_sheet_name = ""

    # МЕТОД НОРМАЛИЗАЦИИ УДАЛЕН ПОЛНОСТЬЮ СОГЛАСНО ТЗ (теперь он часть normalize_group_data)

    def get_sheet_names(self, file_path):
        try:
            wb = load_workbook(file_path, read_only=True, data_only=True)
            sheet_names = wb.sheetnames
            wb.close()
            return sheet_names
        except Exception as e:
            print(f"[CTRL ERROR] Не удалось прочитать листы из {file_path}: {e}")
            return []

    def create_empty_excel_file(self, filename):
        clean_filename = filename.strip()
        if not clean_filename.endswith('.xlsx'):
            clean_filename += '.xlsx'
        if not clean_filename:
             messagebox.showwarning("Предупреждение", "Имя файла не может быть пустым.")
             return None
        invalid_chars_pattern = r'[\\/:*?"<>|]'
        if re.search(invalid_chars_pattern, clean_filename):
            messagebox.showerror('Ошибка', 'Имя файла содержит недопустимые символы \\ / : * ? " < > |')
            return None
        try:
            project_root = Path(__file__).resolve().parents[2]
            experts_folder = project_root / "experts"
            try:
                os.makedirs(experts_folder, exist_ok=True)
            except OSError as e:
                 messagebox.showerror("Ошибка доступа", f"Не удалось создать папку 'experts':\n{e}")
                 return None
            
            full_path = experts_folder / clean_filename
            wb = Workbook()
            if "Sheet" in wb.sheetnames:
                del wb["Sheet"]
            wb.create_sheet(title="Данные")
            wb.save(full_path)
            wb.close()
            messagebox.showinfo("Готово", f"Файл '{clean_filename}' успешно создан.")
            return str(full_path)
        except Exception as e:
            print(f"[CREATE FILE ERROR]: {e}")
            messagebox.showerror("Ошибка", f"Не удалось создать файл:\n{e}")
            return None

    def aggregate_data(self, output_file_path, input_files):
        pass

    def apply_transitivity_corrections(self):
        """
        Исправляет нарушения нестрогой транзитивности (A>=B, B>=C -> A>=C).
        Работает автономно без normalize_cell.
        Матрица начинается со строки 2.
        """
        print("[TRANS FIX]")
        
        if not self.current_file_path or not self.current_sheet_name: 
            return False
            
        EPSILON = 0.001
        
        try:
            wb = load_workbook(self.current_file_path)
            if self.current_sheet_name not in wb.sheetnames:
                return False
                
            ws = wb[self.current_sheet_name]
            
            # Считываем размерность N по СТРОКЕ 2
            n = 0
            while True:
                cell_val = ws.cell(row=2, column=4 + n).value
                if cell_val is not None and str(cell_val).strip() != "":
                    n += 1
                else:
                    break
                    
            if n < 3:
                return True

            MATRIX_START_COL = 4
            DATA_START_ROW = 2

            matrix = [[None] * n for _ in range(n)]
            for i in range(n):
                for j in range(n):
                    cell = ws.cell(row=DATA_START_ROW + i, column=MATRIX_START_COL + j).value
                    val = None
                    if cell is not None and str(cell).strip() != "":
                        s = str(cell).replace(',', '.').strip().upper()
                        try:
                            num = float(s)
                            val = int(num) if abs(num - int(num)) < 1e-9 else round(num, 3)
                        except ValueError:
                            val = s
                    matrix[i][j] = val

            fixed_count = 0
            
            # Перебираем уникальные индексы (диагональ исключена!)
            for i in range(n):
                for j in range(n):
                    if i == j:
                        continue
                    for k in range(n):
                        if k == i or k == j:
                            continue
                            
                        a, b, c = matrix[i][j], matrix[j][k], matrix[i][k]
                        
                        if all(isinstance(x, (int, float)) for x in [a, b, c]):
                            violation_found = False
                            new_c_value = c
                            
                            # Нарушение прямой транзитивности >
                            if a > b and b > c and a <= c:
                                new_c_value = round(a + EPSILON, 3)
                                violation_found = True
                                
                            # Случай равенства спереди, но финал меньше
                            elif a == b and b > c and a < c:
                                new_c_value = round(a + EPSILON, 3)
                                violation_found = True
                                
                            # Строгое убывание, но финал догнал
                            elif a > b and b > c and a == c:
                                new_c_value = round(a + EPSILON, 3)
                                violation_found = True
                                
                            # Полное равенство спереди, а финал ушел вниз
                            elif a == b and b == c and a < c:
                                new_c_value = round(a + EPSILON, 3)
                                violation_found = True
                            
                            if violation_found:
                                ws.cell(row=DATA_START_ROW + i, column=MATRIX_START_COL + k).value = new_c_value
                                matrix[i][k] = new_c_value
                                fixed_count += 1

            if fixed_count > 0:
                wb.save(self.current_file_path)
                print(f"[FIX SAVED] Пофиксили {fixed_count} связей.")
                
            wb.close()
            return True
                
        except Exception as e:
            print(f"[FIX LOAD/SAVE CRITICAL FAIL]: {e}")
            return False

    def normalize_group_data(self):
        """
        Проводит нормализацию по сумме строк.
        Чем выше сумма оценок показателя (строки), тем выше его итоговый вес.
        Записывает результат справа от матрицы в первой свободной колонке.
        Заголовки в строке 1, данные начинаются со строки 2.
        """
        print("[NORMALIZE]")
        
        if not self.current_file_path or not self.current_sheet_name:
            return False, "Файл не выбран"
            
        try:
            wb = load_workbook(self.current_file_path)
            if self.current_sheet_name not in wb.sheetnames:
                return False, "Лист не найден"
                
            ws = wb[self.current_sheet_name]
            
            # Определяем размерность N по СТРОКЕ 2
            n = 0
            while True:
                cell_val = ws.cell(row=2, column=4 + n).value
                if cell_val is not None and str(cell_val).strip() != "":
                    n += 1
                else:
                    break
                    
            if n < 2:
                wb.close()
                return False, "Недостаточно данных (менее 2x2)"

            MATRIX_START_COL = 4
            HEADER_ROW = 1      
            DATA_START_ROW = 2  

            target_col = 4 + n 
            occupied = True
            while occupied and target_col < 4 + n + 10:
                header_cell = ws.cell(row=HEADER_ROW, column=target_col).value
                if header_cell is None or (isinstance(header_cell, str) and "нормализ" in header_cell.lower()):
                    occupied = False
                else:
                    target_col += 1
            
            if occupied:
                wb.close()
                return False, "Нет места для записи нормализованных данных."

            ws.cell(row=HEADER_ROW, column=target_col, value="Нормализованный вес")

            row_sums = []
            
            for i in range(n):
                row_sum = 0.0
                for j in range(n):
                    cell = ws.cell(row=DATA_START_ROW + i, column=MATRIX_START_COL + j).value
                    val = 0.0
                    if cell is not None and str(cell).strip() != "":
                        s = str(cell).replace(',', '.').strip().upper()
                        try:
                            num = float(s)
                            val = int(num) if abs(num - int(num)) < 1e-9 else round(num, 3)
                        except ValueError:
                            val = 0.0
                    row_sum += val
                row_sums.append(row_sum)

            total_sum = sum(row_sums)

            if total_sum == 0:
                wb.close()
                return False, "Сумма всех оценок равна нулю. Нормализация невозможна."

            for i in range(n):
                weight = round((row_sums[i] / total_sum), 4)
                ws.cell(row=DATA_START_ROW + i, column=target_col, value=weight)

            wb.save(self.current_file_path)
            wb.close()
            
            col_letter = ws.cell(row=1, column=target_col).column_letter
            return True, f"Веса успешно рассчитаны (колонка {col_letter})"

        except Exception as e:
            print(f"[NORM ERROR]: {e}")
            return False, f"Ошибка при записи: {str(e)}"
        
#===================================================

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







#====================================================
# excel_export_inter.py - файл - донор
#====================================================
import tkinter as tk
from tkinter import ttk, messagebox
from modules.excel.excel_export_work import ExportWorkController
from modules.utils.window_behavior import LevelTwoBehavior

class ExpertExportInterface(tk.Toplevel):
    def __init__(self, parent):
        super().__init__(parent)
        
        behavior = LevelTwoBehavior()
        behavior.setup_level_two(self.winfo_toplevel())
        
        self.title("Создание файла для эксперта")
        self.geometry("450x200")
        self.resizable(False, False)
        
        self.controller = ExportWorkController()
        main_frame = ttk.Frame(self)
        main_frame.pack(padx=10, pady=10, fill="both", expand=True)
        
        ttk.Label(main_frame, text="Группа:").grid(row=0, column=0, sticky="w")
        self.group_var = tk.StringVar()
        self.combobox = ttk.Combobox(
            main_frame, textvariable=self.group_var, state="readonly", width=25
        )
        self.combobox.grid(row=0, column=1, padx=(5, 10), sticky="ew")
        self.refresh_groups_list()
        
        add_group_btn = ttk.Button(
            main_frame, text="Добавить группу", command=self.add_group
        )
        add_group_btn.grid(row=0, column=2, padx=(0, 10), sticky="e")
        
        ttk.Label(main_frame, text="Выбранные группы:").grid(row=1, column=0, sticky="w")
        self.selected_groups_text = tk.Text(main_frame, width=30, height=4)
        self.selected_groups_text.grid(
            row=1, column=1, columnspan=2, pady=(5, 10), sticky="ew"
        )
        
        ttk.Label(main_frame, text="Количество экспертов:").grid(
            row=2, column=0, sticky="w"
        )
        self.num_experts = tk.IntVar(value=1)
        self.experts_count_entry = ttk.Entry(main_frame, textvariable=self.num_experts, width=5)
        self.experts_count_entry.grid(row=2, column=1, sticky="w")
        
        remove_group_btn = ttk.Button(
            main_frame, text="Удалить группу", command=self.remove_group
        )
        remove_group_btn.grid(row=2, column=2, sticky="e")
        
        export_btn = ttk.Button(
            main_frame, text="Экспортировать в Excel", command=self.export_to_excel
        )
        export_btn.grid(row=3, column=1, pady=(10, 0))

    def refresh_groups_list(self):
        self.controller.refresh_groups()
        self.combobox["values"] = self.controller.groups
        if self.controller.groups:
            self.combobox.current(0)

    def add_group(self):
        group_name = self.group_var.get()
        self.controller.add_group(group_name)
        self._update_selected_text()

    def remove_group(self):
        self.controller.remove_group()
        self._update_selected_text()

    def _update_selected_text(self):
        current_text = "\n".join(self.controller.selected_groups)
        self.selected_groups_text.delete("1.0", tk.END)
        self.selected_groups_text.insert(tk.END, current_text)

    def export_to_excel(self):
        selected_group_names = (
            self.selected_groups_text.get("1.0", tk.END).strip().split("\n")
        )
        try:
            num_experts = int(self.num_experts.get())
            if num_experts <= 0:
                raise ValueError
        except Exception:
            messagebox.showerror(
                "Ошибка", "Количество экспертов должно быть положительным числом."
            )
            return
            
        success = self.controller.export_to_excel(selected_group_names, num_experts)
        if success:
            messagebox.showinfo(
                title="Экспорт завершён",
                message=f"Успешно создано {num_experts} файлов.\n\nРасположение:\n.../experts/",
            )
        
#===================================================

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







#====================================================
# excel_export_work.py - файл - донор
#====================================================
import os
from pathlib import Path
from openpyxl import Workbook

try:
    from modules.data.database_module import get_db_connection
except ImportError:
    print("[EXPORT WORK] Не найден database_module.")
    
    def get_db_connection():
        return None

def create_excel_template(groups_objects, output_filename):
    wb = Workbook()
    
    if "Sheet" in wb.sheetnames:
        del wb["Sheet"]
        
    created_any_sheet = False
    
    for group_data in groups_objects:
        indicators_list = group_data.get('indicators', [])
        if not indicators_list:
            continue
            
        sheet_title = f"{group_data['name']}"
        if len(sheet_title) > 31:
            sheet_title = sheet_title[:31]
            
        ws = wb.create_sheet(title=sheet_title)
        created_any_sheet = True
        
        # --- ЗАГОЛОВКИ ЛЕВОЙ ЧАСТИ ---
        headers = ["№ п/п", "Наименование показателя", "Код"]
        for col_idx, header in enumerate(headers, start=1):
            ws.cell(row=1, column=col_idx, value=header)
        
        # --- ДАННЫЕ ЛЕВОЙ ЧАСТИ ---
        for idx, ind_tuple in enumerate(indicators_list, start=1):
            name, code = ind_tuple[0], ind_tuple[1]
            ws.cell(row=idx + 1, column=1, value=str(idx))
            ws.cell(row=idx + 1, column=2, value=name)
            ws.cell(row=idx + 1, column=3, value=code)
        
        # --- ШАПКА МАТРИЦЫ (ВЕРХ) ---
        matrix_start_col = 4
        matrix_start_row = 1
        
        for col_offset, ind_tuple in enumerate(indicators_list):
            code = ind_tuple[1]
            ws.cell(row=matrix_start_row, column=matrix_start_col + col_offset, value=code)
            
        # --- ТЕЛО МАТРИЦЫ ---
        size = len(indicators_list)
        
        for r in range(size):
            for c in range(size):
                cell = ws.cell(
                    row=matrix_start_row + r + 1,
                    column=matrix_start_col + c
                )
                
                if r == c:
                    cell.value = 1.0
                else:
                    cell.value = ""
                    
    if not created_any_sheet:
        ws = wb.create_sheet(title="Пусто")
        ws['A1'] = 'Нет индикаторов.'
        
    try:
        project_root = Path(__file__).resolve().parents[2]
        experts_folder = project_root / "experts"
        os.makedirs(experts_folder, exist_ok=True)
        full_path = experts_folder / output_filename
        wb.save(full_path)
    except Exception as e:
        print(f"[EXPORT WORK ERROR] {e}")

class ExportWorkController:
    def __init__(self):
        self.groups = []
        self.selected_groups = []

    def refresh_groups(self):
        """Читает названия групп из БД."""
        conn = get_db_connection()
        if not conn:
            self.groups = ["Ошибка БД"]
            return
            
        cursor = conn.cursor()
        cursor.execute("SELECT id, name FROM groups ORDER BY name")
        rows = cursor.fetchall()
        
        self.groups = [row[1] for row in rows]
        # Сохраняем ID групп для экспорта
        self._gid_cache = {row[1]: row[0] for row in rows}
        
        conn.close()

    # === НЕДОСТАЮЩИЕ МЕТОДЫ ДЛЯ ИНТЕРФЕЙСА ===
    def add_group(self, group_name):
        """Добавляет группу в список выбранных (вызывается при нажатии кнопки)."""
        if group_name and group_name.strip() and group_name not in self.selected_groups:
            self.selected_groups.append(group_name)

    def remove_group(self):
        """Удаляет последнюю добавленную группу (кнопка Удалить)."""
        if len(self.selected_groups) > 0:
            self.selected_groups.pop(-1)

    def export_to_excel(self, selected_group_names, num_experts):
        if int(num_experts) <= 0 or not selected_group_names:
            return False

        conn = get_db_connection()
        if not conn:
            return False
            
        cursor = conn.cursor()
        
        enriched_groups = []
        for gname in selected_group_names:
            gid = self._gid_cache.get(gname)
            if not gid:
                continue
                
            cursor.execute("""
                SELECT name, code 
                FROM indicators 
                WHERE group_code IN (
                    SELECT code FROM groups WHERE id = ?
                ) 
                ORDER BY name
            """, (gid,))
            
            indicators = cursor.fetchall()
            
            enriched_groups.append({
                'name': gname,
                'indicators': indicators
            })
            
        conn.close()

        if not enriched_groups:
            return False

        project_root = Path(__file__).resolve().parents[2]
        experts_folder = project_root / "experts"
        os.makedirs(experts_folder, exist_ok=True)
        
        success = True
        for expert_num in range(1, int(num_experts) + 1):
            filename = f"Эксперт_{expert_num}.xlsx"
            full_path = experts_folder / filename
            
            create_excel_template(enriched_groups, str(filename))
            
            if not full_path.exists():
                success = False
                
        return success