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


В МЕНЕДЖЕРЕ БАЗЫ вроде бы все работает, и в то же время не работает.
После ввода названия группы она не сразу появляется.
При выборе из списка группы - список показателей не обновляется.
Бывает нажимаешь на группу в списке - она не открывается.

Проверь файлы на ошибки и дай нужные исправления. 

Учти, что я вайб кодер, замену кусками не давай - желательно метод на метод.


#==============================================
# dukora\modules\manager\manager_group_panel.py
#==============================================
import customtkinter as ctk
from tkinter import ttk, messagebox
from modules.manager.manager_group_work import list_groups, add_group, edit_group, delete_group

class ManagerGroupPanel(ctk.CTkFrame):
    def __init__(self, master):
        super().__init__(master)
        
        self._current_editing_id = None # Для хранения ID редактируемой группы

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

        # === 1. ЛЕВАЯ ЧАСТЬ: ФОРМА ВВОДА ===
        form_frame = ctk.CTkFrame(self, corner_radius=10)
        form_frame.grid(row=0, column=0, sticky="nsew", padx=(10, 5), pady=10)
        
        form_frame.grid_columnconfigure(0, weight=0)
        form_frame.grid_columnconfigure(1, weight=1)

        ctk.CTkLabel(form_frame, text="Название:", anchor="w").grid(row=0, column=0, sticky="w", padx=5, pady=(5, 0))
        self.name_entry = ctk.CTkEntry(form_frame)
        self.name_entry.grid(row=0, column=1, sticky="ew", padx=5, pady=5)

        ctk.CTkLabel(form_frame, text="Код:", anchor="w").grid(row=1, column=0, sticky="w", padx=5, pady=(5, 0))
        self.code_entry = ctk.CTkEntry(form_frame)
        self.code_entry.grid(row=1, column=1, sticky="ew", padx=5, pady=5)

        gain_frame = ctk.CTkFrame(form_frame)
        gain_frame.grid(row=2, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
        ctk.CTkLabel(gain_frame, text="Усиление:").pack(side="left")
        self.gain_var = ctk.StringVar(value="1.0")
        ctk.CTkEntry(gain_frame, textvariable=self.gain_var, width=60).pack(side="left", padx=(5, 0))

        # === 2. ПРАВАЯ ЧАСТЬ: КНОПКИ УПРАВЛЕНИЯ ===
        action_frame = ctk.CTkFrame(self)
        action_frame.grid(row=0, column=2, sticky="n", padx=5, pady=10)
        
        action_frame.grid_columnconfigure(0, weight=1)
        
        # Кнопка Сохранить (создаем сразу, но прячем)
        self.save_btn = ctk.CTkButton(action_frame, text="Сохранить", fg_color="#8B4513", hover_color="#A0522D", command=self.save_group, state="disabled")
        self.save_btn.pack(fill="x", padx=5, pady=5)
        
        # Остальные кнопки
        ctk.CTkButton(action_frame, text="Добавить", fg_color="#3B82F6", command=self.add_group).pack(fill="x", padx=5, pady=5)
        ctk.CTkButton(action_frame, text="Редактировать", fg_color="#10B981", command=self.edit_group_select).pack(fill="x", padx=5, pady=5)
        ctk.CTkButton(action_frame, text="Удалить", fg_color="#EF4444", command=self.delete_group).pack(fill="x", padx=5, pady=5)

        # === 3. НИЖНЯЯ ЧАСТЬ: ТАБЛИЦА ===
        tree_container = ctk.CTkFrame(self)
        tree_container.grid(row=1, column=0, columnspan=4, sticky="nsew", padx=10, pady=(0, 10))
        
        tree_container.grid_columnconfigure(0, weight=1)
        tree_container.grid_rowconfigure(0, weight=1)
        
        columns = ("code", "name", "gain")
        self.tree = ttk.Treeview(tree_container, columns=columns, show="headings")
        self.tree.heading("code", text="Код"); self.tree.column("code", width=12, anchor="center")
        self.tree.heading("gain", text="Усиление"); self.tree.column("gain", width=7, anchor="center")
        self.tree.heading("name", text="Название"); self.tree.column("name", anchor="w")
        self.tree.pack(fill="both", expand=True, side="left")
        
        scrollbar = ctk.CTkScrollbar(tree_container, orientation="vertical", command=self.tree.yview)
        scrollbar.pack(side="right", fill="y")
        self.tree.configure(yscrollcommand=scrollbar.set)
        
        self.update_tree()

    # --- МЕТОДЫ ПОДДЕРЖКИ ---
    
    def update_tree(self):
        for item in self.tree.get_children():
            self.tree.delete(item)
            
        try:
            groups = list_groups()
            for group in groups:
                gain_text = f"{group.gain_group:.1f}" if group.gain_group is not None else ""
                self.tree.insert("", "end", iid=str(group.id), values=(group.code, group.name, gain_text))
        except Exception as e:
            messagebox.showerror("Ошибка БД", str(e))

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

    def clear_form(self):
        self.name_entry.delete(0, "end")
        self.code_entry.delete(0, "end")
        self.gain_var.set("1.0")
        self.code_entry.configure(state="normal") # Разблокируем при очистке
        self.reset_ui_mode()

    def reset_ui_mode(self):
        """Возвращает UI в состояние 'по умолчанию'"""
        self._current_editing_id = None
        self.save_btn.configure(state="disabled")
        # Показываем Редактировать обратно
        for child in self.save_btn.master.winfo_children():
            if isinstance(child, ctk.CTkButton) and child.cget("text") == "Редактировать":
                child.pack(fill="x", padx=5, pady=5)
        
        self.save_btn.pack_forget() # Прячем Сохранить

    # --- ЛОГИКА КНОПОК ---

    def add_group(self):
        name = self.name_entry.get().strip()
        code = self.code_entry.get().strip()
        try:
            gain_val = float(self.gain_var.get().replace(',', '.'))
        except ValueError:
            gain_val = 0.0
            
        if not all([name, code]):
            messagebox.showwarning("Ошибка", "Заполните название и код.")
            return
            
        try:
            add_group(name, code, gain_val)
            messagebox.showinfo("Успех", "Группа добавлена.")
            self.update_tree(); self.clear_form()
        except Exception as e:
            messagebox.showerror("Ошибка БД", str(e))

    def edit_group_select(self):
        data = self.get_selected()
        if not data:
            messagebox.showwarning("Ошибка", "Выберите группу."); return
            
        # Заполняем форму для редактирования
        self.name_entry.delete(0, "end"); self.name_entry.insert(0, data['name'])
        self.code_entry.delete(0, "end"); self.code_entry.insert(0, data['code'])
        
        # Блокируем Код
        self.code_entry.configure(state="disabled")
        
        # Усиление
        val = data['gain']
        self.gain_var.set(val if val else "1.0")
        
        # Меняем режим интерфейса
        self._current_editing_id = data['id']
        
        # Скрываем кнопку Редактировать и показываем Сохранить
        for child in self.save_btn.master.winfo_children():
            if isinstance(child, ctk.CTkButton) and child.cget("text") == "Редактировать":
                child.pack_forget()
                
        self.save_btn.configure(state="normal")
        self.save_btn.pack(fill="x", padx=5, pady=5)

    def save_group(self):
        if not self._current_editing_id:
            return
            
        name = self.name_entry.get().strip()
        
        # Код берем из поля, даже если оно disabled, либо храним его отдельно
        current_data = self.get_selected()
        if not current_data: return
        code = current_data['code'] 
        
        # Безопасный парсинг усиления
        try:
            gain_val = float(self.gain_var.get().replace(',', '.'))
        except ValueError:
            messagebox.showwarning("Ошибка", "Усиление должно быть числом. Значение сброшено.")
            self.gain_var.set("1.0")
            return

        if not name:
            messagebox.showwarning("Ошибка", "Название не может быть пустым."); return
            
        try:
            edit_group(self._current_editing_id, name, code, gain_val)
            messagebox.showinfo("Готово", "Группа обновлена.")
            self.update_tree()
            self.clear_form()
        except Exception as e:
            messagebox.showerror("Ошибка БД", str(e))

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










# ==============================================
# dukora/modules/manager/manager_group_work.py
# ==============================================
import sqlite3
from typing import List
from contextlib import contextmanager
from modules.data.database_module import get_db_connection

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

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

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

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

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

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

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
















# ==============================================
# 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=1); self.grid_columnconfigure(1, weight=1)
        self.grid_rowconfigure(0, weight=0); self.grid_rowconfigure(1, weight=0)
        self.grid_rowconfigure(2, weight=0); self.grid_rowconfigure(3, weight=10) 
        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")
        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", padx=10, pady=(5, 10))
        
        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 = tk.Text(
            input_frame, height=4, width=1, wrap="word", bd=1, relief="solid", 
            bg="white", fg="black", insertbackground="black", font=("Segoe UI", 10)
        )
        self.name_entry.grid(row=1, column=0, sticky="ew", padx=(0, 5), pady=5)

        # Разрешаем правый клик мыши
        self.name_entry.bind("<Button-3>", lambda e: "break")
        
        # Разрешаем вставку через Ctrl+V / Shift+Insert
        self.name_entry.bind("<Control-v>", lambda e: self._paste_via_pyperclip())
        self.name_entry.bind("<Shift-Insert>", lambda e: self._paste_via_pyperclip())

        paste_btn = ctk.CTkButton(input_frame, text="Вставить", fg_color="#2B2B2B", hover_color="#3C3C3C", command=self._paste_via_pyperclip)
        paste_btn.grid(row=1, column=1, sticky="ns", 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=5)
        
        # Распределяем пространство поровну между 4 кнопками
        for i in range(4): 
            action_frame.grid_columnconfigure(i, weight=1, uniform="actions")

        ctk.CTkButton(action_frame, text="Добавить", fg_color="#3B82F6", command=self.add_indicator).pack(side="left", fill="both", expand=True, padx=2, pady=2)
        ctk.CTkButton(action_frame, text="Редактировать", fg_color="#10B981", command=self.edit_indicator_select).pack(side="left", fill="both", expand=True, padx=2, pady=2)
        
        # Изменен цвет на коричневый (#8B4513 - SaddleBrown)
        self.save_btn = ctk.CTkButton(action_frame, text="Сохранить", state="disabled", fg_color="#8B4513", hover_color="#A0522D", command=self.update_indicator_name)
        self.save_btn.pack(side="left", fill="both", expand=True, padx=2, pady=2)
        
        ctk.CTkButton(action_frame, text="Удалить", fg_color="#EF4444", command=self.delete_indicator).pack(side="left", fill="both", expand=True, padx=2, pady=2)

        # === СТРОКА 3: ТАБЛИЦА ДАННЫХ ===
        tree_container = ctk.CTkFrame(self, fg_color="transparent")
        tree_container.grid(row=3, column=0, columnspan=2, sticky="nsew", padx=10, pady=(0, 10))
        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)

        # Закрытие тултипа при потере фокуса окном (Задача 3)
        self.bind("<FocusOut>", self._on_window_focus_out)

        self.update_group_combobox()
        self.update_indicators_list()

    # --- МЕТОДЫ ПОДДЕРЖКИ ---
    
    def _paste_via_pyperclip(self, event=None):
        try:
            clipboard_text = pyperclip.paste()
            if clipboard_text:
                self.name_entry.delete("sel.first", "sel.last")
                self.name_entry.insert("insert", clipboard_text)
        except Exception as e:
            pass
        return None

    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.save_btn.configure(state="normal")

    def update_indicator_name(self):
        if not self._current_editing_code: 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.save_btn.configure(state="disabled"); delattr(self, '_current_editing_code')
        else: messagebox.showerror("Ошибка БД", "Не удалось обновить данные.")

    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

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








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