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


# Dozor\modules\manager\manager_group_inter.py
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, messagebox
from modules.manager.manager_group_work import list_groups, add_group, delete_group
from modules.utils.window_behavior import LevelThreeBehavior
from modules.utils.app_state import disable_app, enable_app


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

        behavior = LevelThreeBehavior()
        behavior.setup_level_three(self)

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

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

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

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

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

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

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


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


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

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

    def on_close(self):
        self.destroy()

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















# Dozor\modules\manager\manager_group_work.py
# -*- coding: utf-8 -*-
import sqlite3
from typing import List
from contextlib import contextmanager
from modules.data.database_module import get_db_connection

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

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

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

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

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

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












# Dozor\modules\manager\manager_metrics_inter.py
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from docx import Document
from modules.manager.manager_metrics_work import (
    get_all_indicators_by_group,
    insert_indicator,
    delete_indicator_by_code,
    IndicatorDTO,
    generate_unique_code,
    save_docx_template
)
from modules.manager.manager_group_work import list_groups as group_list
from modules.utils.window_behavior import LevelTwoBehavior

class ManagerMetricsInter(tk.Toplevel):
    def __init__(self, parent_facade):
        super().__init__(parent_facade.master_root) 
        
        self.parent_main = parent_facade
        
        # === ИСПРАВЛЕНО: Создаем словарь СРАЗУ при создании объекта ===
        self.group_names_to_codes = {}
        
        behavior = LevelTwoBehavior()
        behavior.setup_level_two(self)
        
        self.title("Менеджер показателей")
        self.geometry("600x450")
        
        if not hasattr(self.parent_main, 'current_user') or not self.parent_main.current_user.get("login"):
            messagebox.showwarning("Ошибка", "Сессия пользователя не найдена.")
            self.destroy()
            return
            
        self.create_widgets()
        self.update_group_combobox()
        self.update_indicators_list()
        
        self.update_idletasks() 
        screen_width = self.winfo_screenwidth()
        screen_height = self.winfo_screenheight()
        size = tuple(int(_) for _ in self.geometry().split('+')[0].split('x'))
        x = screen_width // 2 - size[0] // 2
        y = screen_height // 2 - size[1] // 2
        self.geometry(f"+{x}+{y}")

    def create_widgets(self):
        top_panel = tk.Frame(self)
        top_panel.pack(padx=10, pady=10, fill="x")
        
        ttk.Label(top_panel, text="Группа:").pack(anchor="w")
        self.group_var = tk.StringVar()
        self.group_combobox = ttk.Combobox(top_panel, textvariable=self.group_var, state="readonly", width=30)
        self.group_combobox.pack(fill="x", pady=5)
        self.group_combobox.bind("<<ComboboxSelected>>", self.update_indicators_list)
        
        ttk.Label(top_panel, text="Название показателя:").pack(anchor="w")
        self.name_entry = tk.Text(top_panel, height=3, width=40)
        self.name_entry.pack(fill="x", pady=5)
        
        paste_button = tk.Button(top_panel, text="Вставить", command=self.paste_from_clipboard)
        paste_button.pack()
        
        buttons_frame = tk.Frame(top_panel)
        buttons_frame.pack(fill="x", pady=5)
        
        ttk.Button(buttons_frame, text="Добавить", command=self.add_indicator).pack(side="left", padx=2)
        ttk.Button(buttons_frame, text="Удалить", command=self.delete_indicator).pack(side="left", padx=2)
        ttk.Button(buttons_frame, text="Экспорт в Word", command=self.print_to_docx).pack(side="left", padx=5, pady=5)
        
        middle_panel = tk.Frame(self)
        middle_panel.pack(fill="both", expand=True, padx=10, pady=5)
        
        scrollbar = ttk.Scrollbar(middle_panel)
        scrollbar.pack(side="right", fill="y")
        
        columns = ("number", "name", "code")
        self.indicators_tree = ttk.Treeview(
            middle_panel, columns=columns, show="headings", yscrollcommand=scrollbar.set, selectmode="browse"
        )
        self.indicators_tree.heading("number", text="№"); self.indicators_tree.column("number", width=50, anchor="center")
        self.indicators_tree.heading("name", text="Название"); self.indicators_tree.column("name", width=300)
        self.indicators_tree.heading("code", text="Код"); self.indicators_tree.column("code", width=150)
        self.indicators_tree.pack(fill="both", expand=True, side="left")
        scrollbar.config(command=self.indicators_tree.yview)

    def update_group_combobox(self):
        groups = group_list()
        self.group_names_to_codes.clear() # Очищаем перед заполнением
        for group in groups:
            self.group_names_to_codes[group.name] = getattr(group, 'code', '')
        
        group_names = [g.name for g in groups]
        self.group_combobox["values"] = group_names
        if group_names:
            self.group_combobox.current(0)
        self.update_indicators_list()

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

    def paste_from_clipboard(self):
        try:
            clipboard_text = self.clipboard_get()
            self.name_entry.insert(tk.END, clipboard_text)
        except tk.TclError:
            pass

    def add_indicator(self):
        group_name = self.group_var.get()
        group_code = self.group_names_to_codes.get(group_name)
        indicator_name = self.name_entry.get("1.0", tk.END).rstrip("\n").strip()
        
        if not group_code or not indicator_name:
            messagebox.showwarning("Ошибка", "Выберите группу и введите название!")
            return
            
        new_code = generate_unique_code(group_code)
        
        try:
            insert_indicator(new_code, indicator_name, group_code)
            messagebox.showinfo("Готово", "Индикатор добавлен.")
            self.update_indicators_list() 
            self.name_entry.delete("1.0", tk.END)
        except Exception as e:
            messagebox.showerror("Ошибка", str(e))

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

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

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

    def print_to_docx(self):
        group_name = self.group_var.get()
        if not group_name:
            messagebox.showwarning("Ошибка", "Выберите группу.")
            return
            
        group_code = self.group_names_to_codes.get(group_name)
        indicators = get_all_indicators_by_group(group_code)
        if not indicators:
            messagebox.showwarning("Ошибка", "В группе нет показателей.")
            return
            
        filename = f"{group_name.replace(' ', '_')}_indicators.docx"
        path = None
        try:
            path = filedialog.asksaveasfilename(initialfile=filename, defaultextension=".docx",
                                               filetypes=[("Word files", "*.docx"), ("All files", "*.*")])
        except Exception:
            path = "test_export.docx"
            
        if path:
            try:
                from docx import Document
                doc = Document()
                doc.add_heading(f'Показатели группы: {group_name}', 0)
                
                test_data = [
                    type('obj', (object,), {'name': 'Пример 1', 'code': 'T-1'})(),
                    type('obj', (object,), {'name': 'Пример 2', 'code': 'T-2'})()
                ]
                
                for idx, ind in enumerate(test_data, start=1):
                    doc.add_paragraph(f"{idx}. [{getattr(ind, 'code', '--')}] {getattr(ind, 'name', '--')}")
                
                doc.save(path)
                messagebox.showinfo("Успех", f"Файл сохранен:\n{path}")
            except Exception as e:
                messagebox.showerror("Ошибка", f"Docx Error: {e}")

    def on_close(self):
        self.destroy()

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














# Dozor\modules\manager\manager_metrics_work.py
# -*- coding: utf-8 -*-
import os
import sqlite3
from typing import List
from contextlib import contextmanager
from modules.data.database_module import get_db_connection

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

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

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

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

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

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

def save_docx_template(group_name: str, indicators: List[IndicatorDTO], path: str) -> bool:
    print(f"[WORK LOGIC] Вызов: save_docx_template | Путь: {path} | Группа: {group_name}")
    
    if not indicators:
        print("[WORK LOGIC ERROR] Список показателей пуст.")
        return False
    try:
        from docx import Document
        
        dir_path = os.path.dirname(path)
        if dir_path and not os.path.exists(dir_path):
            os.makedirs(dir_path, exist_ok=True)
            
        doc = Document()
        doc.add_heading(f'Группа: {group_name}', 0)
        
        table = doc.add_table(rows=1, cols=3)
        hdr_cells = table.rows[0].cells
        hdr_cells[0].text = '№ п/п'
        hdr_cells[1].text = 'Код'
        hdr_cells[2].text = 'Наименование'
        
        for idx, ind in enumerate(indicators, start=1):
            cells = table.add_row().cells
            cells[0].text = str(idx)
            cells[1].text = ind.code
            cells[2].text = ind.name
            
        doc.save(path)
        return True
    except ImportError:
        print("[WORK LOGIC ERROR] Библиотека python-docx не установлена. Выполните: pip install python-docx")
        return False
    except Exception as e:
        print(f"[WORK LOGIC ERROR] Не удалось сохранить файл: {e}")
        return False















# Dozor\modules\manager\manager_ui.py
# -*- coding: utf-8 -*-
import tkinter as tk

class ManagerPanel(tk.Frame):
    def __init__(self, master, current_user):
        super().__init__(master, bg="#F5F5F5") 
        self.master_root = master
        self.current_user = current_user
        
        print("[DEBUG] ManagerPanel создан.")

        self.grid_rowconfigure(0, weight=1)
        self.grid_rowconfigure(1, weight=5)
        self.grid_columnconfigure(0, weight=1)

        self._create_header()
        self._create_body()

    def _create_header(self):
        header = tk.Frame(self, bg="#34495e", height=50)
        header.pack(fill="x", side="top")
        
        title = tk.Label(header, text="Менеджер базы", bg="#34495e", fg="white", font=("Arial", 16))
        title.pack(side="left", padx=20, pady=10)
        
        back_btn = tk.Button(header, text="← Назад", command=self.destroy, bg="#7f8c8d", fg="white")
        back_btn.pack(side="right", padx=20, pady=10)

    def _create_body(self):
        body = tk.Frame(self, bg="white")
        body.pack(fill="both", expand=True, padx=30, pady=30)
        
        body.grid_columnconfigure(0, weight=1)
        body.grid_columnconfigure(1, weight=1)

        group_btn = tk.Button(body, text="Менеджер группы", font=("Arial", 14), width=18, height=2,
                              command=self.open_group_module,
                              bg="#3498db", fg="white", relief="raised", bd=2)
        group_btn.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")

        metrics_btn = tk.Button(body, text="Менеджер\nпоказателей", font=("Arial", 14), width=18, height=3,
                                command=self.open_metrics_module,
                                bg="#27ae60", fg="white", relief="raised", bd=2, wraplength=120)
        metrics_btn.grid(row=0, column=1, padx=10, pady=10, sticky="nsew")

    def open_group_module(self):
        print("[LOG] Нажата кнопка 'Менеджер группы'.")
        try:
            from modules.manager.manager_group_inter import ManagerGroupInter
            
            win = ManagerGroupInter(self.master_root) 
            self.master_root.wait_window(win)
        
        except Exception as e:
            print(f"[FATAL CRASH IN GROUP MODULE] {e}")
            from tkinter import messagebox
            messagebox.showerror("Ошибка", f"Не удалось открыть Менеджер групп:\n{e}")

    def open_metrics_module(self):
        print("[ACTION] Открытие Менеджера Показателей...")
        try:
            from modules.manager.manager_metrics_inter import ManagerMetricsInter
            
            if hasattr(self.master_root, 'register_child_window'):
                print("[WARNING] Нашли старый register_child_window, игнорируем его.")
            
            win = ManagerMetricsInter(self.master_root) 
            self.master_root.wait_window(win)
        
        except Exception as e:
            print(f"[FATAL CRASH IN METRICS MODULE] {e}")
            from tkinter import messagebox
            messagebox.showerror("Ошибка", f"Не удалось открыть модуль:\n{e}")

















# Dozor\modules\utils\window_behavior.py
import tkinter as tk


class LevelOneBehavior:
    """
    Поведение Главного окна (Уровень 1). Применяется к объекту класса tk.Tk.
    Движение работает ТОЛЬКО по панели заголовка (title_bar).
    """
    def setup_level_one(self, target_window):
        target_window.resizable(False, False)
        
        # Ищем панель заголовка для перетаскивания главного окна
        try:
            self.drag_widget = target_window.children['title_bar']
        except KeyError:
            self.drag_widget = target_window

        self.drag_widget.bind("<ButtonPress-1>", self.save_offset)
        self.drag_widget.bind("<B1-Motion>", self.drag_window)
        
        target_window.bind("<Double-Button-1>", lambda e: None)
        
        if not hasattr(target_window, '_child_windows'):
            target_window._child_windows = []
            
        self._drag_start_x = 0
        self._drag_start_y = 0

    def save_offset(self, event):
        self._drag_start_x = event.x_root - self.winfo_x()
        self._drag_start_y = event.y_root - self.winfo_y()

    def drag_window(self, event):
        x = event.x_root - self._drag_start_x
        y = event.y_root - self._drag_start_y
        self.geometry(f"+{x}+{y}")
        
        if hasattr(self, '_update_child_positions'):
            self._update_child_positions(x, y)

    def register_child_window(self, window, offset_x=0, offset_y=0):
        self._child_windows.append((window, offset_x, offset_y))

    def do_nothing(self, event=None):
        pass


class LevelTwoBehavior:
    """
    Поведение окон второго уровня (привязаны к родителю).
    Окно центрируется над главным и едет строго вместе с ним.
    РЕЖИМ БЕЗОПАСНОСТИ: Игнорируем кастомные методы регистрации root-а.
    """
    def setup_level_two(self, target_window):
        width, height = int(target_window.winfo_reqwidth()), int(target_window.winfo_reqheight())
        target_window.minsize(width, height)
        target_window.resizable(True, True)
        
        # Просто берем родителя окна (это наш ManagerPanel/Facade)
        parent = target_window.master
        p_x = parent.winfo_rootx()
        p_y = parent.winfo_rooty()
        p_w = parent.winfo_width()
        p_h = parent.winfo_height()
        
        win_w = target_window.winfo_reqwidth()
        win_h = target_window.winfo_reqheight()
        
        x = p_x + (p_w - win_w) // 2
        y = p_y + (p_h - win_h) // 2
        target_window.geometry(f"+{x}+{y}")
        
        target_window.update_idletasks()
        
        # === КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ ===
        # Весь блок с register_child_window УДАЛЕН.
        # Если родитель хочет знать о нас, он сам вызовет wait_window().
    def do_nothing(self, event=None):
        pass

    def save_offset(self, event, window):
        self._start_screen_x = event.x_root
        self._start_screen_y = event.y_root
        self._start_win_x = window.winfo_rootx()
        self._start_win_y = window.winfo_rooty()
        parent = window.master
        self._start_parent_x = parent.winfo_rootx()
        self._start_parent_y = parent.winfo_rooty()

    def drag_with_parent(self, event, child_win, parent_win):
        delta_x = event.x_root - self._start_screen_x
        delta_y = event.y_root - self._start_screen_y

        new_parent_x = self._start_parent_x + delta_x
        new_parent_y = self._start_parent_y + delta_y
        parent_win.geometry(f"+{new_parent_x}+{new_parent_y}")

        offset_x = self._start_win_x - self._start_parent_x
        offset_y = self._start_win_y - self._start_parent_y
        
        target_child_x = new_parent_x + offset_x
        target_child_y = new_parent_y + offset_y
        child_win.geometry(f"+{target_child_x}+{target_child_y}")


class LevelThreeBehavior:
    """
    Поведение окон третьего уровня (Менеджер групп).
    Свободное перемещение. 
    Чтобы выделить текст, просто кликай мышкой.
    Чтобы двигать окно, зажми ALT + ЛКМ.
    """
    def setup_level_three(self, target_window):
        width, height = int(target_window.winfo_reqwidth()), int(target_window.winfo_reqheight())
        target_window.minsize(width, height)
        target_window.resizable(True, True)
        target_window.geometry("+0+0")
        
        target_window.protocol("WM_MINIMIZE_WINDOW", self.do_nothing)
        target_window.protocol("WM_DELETE_WINDOW", target_window.destroy)
        
        # Снимаем старые бинды
        target_window.unbind("<ButtonPress-1>")
        target_window.unbind("<B1-Motion>")
        
        # 1. Сохраняем точку старта. Передаем 'target_window' вторым аргументом через lambda
        target_window.bind(
            "<ButtonPress-1>", 
            lambda e, tw=target_window: self.save_offset(e, tw), 
            add='+'
        )
        
        # 2. Движение работает ТОЛЬКО если зажата клавиша Alt (маска 0x0004)
        target_window.bind(
            "<B1-Motion>", 
            lambda e, tw=target_window: self.drag_free(e, tw) if e.state & 0x0004 else "", 
            add='+'
        )

    def do_nothing(self, event=None):
        pass

    def save_offset(self, event, window):
        # Запоминаем ГДЕ НА ЭКРАНЕ была нажата мышь
        self._start_screen_x = event.x_root
        self._start_screen_y = event.y_root
        
        # Запоминаем, где В ЭТОТ МОМЕНТ было окно (его угол) ПО ЭКРАНУ
        self._start_win_x = window.winfo_rootx()
        self._start_win_y = window.winfo_rooty()

    def drag_free(self, event, window):
        dx = event.x_root - self._start_screen_x
        dy = event.y_root - self._start_screen_y
        new_x = self._start_win_x + dx
        new_y = self._start_win_y + dy
        window.geometry(f"+{new_x}+{new_y}")