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


# Dozor/gui/main_interface.py

import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageTk
import os
from modules.administrator.admin_ui import AdminPanel

class MainInterface(tk.Tk):
    def __init__(self):
        super().__init__()
        self.current_user = {"login": None}
        
        self.geometry("458x360")
        self.resizable(False, False)
        bg_color = "#EEEEEE"
        self.configure(bg=bg_color)
        
        main_frame = tk.Frame(self, bg=bg_color)
        main_frame.pack(padx=1, pady=1, fill='both', expand=True)
        
        button_width = 20
        button_height = 2
        button_font = ("Arial", 12, "bold")
        
        for i in range(7):
            main_frame.grid_rowconfigure(i, weight=0)
            main_frame.grid_columnconfigure(0, minsize=210, weight=0)
            main_frame.grid_columnconfigure(1, minsize=235, weight=0)

        # --- ЛЕВАЯ ПАНЕЛЬ ---
        left_buttons = [
            ("Администратор", self._show_admin_menu),
            ("Менеджер базы", self._show_manager_menu), 
            ("Эксперты", self._show_experts_menu),
            ("Проект", self._show_project_menu),
            ("Прогнозирование", self._show_forecast_menu)
        ]
        
        for row_index, (btn_text, command_func) in enumerate(left_buttons):
            btn = tk.Button(
                main_frame,
                text=btn_text,
                font=button_font,
                width=button_width,
                height=button_height,
                anchor="w",
                command=command_func
            )
            btn.grid(row=row_index, column=0, padx=5, pady=5, sticky="ew")

        # --- ПРАВАЯ ПАНЕЛЬ (СОВА) ---
        right_box = tk.Frame(main_frame, bg=bg_color, width=235, height=300)
        right_box.grid(row=0, column=1, rowspan=6, pady=5, sticky="nsew", padx=(0, 5))
        right_box.grid_propagate(False)
        right_box.columnconfigure(0, weight=1)
        
        picture_frame = tk.Frame(right_box, bg="#d9ead3", highlightbackground="gray",
                                 highlightthickness=1, width=225, height=160)
        picture_frame.place(relx=0.5, y=5, anchor="n")
        picture_frame.grid_propagate(False)
        
        current_dir = os.path.dirname(os.path.abspath(__file__))
        image_path = os.path.join(current_dir, "images", "Сова1.png")
        try:
            original_img = Image.open(image_path)
            target_w, target_h = 215, 130
            img_ratio = original_img.width / original_img.height
            box_ratio = target_w / target_h
            if img_ratio > box_ratio:
                new_w, new_h = target_w, int(target_w / img_ratio)
            else:
                new_w, new_h = int(target_h * img_ratio), target_h
            resized_img = original_img.resize((new_w, new_h), resample=Image.LANCZOS)
            self.tk_image = ImageTk.PhotoImage(resized_img)
            label = tk.Label(picture_frame, image=self.tk_image, bg="#d9ead3", borderwidth=0)
            label.image = self.tk_image
            label.place(relx=0.5, rely=0.5, anchor='center', width=new_w, height=new_h)
        except Exception as e:
            print(f"[FATAL IMAGE ERROR]: {e}")
            canvas = tk.Canvas(picture_frame, width=225, height=160, bg="red", highlightthickness=0)
            canvas.create_text(112, 80, text="IMG\nFAIL", fill="white")
            canvas.place(x=0, y=0, relwidth=1, relheight=1)
            
        spacer = tk.Frame(right_box, bg=bg_color, height=1)
        spacer.grid(row=0, column=0, pady=(170, 10), sticky="ew")
        
        upper_padding = tk.Frame(right_box, bg=bg_color, height=3)
        upper_padding.grid(row=1, column=0)
        
        btn6 = tk.Button(right_box, text="Руководство", font=button_font, width=button_width, height=button_height)
        btn6.grid(row=2, column=0, ipadx=10, pady=(0, 5), sticky="ew")
        btn7 = tk.Button(right_box, text="О программе", font=button_font, width=button_width, height=button_height)
        btn7.grid(row=3, column=0, ipadx=10, pady=(5, 15), sticky="ew")

        footer_frame = tk.Frame(main_frame, bg="#0078D7")
        footer_frame.grid(row=6, column=0, columnspan=2, sticky="ew", pady=(10, 0))
        footer_label = tk.Label(footer_frame, text="Copyright © Минск, НИИ, 2027 год", fg="#FF8C00",
                                bg="#0078D7", font=("Arial", 12, "bold"))
        footer_label.pack(expand=True, ipady=4)

        # --- КОНТЕЙНЕРЫ ДЛЯ ПОДМЕНЮ ---
        self.menu_container = tk.Frame(main_frame, bg=bg_color)
        self.menu_container.grid(row=0, column=0, rowspan=6, columnspan=2, sticky="nsew")
        self.menu_container.grid_rowconfigure(0, weight=1)
        self.menu_container.grid_columnconfigure(0, weight=1)
        
        # Убираем старые заглушки, оставляем только Админа и Менеджера
        self.admin_panel = None
        self.manager_panel = None
        
        self._current_menu_visible = None

    # --- МЕТОД ВОЗВРАТА НА ГЛАВНЫЙ ЭКРАН ---
    def _show_main_screen(self):
        self._hide_current_menu()
        self.menu_container.grid_remove()
        self.grid_columnconfigure(0, minsize=210, weight=0)
        self.grid_columnconfigure(1, minsize=235, weight=0)

    def _hide_current_menu(self):
        print("[LOG] Скрываем текущее меню...")
        
        # Добавили удаление фасада Менеджера базы
        if hasattr(self, 'manager_panel') and self.manager_panel is not None:
            self.manager_panel.destroy()
            self.manager_panel = None

        # Оставили удаление фасада Администратора (он уже был)
        if hasattr(self, 'admin_panel') and self.admin_panel is not None:
            self.admin_panel.destroy()
            self.admin_panel = None

        # Очистка контейнера с серыми заглушками (на всякий случай)
        for child in self.menu_container.winfo_children():
            child.destroy()
            
        self._current_menu_visible = None

    def _show_admin_menu(self):
        print("[LOG] Нажата кнопка 'Администратор'.")
        self._hide_current_menu()
        try:
            from modules.administrator.admin_ui import AdminPanel
            self.admin_panel = AdminPanel(self, current_user=self.current_user)
            self.admin_panel.place(relx=0, rely=0, relwidth=1, relheight=1)
            self.admin_panel.lift()
            self.update_idletasks()
            self._current_menu_visible = 'admin'
        except Exception as e:
            print(f"[FATAL ERROR] Не удалось создать панель администратора: {e}")

    def _show_manager_menu(self):
        print("[LOG] Нажата кнопка 'Менеджер базы'.")
        
        # Прячем всё старое одним махом
        self._hide_current_menu()
        
        try:
            from modules.manager.manager_ui import ManagerPanel
            
            # Создаем панель ПРЯМО В ГЛАВНОМ ОКНЕ (self), а не в menu_container
            self.manager_panel = ManagerPanel(self, current_user=self.current_user)
            
            # Кладем её НАД ВСЕМ (поверх левой панели, правой панели и картинки)
            self.manager_panel.place(relx=0, rely=0, relwidth=1, relheight=1)
            
            # Принудительно просим поднять Z-index именно этой панели
            self.manager_panel.lift()
            
            # Заставляем Tkinter перерисоваться СЕЙЧАС
            self.update_idletasks()
            
            self._current_menu_visible = 'manager'
            print("[LOG] Фасад Менеджера успешно размещен.")
            
        except Exception as e:
            # Теперь мы точно увидим ошибку, если она есть
            print(f"[FATAL CRASH IN MANAGER MENU] {e}")

    # Остальные кнопки пока просто закрывают оверлей (возвращают к сове)
    def _show_experts_menu(self):
        self._show_main_screen()

    def _show_project_menu(self):
        self._show_main_screen()

    def _show_forecast_menu(self):
        self._show_main_screen()

    def _show_guide_menu(self):
        self._show_main_screen()

    def _show_about_menu(self):
        self._show_main_screen()

    def switch_user_radical(self):
        root_win = self.winfo_toplevel()
        from modules.utils.app_state import disable_app
        disable_app(root_win)
        if hasattr(self, 'current_user'):
            self.current_user = {"login": None}
        if hasattr(self, '_child_windows'):
            self._child_windows.clear()
        try:
            root_win.destroy()
        except Exception:
            pass
        import sys, subprocess, os, shutil
        script_path = os.path.abspath("main.py")
        gui_python = shutil.which("pythonw.exe")
        if not gui_python:
            current_dir = os.path.dirname(sys.executable)
            potential_path = os.path.join(current_dir, "pythonw.exe")
            gui_python = potential_path if os.path.exists(potential_path) else sys.executable
        try:
            subprocess.Popen([gui_python, script_path], shell=True, creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
        except Exception as e:
            print(f"[FATAL] Не удалось запустить новую копию Dozor: {e}")
        sys.exit(0)









# 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:
    """
    Поведение окон второго уровня (привязаны к родителю).
    Окно центрируется над главным и едет строго вместе с ним.
    """
    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)

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

        offset_x = target_window.winfo_rootx() - p_x
        offset_y = target_window.winfo_rooty() - p_y

        if not hasattr(parent, '_child_windows'):
            parent._child_windows = []
        parent.register_child_window(target_window, offset_x, offset_y)

        target_window.protocol("WM_MINIMIZE_WINDOW", self.do_nothing)
        target_window.protocol("WM_DELETE_WINDOW", target_window.destroy)

        target_window.bind("<ButtonPress-1>", lambda e, tw=target_window: self.save_offset(e, tw))
        target_window.bind("<B1-Motion>", lambda e, tw=target_window: self.drag_with_parent(e, tw, parent))

    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}")







# 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, edit_group, delete_group
from modules.utils.window_behavior import LevelThreeBehavior

class ManagerGroupInter(tk.Toplevel):
    def __init__(self, parent):
        super().__init__(parent)
        self.master = parent

        # Оставляем только это для иерархии окон
        self.transient(parent) 

        # Настройки окна
        self.title("Менеджер групп")
        self.geometry("600x450")
        
        # LevelThreeBehavior сам вызовет .grab_set() внутри setup_level_three
        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" # <-- Даем имя, которое ищет LevelThreeBehavior

        # Кнопка закрытия внутри шапки (опционально, но удобно)
        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)) # Уменьшил верхний отступ, т.к. есть шапка

        # Поле 1: Название
        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)

        # Поле 2: Код (только чтение при редактировании)
        ttk.Label(top_frame, text="Код:").grid(row=1, column=0, sticky="w")
        self.code_entry = ttk.Entry(top_frame, width=30)
        self.code_entry.grid(row=1, column=1, padx=5, pady=5)

        # === ПОЛЕ 3: УСИЛЕНИЕ (SPINBOX - ТОЛЬКО ЧИСЛА) ===
        ttk.Label(top_frame, text="Усиление:").grid(row=2, column=0, sticky="w")
        self.gain_var = tk.StringVar(value="1.0") # Начальное значение
        
        self.gain_entry = ttk.Spinbox(
            top_frame,
            from_=0.0, to=99.9, increment=0.1,
            textvariable=self.gain_var,
            width=30,
            format="%.1f"
        )
        self.gain_entry.grid(row=2, column=1, padx=5, pady=5)

        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", 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"]
        try:
            gain_val = float(gain.replace(',', '.')) if gain else None
        except ValueError:
            gain_val = None
        return {"code": code, "name": name, "gain_group": gain_val}

    def add_group(self):
        name = self.name_entry.get().strip()
        code = self.code_entry.get().strip()
        gain_raw = self.gain_var.get().strip() # Берем из Spinbox
        
        try:
            gain_val = float(gain_raw.replace(',', '.')) if gain_raw else None
        except ValueError:
            messagebox.showwarning("Ошибка", "Усиление должно быть числом.")
            return

        if not name or not 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(self):
        data = self.get_selected()
        if not data:
            messagebox.showwarning("Ошибка", "Выберите группу.")
            return

        old_code = data["code"] # Код берем старый, менять нельзя!
        new_name = self.name_entry.get().strip() or data["name"]
        
        gain_raw = self.gain_var.get().strip()
        current_gain = data.get("gain_group")
        
        try:
            new_gain = float(gain_raw.replace(',', '.')) if gain_raw else current_gain
        except ValueError:
            messagebox.showwarning("Ошибка", "Усиление должно быть числом.")
            return

        from modules.utils.app_state import disable_app
        disable_app(self.master)

        # Окно редактора открывается ПОД курсором мыши (без прыжков)
        edit_win = tk.Toplevel(self.master)
        edit_win.title("Подтверждение")
        edit_win.geometry("350x150")
        
        x = self.winfo_pointerx()
        y = self.winfo_pointery()
        edit_win.geometry(f"+{x}+{y}")

        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: self._apply_edit(old_code, new_name, new_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, code, name, gain, win):
        try:
            edit_group(code, name, code, gain) # ID=code, Новый Code тоже code
            messagebox.showinfo("Готово", "Группа обновлена.")
            self.update_tree()
            
            from modules.utils.app_state import enable_app
            enable_app(self.master)
            
            win.destroy()
        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["code"])
                messagebox.showinfo("Успех", "Группа удалена.")
                self.update_tree()
                self._clear_form()
            except Exception as e:
                messagebox.showerror("Ошибка БД", str(e))

    def _clear_form(self):
        self.name_entry.delete(0, tk.END)
        self.code_entry.delete(0, tk.END)
        self.gain_var.set("1.0") # Очищаем спинбокс до значения по умолчанию

    def on_close(self):
        self.destroy()









# 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:
    """Добавление группы. Пробрасывает IntegrityError при дублях."""
    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):
    """Редактирование группы по ID."""
    with _get_cursor() as cursor:
        cursor.execute(
            "UPDATE groups SET name = ?, code = ?, gain_group = ? WHERE id = ?", 
            (new_name, new_code, new_gain, group_id)
        )

def delete_group(group_id: int):
    """Удаление группы по ID. Индикаторы удалятся каскадно через FK в другой таблице."""
    with _get_cursor() as cursor:
        cursor.execute("DELETE FROM groups WHERE id = ?", (group_id,))

def delete_group(group_code: str):
    """Удаление группы из реальной БД"""
    print(f"[WORK LOGIC] Вызов: delete_group | Код: {group_code}")
    conn = get_db_connection()
    cursor = conn.cursor()
    try:
        cursor.execute("DELETE FROM groups WHERE code = ?", (group_code,))
        conn.commit()
        print("[SUCCESS] Группа удалена из БД.")
    except Exception as e:
        print(f"[DB ERROR] delete_group: {e}")
        conn.rollback()
        raise e
    finally:
        conn.close()












# Dozor\modules\manager\manager_metrics_inter.py

# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from docx import Document
import random
from modules.manager.manager_metrics_work import (
    get_all_indicators_by_group,
    insert_indicator,
    delete_indicator_by_code,
    IndicatorDTO,
    generate_unique_code,
    save_docx_template
)
# ИСПРАВЛЕНО: Импортируем список групп из файла менеджера групп
from modules.manager.manager_group_work import list_groups as group_list
from modules.utils.window_behavior import LevelTwoBehavior

class ManagerMetricsInter(tk.Toplevel):
    def __init__(self, parent):
        super().__init__(parent)
        self.parent_main = parent
        
        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

        root = self.master
        if hasattr(root, 'register_child_window'):
            try:
                root.register_child_window(self, offset_x=20, offset_y=20)
            except Exception:
                pass 

        self.use_master_db = False
        
        class FakeSession:
            def query(self, *args, **kwargs):
                class QueryObj:
                    def filter_by(self, **kw): return self
                    def all(self): return []
                return QueryObj()
            def close(self): pass
        self.session = FakeSession()

        self.group_names_to_codes = {}
        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)
        scrollbar.config(command=self.indicators_tree.yview)

        self.indicators_tree.bind("<Double-Button-1>", self.edit_indicator)

    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.parent_main.clipboard_get()
            self.name_entry.insert(tk.END, clipboard_text)
        except tk.TclError:
            messagebox.showwarning("Буфер обмена", "Буфер обмена пуст.")

    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:
                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()











# 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_id(indicator_id: int):
    print(f"[WORK LOGIC] DELETE | ID: {indicator_id}")
    with _get_cursor() as cursor:
        cursor.execute("DELETE FROM indicators WHERE id = ?", (indicator_id,))

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_sequential_code(group_code: str) -> str:
    """
    Генерирует код вида ГРУППА-N, где N - следующий свободный порядковый номер в этой группе.
    Например: Эко-1, Эко-2.
    """
    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):
    """
    Главный фасад модуля "Менеджер базы".
    Полный визуальный аналог AdminPanel из administrator/admin_ui.py.
    """
    def __init__(self, master, current_user):
        # Используем тот же светло-серый цвет фона, что и у Админа (#F5F5F5)
        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) # Высота 50px как у админа
        header.grid(row=0, column=0, sticky="ew")
        
        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.grid(row=1, column=0, sticky="nsew", 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):
        """Открывает рабочее окно менеджера групп."""
        from modules.utils.app_state import disable_app, enable_app
        disable_app(self.master_root)
        
        try:
            from modules.manager.manager_group_inter import ManagerGroupInter
            
            win = ManagerGroupInter(self.master_root)
            
            if hasattr(self.master_root, 'register_child_window'):
                self.master_root.register_child_window(win, offset_x=20, offset_y=20)
                
            self.master_root.wait_window(win)
        
        except Exception as e:
            print(f"[FATAL] Ошибка запуска Менеджера Групп: {e}")
        
        finally:
            enable_app(self.master_root)

    def open_metrics_module(self):
        """Открывает рабочий редактор индикаторов"""
        print("[ACTION] Открытие Менеджера Показателей...")
        
        from modules.utils.app_state import disable_app, enable_app
        
        # 1. Сначала блокируем главное окно
        disable_app(self.master_root)
        
        try:
            # 2. Импортируем ваш рабочий класс
            from modules.manager.manager_metrics_inter import ManagerMetricsInter
            
            # 3. Создаем экземпляр окна
            metrics_win = ManagerMetricsInter(self.master_root)
            
            # === БЕЗОПАСНАЯ РЕГИСТРАЦИЯ ОКНА (Защита №1) ===
            root = self.master_root 
            if root and hasattr(root, 'winfo_exists') and root.winfo_exists():
                register_method = getattr(root, 'register_child_window', None)
                
                if callable(register_method):
                    try:
                        register_method(metrics_win, offset_x=20, offset_y=20)
                    except Exception as e:
                        print(f"[UI] Ошибка внутри метода register: {e}")
                else:
                    print("[UI INFO] Метод register_child_window не найден у Root.")
            else:
                print("[UI WARNING] Главное окно недоступно для регистрации.")

            # 4. Ждем закрытия рабочего окна
            self.master_root.wait_window(metrics_win)
        
        except Exception as e:
            print(f"[FATAL] Ошибка запуска Менеджера Показателей: {e}")
            from tkinter import messagebox
            messagebox.showerror("Ошибка", f"Не удалось открыть модуль:\\n{e}")
        
        finally:
            # 5. Обязательно разблокируем интерфейс в любом случае
            enable_app(self.master_root)