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


Промпт: Завершение реализации «Открыть для ввода событий» и рефакторинг
Цель: Полностью отладить функционал второго заголовка аккордеона («Открыть для ввода событий»), обеспечить стабильную запись данных в индивидуальные базы проектов и очистить проект от мусорных файлов-доноров.
1. Задачи по отладке Кнопки №2 («Открыть для ввода событий»)
Необходимо проверить полный цикл работы пользователя:
•	Инициализация UI: При открытии вкладки списки должны быть активны сразу (без клика).
•	Смена Группы: При выборе группы из верхнего списка список «Показатель» должен мгновенно очищаться и заполняться соответствующими значениями.
•	Кнопки управления:
o	Проверить переключение состояний кнопок «Добавить», «Редактировать», «Сохранить», «Удалить». Цвета должны соответствовать (Синяя/Зеленая/Коричневая/Красная).
o	Убедиться, что кнопка «Сохранить» появляется только после нажатия на кнопку «Редактировать» и вместо нее и наоборот после сохранения.
•	Запись события:
o	Нажать «Добавить». Событие должно уйти в файл и отобразиться в таблице в окне.
dukora\project_dbs\{КОД_ПРОЕКТА}.db.
o	Поле Дата события должно заполняться через виджет Календаря (русской версии).
o	Код Событию присваивается автоматически, он состоит из имеющегося кода выбранного показателя+подчеркивание и очередной номер События при этом показателе. Отсчет начинается с единицы, а не с нуля.
o	В таблицу событий добавить слева поле №п/п. Данные должны быть всегда по порядку в самой таблице, пересчитываться после удаления (для красоты).
o	Поля №п/п, Код, Дата, +/- должны быть фиксированной ширины и равняться: 
№п/п – 4 знака
Код – 10 знаков
Дата – 11 знаков
+/- - 3 знака
o	Проверить таблицу под формой: новая строка должна появиться визуально (Treeview или Listbox) без перезагрузки окна.
o	Проверить генерацию кода события (формат {ИНДИКАТОР}_{НОМЕР}).
o	Проверить редактирование и удаление Событий.
o	Проверить вставку событий из буфера обмена кнопкой Вставить, а также кликом правой кнопки мыши в поле для вставки.
o	Проверить всплывающее полное наименование события при клике на нем. Оно должно держаться пока наведена мышь и 1 секунду после перемещения мыши.
•	Глобальная синхронизация: Если пользователь меняет проект в самом верхнем комбобоксе над аккордеоном, данные в открытой секции «Ввод событий» должны обновиться (подтянуть новые группы этого проекта). В выпадающем списке Выбрать проект, в настоящее время подключается Название проекта. Название проекта необходимо заменить на Шифр проекта
2. Список необходимых файлов для проверки и правки
Чтобы провести отладку, даю актуальное содержимое следующих файлов целиком:
1.	Интерфейс формы событий:
o	modules/project/project_event_ui.py
2.	Бизнес-логика чтения/записи:
o	modules/project/project_event_logic.py
3.	Главный координатор (аккордеон):
o	modules/project/project_window.py (особенно методы _fill_events_section, обработчики выбора проекта сверху — если они там есть).
4.	Менеджер БД:
o	modules/data/project_database.py
5.	Единая база справочников:
o	modules/data/database_module.py
6.	Точка входа (проверка инициализации мастер-данных):
o	gui/main_interface.py (посмотреть вызов init_master_data при старте).
7.	Пример организации редактирования на Показателях:
o	dukora/modules/manager/manager_metrics_work.py
o	dukora\modules\manager\manager_metrics_panel.py

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

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








#==============================================================================
# dukora/modules/project/project_event_ui.py
#==============================================================================
import customtkinter as ctk
from tkinter import ttk, messagebox
import datetime
import tkinter as tk
from .project_event_logic import ProjectEventLogic

class ProjectEventUI(ctk.CTkFrame):
    EDIT_MODE_COLOR = "#8B4513"
    
    def __init__(self, master, window_ref, logic: ProjectEventLogic):
        super().__init__(master, fg_color="transparent")
        self.window_ref = window_ref
        self.logic = logic
        
        self.group_names_to_codes = {}
        self.indicator_cache = {}
        
        self._setup_widgets()
        self._bind_global_events()
        self.update_project_dependent_lists()

    def _setup_widgets(self):
        main_form = ctk.CTkFrame(self, fg_color="transparent")
        main_form.pack(fill="both", expand=True, padx=10, pady=10)
        main_form.grid_columnconfigure(1, weight=1)

        # Блок Выбрать группу
        lbl_group = ctk.CTkLabel(main_form, text="Выбрать группу:", anchor="w")
        lbl_group.grid(row=0, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
        self.cmb_group = ctk.CTkComboBox(main_form, state="readonly", width=250, command=self._on_group_change)
        self.cmb_group.grid(row=0, column=1, sticky="ew", pady=(0, 5))

        # Блок Выбрать показатель
        lbl_indicator = ctk.CTkLabel(main_form, text="Выбрать показатель:", anchor="w")
        lbl_indicator.grid(row=1, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
        self.cmb_indicator = ctk.CTkComboBox(main_form, state="readonly", width=250)
        self.cmb_indicator.grid(row=1, column=1, sticky="ew", pady=(0, 10))

        # Поле Содержание события
        lbl_event = ctk.CTkLabel(main_form, text="Содержание события:", anchor="w")
        lbl_event.grid(row=2, column=0, sticky="nw", pady=(0, 5), padx=(0, 10))
        self.txt_event = ctk.CTkTextbox(main_form, height=80, wrap="word")
        self.txt_event.grid(row=2, column=1, sticky="ew", pady=(0, 5))
        
        paste_btn = ctk.CTkButton(main_form, text="Вставить", fg_color="#2B2B2B", hover_color="#3C3C3C", command=self._paste_from_clipboard)
        paste_btn.grid(row=3, column=1, sticky="e", pady=(0, 10), ipadx=10)

        # Дата и Направленность
        meta_frame = ctk.CTkFrame(main_form, fg_color="transparent")
        meta_frame.grid(row=4, column=0, columnspan=2, sticky="ew", pady=(0, 10))
        meta_frame.grid_columnconfigure(1, weight=1)
        
        ttk.Label(meta_frame, text="Дата события:").grid(row=0, column=0, sticky="w", padx=(0, 10))
        self.date_entry = ctk.CTkEntry(meta_frame, width=120)
        today = datetime.datetime.now().strftime("%Y-%m-%d")
        self.date_entry.insert(0, today)
        self.date_entry.grid(row=0, column=1, sticky="w")

        ttk.Label(meta_frame, text="Направленность:").grid(row=0, column=2, sticky="w", padx=(20, 10))
        self.dir_var = tk.IntVar(value=1)
        rb_plus = ctk.CTkRadioButton(meta_frame, text="(+)", variable=self.dir_var, value=1)
        rb_plus.grid(row=0, column=3, sticky="w")
        rb_minus = ctk.CTkRadioButton(meta_frame, text="(-)", variable=self.dir_var, value=-1)
        rb_minus.grid(row=0, column=4, sticky="w")

        # Кнопки управления событием
        ctrl_frame = ctk.CTkFrame(main_form, fg_color="transparent")
        ctrl_frame.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(0, 10))
        for i in range(4): ctrl_frame.grid_columnconfigure(i, weight=1)
        
        self.btn_add = ctk.CTkButton(ctrl_frame, text="Добавить", fg_color="#3B82F6", command=self._add_event)
        self.btn_add.grid(row=0, column=0, sticky="ew", padx=2)
        
        self.btn_edit = ctk.CTkButton(ctrl_frame, text="Редактировать", fg_color="#10B981", command=self._enter_edit_mode)
        self.btn_edit.grid(row=0, column=1, sticky="ew", padx=2)
        
        self.btn_save = ctk.CTkButton(ctrl_frame, text="Сохранить", state="disabled", fg_color=self.EDIT_MODE_COLOR, command=self._save_edited_event)
        self.btn_save.grid(row=0, column=2, sticky="ew", padx=2)
        
        self.btn_del = ctk.CTkButton(ctrl_frame, text="Удалить", fg_color="#EF4444", command=self._delete_selected_event)
        self.btn_del.grid(row=0, column=3, sticky="ew", padx=2)

        # Таблица событий
        tree_container = ctk.CTkFrame(main_form, fg_color="transparent")
        tree_container.grid(row=6, column=0, columnspan=2, sticky="nsew", pady=(10, 0))
        tree_container.grid_rowconfigure(0, weight=1); tree_container.grid_columnconfigure(0, weight=1)
        
        style = ttk.Style()
        theme_name = "clam" if "dark" in ctk.get_appearance_mode() else "default"
        style.theme_use(theme_name)
        columns = ("code", "date", "sign", "event")
        self.tree = ttk.Treeview(tree_container, columns=columns, show="headings", selectmode="browse", height=3)
        self.tree.heading("code", text="Код"); self.tree.column("code", width=80, anchor="center")
        self.tree.heading("date", text="Дата"); self.tree.column("date", width=90, anchor="center")
        self.tree.heading("sign", text="+/-"); self.tree.column("sign", width=45, anchor="center")
        self.tree.heading("event", text="Событие"); self.tree.column("event", minwidth=200, stretch=True)
        self.tree.pack(side="left", fill="both", expand=True)
        
        scrollbar = ctk.CTkScrollbar(tree_container, orientation="vertical", command=self.tree.yview)
        scrollbar.pack(side="right", fill="y")
        self.tree.configure(yscrollcommand=scrollbar.set)
        
        # Поиск по коду
        search_frame = ctk.CTkFrame(main_form, fg_color="transparent")
        search_frame.grid(row=7, column=0, columnspan=2, sticky="ew", pady=(10, 0))
        search_frame.grid_columnconfigure(1, weight=1)
        
        ttk.Label(search_frame, text="Наберите код:").grid(row=0, column=0, sticky="w", padx=(0, 5))
        self.search_entry = ctk.CTkEntry(search_frame)
        self.search_entry.grid(row=0, column=1, sticky="ew", padx=(0, 5))
        
        find_btn = ctk.CTkButton(search_frame, text="Искать", fg_color="#3B82F6", command=self._search_by_code)
        find_btn.grid(row=0, column=2, sticky="e")

    def update_project_dependent_lists(self):
        """Глобальное обновление списков Группа/Показатель."""
        
        # Проверка 1: Существует ли ссылка на главное окно?
        if not hasattr(self, 'window_ref') or not self.window_ref:
            print("[UI ERROR] window_ref is missing.")
            return

        selected_project_name = self.window_ref.project_selector.get()
        print(f"[UI SYNC] Updating for project: {selected_project_name}")

        # --- ЗАГРУЗКА ДАННЫХ ---
        try:
            groups_raw, name_to_code_map = self.logic.load_groups_and_indicators()
            
            # Если база вернула пустой список — выходим раньше времени
            if not groups_raw:
                print("[UI WARNING] Groups list is empty from logic.")
                group_names = ["Нет групп"]
            else:
                group_names = [str(g).strip() for g in groups_raw]
                
        except Exception as e:
            print(f"[CRITICAL LOGIC FAIL]: {e}")
            group_names = ["Ошибка БД"]
            name_to_code_map = {}

        # --- ПРИНУДИТЕЛЬНОЕ ОБНОВЛЕНИЕ ВИДЖЕТА ГРУПП ---
        try:
            # Сначала очищаем кэш значений CTkComboBox
            self.cmb_group.configure(values=[])
            
            # Устанавливаем новые значения
            self.cmb_group.configure(values=group_names)
            
            # Выбираем первый элемент программно
            if len(group_names) > 0 and group_names[0] != "Ошибка БД":
                self.cmb_group.set(group_names[0])
            else:
                self.cmb_group.set("")
                
        except Exception as ui_err:
            print(f"[UI CRITICAL] Failed to configure Group ComboBox: {ui_err}")

        # Сохраняем словарь для поиска кода группы по имени
        self.group_names_to_codes = name_to_code_map

        # --- АВТОМАТИЧЕСКОЕ ОБНОВЛЕНИЕ ПОКАЗАТЕЛЕЙ ---
        # Вызываем принудительно для первой группы в списке
        first_group = ""
        if group_names and group_names[0] not in ("Нет групп", "Ошибка БД"):
            first_group = group_names[0]
            
        self._update_indicator_combobox(first_group)

    def _load_indicators_into_cache(self, groups):
        self.indicator_cache.clear()
        for group in groups:
            inds = self.logic.load_indicators_for_group(group.code)
            self.indicator_cache[group.code] = inds

    def _on_group_change(self, event=None):
        group_name = self.cmb_group.get()
        self._update_indicator_combobox(group_name)

    def _update_indicator_combobox(self, group_name: str):
        self.cmb_indicator.set("")
        if not group_name or group_name not in self.group_names_to_codes:
            self.cmb_indicator.configure(values=[])
            return

        group_code = self.group_names_to_codes.get(group_name)
        indicators = self.logic.load_indicators_for_group(group_code)
        
        # Извлекаем имена из списка кортежей [(name1,), (name2,)]
        ind_names = [ind[0] for ind in indicators]
        
        self.cmb_indicator.configure(values=ind_names)
        if ind_names:
            self.cmb_indicator.set(ind_names[0])

    def _paste_from_clipboard(self):
        try:
            txt = self.clipboard_get()
            self.txt_event.insert(tk.INSERT, txt)
        except Exception:
            pass

    def _collect_current_selection(self) -> dict:
        proj_name = self.window_ref.project_selector.get()
        proj_objs = self.logic.load_project_list()
        proj_code = next((p[2] for p in proj_objs if p[1] == proj_name), "")
        
        group_name = self.cmb_group.get()
        group_code = self.group_names_to_codes.get(group_name, "")
        
        indicator_name = self.cmb_indicator.get()
        indicator_code = ""
        if group_code in self.indicator_cache:
            for ind in self.indicator_cache[group_code]:
                if ind.name == indicator_name:
                    indicator_code = ind.code
                    break
                    
        return {
            "project_name": proj_name,
            "project_code": proj_code,
            "group_name": group_name,
            "group_code": group_code,
            "indicator_name": indicator_name,
            "indicator_code": indicator_code
        }

    def _add_event(self):
        data = self._collect_current_selection()
        if not all([data['project_code'], data['group_code'], data['indicator_code']]):
            messagebox.showwarning("Ошибка", "Заполнены не все обязательные поля.")
            return
            
        text = self.txt_event.get("1.0", "end").strip()
        if not text:
            messagebox.showwarning("Ошибка", "Поле 'Содержание события' пустое.")
            return
            
        date_str = self.date_entry.get()
        sign = self.dir_var.get()
        
        success, msg = self.logic.add_event_to_db(data, text, date_str, sign)
        messagebox.showinfo("Результат", msg)
        
        if success:
            self.txt_event.delete("1.0", "end")
            self._refresh_event_table(data['project_code'])

    def _refresh_event_table(self, project_code: str):
        for row in self.tree.get_children(): self.tree.delete(row)
        db_path = os.path.join(self.logic.project_dbs_folder, f"{project_code}.db")
        if not os.path.exists(db_path): return
        
        conn = sqlite3.connect(db_path)
        cur = conn.cursor()
        cur.execute("SELECT event_code, event_data, event_sign, event_text_content FROM project_data ORDER BY id DESC LIMIT 50")
        rows = cur.fetchall()
        conn.close()
        
        last_iid = None
        for row in reversed(rows):
            sign_char = "+" if row[2] == 1 else "-"
            iid = self.tree.insert("", "end", values=(row[0], row[1], sign_char, row[3]))
            last_iid = iid
        if last_iid: self.tree.see(last_iid)

    def _enter_edit_mode(self):
        sel = self.tree.selection()
        if not sel: 
            messagebox.showwarning("Ошибка", "Выберите событие в таблице."); return
            
        vals = self.tree.item(sel[0])["values"]
        self.editing_event_code = vals[0]
        
        full_text = vals[3]
        self.txt_event.delete("1.0", "end")
        self.txt_event.insert("1.0", full_text)
        
        self.btn_add.configure(state="disabled")
        self.btn_edit.configure(state="disabled")
        self.btn_save.configure(state="normal")
        self.btn_del.configure(state="disabled")

    def _save_edited_event(self):
        if not getattr(self, 'editing_event_code', None):
            return
            
        new_text = self.txt_event.get("1.0", "end").strip()
        if not new_text:
            messagebox.showwarning("Ошибка", "Текст не может быть пустым."); return
            
        db_path = os.path.join(self.logic.project_dbs_folder, f"{self._collect_current_selection()['project_code']}.db")
        conn = sqlite3.connect(db_path)
        cur = conn.cursor()
        cur.execute("UPDATE project_data SET event_text_content = ? WHERE event_code = ?", (new_text, self.editing_event_code))
        conn.commit()
        conn.close()
        
        messagebox.showinfo("Готово", "Изменения сохранены.")
        self._reset_controls()
        self._refresh_event_table(self._collect_current_selection()['project_code'])

    def _delete_selected_event(self):
        sel = self.tree.selection()
        if not sel: return
        if not messagebox.askyesno("Подтверждение", "Удалить выбранное событие?"): return
        
        event_code = self.tree.item(sel[0])["values"][0]
        db_path = os.path.join(self.logic.project_dbs_folder, f"{self._collect_current_selection()['project_code']}.db")
        conn = sqlite3.connect(db_path)
        cur = conn.cursor()
        cur.execute("DELETE FROM project_data WHERE event_code = ?", (event_code,))
        conn.commit()
        conn.close()
        
        self._refresh_event_table(self._collect_current_selection()['project_code'])

    def _search_by_code(self):
        target = self.search_entry.get().strip()
        if not target:
            messagebox.showwarning("Поиск", "Введите код."); return
            
        items = self.tree.get_children()
        found = None
        for item in items:
            if self.tree.item(item)["values"][0] == target:
                found = item
                break
                
        if found:
            self.tree.selection_set(found)
            self.tree.see(found)
            self.tree.focus(found)
        else:
            messagebox.showinfo("Поиск", "Событие с таким кодом не найдено.")

    def _reset_controls(self):
        self.btn_add.configure(state="normal")
        self.btn_edit.configure(state="normal")
        self.btn_save.configure(state="disabled")
        self.btn_del.configure(state="normal")
        self.txt_event.delete("1.0", "end")

    def _bind_global_events(self):
        self.txt_event.bind("<Button-3>", lambda e: (self._paste_from_clipboard(), "break"))
        
#==============================================================================








#==============================================================================
# dukora/modules/project/project_event_logic.py
#==============================================================================
import os
import sqlite3
from typing import List, Tuple, Dict
from pathlib import Path
from modules.data.project_database import ProjectDatabase

# --- НАДЕЖНОЕ ОПРЕДЕЛЕНИЕ ПУТИ К БАЗАМ ---
try:
    # Используем нашу рабочую функцию из resources.py
    from modules.utils.resources import get_project_root
    
    ROOT_DIR = get_project_root()
    DB_FOLDER = ROOT_DIR / "db"
    DATABASE_DB_PATH = DB_FOLDER / "database.db"
    PROJECT_DBS_FOLDER = ROOT_DIR / "project_dbs"
except Exception as e:
    print(f"[FATAL] Не удалось определить корень проекта для путей к БД: {e}")
    sys.exit(1)

class ProjectEventLogic:
    def __init__(self, manager_db: ProjectDatabase):
        self.manager_db = manager_db # Теперь снова можем брать список проектов
        
        # Пути берем из глобальных констант модуля (которые мы настроили ранее)
        self.project_dbs_folder = PROJECT_DBS_FOLDER 
        os.makedirs(self.project_dbs_folder, exist_ok=True)

    # --- СПИСОК ПРОЕКТОВ (из projects.db) ---
    def load_project_list(self) -> List[Tuple]:
        return self.manager_db.list_projects()

    # --- ГРУППЫ И ПОКАЗАТЕЛИ (из database.db - без изменений) ---
    def load_groups_and_indicators(self) -> Tuple[List[str], Dict[str, str]]:
        if not DATABASE_DB_PATH.exists():
            return [], {}
            
        try:
            conn = sqlite3.connect(DATABASE_DB_PATH)
            cursor = conn.cursor()
            
            cursor.execute("SELECT name FROM groups ORDER BY name")
            group_names = [row[0] for row in cursor.fetchall()]
            
            name_to_code_map = {}
            if group_names:
                cursor.execute("SELECT code, name FROM groups ORDER BY name")
                rows = cursor.fetchall()
                name_to_code_map = {name: code for code, name in rows}
                
            conn.close()
            return group_names, name_to_code_map
        except Exception as e:
            print(f"[LOGIC ERROR] Failed to read from master DB: {e}")
            return [], {}

    def load_indicators_for_group(self, group_code: str) -> List:
        if not DATABASE_DB_PATH.exists():
            return []
            
        try:
            conn = sqlite3.connect(DATABASE_DB_PATH)
            cursor = conn.cursor()
            cursor.execute("SELECT name FROM indicators WHERE group_code = ? ORDER BY name", (group_code,))
            indicators = [(row[0], ) for row in cursor.fetchall()] 
            conn.close()
            return indicators
        except Exception as e:
            print(f"[LOGIC ERROR] Failed to read indicators: {e}")
            return []

    # --- ЗАПИСЬ СОБЫТИЙ (без изменений) ---
    def add_event_to_db(self, selected_data: Dict, event_text: str, date_str: str, direction: int) -> tuple[bool, str]:
        db_path = os.path.join(self.project_dbs_folder, f"{selected_data['project_code']}.db")
        
        is_new_db = not os.path.exists(db_path)
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()
        
        if is_new_db:
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS project_data (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    project_code TEXT NOT NULL,
                    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                    group_name TEXT,
                    group_code TEXT,
                    indicator_name TEXT,
                    indicator_code TEXT,
                    event_data DATE,
                    event_sign INTEGER,
                    event_code TEXT,
                    event_text_content TEXT
                )
            ''')

        cursor.execute("SELECT COUNT(*) FROM project_data WHERE project_code = ? AND indicator_code = ?", 
                       (selected_data['project_code'], selected_data['indicator_code']))
        count = cursor.fetchone()[0]
        event_code = f"{selected_data['indicator_code']}_{count + 1}"

        try:
            cursor.execute('''
                INSERT INTO project_data (
                    project_code, group_name, group_code, indicator_name, indicator_code,
                    event_data, event_sign, event_code, event_text_content
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
            ''', (
                selected_data['project_code'],
                selected_data['group_name'],
                selected_data['group_code'],
                selected_data['indicator_name'],
                selected_data['indicator_code'],
                date_str,
                direction,
                event_code,
                event_text
            ))
            conn.commit()
            return True, f"Данные добавлены. Код события: {event_code}"
        except Exception as e:
            conn.rollback()
            return False, f"Ошибка записи в БД: {e}"
        finally:
            conn.close()
            
#==============================================================================








#==============================================================================
# modules/project/project_window.py
#==============================================================================
import customtkinter as ctk
from tkinter import ttk, messagebox
from modules.project.project_event_logic import ProjectEventLogic

from typing import List
import datetime
import os

# Попытка импорта внешних библиотек (оставляем ваши try/except)
try:
    from tkcalendar import DateEntry
except ImportError:
    DateEntry = None
try:
    import docx
except ImportError:
    docx = None

from modules.project.project_event_ui import ProjectEventUI
from modules.project.project_create_logic import ProjectCreateLogic

class ProjectWindow(ctk.CTkToplevel):
    def __init__(self, master):
        super().__init__(master)
               
        self.parent_main = master
        
        self.title("Журнал событий")
        self.current_user = getattr(master, 'current_user', None)
        
        # --- Настройка размеров (центрирование экрана + модальность) ---
        screen_height = self.winfo_screenheight()
        window_width = 650
        initial_height = 380 
        screen_w = self.winfo_screenwidth()
        x_cord = int((screen_w - window_width) / 2)
        taskbar_offset_top = 40 
        y_cord = taskbar_offset_top
        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)

        # === ЗАМЕНА МЕТОДА: Заголовок вынесен отдельно ===
        self._build_title_bar(main_container)
        
        self._build_project_selector(main_container)
        
        accordion_frame = ctk.CTkFrame(main_container)
        accordion_frame.pack(fill="both", expand=True, pady=(10, 0))
        
        self.accordion_data = {}
        self.currently_open_button = None
        
        # === ЗАГОЛОВКИ АККОРДЕОНА (ТЕПЕРЬ ИХ 4) ===
        create_item = self._create_accordion_header(accordion_frame, "Создать проект")
        events_item = self._create_accordion_header(accordion_frame, "Открыть для ввода событий")
        charts_item = self._create_accordion_header(accordion_frame, "Показать графики")
        print_item = self._create_accordion_header(accordion_frame, "Настройка печати")
        
        # Наполняем секции контентом (ваши методы наполнения остаются прежними)
        self._fill_create_section(create_item["content"]) # Новый метод для новой секции
        self._fill_events_section(events_item["content"])
        self._fill_charts_section(charts_item["content"])
        self._fill_print_section(print_item["content"])
        
        # Привязка команд переключения
        for item in [create_item, events_item, charts_item, print_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(create_item["button"], create_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)

    # === НОВЫЙ МЕТОД ДЛЯ ШАПКИ НАД СЕЛЕКТОРОМ ПРОЕКТА ===
    def _build_title_bar(self, container):
        title_lbl = ctk.CTkLabel(container, text="Наполнение проекта", font=ctk.CTkFont(size=20, weight="bold"))
        # Убран anchor="w". По умолчанию CTkLabel центрирует текст.
        title_lbl.pack(pady=(0, 15)) 

    def _build_project_selector(self, container):
        """
        Создает блок выбора проекта над аккордеоном без использования placeholder_text.
        Логика "Проектов нет" реализована через set() текстового поля.
        """
        frame = ctk.CTkFrame(container)
        frame.pack(pady=(0, 10)) 
        
        label_project = ctk.CTkLabel(frame, text="Выбрать проект:", anchor="w")
        label_project.pack(side="left", padx=(0, 10))
        
        # Передаем пустой список значений
        self.project_selector = ctk.CTkComboBox(
            frame,
            values=[], 
            state="readonly",
            width=250
        )
        
        # Так как списка нет, пишем текст напрямую в поле ввода комбобокса
        self.project_selector.set("Проектов нет") 
        
        self.project_selector.pack(side="left")
        
    def _enforce_onscreen_position(self):
        """Гарантирует, что нижняя часть окна не уйдет за пределы экрана."""
        # Проверяем, что окно еще живо
        if not self.winfo_exists():
            return
            
        self.update_idletasks()
        
        win_w = self.winfo_width()
        win_h = self.winfo_height()
        screen_w = self.winfo_screenwidth()
        screen_h = self.winfo_screenheight()
        
        x = int((screen_w - win_w) / 2)
        
        bottom_edge = self.winfo_rooty() + win_h
        
        overflow = bottom_edge - screen_h + 50 
        if overflow > 0:
            y = max(40, self.winfo_rooty() - overflow)
        else:
            y = 40
            
        self.geometry(f"+{x}+{y}")
        
    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)
        
        # Вызываем ПОСЛЕ установки geometry, чтобы считать актуальные размеры
        self.after(50, self._enforce_onscreen_position)

    # === ОБНОВЛЕННЫЙ РАСЧЕТ ГЕОМЕТРИИ (УЧИТЫВАЕТ 4 СЕКЦИИ И НИЖНЮЮ ПАНЕЛЬ) ===
    def _calculate_window_geometry(self, collapsed=False):
        current_width = self.winfo_width()
        if current_width == 1:
            current_width = 650
            
        header_height = 50 * 4 + 30  # Высота шапок всех 4 секций
        
        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
        
        # НОВАЯ ЛОГИКА: Сначала считаем нужную высоту, потом режем под экран
        final_height = min(target_height, max_h)
        
        # Увеличенный порог видимости всех заголовков (расчет выше показал ~453px)
        min_h = 460 
        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_create_section(self, frame):
        # Импортируем нашу новую логику внутри функции, чтобы избежать циклических зависимостей при старте приложения
        from modules.data.project_database import ProjectDatabase
        from modules.project.project_create_logic import ProjectCreateLogic

        # Инициализируем базу данных проектов один раз при открытии окна
        if not hasattr(self, 'manager_db'):
            self.manager_db = ProjectDatabase()
            self.manager_db.create_tables()
            self.event_logic = ProjectEventLogic(self.manager_db)
        
        logic = ProjectCreateLogic(self.manager_db)
        
        form_frame = ctk.CTkFrame(frame, fg_color="transparent")
        form_frame.pack(fill="x", expand=True, padx=10, pady=10)
        
        # Настройка сетки: второй столбец растягивается
        form_frame.grid_columnconfigure(1, weight=1)

        # --- Поля ввода ---
        ttk.Label(form_frame, text="Название:").grid(row=0, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
        name_entry = ctk.CTkEntry(form_frame, placeholder_text="Введите полное название...")
        name_entry.grid(row=0, column=1, sticky="ew", pady=(0, 5))

        ttk.Label(form_frame, text="Шифр:").grid(row=1, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
        short_code_entry = ctk.CTkEntry(form_frame, placeholder_text="Краткое имя (латиница)...")
        short_code_entry.grid(row=1, column=1, sticky="ew", pady=(0, 5))

        ttk.Label(form_frame, text="Код (ID):").grid(row=2, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
        code_entry = ctk.CTkEntry(form_frame, placeholder_text="Уникальный идентификатор...")
        code_entry.grid(row=2, column=1, sticky="ew", pady=(0, 5))

        ttk.Label(form_frame, text="Дата открытия:").grid(row=3, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
        date_entry = ctk.CTkEntry(form_frame)
        today = datetime.datetime.now().strftime("%Y-%m-%d")
        date_entry.insert(0, today)
        date_entry.grid(row=3, column=1, sticky="ew", pady=(0, 5))

        ttk.Label(form_frame, text="Ответственный:").grid(row=4, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
        resp_entry = ctk.CTkEntry(form_frame, placeholder_text="ФИО ответственного лица...")
        resp_entry.grid(row=4, column=1, sticky="ew", pady=(0, 10))

        # --- Кнопки действий ---
        action_frame = ctk.CTkFrame(form_frame, fg_color="transparent")
        action_frame.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(5, 0))
        action_frame.grid_columnconfigure((0, 1), weight=1) # Равномерное распределение кнопок

        create_btn = ctk.CTkButton(
            action_frame, 
            text="Создать проект", 
            fg_color="#3B82F6", 
            hover_color="#2563EB",
            command=lambda n=name_entry, sc=short_code_entry, c=code_entry, d=date_entry, r=resp_entry: self._handle_project_creation(
                {
                    'human_name': n.get(),
                    'short_code': sc.get(),
                    'full_code': c.get(),
                    'description': "",
                    'date': d.get(),
                    'responsible': r.get()
                },
                logic,
                lambda: [n.delete(0, "end"), sc.delete(0, "end"), c.delete(0, "end"), r.delete(0, "end")] # Теперь очистка полей сработает
            )
        )
        create_btn.grid(row=0, column=0, sticky="ew", padx=(0, 5))

        clear_btn = ctk.CTkButton(
            action_frame, 
            text="Очистить поля", 
            fg_color="#6B7280", 
            hover_color="#4B5563",
            command=lambda: [name_entry.delete(0, "end"), short_code_entry.delete(0, "end"), code_entry.delete(0, "end"), resp_entry.delete(0, "end")]
        )
        clear_btn.grid(row=0, column=1, sticky="ew", padx=(5, 0))
        
    def _handle_project_creation(self, data_dict: dict, logic_instance: ProjectCreateLogic, on_success_callback):
        success, message = logic_instance.create_project(data_dict)
        
        if not success:
            messagebox.showerror("Ошибка создания", message)
            return

        # Уведомляем пользователя
        messagebox.showinfo("Проект создан", message)
        on_success_callback()

        # --- ПРИНУДИТЕЛЬНАЯ СИНХРОНИЗАЦИЯ ЧЕРЕЗ ОСНОВНОЙ МЕНЕДЖЕР ---
        
        # 1. Закрываем текущее соединение manager_db, чтобы сбросить кэш транзакций
        try:
            self.manager_db.close()
        except Exception:
            pass

        # 2. Читаем свежие данные ИЗ ОТКРЫТОЙ БАЗЫ ОКНА
        fresh_tuples = self.manager_db.list_projects() 
        print(f"[DEBUG HARD SYNC] Прямое чтение list_projects(): {fresh_tuples}")

        # Извлекаем только имена для ComboBox
        fresh_names = [p[1] for p in fresh_tuples]

        # 3. Радикальное обновление UI (Destroy + Create)
        parent_widget = self.project_selector.master
        self.project_selector.destroy()
        
        self.project_selector = ctk.CTkComboBox(
            master=parent_widget,
            values=fresh_names,
            state="readonly",
            width=250
        )
        self.project_selector.pack(side="left")

        if fresh_names:
            self.project_selector.set(fresh_names[-1])
        else:
            self.project_selector.set("Проектов нет")

        # 4. Обновляем Treeview внизу формы создания
        try:
            for row in self.tree_projects.get_children(): 
                self.tree_projects.delete(row)
            
            for proj in sorted(fresh_tuples, key=lambda x: x[0]):
                self.tree_projects.insert("", "end", values=proj)
        except Exception as tree_err:
            print(f"[UI ERROR] Treeview refresh failed: {tree_err}")

        # 5. Вкладка событий (если открыта)
        current_title = ""
        if self.currently_open_button:
            current_title = self.currently_open_button.cget("text")

        if current_title == "Открыть для ввода событий":
            for child in self.currently_open_content.winfo_children():
                if isinstance(child, ProjectEventUI):
                    try:
                        child.update_project_dependent_lists()
                    except Exception as update_err:
                        print(f"[LOG] Ошибка обновления UI событий: {update_err}")
                    break
        
    # ВАШИ СТАРЫЕ МЕТОДЫ НАПОЛНЕНИЯ (_fill_events...) ОСТАЮТСЯ БЕЗ ИЗМЕНЕНИЙ
    def _fill_events_section(self, frame):
        from modules.project.project_event_ui import ProjectEventUI
        from modules.project.project_event_logic import ProjectEventLogic

        if not hasattr(self, 'event_logic'):
            self.event_logic = ProjectEventLogic(self.manager_db)

        # --- ДИАГНОСТИКА: Смотрим, что реально приходит из базы ---
        print("[DEBUG] Начинаем проверку загрузки справочников...")
        
        groups_raw, names_to_codes = self.event_logic.load_groups_and_indicators()
        print(f"[DEBUG RAW GROUPS FROM DB: {groups_raw}") 
        print(f"[DEBUG MAPPING: {names_to_codes}")

        ui = ProjectEventUI(frame, self, self.event_logic)
        
        # Вызываем обновление принудительно
        try:
            ui.update_project_dependent_lists()
        except Exception as e:
            print(f"[CRASH UI LOAD]: {e}")
        
        ui.pack(fill="both", expand=True)
        
    def _fill_charts_section(self, frame):
        if DateEntry is None:
            error_lbl = ctk.CTkLabel(frame, text="Ошибка: установите библиотеку\npip install tkcalendar", text_color="red")
            error_lbl.pack(expand=True)
            return
    
    def _fill_print_section(self, frame):
        if DateEntry is None:
            error_lbl = ctk.CTkLabel(frame, text="Ошибка: установите библиотеку\npip install tkcalendar", text_color="red")
            error_lbl.pack(expand=True)
            return
        
    def _generate_word_report(self, group, report_date, orientation):
        if docx is None:
            messagebox.showerror("Библиотека не найдена", "Для экспорта нужна библиотека 'python-docx'.\nВыполните:\npip install python-docx")
            return
        
    def on_closing(self):
        try:
            self.after_cancel("all")
        except Exception:
            pass
        for item in list(self.accordion_data.values()):
            try:
                content = item.get("content")
                if content:
                    content.destroy()
            except Exception:
                pass
        try:
            self.destroy()
        except Exception:
            pass
            
#==============================================================================








#==============================================================================
# dukora/modules/data/project_database.py
#==============================================================================
import os
import sqlite3
from pathlib import Path
from typing import Optional, List, Tuple
from types import SimpleNamespace

class ProjectDatabase:
    """
    Управляет общей базой данных 'projects.db', хранящей метаданные о проектах.
    Создает файл БД в директории db относительно корня проекта.
    """
    def __init__(self):
        # Используем утилиту получения корня проекта
        from modules.utils.resources import get_project_root
        
        self.base_dir = get_project_root() / "db"
        self.base_dir.mkdir(parents=True, exist_ok=True)
        
        self.db_path = self.base_dir / "projects.db"
        self.connection: Optional[sqlite3.Connection] = None

    def connect(self) -> sqlite3.Connection:
        if self.connection is None:
            try:
                self.connection = sqlite3.connect(self.db_path)
                self.connection.row_factory = sqlite3.Row
            except sqlite3.Error as e:
                raise RuntimeError(f"Ошибка подключения к {self.db_path}: {e}")
        return self.connection

    def close(self):
        if self.connection:
            self.connection.close()
            self.connection = None

    def create_tables(self):
        conn = self.connect()
        cursor = conn.cursor()
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS projects (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL UNIQUE,
                code TEXT NOT NULL UNIQUE,
                description TEXT DEFAULT '',
                date_opened TEXT DEFAULT '',
                responsible TEXT DEFAULT ''
            )
        ''')
        conn.commit()

    def add_project(self, name: str, code: str, description: str, date_opened: str, responsible: str) -> bool:
        """Добавляет запись о проекте в общий список."""
        try:
            with self.connect() as conn:
                conn.execute(
                    "INSERT INTO projects (name, code, description, date_opened, responsible) VALUES (?, ?, ?, ?, ?)",
                    (name, code, description, date_opened, responsible)
                )
            return True
        except sqlite3.IntegrityError:
            return False
        except Exception:
            return False

    def list_projects(self) -> List[Tuple]:
        """Возвращает список кортежей (id, name, code) для комбобоксов."""
        with self.connect() as conn:
            cursor = conn.execute("SELECT id, name, code FROM projects ORDER BY name")
            return cursor.fetchall()

    def list_projects_names(self) -> List[str]:
        """Возвращает плоский список только названий проектов для ComboBox."""
        with self.connect() as conn:
            cursor = conn.execute("SELECT name FROM projects ORDER BY name")
            return [row[0] for row in cursor.fetchall()]

    def get_all_groups(self):
        """Читает общую таблицу групп."""
        try:
            with self.connect() as conn:
                cursor = conn.cursor()
                # Проверяем наличие таблицы перед запросом
                cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='groups';")
                if not cursor.fetchone():
                    return []
                
                cursor.execute("SELECT id, code, name FROM groups ORDER BY name")
                rows = cursor.fetchall()
                # Возвращаем объекты SimpleNamespace для совместимости с вашим старым кодом UI
                from types import SimpleNamespace
                return [SimpleNamespace(id=r[0], code=r[1], name=r[2]) for r in rows]
        except Exception as e:
            print(f"[DB ERROR] Groups load failed: {e}")
            return []

    def get_indicators_by_group(self, group_code: str):
        """Читает показатели конкретной группы из общей базы."""
        try:
            with self.connect() as conn:
                cursor = conn.cursor()
                # Проверяем наличие таблицы индикаторов
                cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='indicators';")
                if not cursor.fetchone():
                    return []

                cursor.execute("SELECT id, code, name, group_code FROM indicators WHERE group_code = ? ORDER BY name", (group_code,))
                rows = cursor.fetchall()
                from types import SimpleNamespace
                return [SimpleNamespace(id=r[0], code=r[1], name=r[2], group_code=r[3]) for r in rows]
        except Exception as e:
            print(f"[DB ERROR] Indicators load failed: {e}")
            return []
            
#==============================================================================








#==============================================================================
# dukora/modules/data/database_module.py
#==============================================================================
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()
    
#====================================================











#====================================================
# 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 
from typing import List, Any


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()
        # Высота верхней панели задач Windows обычно около 40 пикселей. 
        # Если панель скрыта или находится сбоку/снизу, отступ можно уменьшить до 0-10.
        taskbar_offset_top = 40 

        pos_x = (screen_w - target_w) // 2
        pos_y = taskbar_offset_top

        # Устанавливаем размер И позицию одной командой через configure
        self.geometry(f"{target_w}x{target_h}+{pos_x}+{pos_y}")

    def _position_center_top(self, window_instance, width: int, height: int):
        screen_w = window_instance.winfo_screenwidth()
        pos_x = (screen_w - width) // 2
        pos_y = 40 # тот же отступ сверху
        window_instance.geometry(f"{width}x{height}+{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
        try:
            from modules.experts.experts_window import ExpertsWindow
            self._experts_win = ExpertsWindow(master=self)
            self.wait_window(self._experts_win)
        except Exception as e:
            messagebox.showerror("Фатальный сбой", f"Не удалось открыть раздел Экспертов.\n\n{e}")
            import traceback
            traceback.print_exc()
        
    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)
        
        # --- НОВЫЙ БЛОК: Принудительное чтение актуальных данных из БД ---
        try:
            # Закрываем соединение менеджера внутри окна, чтобы сбросить кэш
            self._project_win.manager_db.close()
            
            # Получаем свежие данные напрямую
            fresh_projects = self._project_win.manager_db.list_projects()
            project_names_only = [p[1] for p in fresh_projects]
            
            print(f"[DEBUG MAIN MENU] Проекты при открытии модуля: {project_names_only}")

            # 1. Обновляем верхний ComboBox над аккордеоном
            if hasattr(self._project_win, 'project_selector'):
                self._project_win.project_selector.configure(values=project_names_only)
                
                if project_names_only:
                    # Выбираем первый проект по умолчанию при открытии окна
                    self._project_win.project_selector.set(project_names_only[0])
                else:
                    self._project_win.project_selector.set("Проектов нет")

            # 2. Обновляем Treeview внутри секции создания проекта (если она открыта или для будущего)
            if hasattr(self._project_win, 'tree_projects'):
                tree = self._project_win.tree_projects
                for row in tree.get_children(): 
                    tree.delete(row)
                
                for proj in sorted(fresh_projects, key=lambda x: x[0]):
                    tree.insert("", "end", values=proj)

        except Exception as e:
            print(f"[ERROR] Failed to sync projects on module open: {e}")
            # В случае ошибки оставляем заглушку
            if hasattr(self._project_win, 'project_selector'):
                self._project_win.project_selector.configure(values=[])
                self._project_win.project_selector.set("Проектов нет")

        # Отображаем окно
        self._project_win.grab_set() 
        
    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\manager\manager_metrics_panel.py
# ===============================================
import customtkinter as ctk
from tkinter import ttk, messagebox, filedialog
import tkinter as tk  # Импортируем базовый tkinter для Text виджета
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, update_indicator_name
)
from modules.manager.manager_group_work import list_groups as group_list

class ManagerMetricsPanel(ctk.CTkFrame):
    def __init__(self, master):
        super().__init__(master)
        
        self.parent_main = master
        self.group_names_to_codes = {}
        self._current_editing_code = None
        self._active_tooltip = None 

        # === СЕТКА ОКНА ===
        self.grid_columnconfigure(0, weight=0)
        self.grid_columnconfigure(1, weight=0)
        self.grid_rowconfigure(0, weight=0)
        self.grid_rowconfigure(1, weight=0)
        self.grid_rowconfigure(2, weight=0)
        self.grid_rowconfigure(3, weight=1)
        self.grid_rowconfigure(4, weight=0)

        # === СТРОКА 0: ВЫБОР ГРУППЫ ===
        ctk.CTkLabel(self, text="Группа:").grid(row=0, column=0, sticky="w", padx=(10, 5), pady=(10, 0))
        self.group_combobox = ctk.CTkComboBox(self, state="readonly", command=self._on_group_selected)
        self.group_combobox.grid(row=0, column=0, columnspan=2, sticky="ew", padx=10, pady=(10, 0))

        # === СТРОКА 1: КОНТЕЙНЕР ДЛЯ ПОЛЕЙ ===
        input_frame = ctk.CTkFrame(self)
        # Прижимаем к верху (Группа) и низу (Кнопки), убрав лишние отступы
        input_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(5, 5)) 
                
        input_frame.grid_columnconfigure(0, weight=1)
        # Вторая колонка больше не нужна для выравнивания поля, но может понадобиться для кнопки
        input_frame.grid_columnconfigure(1, weight=0)

        # Метка "Название"
        ctk.CTkLabel(input_frame, text="Название показателя:", anchor="w").grid(row=0, column=0, sticky="w")
                
        # ЕДИНСТВЕННОЕ ОСТАВШЕЕСЯ ПОЛЕ (многострочное)
        self.name_entry = ctk.CTkTextbox(
            input_frame, height=60, wrap="word", border_width=1
        )
        # Теперь это первая строка с контентом внутри фрейма (row=1)
        self.name_entry.grid(row=1, column=0, sticky="ew", padx=(0, 5), pady=(2, 2)) 

        # --- ЭТА ЧАСТЬ УДАЛЕНА ---
        # self.code_entry = ctk.CTkEntry(input_frame, height=28)
        # self.code_entry.grid(row=2, column=0, sticky="ew", padx=(0, 5), pady=(2, 5))
        # -------------------------

        # Привязки для Textbox остаются прежними
        self.name_entry.bind("<Button-3>", self._paste_via_pyperclip)
        self.name_entry.bind("<Control-v>", lambda e: (self._paste_via_pyperclip(e), "break"))
        self.name_entry.bind("<Shift-Insert>", lambda e: (self._paste_via_pyperclip(e), "break"))
        self.name_entry.bind("<Escape>", lambda e: self._reset_indicator_edit_mode())

        # Кнопка вставки. Так как текстбокс теперь один, логичнее перенести кнопку на row=2,
        # чтобы она визуально стояла под полем ввода, а не сбоку от него по центру высоты.
        paste_btn = ctk.CTkButton(input_frame, text="Вставить", fg_color="#2B2B2B", hover_color="#3C3C3C", command=self._paste_via_pyperclip)
        paste_btn.grid(row=2, column=0, sticky="e", padx=(0, 5), pady=(2, 5), ipadx=10)

        # Если кнопка должна быть строго справа, используем две колонки:
        # paste_btn.grid(row=1, column=1, sticky="nsew", padx=(5, 0), pady=5, ipadx=10)

        # === СТРОКА 2: КНОПКИ УПРАВЛЕНИЯ ===
        action_frame = ctk.CTkFrame(self)
        action_frame.grid(row=2, column=0, columnspan=2, sticky="ew", padx=10, pady=(0, 0)) 
        
        for i in range(4): 
            action_frame.grid_columnconfigure(i, weight=1, uniform="actions")

        self.add_btn = ctk.CTkButton(action_frame, text="Добавить", fg_color="#3B82F6", command=self.add_indicator)
        self.add_btn.grid(row=0, column=0, sticky="ew", padx=2, pady=2)
            
        self.edit_btn = ctk.CTkButton(action_frame, text="Редактировать", fg_color="#10B981", command=self.edit_indicator_select)
        self.edit_btn.grid(row=0, column=1, sticky="ew", padx=2, pady=2)
            
        self.delete_btn = ctk.CTkButton(action_frame, text="Удалить", fg_color="#EF4444", command=self.delete_indicator)
        self.delete_btn.grid(row=0, column=2, sticky="ew", padx=2, pady=2)

        self.save_btn = ctk.CTkButton(action_frame, text="Сохранить", state="disabled", fg_color="#8B4513", hover_color="#A0522D", command=self.update_indicator_name)
        self.cancel_edit_btn = ctk.CTkButton(action_frame, text="Отмена", fg_color="#6B7280", hover_color="#4B5563", command=self._reset_indicator_edit_mode)

        # === СТРОКА 3: ТАБЛИЦА ДАННЫХ ===
        tree_container = ctk.CTkFrame(self, fg_color="transparent")
        tree_container.grid(row=3, column=0, columnspan=2, sticky="nsew", padx=10, pady=0) 
        tree_container.grid_columnconfigure(0, weight=1); tree_container.grid_rowconfigure(0, weight=1)

        style = ttk.Style()
        try:
            theme_name = "clam" if "dark" in ctk.get_appearance_mode() else "default"
            style.theme_use(theme_name)
            style.configure("Custom.Treeview", background="#2b2b2b", fieldbackground="#2b2b2b", foreground="white", rowheight=30)
            style.configure("Custom.Treeview.Heading", background="#34495e", foreground="white")
            style.map("Custom.Treeview", background=[("selected", "#3c5a72")])
        except Exception: pass

        columns = ("number", "name", "code")
        self.indicators_tree = ttk.Treeview(tree_container, columns=columns, show="headings", selectmode="browse", style="Custom.Treeview")
        self.indicators_tree.grid(row=0, column=0, sticky="nsew")
        
        scrollbar = ctk.CTkScrollbar(tree_container, orientation="vertical", command=self.indicators_tree.yview)
        scrollbar.grid(row=0, column=1, sticky="ns", padx=(0, 2))
        self.indicators_tree.configure(yscrollcommand=scrollbar.set)

        self.indicators_tree.heading("number", text="№"); self.indicators_tree.column("number", width=50, anchor="center", stretch=False)
        self.indicators_tree.heading("name", text="Название"); 
        self.indicators_tree.column("name", width=0, minwidth=250, anchor="w", stretch=True) 
        self.indicators_tree.heading("code", text="Код"); self.indicators_tree.column("code", width=110, anchor="center", stretch=False)

        self.indicators_tree.bind("<Button-1>", self._on_tree_click, add="+")

        # === СТРОКА 4: ЭКСПОРТ ===
        bottom_frame = ctk.CTkFrame(self)
        bottom_frame.grid(row=4, column=0, columnspan=2, sticky="e", padx=10, pady=(0, 10))
        ctk.CTkButton(bottom_frame, text="Экспорт в WORD", fg_color="#2B2B2B", hover_color="#3C3C3C", command=self.print_to_docx).pack(side="right", padx=5)

        self.bind("<FocusOut>", self._on_window_focus_out)

        self.update_group_combobox()
        self.update_indicators_list()
        
        self.after(100, lambda: self.name_entry.focus_set())

    # --- МЕТОДЫ ПОДДЕРЖКИ ---

    def refresh_groups_ui(self):
        """Публичный метод для принудительной перерисовки списка групп"""
        self.update_group_combobox()

    def _on_group_selected(self, event=None):
        """
        Вызывается каждый раз, когда пользователь выбирает пункт в выпадающем списке.
        Принудительно очищает таблицу и загружает актуальные данные.
        """
        self.update_indicators_list()
    
    def _paste_via_pyperclip(self, event=None):
        try:
            clipboard_text = pyperclip.paste()
            if not clipboard_text:
                return "break"
                
            # Удаляем выделенный текст, если он есть
            try:
                self.name_entry.delete("sel.first", "sel.last")
            except tk.TclError:
                pass

            # Вставляем содержимое буфера
            self.name_entry.insert("insert", clipboard_text)
            
            # Возвращаем фокус и ставим курсор в конец текста
            self.name_entry.focus_set()
            self.name_entry.mark_set("insert", "end")
            
        except Exception as e:
            print(f"[CLIPBOARD] Error: {e}")
            
        return "break"

    def update_group_combobox(self):
        groups = group_list(); self.group_names_to_codes.clear()
        for g in groups: self.group_names_to_codes[g.name] = getattr(g, 'code', '')
        names = [g.name for g in groups]; self.group_combobox.configure(values=names)
        if names: self.group_combobox.set(names[0]); self.update_indicators_list()

    def update_indicators_list(self, event=None):
        selected_group_code = self.group_names_to_codes.get(self.group_combobox.get())
        indicators = get_all_indicators_by_group(selected_group_code) if selected_group_code else []
        
        for item in self.indicators_tree.get_children(): self.indicators_tree.delete(item)
            
        last_iid = None
        for idx, ind in enumerate(indicators, start=1):
            val_name = getattr(ind, 'name', '--')
            val_code = getattr(ind, 'code', '--')
            last_iid = val_code
            self.indicators_tree.insert("", "end", iid=val_code, values=(f"{idx}.", val_name, val_code))
            
        # Прокрутка к последнему добавленному элементу (Задача 4)
        if last_iid:
            self.indicators_tree.see(last_iid)

    # --- ЛОГИКА КНОПОК И ИНТЕРАКТИВА ---

    def add_indicator(self):
        group_code = self.group_names_to_codes.get(self.group_combobox.get())
        indicator_name = self.name_entry.get("1.0", "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", "end")
        except Exception as e: messagebox.showerror("Ошибка", str(e))

    def edit_indicator_select(self):
        selected = self.indicators_tree.selection()
        if not selected: 
            messagebox.showwarning("Ошибка", "Выберите показатель в таблице."); return

        code = selected[0]
        item = self.indicators_tree.item(code)
        current_name = item["values"][1].replace('\n', ' ')

        self.name_entry.delete("1.0", "end")
        self.name_entry.insert("1.0", current_name)
        
        # Используем правильное имя атрибута
        self._current_editing_code = code

        # Скрываем стандартные кнопки
        self.edit_btn.grid_remove()
        self.delete_btn.grid_remove()
        
        # Показываем новые кнопки В ТЕ ЖЕ КОЛОНКИ (col 1 и col 2)
        self.save_btn.grid(row=0, column=1, sticky="ew", padx=2, pady=2)
        self.cancel_edit_btn.grid(row=0, column=2, sticky="ew", padx=2, pady=2)
        
        self.save_btn.configure(state="normal")

    def update_indicator_name(self):
        # Исправлено с self._current_editing на self._current_editing_code
        if not getattr(self, '_current_editing_code', None): 
            messagebox.showwarning("Ошибка", "Сначала выберите элемент через 'Редактировать'."); return

        new_name = self.name_entry.get("1.0", "end").rstrip("\n").strip()
        if not new_name:
            messagebox.showwarning("Ошибка", "Название не может быть пустым."); return

        success = update_indicator_name(self._current_editing_code, new_name)
        
        if success:
            messagebox.showinfo("Готово", "Название обновлено.")
            self.update_indicators_list()
            self._reset_indicator_edit_mode()
        else:
            messagebox.showerror("Ошибка БД", "Не удалось обновить данные.")

    def _reset_indicator_edit_mode(self):
        # Проверяем наличие правильного атрибута
        if hasattr(self, '_current_editing_code'):
            delattr(self, '_current_editing_code')
                
        # Для CTkTextBox используем delete без координат
        self.name_entry.delete("1.0", "end") 

        # --- ЭТО УДАЛЕНО ---
        # self.code_entry.delete(0, "end")
        # self.code_entry.configure(state="normal")
        # -------------------

        self.save_btn.configure(state="disabled")
        self.save_btn.grid_remove()
        self.cancel_edit_btn.grid_remove()
        
        # Возвращаем кнопки на место. Обратите внимание: колонки grid_columnconfigure(action_frame) идут 0, 1, 2, 3.
        # add_btn сидит в 0, значит edit_btn должен сидеть в 1, а delete_btn в 2.
        self.edit_btn.grid(row=0, column=1, sticky="ew", padx=2, pady=2)
        self.delete_btn.grid(row=0, column=2, sticky="ew", padx=2, pady=2)
            
        self.indicators_tree.focus_set()

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

    def _on_tree_click(self, event):
        region = self.indicators_tree.identify_region(event.x, event.y)
        if region == "cell":
            column_id = self.indicators_tree.identify_column(event.x)
            if column_id == "#2": 
                item_id = self.indicators_tree.identify_row(event.y)
                if item_id:
                    full_text = self.indicators_tree.item(item_id)['values'][1]
                    
                    # Задача 3: Если тултип уже открыт — убиваем его перед созданием нового
                    if self._active_tooltip and self._active_tooltip.winfo_exists():
                        self._active_tooltip.destroy()
                    
                    tooltip = ctk.CTkToplevel(self)
                    self._active_tooltip = tooltip
                    tooltip.attributes("-topmost", True)
                    tooltip.overrideredirect(True)
                    
                    x = self.winfo_rootx() + event.x + 15
                    y = self.winfo_rooty() + event.y + 15
                    tooltip.geometry(f"+{x}+{y}")
                    
                    label = ctk.CTkLabel(tooltip, text=full_text, wraplength=400, justify="left")
                    label.pack(padx=10, pady=10)
                    
                    def on_leave(_): 
                        if self._active_tooltip and self._active_tooltip.winfo_exists():
                            self._active_tooltip.destroy()
                            self._active_tooltip = None
                    
                    tooltip.bind("<Leave>", on_leave)
                    
                    def keep_open(_): return "break"
                    tooltip.bind("<Button-1>", keep_open)

    def print_to_docx(self):
        group_name = self.group_combobox.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
        filename = f"{group_name.replace(' ', '_')}_{date.today().strftime('%Y-%m-%d')}.docx"
        path = filedialog.asksaveasfilename(initialfile=filename, defaultextension=".docx", filetypes=[("Word files", "*.docx"), ("All files", "*.*")])
        if not path: return
            
        success = save_docx_template(group_name, indicators, path)
        if success: messagebox.showinfo("Успех", f"Сохранено:\n{path}")
        else: messagebox.showerror("Ошибка записи", "Проверьте консоль или права доступа к папке.")

    def _on_window_focus_out(self, event):
        """Закрывает всплывающую подсказку, если фокус ушел из основного окна."""
        if self._active_tooltip and self._active_tooltip.winfo_exists():
            self._active_tooltip.destroy()
            self._active_tooltip = None

    def on_close(self): 
        # Чистим за собой при закрытии панели
        if self._active_tooltip and self._active_tooltip.winfo_exists():
            self._active_tooltip.destroy()
        self.destroy()
    
#====================================================













# ===============================================
# dukora/modules/manager/manager_metrics_work.py
# ===============================================
import os
import sqlite3
from typing import List, Any
from contextlib import contextmanager
from modules.data.database_module import get_db_connection
from typing import List, Any


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]

# === ИСПРАВЛЕННАЯ ФУНКЦИЯ ОБНОВЛЕНИЯ ДЛЯ SQLITE ===
def update_indicator_name(code: str, new_name: str) -> bool:
    """
    Обновляет название индикатора по его уникальному коду через SQLite.
    """
    try:
        with _get_cursor() as cursor:
            # Сначала проверяем, существует ли такой индикатор
            cursor.execute("SELECT name FROM indicators WHERE code = ?", (code,))
            result = cursor.fetchone()
            
            if not result:
                print(f"[DB] Индикатор с кодом {code} не найден.")
                return False
                
            old_name = result[0]
            
            # Выполняем обновление
            cursor.execute("UPDATE indicators SET name = ? WHERE code = ?", (new_name.strip(), code))
            
            # commit произойдет автоматически благодаря @contextmanager
            print(f"[DB] Название обновлено: '{old_name}' -> '{new_name.strip()}'")
            return True
    except Exception as e:
        # Rollback сработает автоматически благодаря декораторам контекста
        print(f"[DB FATAL] Ошибка при обновлении индикатора: {repr(e)}")
        return False


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[Any], path: str) -> bool:
    if not indicators:
        return False

    try:
        from docx import Document
        from docx.shared import Cm, Pt
        from docx.enum.section import WD_ORIENTATION
        from docx.enum.text import WD_ALIGN_PARAGRAPH
        from docx.enum.table import WD_TABLE_ALIGNMENT, WD_CELL_VERTICAL_ALIGNMENT

        doc = Document()
        
        # --- СТРАНИЦА ---
        section = doc.sections[0]
        section.orientation = WD_ORIENTATION.LANDSCAPE
        section.page_width = Cm(29.7)
        section.page_height = Cm(21.0)
        section.left_margin = Cm(3.0)
        section.right_margin = Cm(2.0)
        section.top_margin = Cm(2.0)
        section.bottom_margin = Cm(2.0)

        # --- СТИЛЬ ---
        style = doc.styles['Normal']
        font = style.font
        font.name = 'Times New Roman'
        font.size = Pt(12)

        # --- ЗАГОЛОВОК ---
        p = doc.add_paragraph(f'Показатели группы: {group_name}')
        p.alignment = WD_ALIGN_PARAGRAPH.CENTER
        p.runs[0].font.bold = True
        p.runs[0].font.size = Pt(14)
        doc.add_paragraph(' ')

        # --- ТАБЛИЦА ---
        table = doc.add_table(rows=len(indicators) + 1, cols=3)
        table.style = 'Table Grid'
        table.autofit = False
        table.alignment = WD_TABLE_ALIGNMENT.CENTER

        col_widths_cm = [2.0, 2.0, 20.5] 
        
        for i in range(3):
            for cell in table.columns[i].cells:
                cell.width = Cm(col_widths_cm[i])

        # Шапка
        hdr_cells = table.rows[0].cells
        titles = ['№ п/п', 'Код', 'Наименование']
        for i in range(3):
            hdr_cells[i].text = titles[i]
            para = hdr_cells[i].paragraphs[0]
            para.alignment = WD_ALIGN_PARAGRAPH.CENTER
            run = para.runs[0]
            run.bold = True
            run.font.size = Pt(14)
            run.font.name = 'Times New Roman'

        # Данные
        for idx, ind in enumerate(indicators, start=1):
            row_cells = table.rows[idx].cells
            
            for i, cell in enumerate(row_cells):
                cell.width = Cm(col_widths_cm[i])
                cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.TOP

            row_cells[0].text = str(idx)
            row_cells[1].text = ind.code
            row_cells[2].text = ind.name.replace('\\n', '\n')
            
            for cell in row_cells:
                for para in cell.paragraphs:
                    for run in para.runs:
                        run.font.name = 'Times New Roman'
                        run.font.size = Pt(12)
                        
        doc.save(path)
        return True
    except Exception as e:
        print(f"[WORK LOGIC FATAL] Ошибка: {repr(e)}")
        return False
        
# ===============================================