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


# Файл-донор. Был предназначен для создания Эксель файлов для Экспертов.

import os
import tkinter as tk
from tkinter import messagebox, ttk
# ------------------------------------
import pandas as pd
from openpyxl import Workbook
from openpyxl.utils.dataframe import dataframe_to_rows
# --- ИСПРАВЛЕННЫЙ ИМПОРТ ---
# Теперь мы импортируем обе необходимые функции из базы данных
from modules.database_module import list_groups, list_indicators


def create_excel_template(groups_codes, output_filename):
    """
    Функция создаёт шаблон Excel-файла для заданных групп и сохраняет его.
    Все необходимые данные подгружаются за один проход без повторных запросов.
    """
    wb = Workbook()
    
    # Загружаем группы и их индикаторы со связями EAGER loading, 
    # чтобы избежать обращения к БД при обращении к .group или .matrices
    all_groups_with_data = list_groups()
    
    for group_code in groups_codes:
        try:
            # Ищем группу в уже загруженном списке
            group = next((g for g in all_groups_with_data if g.code == group_code), None)
            
            # Проверяем наличие группы и её индикаторов
            indicators_list = getattr(group, 'indicators', []) if group else []
            if not indicators_list:
                continue

            sheet_title = f"{group.name}"
            ws = wb.create_sheet(title=sheet_title)

            # --- ЗАГОЛОВКИ ТАБЛИЦЫ ---
            ws.cell(row=1, column=1, value="№ п/п")
            ws.cell(row=1, column=2, value="Показатель")
            ws.cell(row=1, column=3, value="Код")

            # --- ЗАПОЛНЯЕМ ТАБЛИЦУ ПОКАЗАТЕЛЕЙ ---
            for idx, ind in enumerate(indicators_list):
                ws.cell(row=idx + 2, column=1, value=str(idx + 1))
                ws.cell(row=idx + 2, column=2, value=ind.name)
                ws.cell(row=idx + 2, column=3, value=ind.code)

            # --- ПОДГОТОВКА К СОЗДАНИЮ МАТРИЦЫ ---
            start_col = 4
            matrix_start_row = 1
            for col_idx, label in enumerate([ind.code for ind in indicators_list]):
                ws.cell(row=matrix_start_row, column=start_col + col_idx, value=label)
                
            matrix_size = len(indicators_list)

            # --- СОЗДАЕМ СЛОВАРЬ ДЛЯ БЫСТРОГО ПОИСКА ЗНАЧЕНИЙ ИЗ БАЗЫ ---
            matrix_data_dict = {}
            if group and hasattr(group, 'indicators') and group.indicators:
                for ind in group.indicators: # Проходим по индикаторам конкретной группы
                    if hasattr(ind, 'matrices'):
                        for entry in ind.matrices: # Берём матрицы конкретного индикатора
                            matrix_data_dict[(entry.row, entry.col)] = entry.value

            # --- ЗАПОЛНЯЕМ ЯЧЕЙКИ МАТРИЦЫ ДАННЫМИ ИЗ БАЗЫ ---
            for r_idx in range(matrix_size):
                for c_idx in range(matrix_size):
                    db_value = matrix_data_dict.get((r_idx, c_idx))
                    target_cell = ws.cell(
                        row=matrix_start_row + r_idx + 1,
                        column=start_col + c_idx
                    )
                    
                    if db_value is not None:
                        target_cell.value = float(db_value)
                    elif r_idx == c_idx:
                        target_cell.value = 1.0
                    else:
                        target_cell.value = ""

        except Exception as e:
            print(f"Ошибка при обработке группы '{group_code}': {e}")

    if "Sheet" in wb.sheetnames:
        del wb["Sheet"]
        
    wb.save(output_filename)


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

    def refresh_groups(self):
        self.groups = [grp.name for grp in list_groups()]
        self.combobox.config(values=self.groups)

    def add_group(self):
        group_name = self.group_var.get()
        if group_name and group_name.strip():
            self.selected_groups.append(group_name)
            self.selected_groups_text.insert(tk.END, f"{group_name}\n")

    def remove_group(self):
        if len(self.selected_groups) > 0:
            last_group = self.selected_groups.pop(-1)
            current_text = self.selected_groups_text.get("1.0", tk.END).strip()
            updated_text = "\n".join(
                line for line in current_text.split("\n") if line != last_group
            )
            self.selected_groups_text.delete("1.0", tk.END)
            self.selected_groups_text.insert(tk.END, updated_text)

    def export_to_excel(self):
        """Логика экспорта в Excel (ИСПРАВЛЕННАЯ ВЕРСИЯ)."""
        selected_group_names = (
            self.selected_groups_text.get("1.0", tk.END).strip().split("\n")
        )
        # Составляем список кодов групп на основе выбранных названий
        groups_codes = []
        all_db_groups = list_groups()
        for name in selected_group_names:
            if name:
                group = next((g for g in all_db_groups if g.name == name), None)
                if group:
                    groups_codes.append(group.code)
        if not groups_codes:
            messagebox.showwarning("Предупреждение", "Не выбрано ни одной группы.")
            return

        try:
            num_experts = int(self.num_experts.get())
            if num_experts <= 0:
                raise ValueError
        except Exception:
            messagebox.showerror(
                "Ошибка", "Количество экспертов должно быть положительным числом."
            )
            return

        experts_folder = os.path.join(os.path.dirname(__file__), "..", "experts")
        os.makedirs(experts_folder, exist_ok=True)

        for expert_num in range(1, num_experts + 1):
            output_filename = os.path.join(experts_folder, f"Эксперт_{expert_num}.xlsx")
            create_excel_template(groups_codes, output_filename)

        messagebox.showinfo(
            title="Экспорт завершён",
            message=f"Успешно создано {num_experts} файлов экспертов.\n\nРасположение файлов:\n{experts_folder}",
        )













# Файл-донор, создавался для обработки Эксель файлов
# в новом файле отправки в базу не будет.
# После того как будет сделана нормализация - ее результаты должны вернуться в 
# соответствующую группу в крайний правый столбец и быть занесены напротив своих показателей.

import os
import re
import tkinter as tk
import numpy as np
from pathlib import Path
from statistics import mode as stat_mode
from tkinter import filedialog, messagebox, ttk
from modules.database_module import Group, Indicator, MatrixEntry, Session, calculate_indicator_weights

import pandas as pd
from openpyxl import Workbook, load_workbook
from openpyxl.styles import PatternFill

# --- Импорт из модуля базы данных ---
from modules.database_module import Group, Indicator, MatrixEntry, Session


# --- Вспомогательные функции ---
def normalize_value_to_strict(value):
    if value is None or str(value).strip() == "":
        return "1"
    val_str = str(value).replace(",", ".").strip()
    try:
        num = float(val_str)
        if abs(num - 1.0) < 0.1:
            return "1"
        elif abs(num - 3.0) < 0.1:
            return "3"
        elif num < 1.0:
            return "0.333"
        else:
            return "3"
    except ValueError:
        return "1"


def is_transitive_matrix_upper_triangular(matrix):
    violations = 0
    valid_comparisons = 0
    n = len(matrix)
    for i in range(n):
        for j in range(i + 1, n):
            aij = matrix[i][j]
            if abs(aij) < 1e-6:
                continue
            valid_comparisons += 1
            for k in range(j + 1, n):
                ajk = matrix[j][k]
                aik = matrix[i][k]
                if abs(ajk) < 1e-6 or abs(aik) < 1e-6:
                    continue
                if (aij >= ajk and aik < ajk) or (aij <= ajk and aik > ajk):
                    violations += 1
    if valid_comparisons == 0:
        return 0.0
    return max(0.0, ((valid_comparisons - violations) / valid_comparisons) * 100)


def fix_transitivity_in_matrix(matrix):
    n = len(matrix)
    fixed_matrix = [row.copy() for row in matrix]
    for i in range(n):
        for j in range(i + 1, n):
            for k in range(j + 1, n):
                aij = fixed_matrix[i][j]
                ajk = fixed_matrix[j][k]
                aik = fixed_matrix[i][k]
                if (aij >= ajk and aik < ajk) or (aij <= ajk and aik > ajk):
                    if aij >= ajk:
                        fixed_matrix[i][k] = ajk
                    else:
                        fixed_matrix[i][k] = 1 / ajk if ajk != 0 else 1.0
    return fixed_matrix


def aggregate_matrices_by_mode(matrices):
    result = []
    max_rows = max(len(mat) for mat in matrices)
    max_cols = max(len(row) for mat in matrices for row in mat)
    for row_idx in range(max_rows):
        row = []
        for col_idx in range(max_cols):
            cell_values = []
            for matrix in matrices:
                if row_idx < len(matrix) and col_idx < len(matrix[row_idx]):
                    cell_values.append(matrix[row_idx][col_idx])
            if cell_values:
                try:
                    most_common_val = stat_mode(cell_values)
                    row.append(most_common_val)
                except Exception:
                    row.append(1.0)
            else:
                row.append(None)
        result.append(row)
    return result


def create_empty_excel_template(filename):
    workbook = Workbook()
    workbook.save(filename)

# --- Класс окна ---
class AggregateFilesWindow(tk.Toplevel):
    def __init__(self, parent):
        super().__init__(parent)
        self.title("Агрегирование файлов")
        self.geometry("470x250")

        main_frame = ttk.Frame(self)
        main_frame.pack(padx=10, pady=5, fill="both", expand=True)
        main_frame.columnconfigure(0, weight=1)
        main_frame.columnconfigure(1, weight=2)
        main_frame.columnconfigure(2, weight=1)

        tk.Label(main_frame, text="Название создаваемого файла:").grid(
            row=0, column=0, sticky="w"
        )
        self.new_file_name_entry = tk.Entry(main_frame, width=20)
        self.new_file_name_entry.grid(row=0, column=1, sticky="we", pady=5)

        # ИЗМЕНЕНИЕ: Кнопка теперь вызывает метод с валидацией
        create_file_button = ttk.Button(
            main_frame, text="Создать файл", command=self.create_file_with_validation
        )
        create_file_button.grid(row=0, column=2, sticky="e", pady=5)

        aggregate_button = ttk.Button(
            main_frame, text="Объединить информацию", command=self.aggregate_data
        )
        aggregate_button.grid(row=1, column=0, columnspan=4, sticky="we", pady=5)

        tk.Label(main_frame, text="Выберите файл Excel:").grid(
            row=2, column=0, sticky="w"
        )
        self.file_path_entry = tk.Entry(main_frame, width=30)
        self.file_path_entry.grid(row=2, column=1, sticky="we", pady=5)
        browse_button = ttk.Button(
            main_frame, text="Обзор...", command=self.select_file_for_aggregation
        )
        browse_button.grid(row=2, column=2, sticky="e", pady=5)

        tk.Label(main_frame, text="Выберите лист (группу):").grid(
            row=3, column=0, sticky="w"
        )
        self.sheet_combobox = ttk.Combobox(main_frame, state="readonly", width=20)
        self.sheet_combobox.grid(row=3, column=1, sticky="we", pady=5)
        self.sheet_combobox.bind(
            "<<ComboboxSelected>>", lambda _: self.reset_transitivity_field()
        )

        tk.Label(main_frame, text="Проверить на транзитивность:").grid(
            row=4, column=0, sticky="w"
        )
        self.transitivity_percentage_label = tk.Label(
            main_frame, text="", relief="groove", width=8
        )
        self.transitivity_percentage_label.grid(row=4, column=1, sticky="w", pady=5)

        check_button = ttk.Button(
            main_frame, text="Проверить", command=self.check_transitivity
        )
        check_button.grid(row=4, column=2, sticky="e", pady=5)

        bottom_buttons_frame = ttk.Frame(main_frame)
        bottom_buttons_frame.grid(row=5, column=0, columnspan=4, sticky="we", pady=5)

        fix_transitivity_button = ttk.Button(
            bottom_buttons_frame,
            text="Исправить транзитивность",
            command=self.apply_transitivity_corrections,
        )
        fix_transitivity_button.grid(row=0, column=0, sticky="we", padx=5)

        send_to_db_button = ttk.Button(
            bottom_buttons_frame,
            text="Отправить в базу данных",
            command=self.send_to_database,
        )
        send_to_db_button.grid(row=0, column=1, sticky="we", padx=5)

    # ИЗМЕНЕНИЕ: Метод с валидацией имени файла
    def create_file_with_validation(self):
        filename = self.new_file_name_entry.get().strip()

        # Проверка на пустое имя
        if not filename:
            messagebox.showwarning("Предупреждение", "Имя файла не может быть пустым.")
            return

        # Проверка на запрещенные символы в имени файла Windows и Excel
        invalid_chars_pattern = r'[\\/:*?"<>|]'
        if re.search(invalid_chars_pattern, filename):
            messagebox.showerror(
                "Ошибка", f'Имя файла содержит недопустимые символы: \\ / : * ? " < > |'
            )
            return

        # Проверка на зарезервированные имена Windows (CON , PRN , AUX , NUL и
        # т.д.)
        reserved_names = [
            "CON",
            "PRN",
            "AUX",
            "NUL",
            "COM1",
            "COM2",
            "COM3",
            "COM4",
            "COM5",
            "COM6",
            "COM7",
            "COM8",
            "COM9",
            "LPT1",
            "LPT2",
            "LPT3",
            "LPT4",
            "LPT5",
            "LPT6",
            "LPT7",
            "LPT8",
            "LPT9",
        ]

        # Получаем имя без расширения и приводим к верхнему регистру для
        # сравнения
        base_name = Path(filename).stem.upper()
        if base_name in reserved_names:
            messagebox.showerror(
                "Ошибка",
                f"'{base_name}' — зарезервированное имя. Пожалуйста выберите другое.",
            )
            return

        # Если имя прошло все проверки — создаем файл.
        try:
            project_root = Path(__file__).resolve().parents[1]
            experts_folder = project_root / "experts"
            os.makedirs(experts_folder, exist_ok=True)

            full_path = experts_folder / filename

            # Проверяем наличие расширения .xlsx или .xlsm и добавляем .xlsx
            # если его нет.
            if not filename.endswith((".xlsx", ".xlsm")):
                full_path = full_path.with_name(full_path.name + ".xlsx")

            if not full_path.exists():
                create_empty_excel_template(full_path)
                messagebox.showinfo("Готово", f"Файл '{full_path.name}' создан.")
            else:
                messagebox.showinfo(
                    "Готово", f"Файл '{full_path.name}' уже существует."
                )
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось создать файл: {e}")

    def select_file_for_aggregation(self):
        file_path = filedialog.askopenfilename(
            filetypes=[
                ("Excel Files", "*.xlsx"),
                ("Excel Macro Enabled Workbooks", "*.xlsm"),
            ]
        )
        if file_path:
            self.file_path_entry.delete(0, tk.END)
            self.file_path_entry.insert(0, file_path)
            self.load_sheets_from_file(file_path)
            messagebox.showinfo("Выбор файла", f"Файл выбран: {file_path}")

    def load_sheets_from_file(self, file_path):
        try:
            workbook = load_workbook(file_path)
            sheets = workbook.sheetnames
            self.sheet_combobox["values"] = sheets
            if sheets:
                self.sheet_combobox.current(0)
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось загрузить листы: {e}")

    def reset_transitivity_field(self):
        self.transitivity_percentage_label.config(text="")

    def check_transitivity(self):
        file_path = self.file_path_entry.get()
        selected_sheet = self.sheet_combobox.get()
        if not file_path or not selected_sheet:
            messagebox.showwarning("Внимание", "Пожалуйста, укажите файл и лист!")
            return
        try:
            wb = load_workbook(file_path)
            ws = wb[selected_sheet]

            matrix_numbers = []
            for row in ws.iter_rows(min_row=2):
                current_row = []
                for cell in row:
                    val = cell.value
                    if val is None or val == "" or str(val).lower() == "none":
                        current_row.append(1.0)
                    else:
                        try:
                            num = float(str(val).replace(",", "."))
                            current_row.append(num)
                        except BaseException:
                            current_row.append(1.0)
                matrix_numbers.append(current_row)

            matrix_for_check = [row[3:] for row in matrix_numbers]

            percent = is_transitive_matrix_upper_triangular(matrix_for_check)
            self.transitivity_percentage_label.config(text=f"{percent:.1f}%")
        except Exception as e:
            print(f"Ошибка при проверке транзитивности: {e}")
            self.transitivity_percentage_label.config(text="Ошибка")

    def apply_transitivity_corrections(self):
        # --- ОСТАВЛЯЕМ КОД БЕЗ ИЗМЕНЕНИЙ ---
        file_path = self.file_path_entry.get()
        selected_sheet = self.sheet_combobox.get()
        if not file_path or not selected_sheet:
            messagebox.showwarning("Внимание", "Пожалуйста, укажите файл и лист!")
            return
        try:
            wb = load_workbook(file_path)
            ws = wb[selected_sheet]

            matrix_numbers = []
            for row in ws.iter_rows(min_row=2):
                current_row = []
                for cell in row:
                    val = cell.value
                    if val is None or val == "" or str(val).lower() == "none":
                        current_row.append(1.0)
                    else:
                        try:
                            num = float(str(val).replace(",", "."))
                            current_row.append(num)
                        except BaseException:
                            current_row.append(1.0)
                matrix_numbers.append(current_row)

            matrix_for_fix = [row[3:] for row in matrix_numbers]

            upper_fixed_matrix = fix_transitivity_in_matrix(matrix_for_fix.copy())

            n = len(upper_fixed_matrix)
            final_matrix_numbers = [[0.0 for _ in range(n)] for _ in range(n)]

            for i in range(n):
                for j in range(n):
                    if i == j:
                        final_matrix_numbers[i][j] = 1.0
                    elif i < j:
                        final_matrix_numbers[i][j] = upper_fixed_matrix[i][j]
                    else:
                        val = upper_fixed_matrix[j][i]
                        final_matrix_numbers[i][j] = 1.0 / val if val != 0 else 1.0

            data_to_write = []
            for row in final_matrix_numbers:
                new_row = []
                for num in row:
                    if abs(num - 1) < 0.1:
                        new_row.append("1")
                    elif num < 1.0:
                        new_row.append("0.333")
                    else:
                        new_row.append("3")
                data_to_write.append(new_row)

            start_row = 2
            start_col = 4

            for i, row in enumerate(data_to_write):
                for j, value in enumerate(row):
                    ws.cell(row=start_row + i, column=start_col + j, value=value)
                    cell = ws.cell(row=start_row + i, column=start_col + j)
                    cell.number_format = "@"

                    if i == j:
                        cell.fill = PatternFill(
                            start_color="FFFF00", end_color="FFFF00", fill_type="solid"
                        )

            wb.save(file_path)

            messagebox.showinfo("Готово", "Транзитивность исправлена и файл сохранён.")

        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось исправить: {e}")

    def aggregate_data(self):
        # --- ОСТАВЛЯЕМ КОД БЕЗ ИЗМЕНЕНИЙ ---
        output_file = self.new_file_name_entry.get()
        if not output_file:
            messagebox.showwarning("Предупреждение", "Сначала создайте имя файла.")
            return
        if not output_file.endswith(".xlsx"):
            output_file += ".xlsx"

        project_root = Path(__file__).resolve().parents[1]
        experts_folder = project_root / "experts"
        os.makedirs(experts_folder, exist_ok=True)
        full_output_path = experts_folder / output_file

        file_paths = filedialog.askopenfilenames(
            title="Выберите файлы экспертов", filetypes=[("Excel Files", "*.xlsx")]
        )

        if not file_paths:
            messagebox.showwarning("Предупреждение", "Файлы не выбраны.")
            return

        sheets_results = {}
        for path in file_paths:
            try:
                wb = load_workbook(path)
                for sheet_name in wb.sheetnames:
                    ws = wb[sheet_name]
                    data = [[cell.value for cell in row] for row in ws.rows]
                    sheets_results.setdefault(sheet_name, []).append(data)
            except Exception as e:
                messagebox.showerror("Ошибка чтения", f"Ошибка в файле {path}: {e}")
                return  # Останавливаем процесс

        if not sheets_results:
            raise ValueError("Нет данных для объединения.")

        new_wb = Workbook()
        new_wb.remove(new_wb.active)  # Удаляем лист по умолчанию

        for sheet_name, matrices_list in sheets_results.items():
            agg_matrix = aggregate_matrices_by_mode(matrices_list)
            new_ws = new_wb.create_sheet(title=sheet_name)
            for r_idx, row in enumerate(agg_matrix):
                for c_idx, val in enumerate(row):
                    new_ws.cell(row=r_idx + 1, column=c_idx + 1, value=str(val))

        new_wb.save(full_output_path)
        new_wb.close()

        messagebox.showinfo("Готово", f"Данные объединены в {output_file}.")

    # --- ОТПРАВКА В БАЗУ ДАННЫХ ---
    def send_to_database(self):
        file_path = self.file_path_entry.get()
        selected_sheet = self.sheet_combobox.get()

        if not file_path:
            messagebox.showwarning("Внимание", "Пожалуйста, выберите файл Excel!")
            return

        if not selected_sheet:
            messagebox.showwarning("Внимание", "Пожалуйста, выберите лист (группу)!")
            return

        try:
            # Читаем данные из выбранного листа Excel
            df = pd.read_excel(file_path, sheet_name=selected_sheet, header=None)
            session = Session()

            # Ищем группу по названию листа
            group = session.query(Group).filter_by(name=selected_sheet).first()
            if not group:
                # Если группы нет, создаем новую
                group_code = "".join(filter(str.isalnum, selected_sheet.lower()))
                group = Group(name=selected_sheet, code=group_code)
                session.add(group)

            # --- ВАЖНО: Сбрасываем сессию, чтобы у ГРУППЫ появился ID ---
            session.flush()

            # Очищаем старые данные этой группы перед импортом новых
            session.query(MatrixEntry).filter_by(group_id=group.id).delete()
            session.query(Indicator).filter_by(group_code=group.code).delete()

            # --- ЧТЕНИЕ ИНДИКАТОРОВ (Названий) ---
            indicators_to_add = []
            for row_idx, row in df.iloc[1:].iterrows():
                indicator_name = str(row[1]).strip()

                # Если ячейка пустая, пропускаем строку
                if not indicator_name:
                    continue

                indicator_code = f"{group.code}-{row_idx}"

                # Создаем объект индикатора и добавляем в список
                indicators_to_add.append(
                    Indicator(
                        name=indicator_name,
                        code=indicator_code,
                        group_code=group.code,  # Привязываем к группе по коду
                    )
                )

            # Сохраняем все индикаторы в базу одним запросом
            if indicators_to_add:
                session.bulk_save_objects(indicators_to_add)

            # --- ЧТЕНИЕ МАТРИЦЫ (Значений) ---
            matrix_entries = []
            for r_idx, row in df.iloc[1:].iterrows():
                for c_offset, cell_value in enumerate(row[3:], start=0):
                    try:
                        if pd.isna(cell_value):
                            continue

                        num = float(str(cell_value).replace(",", "."))

                        # Нормализация значений к 1 или 3
                        if abs(num - 1.0) < 0.1:
                            norm_value = 1.0
                        elif abs(num - 3.0) < 0.1:
                            norm_value = 3.0
                        elif num < 1.0:
                            norm_value = 1 / 3
                        else:
                            norm_value = 3.0

                        matrix_entries.append(
                            MatrixEntry(
                                group_id=group.id,
                                row=r_idx - 1,
                                col=c_offset,
                                value=norm_value,
                            )
                        )
                    except (ValueError, TypeError):
                        continue

            # Сохраняем все записи матрицы одним запросом
            if matrix_entries:
                session.bulk_save_objects(matrix_entries)

            # Финальное подтверждение всех изменений в базе данных
            session.commit()

        except Exception as e:
            # Если что-то пошло не так, откатываем изменения (это важно!)
            session.rollback()
            messagebox.showerror("Ошибка", f"Не удалось отправить данные: {e}")
        finally:
            # Обязательно закрываем сессию в конце
            session.close()

        messagebox.showinfo("Готово", "Данные успешно отправлены в базу данных.")
 

    def send_to_database(self):
        file_path = self.file_path_entry.get()

        # Проверка выбора файла
        if not file_path:
            messagebox.showwarning("Внимание", "Пожалуйста, выберите файл Excel!")
            return

        try:
            # Получаем список всех листов в файле
            all_sheets = pd.ExcelFile(file_path).sheet_names

            if not all_sheets:
                messagebox.showwarning("Внимание", "В файле нет листов для обработки!")
                return

            session = Session()
            success = True # Флаг для отслеживания общего успеха

            # Функция для обработки ОДНОГО листа (группы)
            def process_sheet(sheet_name, session_obj):
                print(f"\n--- Начало обработки листа: {sheet_name} ---")
                df = pd.read_excel(file_path, sheet_name=sheet_name, header=None)

                # Поиск или создание группы
                group = session_obj.query(Group).filter_by(name=sheet_name).first()
                if not group:
                    group_code = "".join(filter(str.isalnum, sheet_name.lower()))
                    group = Group(name=sheet_name, code=group_code)
                    session_obj.add(group)
                
                session_obj.flush() # Чтобы у группы появился ID

                # Очистка старых данных группы
                session_obj.query(MatrixEntry).filter_by(group_id=group.id).delete()
                session_obj.query(Indicator).filter_by(group_code=group.code).delete()

                # --- ЧТЕНИЕ ИНДИКАТОРОВ (Названий) ---
                indicators_to_add = []
                for row_idx, row in df.iloc[1:].iterrows():
                    indicator_name = str(row[1]).strip()
                    if not indicator_name:
                        continue
                    indicator_code = f"{group.code}-{row_idx}"
                    indicators_to_add.append(
                        Indicator(
                            name=indicator_name,
                            code=indicator_code,
                            group_code=group.code,
                        )
                    )

                # Сохраняем индикаторы в БД
                if indicators_to_add:
                    session_obj.bulk_save_objects(indicators_to_add)
                    session_obj.flush() # Сохраняем, чтобы получить ID индикаторов

                # --- ЧТЕНИЕ МАТРИЦЫ (Значений) ---
                matrix_entries = []
                for r_idx, row in df.iloc[1:].iterrows():
                    for c_offset, cell_value in enumerate(row[3:], start=0):
                        try:
                            if pd.isna(cell_value):
                                continue
                            num = float(str(cell_value).replace(",", "."))

                            # Нормализация значений
                            if abs(num - 1.0) < 0.1:
                                norm_value = 1.0
                            elif abs(num - 3.0) < 0.1:
                                norm_value = 3.0
                            elif num < 1.0:
                                norm_value = 1 / 3
                            else:
                                norm_value = 3.0

                            matrix_entries.append(
                                MatrixEntry(
                                    group_id=group.id,
                                    row=r_idx - 1,
                                    col=c_offset,
                                    value=norm_value,
                                )
                            )
                        except (ValueError, TypeError):
                            continue

                # Сохраняем матрицу в БД
                if matrix_entries:
                    session_obj.bulk_save_objects(matrix_entries)

                # --- РАСЧЕТ ВЕСОВ (КОВ) ---
                # Получаем только что сохраненные записи матрицы для этой группы
                matrix_entries_for_calc = session_obj.query(MatrixEntry).filter_by(group_id=group.id).all()
                
                indicator_weights = calculate_indicator_weights(matrix_entries_for_calc)

                # --- СОХРАНЕНИЕ ВЕСОВ В БД ---
                if indicator_weights is not None and len(indicator_weights) > 0:
                    # Получаем индикаторы этой группы в порядке их создания (по ID)
                    indicators_in_db = (
                        session_obj.query(Indicator)
                        .filter_by(group_code=group.code)
                        .order_by(Indicator.id.asc())
                        .all()
                    )

                    # Проверка на совпадение количества (защита от ошибок)
                    if len(indicators_in_db) == len(indicator_weights):
                        for idx, indicator in enumerate(indicators_in_db):
                            indicator.kov = indicator_weights[idx]
                        print(f"--- ВЕСА УСПЕШНО СОХРАНЕНЫ ДЛЯ ГРУППЫ: {sheet_name} ---")
                        success_local = True
                    else:
                        print(f"--- ОШИБКА: Несовпадение количества! Индикаторов: {len(indicators_in_db)}, Весов: {len(indicator_weights)} ---")
                        success_local = False
                else:
                    print(f"--- ОШИБКА: Расчёт весов для группы {sheet_name} не удался. ---")
                    success_local = False

                return success_local

            # --- ГЛАВНЫЙ ЦИКЛ ПО ВСЕМ ЛИСТАМ ---
            for sheet in all_sheets:
                try:
                    sheet_success = process_sheet(sheet, session)
                    success = success and sheet_success # Если хоть одна группа не обработалась - общий успех False
                except Exception as e:
                    print(f"--- Критическая ошибка при обработке листа '{sheet}': {e} ---")
                    success = False
            
            # Финальное сохранение всех изменений (одной транзакцией)
            if success:
                session.commit()
            
        except Exception as e:
            session.rollback()
            messagebox.showerror("Ошибка", f"Не удалось отправить данные: {e}")
        finally:
            session.close()

        # Сообщение пользователю после завершения цикла
        if success:
            messagebox.showinfo("Готово", "Данные успешно отправлены в базу данных. Расчёт весов выполнен.")
        else:
            messagebox.showerror("Ошибка", "Обработка завершена с ошибками. Проверьте лог в консоли.")













# Dozor\modules\excel\excel_aggregate_inter.py 
# Файл-донор
# -*- coding: utf-8 -*-
"""
Интерфейс окна 'Агрегирование файлов' для Эксперта.
"""
import re
from tkinter import filedialog, messagebox, ttk
import tkinter as tk
from modules.excel.excel_aggregate_work import AggregateWorkController
from modules.utils.window_behavior import LevelTwoBehavior


class ExpertAggregateInterface(tk.Toplevel):
    def __init__(self, parent):
        super().__init__(parent)
        
        # --- НОВАЯ СТРОКА: Применяем поведение Уровня 2 ---
        behavior = LevelTwoBehavior()
        behavior.setup_level_two(self)
        
        self.title("Агрегирование файлов")
        self.geometry("470x250")
        self.resizable(False, False)
        
        self.controller = AggregateWorkController()

        main_frame = ttk.Frame(self)
        main_frame.pack(padx=10, pady=5, fill="both", expand=True)
        main_frame.columnconfigure(1, weight=1)

        # --- Создание нового файла ---
        ttk.Label(main_frame, text="Название создаваемого файла:").grid(
            row=0, column=0, sticky="w"
        )
        self.new_file_name_entry = tk.Entry(main_frame, width=20)
        self.new_file_name_entry.grid(row=0, column=1, sticky="we", pady=5)
        
        create_file_button = ttk.Button(
            main_frame, text="Создать файл", command=self.create_file_with_validation
        )
        create_file_button.grid(row=0, column=2, sticky="e", pady=5)

        # --- Объединение информации ---
        aggregate_button = ttk.Button(
            main_frame, text="Объединить информацию", command=self.aggregate_data
        )
        aggregate_button.grid(row=1, column=0, columnspan=3, sticky="we", pady=5)

        # --- Выбор существующего файла ---
        ttk.Label(main_frame, text="Выберите файл Excel:").grid(
            row=2, column=0, sticky="w"
        )
        self.file_path_entry = tk.Entry(main_frame, width=30)
        self.file_path_entry.grid(row=2, column=1, sticky="we", pady=5)
        
        browse_button = ttk.Button(
            main_frame, text="Обзор...", command=self.select_file_for_aggregation
        )
        browse_button.grid(row=2, column=2, sticky="e", pady=5)

        # --- Выбор листа ---
        ttk.Label(main_frame, text="Выберите лист (группу):").grid(
            row=3, column=0, sticky="w"
        )
        self.sheet_combobox = ttk.Combobox(main_frame, state="readonly", width=20)
        self.sheet_combobox.grid(row=3, column=1, sticky="we", pady=5)
        self.sheet_combobox.bind(
            "<<ComboboxSelected>>", lambda _: self.reset_transitivity_field()
        )

        # --- Проверка транзитивности ---
        ttk.Label(main_frame, text="Проверить на транзитивность:").grid(
            row=4, column=0, sticky="w"
        )
        self.transitivity_percentage_label = tk.Label(
            main_frame, text="", relief="groove", width=8
        )
        self.transitivity_percentage_label.grid(row=4, column=1, sticky="w", pady=5)
        
        check_button = ttk.Button(
            main_frame, text="Проверить", command=self.check_transitivity
        )
        check_button.grid(row=4, column=2, sticky="e", pady=5)

        # --- Нижние кнопки действий ---
        bottom_buttons_frame = ttk.Frame(main_frame)
        bottom_buttons_frame.grid(row=5, column=0, columnspan=3, sticky="we", pady=5)
        
        fix_transitivity_button = ttk.Button(
            bottom_buttons_frame,
            text="Исправить транзитивность",
            command=self.apply_transitivity_corrections,
        )
        fix_transitivity_button.grid(row=0, column=0, sticky="we", padx=5)

        send_to_db_button = ttk.Button(
            bottom_buttons_frame,
            text="Отправить в базу данных",
            command=self.send_to_database,
        )
        send_to_db_button.grid(row=0, column=1, sticky="we", padx=5)

    # === МЕТОДЫ ИНТЕРФЕЙСА (связь с контроллером) ===
    def create_file_with_validation(self):
        filename = self.new_file_name_entry.get().strip()
        invalid_chars_pattern = r'[\\/:*?"<>|]'
        
        if not filename:
            messagebox.showwarning("Предупреждение", "Имя файла не может быть пустым.")
            return
        if re.search(invalid_chars_pattern, filename):
            messagebox.showerror("Ошибка", "Имя файла содержит недопустимые символы.")
            return
        
        reserved_names = [
            "CON", "PRN", "AUX", "NUL", "COM1", "LPT1", "COM2", "LPT2", 
            "COM3", "LPT3", "COM4", "LPT4", "COM5", "LPT5"
        ]
        base_name = filename.split(".")[0].upper()
        if base_name in reserved_names:
            messagebox.showerror("Ошибка", "'{}' — зарезервированное имя.".format(base_name))
            return
            
        self.controller.create_empty_excel_template(filename)
        messagebox.showinfo("Готово", f"Файл '{filename}' создан.")

    def select_file_for_aggregation(self):
        file_path = filedialog.askopenfilename(
            filetypes=[("Excel Files", "*.xlsx"), ("Excel Macro Enabled Workbooks", "*.xlsm")]
        )
        if file_path:
            self.file_path_entry.delete(0, tk.END)
            self.file_path_entry.insert(0, file_path)
            sheets = self.controller.load_sheets_from_file(file_path)
            self.sheet_combobox["values"] = sheets
            if sheets:
                self.sheet_combobox.current(0)
            messagebox.showinfo("Выбор файла", f"Файл выбран: {file_path}")

    def reset_transitivity_field(self):
        self.transitivity_percentage_label.config(text="")

    def check_transitivity(self):
        file_path = self.file_path_entry.get()
        selected_sheet = self.sheet_combobox.get()
        if not file_path or not selected_sheet:
            messagebox.showwarning("Внимание", "Пожалуйста, укажите файл и лист!")
            return
            
        result = self.controller.check_transitivity()
        self.transitivity_percentage_label.config(text=result)

    def apply_transitivity_corrections(self):
        file_path = self.file_path_entry.get()
        selected_sheet = self.sheet_combobox.get()
        if not file_path or not selected_sheet:
            messagebox.showwarning("Внимание", "Пожалуйста, укажите файл и лист!")
            return
            
        self.controller.apply_transitivity_corrections()
        messagebox.showinfo("Готово", "Транзитивность исправлена (заглушка).")

    def aggregate_data(self):
        output_file = self.new_file_name_entry.get()
        if not output_file:
            messagebox.showwarning("Предупреждение", "Сначала создайте имя файла.")
            return
        if not output_file.endswith(".xlsx"):
            output_file += ".xlsx"
            
        file_paths = filedialog.askopenfilenames(
            title="Выберите файлы экспертов", filetypes=[("Excel Files", "*.xlsx")]
        )
        if not file_paths:
            messagebox.showwarning("Предупреждение", "Файлы не выбраны.")
            return
            
        success = self.controller.aggregate_data(output_file, file_paths)
        if success:
            messagebox.showinfo("Готово", f"Данные объединены в {output_file}.")

    def send_to_database(self):
        file_path = self.file_path_entry.get()
        if not file_path:
            messagebox.showwarning("Внимание", "Пожалуйста, выберите файл Excel!")
            return
            
        self.controller.send_to_database(file_path)
        messagebox.showinfo("Готово", "Данные успешно отправлены в базу данных (заглушка).")












# Dozor/modules/excel/excel_aggregate_work.py
# Файл-донор

import os
from pathlib import Path


class AggregateWorkController:
    def __init__(self):
        self.file_path = ""
        self.selected_sheet = ""
        self._fake_sheets_cache = ["Группа А", "Группа Б"] # Фейковые данные листа

    def select_file(self):
        """Возвращает фейковый путь, если пользователь не выбрал файл сам."""
        project_root = Path(__file__).resolve().parents[2]
        experts_folder = project_root / "experts"
        os.makedirs(experts_folder, exist_ok=True)
        
        fake_file = experts_folder / "Эксперт_заглушка.xlsx"
        if not fake_file.exists():
            with open(fake_file, 'w') as f:
                pass
        return str(fake_file)

    def load_sheets_from_file(self, file_path):
        """Если файл выбран - возвращаем фейковые листы, иначе пустой список."""
        self.file_path = file_path
        if file_path and os.path.exists(file_path):
            return self._fake_sheets_cache
        return []

    def check_transitivity(self):
        """Заглушка проверки транзитности всегда возвращает успех."""
        dummy_matrix = [[1.0, 3.0], [0.333, 1.0]]
        percent = 100.0 # Всегда успешно для интерфейса
        return f"{percent:.1f}%"

    def apply_transitivity_corrections(self):
        """Пустая заглушка применения исправлений."""
        pass

    def create_empty_excel_template(self, filename):
        """Создает абсолютно пустой файл на диске."""
        try:
            project_root = Path(__file__).resolve().parents[2]
            experts_folder = project_root / "experts"
            os.makedirs(experts_folder, exist_ok=True)
            
            full_path = experts_folder / filename
            if not full_path.exists():
                with open(full_path, 'w') as f:
                    pass
        except Exception:
            pass

    def aggregate_data(self, output_file, input_files):
        """Просто создает целевой файл, имитируя объединение."""
        if not input_files:
            return False
        
        project_root = Path(__file__).resolve().parents[2]
        experts_folder = project_root / "experts"
        os.makedirs(experts_folder, exist_ok=True)
        
        dest_path = experts_folder / output_file
        with open(dest_path, 'w') as f:
            pass
        return True

    def send_to_database(self, file_path):
        """Заглушка отправки в базу данных."""
        pass












# Dozor\modules\excel\excel_export_inter.py
# Файл-донор
# -*- coding: utf-8 -*-
"""
Интерфейс окна 'Создание файлов' для Эксперта.
"""
import tkinter as tk
from tkinter import ttk, messagebox
from modules.excel.excel_export_work import ExportWorkController
from modules.utils.window_behavior import LevelTwoBehavior


class ExpertExportInterface(tk.Toplevel):
    def __init__(self, parent):
        super().__init__(parent)
        
        # --- НОВАЯ СТРОКА: Применяем поведение Уровня 2 ---
        behavior = LevelTwoBehavior()
        behavior.setup_level_two(self)
        
        self.title("Создание файла для эксперта")
        self.geometry("450x200")
        self.resizable(False, False)
        
        # Связываем интерфейс с контроллером (бизнес-логикой)
        self.controller = ExportWorkController()

        main_frame = ttk.Frame(self)
        main_frame.pack(padx=10, pady=10, fill="both", expand=True)

        # --- Группа ---
        ttk.Label(main_frame, text="Группа:").grid(row=0, column=0, sticky="w")
        self.group_var = tk.StringVar()
        self.combobox = ttk.Combobox(
            main_frame, textvariable=self.group_var, state="readonly", width=25
        )
        self.combobox.grid(row=0, column=1, padx=(5, 10), sticky="ew")
        self.refresh_groups_list()

        add_group_btn = ttk.Button(
            main_frame, text="Добавить группу", command=self.add_group
        )
        add_group_btn.grid(row=0, column=2, padx=(0, 10), sticky="e")

        # --- Выбранные группы ---
        ttk.Label(main_frame, text="Выбранные группы:").grid(row=1, column=0, sticky="w")
        self.selected_groups_text = tk.Text(main_frame, width=30, height=4)
        self.selected_groups_text.grid(
            row=1, column=1, columnspan=2, pady=(5, 10), sticky="ew"
        )

        # --- Количество экспертов ---
        ttk.Label(main_frame, text="Количество экспертов:").grid(
            row=2, column=0, sticky="w"
        )
        self.num_experts = tk.IntVar(value=1)
        self.experts_count_entry = ttk.Entry(main_frame, textvariable=self.num_experts, width=5)
        self.experts_count_entry.grid(row=2, column=1, sticky="w")

        remove_group_btn = ttk.Button(
            main_frame, text="Удалить группу", command=self.remove_group
        )
        remove_group_btn.grid(row=2, column=2, sticky="e")

        # --- Кнопка экспорта ---
        export_btn = ttk.Button(
            main_frame, text="Экспортировать в Excel", command=self.export_to_excel
        )
        export_btn.grid(row=3, column=1, pady=(10, 0))

    def refresh_groups_list(self):
        """Обновляет выпадающий список групп данными из контроллера."""
        self.controller.refresh_groups()
        self.combobox["values"] = self.controller.groups
        if self.controller.groups:
            self.combobox.current(0)

    def add_group(self):
        """Пробрасывает команду добавления в контроллер и обновляет текстовое поле."""
        group_name = self.group_var.get()
        self.controller.add_group(group_name)
        self._update_selected_text()

    def remove_group(self):
        """Пробрасывает команду удаления в контроллер и обновляет текстовое поле."""
        self.controller.remove_group()
        self._update_selected_text()

    def _update_selected_text(self):
        """Внутренний метод обновления виджета Text."""
        current_text = "\n".join(self.controller.selected_groups)
        self.selected_groups_text.delete("1.0", tk.END)
        self.selected_groups_text.insert(tk.END, current_text)

    def export_to_excel(self):
        """Собирает данные из полей и отдает их в рабочий модуль."""
        selected_group_names = (
            self.selected_groups_text.get("1.0", tk.END).strip().split("\n")
        )
        
        try:
            num_experts = int(self.num_experts.get())
            if num_experts <= 0:
                raise ValueError
        except Exception:
            messagebox.showerror(
                "Ошибка", "Количество экспертов должно быть положительным числом."
            )
            return

        # Передаем только имена, поиск кодов теперь внутри work-модуля отсутствует
        self.controller.export_to_excel(selected_group_names, num_experts)
        messagebox.showinfo(
            title="Экспорт завершён",
            message=f"Успешно создано {num_experts} файлов.\n\nРасположение:\n.../experts/",
        )












# Dozor/modules/excel/excel_export_work.py
# -*- coding: utf-8 -*-
"""
Модуль бизнес-логики для создания файлов экспертов.
ВРЕМЕННАЯ ЗАГУБКА: отключена работа с матрицей индикаторов.
"""
import os
from pathlib import Path
from openpyxl import Workbook

# Относительный импорт пакета database_module
try:
    from ..database_module import list_groups
except ImportError:
    # Если структура пакетов нарушена, создаем фейковую функцию
    def list_groups():
        class FakeGrp:
            def __init__(self, name, code):
                self.name = name
                self.code = code
        return [FakeGrp("Группа А", "GRP-01"), FakeGrp("Группа Б", "GRP-02")]


def create_excel_template(groups_codes, output_filename):
    """
    Создает ПУСТОЙ шаблон Excel-файла. 
    Логика наполнения данными перенесена на потом.
    """
    try:
        project_root = Path(__file__).resolve().parents[2]
        experts_folder = project_root / "experts"
        os.makedirs(experts_folder, exist_ok=True)
        
        full_path = experts_folder / output_filename
        
        wb = Workbook()
        ws = wb.active
        ws.title = "Шаблон"
        
        # Просто пишем заголовки, чтобы файл открывался без ошибок структуры
        ws['A1'] = '№ п/п'
        ws['B1'] = 'Показатель'
        ws['C1'] = 'Код'
        
        # Удаляем лист по умолчанию, если он называется Sheet
        if "Sheet" in wb.sheetnames and wb["Sheet"].max_row == 1 and wb["Sheet"].max_column == 1:
            del wb["Sheet"]
            
        wb.save(full_path)
        
    except Exception as e:
        print(f"[EXPORT WORK ERROR] Не удалось создать заглушку {output_filename}: {e}")


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

    def refresh_groups(self):
        """Получаем ТОЛЬКО список имен групп."""
        try:
            db_groups = list_groups()
            self.groups = [grp.name for grp in db_groups]
        except Exception as e:
            print(f"[ERROR] Не удалось загрузить группы: {e}")
            self.groups = ["Ошибка загрузки"]

    def add_group(self, group_name):
        if group_name and group_name.strip() and group_name not in self.selected_groups:
            self.selected_groups.append(group_name)

    def remove_group(self):
        if len(self.selected_groups) > 0:
            self.selected_groups.pop(-1)

    def export_to_excel(self, selected_group_names, num_experts):
        """
        Главная функция экспорта. Теперь она безопасна.
        """
        groups_codes = [] # Мы больше не ищем коды здесь, чтобы избежать запроса индикаторов
        
        project_root = Path(__file__).resolve().parents[2]
        experts_folder = project_root / "experts"
        os.makedirs(experts_folder, exist_ok=True)

        for expert_num in range(1, int(num_experts) + 1):
            filename = f"Эксперт_{expert_num}.xlsx"
            full_path = experts_folder / filename
            
            # Вызываем упрощенную функцию-заглушку
            create_excel_template(groups_codes, str(full_path))












# Dozor\modules\administrator\admin_ui.py
# Файл для образца/примера

import tkinter as tk
from tkinter import messagebox

# Импортируем ТВОИ старые рабочие модули.
# Убедись, что пути правильные! Если файлы лежат в другой папке, поправь путь здесь.
try:
    from modules.administrator.admin_user_inter import AdminUserInter
except ImportError:
    print("[WARNING] admin_user_inter.py не найден по стандартному пути.")
    class AdminUserInter(tk.Toplevel): pass # Заглушка для теста

try:
    from modules.administrator.admin_db_inter import AdminDBInter
except ImportError:
    print("[WARNING] admin_db_inter.py не найден по стандартному пути.")
    class AdminDBInter(tk.Toplevel): pass # Заглушка для теста


class AdminPanel(tk.Frame):
    """
    Главный фасад модуля "Администратор".
    Он создается внутри Главного Окна (MainInterface) и занимает его полностью.
    При нажатии кнопок он открывает СТАРЫЕ модальные окна.
    """
    def __init__(self, master, current_user):
        # Используем нормальный светло-серый цвет вместо лайма
        super().__init__(master, bg="#F5F5F5") 
        self.master_root = master  # Сохраняем ссылку на главное окно (Root)
        self.current_user = current_user
        
        print("[DEBUG] AdminPanel создан.")

        # Настраиваем сетку самого фасада (внутри него будут хедер и тело)
        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.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)

        users_btn = tk.Button(body, text="Пользователи", font=("Arial", 14), width=18, height=2,
                              command=self.open_users_module,
                              bg="#3498db", fg="white", relief="raised", bd=2)
        users_btn.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")

        db_btn = tk.Button(body, text="Базы данных", font=("Arial", 14), width=18, height=2,
                           command=self.open_db_module,
                           bg="#27ae60", fg="white", relief="raised", bd=2)
        db_btn.grid(row=0, column=1, padx=10, pady=10, sticky="nsew")

    def open_users_module(self):
        """Открывает старое окно управления пользователями."""
        print("[ACTION] Открытие Админ-Пользователей...")
        
        # Блокируем интерфейс через утилиты приложения
        from modules.utils.app_state import disable_app, enable_app
        disable_app(self.master_root)
        
        # Создаем экземпляр старого рабочего класса
        users_win = AdminUserInter(self.master_root)
        
        # Регистрируем его в системе окон Dozor (чтобы знать о дочерних окнах)
        if hasattr(self.master_root, 'register_child_window'):
            self.master_root.register_child_window(users_win, offset_x=20, offset_y=20)
            
        # Ждем закрытия окна (модальность)
        self.master_root.wait_window(users_win)
        
        # Разблокируем интерфейс
        enable_app(self.master_root)
        print("[ACTION] Модуль Пользователей закрыт.")

    def open_db_module(self):
        """Запускает рабочее окно управления базами данных."""
        
        # Блокируем главное приложение Dozor
        from modules.utils.app_state import disable_app, enable_app
        disable_app(self.master_root)
        
        try:
            # Импортируем ТВОЙ РАБОЧИЙ ИНТЕРФЕЙС
            # ЗАМЕНИ admin_db_inter НА ТОТ ФАЙЛ, КОТОРЫЙ У ТЕБЯ ЕСТЬ!
            # Например, если он называется db_backup.py или backup_manager.py
            from modules.administrator.admin_db_inter import AdminDBInter 
            
            # Создаем модальное окно
            db_win = AdminDBInter(self.master_root)  # Или self.parent_main, если у тебя так
            
            # Регистрируем его в системе окон (если у вас есть такой механизм)
            if hasattr(self.master_root, 'register_child_window'):
                self.master_root.register_child_window(db_win, offset_x=20, offset_y=20)
                
            # Ждем закрытия этого окна
            self.master_root.wait_window(db_win)
        
        except Exception as e:
            print(f"[FATAL] Ошибка запуска модуля Баз Данных: {e}")
            messagebox.showerror("Ошибка", f"Не удалось открыть модуль:\\n{e}")
        
        finally:
            enable_app(self.master_root)








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











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