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


# Dozor/modules/excel/excel_aggregate_inter.py
import os
import re
from tkinter import filedialog, messagebox, ttk
import tkinter as tk
from pathlib import Path 
from modules.excel.excel_aggregate_work import AggregateWorkController
from modules.utils.window_behavior import LevelTwoBehavior
from openpyxl import load_workbook 

class ExpertAggregateInterface(tk.Toplevel):
    def __init__(self, parent):
        super().__init__(parent)
        
        behavior = LevelTwoBehavior()
        behavior.setup_level_two(self.winfo_toplevel())
        
        self.title("Агрегирование файлов")
        # Уменьшили высоту на 5px для компенсации плотности элементов
        self.geometry("470x265")  
        self.resizable(False, False) 
        
        self.controller = AggregateWorkController()
        main_frame = ttk.Frame(self)
        main_frame.pack(padx=10, pady=5, fill="both", expand=True, anchor="center")
        
        # === Блок создания файла ===
        file_frame = ttk.Frame(main_frame)
        file_frame.pack(fill="x", pady=(0, 10))
        
        ttk.Label(file_frame, text="Название создаваемого файла:").pack(side="left")
        self.new_file_name_entry = tk.Entry(file_frame, width=20)
        self.new_file_name_entry.pack(side="left", padx=(5, 0), ipady=2)
        
        create_file_button = ttk.Button(
            file_frame, text="Создать файл", command=self.create_file_with_validation
        )
        create_button_packing_options = {"side": "right", "padx": (5, 0)}
        create_file_button.pack(**create_button_packing_options)
        
        # === Выбор файла Excel ===
        select_frame = ttk.Frame(main_frame)
        select_frame.pack(fill="x", pady=(0, 10))
        
        ttk.Label(select_frame, text="Выберите файл Excel:").pack(side="left")
        self.file_path_entry = tk.Entry(select_frame, width=30)
        self.file_path_entry.pack(side="left", padx=(5, 2), ipady=2) # Подтянули чуть выше
        
        browse_button = ttk.Button(
            select_frame, text="Обзор...", command=self.select_file_for_aggregation
        )
        browse_button.pack(side="right", padx=(5, 0))
        
        aggregate_button = ttk.Button(
            main_frame,
            text="Объединить информацию",
            command=self.aggregate_data
        )
        aggregate_button.pack(fill="x", pady=(5, 10), padx=(0, 0)) 
        
        # === ЦЕНТРИРОВАННЫЙ БЛОК ТРАНЗИТИВНОСТИ ===
        sheet_frame = ttk.Frame(main_frame)
        sheet_frame.pack(anchor="center", pady=(0, 10))
        
        top_sheet_line = ttk.Frame(sheet_frame)
        top_sheet_line.pack(fill="x", pady=(0, 5))
        
        label_sheet = ttk.Label(top_sheet_line, text="Выберите лист (группу):")
        label_sheet.pack(side="left")
        
        self.sheet_combobox = ttk.Combobox(top_sheet_line, state="readonly", width=20)
        self.sheet_combobox.pack(side="left", padx=(5, 0), ipady=2)
        self.sheet_combobox.bind(
            "<<ComboboxSelected>>", lambda _: self.reset_transitivity_field()
        )
        
        bottom_check_line = ttk.Frame(sheet_frame)
        bottom_check_line.pack(fill="x", pady=(0, 5))
        
        # КНОПКА СПРАВА, ПРОЦЕНТЫ СЛЕВА (для визуального центра)
        check_button = ttk.Button(
            bottom_check_line, text="Проверить на транзитивность", command=self.check_transitivity
        )
        check_button.pack(side="right", padx=(5, 0))
        
        self.transitivity_percentage_label = tk.Label(  
            bottom_check_line,
            text="", relief="groove",
            width=8, bg="white"
        )
        self.transitivity_percentage_label.pack(side="right", padx=(5, 0))
        
        # === НИЖНИЙ БЛОК (Кнопки действий) - Плотная упаковка ===
        action_buttons_frame = ttk.Frame(main_frame)
        # Уменьшили отступ сверху с 15 до 10px
        action_buttons_frame.pack(anchor="center", pady=10) 
        
        fix_button = ttk.Button(
            action_buttons_frame,
            text="Исправить транзитивность",
            command=self.apply_transitivity_corrections,
        )
        # Убрали лишние вертикальные отступы вокруг кнопок
        fix_button.pack() 
        
        send_to_db_button = ttk.Button(
            action_buttons_frame,
            text="Нормализовать данные",
            command=self.send_to_database,
        )
        send_to_db_button.pack()

    
    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("Ошибка", f"'{base_name}' — зарезервированное имя.")
            return
            
        project_root = Path(__file__).resolve().parents[2]
        experts_folder = project_root / "experts"
        
        full_path = experts_folder / filename
        if not full_path.exists():
            with open(full_path, 'w') as f:
                pass
            messagebox.showinfo("Готово", f"Файл '{full_path.name}' создан.")
        else:
            messagebox.showinfo("Готово", f"Файл '{full_path.name}' уже существует.")

    def select_file_for_aggregation(self):
        print("[GUI SELECT] Нажат Обзор...")
        
        from tkinter import filedialog
        file_paths = filedialog.askopenfilenames(
            title="Выберите файлы экспертов", 
            filetypes=[("Excel Files", "*.xlsx")]
        )
        
        if not file_paths:
            print("[GUI SELECT] Выбор отменен пользователем.")
            return

        # Сохраняем пути для агрегации
        self.controller.input_files = list(file_paths)
        
        # Читаем листы ТОЛЬКО из первого файла для комбобокса
        first_fp = file_paths[0]
        sheet_names = []
        
        try:
            print(f"[GUI SELECT] Читаю листы из '{os.path.basename(first_fp)}'...")
            temp_wb = load_workbook(filename=first_fp, data_only=True)
            sheet_names = temp_wb.sheetnames
            print(f"[GUI SELECT OK] Найдено листов: {sheet_names}")
            
            # Помещаем первый файл в постоянный кэш контроллера
            main_wb = load_workbook(filename=first_fp, data_only=True)
            self.controller._loaded_wbs[first_fp] = main_wb
            
        except Exception as e:
            print(f"[GUI SELECT FATAL] Не удалось прочитать {first_fp}: {e}")
            messagebox.showerror("Ошибка Excel", f"Файл поврежден или занят:\n{e}")
            return

        # === ЗАПОЛНЕНИЕ ВЫПАДАЮЩЕГО СПИСКА ===
        self.sheet_combobox['values'] = sheet_names
        
        if sheet_names:
            self.sheet_combobox.current(0) # Выбираем первый лист автоматически
            print("[GUI SELECT] Комбобокс обновлен.")
        else:
            print("[GUI SELECT WARN] Листы не найдены даже в первом файле.")
    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
            
        success = self.controller.apply_transitivity_corrections(file_path, selected_sheet)
        if success:
            messagebox.showinfo("Готово", "Транзитивность исправлена и файл сохранён.")


    def aggregate_data(self):
        print("[GUI] Кнопка нажата")
        
        output_file = self.new_file_name_entry.get().strip()
        if not output_file:
            messagebox.showwarning("Предупреждение", "Сначала создайте имя файла.")
            return
        if not output_file.endswith(".xlsx"):
            output_file += ".xlsx"
            
        input_files = getattr(self.controller, 'input_files', [])
        if not input_files or len(input_files) == 0:
            messagebox.showwarning("Внимание", "Сначала выберите файлы кнопкой 'Обзор...'.")
            return

        workbook = self.controller.aggregate_data(output_file, input_files)
        
        if workbook is None:
            messagebox.showerror("Ошибка данных", "Не удалось собрать данные.\nСмотрите консоль.")
            return
            
        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

        print(f"[GUI SAVE TARGET] Путь назначения: {dest_path}")
        
        try:
            # === ИСПРАВЛЕНО: Передаем СТРОКУ пути, а не объект файла! ===
            workbook.save(str(dest_path)) 
            
            # Закрываем книгу для освобождения памяти Windows
            workbook.close() 
            
            print("[GUI SAVE SUCCESS]")
            
            # Обновляем интерфейс перед показом окна сообщения
            self.update_idletasks() 
            
            messagebox.showinfo("Готово", f"Сводный отчет создан:\\n{dest_path}")
            
            # Принудительный вынос окна поверх всех остальных окон ОС
            self.lift()
            self.attributes("-topmost", True)
            self.after_idle(self.attributes, "-topmost", False)
               
        except Exception as e:
            print(f"[GUI FATAL ERROR]: {e}")
            messagebox.showerror("Ошибка сохранения", f"Не удалось сохранить файл:\n{e}")
            return None
            
    def send_to_database(self):
        file_path = self.file_path_entry.get()
        if not file_path:
            messagebox.showwarning("Внимание", "Пожалуйста, выберите файл Excel!")
            return
            
        success = self.controller.send_to_database(file_path)
        if success:
            messagebox.showinfo("Готово", "Данные успешно отправлены в базу данных.")













# Dozor/modules/excel/excel_aggregate_work.py

import os
from pathlib import Path
from statistics import mean, mode, StatisticsError
from openpyxl import load_workbook, Workbook
from collections import defaultdict

def normalize_cell(cell):
    if cell is None or str(cell).strip() == "":
        return ""
    
    val_str = str(cell).replace(',', '.').strip().upper()
    try:
        num = float(val_str)
        # Если число целое (например, 5.0), сохраняем как int
        return int(num) if abs(num - int(num)) < 1e-9 else round(num, 3)
    except ValueError:
        return val_str

class AggregateWorkController:
    def __init__(self):
        self.input_files = [] # Здесь храним РЕАЛЬНЫЕ ПУТИ
        
    def select_file(self):
        from tkinter import filedialog
        file_paths = filedialog.askopenfilenames(
            title="Выберите файлы экспертов", 
            filetypes=[("Excel Files", "*.xlsx")]
        )
        
        if not file_paths:
            print("[GUI SELECT] Выбор отменен.")
            return []
            
        # Сохраняем ЧИСТЫЕ пути в память контроллера
        self.input_files = list(file_paths)
        
        # Обновляем видимое поле (для пользователя)
        display_path = f"{Path(file_paths[0]).parent}/... ({len(file_paths)} files)"
        # ВАЖНО: обновляем entry только если он существует
        if hasattr(self, 'file_path_entry'):
             self.file_path_entry.delete(0, len(self.file_path_entry.get()))
             self.file_path_entry.insert(0, display_path)
             
        messagebox.showinfo("Выбор файлов", f"Выбрано {len(file_paths)} файлов.")
        return self.input_files

    def aggregate_data(self, output_filename, input_files):
        print("[AGG START]")
        
        if not input_files or len(input_files) < 2:
            messagebox.showwarning("Внимание", "Нужно выбрать минимум 2 файла.")
            return None
            
        grouped_by_sheet = defaultdict(list)
        
        for fp in input_files:
            try:
                wb = load_workbook(fp, data_only=True)
                for sheet_name in wb.sheetnames:
                    ws = wb[sheet_name]
                    
                    matrix = []
                    for row in ws.iter_rows(values_only=True):
                        current_row = [normalize_cell(c) for c in row]
                        matrix.append(current_row)
                
                has_data = False
                for mat_r in matrix:
                    for v in mat_r:
                        if isinstance(v, (int, float)):
                            has_data = True; break
                    if has_data: break
                        
                if has_data and matrix:
                    grouped_by_sheet[sheet_name].append(matrix)
                            
            except Exception as e:
                print(f"[AGG READ ERROR]: {e}")
                continue
                
        final_wb = Workbook()
        if "Sheet" in final_wb.sheetnames:
            del final_wb["Sheet"]
            
        names = list(grouped_by_sheet.keys())
        if not names:
            messagebox.showinfo("Результат", "Нет валидных данных для объединения.")
            return None
            
        from statistics import mode, StatisticsError
        
        for s_idx, sheet_name in enumerate(names):
            matrices_list = grouped_by_sheet[sheet_name]
            
            max_r = 0; max_c = 0
            for mat in matrices_list:
                if len(mat) > max_r: max_r = len(mat)
                for row in mat:
                    if len(row) > max_c: max_c = len(row)
                    
            result_mat = [["" for _ in range(max_c)] for _ in range(max_r)]
            
            for r in range(max_r):
                new_row = []
                for c in range(max_c):
                    vals = []
                    for mat in matrices_list:
                        if r < len(mat) and c < len(mat[r]):
                            norm_val = normalize_cell(mat[r][c])
                            if isinstance(norm_val, (int, float)):
                                vals.append(norm_val)
                    
                    if not vals:
                        new_row.append("")
                        continue
                        
                    val = ""
                    try:
                        val = mode(vals)
                    except StatisticsError:
                        numeric_vals = [float(x) for x in vals if isinstance(x, (int, float))]
                        if numeric_vals:
                            val = round(mean(numeric_vals), 3)
                    
                    new_row.append(val)
                result_mat.append(new_row)
            
            new_ws = final_wb.create_sheet(title=sheet_name)
            
            # Шапка I-1...
            for col_idx in range(max_c):
                new_ws.cell(row=1, column=col_idx + 4, value=f"I-{col_idx+1}")
            
            # Тело матрицы
            for r_idx in range(len(result_mat)):
                row = result_mat[r_idx]
                for c_idx in range(len(row)):
                    val = row[c_idx]
                    
                    target_row = r_idx + 2      
                    target_col = c_idx + 4      
                    
                    if isinstance(val, (int, float)):
                        new_ws.cell(row=r_idx + 1, column=c_idx + 1, value=str(r_idx + 1)) 
                        new_ws.cell(row=r_idx + 1, column=c_idx + 2, value="ID") 
                        new_ws.cell(row=target_row, column=target_col, value=val)
                    elif val != "":
                        new_ws.cell(row=r_idx + 1, column=c_idx + 1, value=str(r_idx + 1))
                        new_ws.cell(row=r_idx + 1, column=c_idx + 2, value="ID") 
                        new_ws.cell(row=target_row, column=target_col, value=str(val))
            
            # Нижняя половина (симметрия)
            for i in range(len(result_mat)):
                for j in range(i + 1, len(result_mat[i])):
                    upper_val = result_mat[i][j]
                    
                    target_row = j + 1 
                    target_col = i + 4 
                    
                    target_cell = new_ws.cell(row=target_row, column=target_col)
                    
                    if isinstance(upper_val, (int, float)) and upper_val != 0:
                        lower_val = round(1 / upper_val, 3)
                        new_ws.cell(row=target_row, column=target_col, value=lower_val)
                    else:
                        new_ws.cell(row=target_row, column=target_col, value="")

        dest_path = Path(__file__).resolve().parents[2] / "experts" / output_filename
        try:
            final_wb.save(str(dest_path))
            final_wb.close() 
            
            print("[GUI SAVE SUCCESS]")
            return final_wb
            
        except Exception as e:
            print(f"[GUI FATAL ERROR]: {e}")
            return None
                        
    def check_transitivity(self):
        print("[TRANS CHECK]")
        input_files = getattr(self, 'input_files', [])
        if not input_files: return "N/A"
        first_fp = input_files[0]
        wb = self._loaded_wbs.get(first_fp)
        if not wb: return "Ошибка"
        ws = wb.active; n = ws.max_row - 1
        values = [[0.0]*n for _ in range(n)]
        for i in range(1, n+1):
            for j in range(1, n+1):
                cell = ws.cell(row=i+1, column=j+3).value
                norm = normalize_cell(cell)
                values[i-1][j-1] = norm if isinstance(norm, (int, float)) else 0.0
        total = consistent = 0
        for i in range(n):
            for j in range(n):
                for k in range(n):
                    a,b,c = values[i][j], values[j][k], values[i][k]
                    if all(isinstance(x, (int, float)) for x in [a,b,c]) and a>0 and b>0:
                        total += 1
                        if a > b and b > c and a >= c: consistent += 1
        return f"{int((consistent/total)*100)}%" if total else "0%"

    def apply_transitivity_corrections(self, file_path=None, sheet_name=None):
        print("[TRANS FIX]")
        if not file_path or not sheet_name: return False
        wb = self._loaded_wbs.get(file_path)
        if not wb:
            try: wb = load_workbook(file_path); self._loaded_wbs[file_path] = wb
            except Exception as e: print(f"[FIX LOAD ERR]: {e}"); return False
        if sheet_name not in wb.sheetnames: return False
        ws = wb[sheet_name]; n = ws.max_row - 1
        values = [[0.0]*n for _ in range(n)]
        for i in range(1, n+1):
            for j in range(1, n+1):
                cell = ws.cell(row=i+1, column=j+3).value
                norm = normalize_cell(cell)
                values[i-1][j-1] = norm if isinstance(norm, (int, float)) else 0.0
        corrected = False
        for i in range(n):
            for j in range(i + 1, n):
                a = values[i][j]; inv_a = values[j][i]
                if isinstance(a, (int, float)) and isinstance(inv_a, (int, float)):
                    if abs(a * inv_a - 1) > 0.01:
                        new_inv = round(1 / a, 3)
                        ws.cell(row=j+1, column=i+3, value=new_inv)
                        corrected = True
        if corrected:
            try: wb.save(file_path); print("[FIX SAVED]"); return True
            except Exception as e: print(f"[FIX SAVE ERR]: {e}"); return False
        else: print("[FIX NONE]"); return True