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


# =========================================
# dukora\modules\project\project_window.py
# =========================================
import customtkinter as ctk
from tkinter import ttk
from modules.project.project_event_logic import ProjectEventLogic
from typing import List
import datetime
import os
from pathlib import Path
try:
    from tkcalendar import DateEntry
except ImportError:
    DateEntry = None
try:
    import docx
except ImportError:
    docx = None
from modules.project.project_event_ui import ProjectEventUI
from modules.project.project_create_logic import ProjectCreateLogic
from modules.data.project_database import ProjectDatabase

class ProjectWindow(ctk.CTkToplevel):
    def __init__(self, master):
        super().__init__(master)
        
        self.parent_main = master
        self.title("Журнал событий")
        
        # === ИНИЦИАЛИЗАЦИЯ ПЕРЕМЕННЫХ (ОДИН РАЗ) ===
        self.accordion_data = {}
        self.currently_open_button = None
        self.var_group = None 
        
        self.current_user = getattr(master, 'current_user', None)
        
        screen_height = self.winfo_screenheight()
        window_width = 650
        initial_height = 380
        screen_w = self.winfo_screenwidth()
        x_cord = int((screen_w - window_width) / 2)
        taskbar_offset_top = 40 
        y_cord = taskbar_offset_top
        self.geometry(f"{window_width}x{initial_height}+{x_cord}+{y_cord}")
        
        self.resizable(True, False)
        self.grab_set() 
        self.transient(master)
        
        main_container = ctk.CTkFrame(self, corner_radius=10)
        main_container.pack(padx=10, pady=10, fill="both", expand=True)
        
        self._build_title_bar(main_container)
        
        if not hasattr(self, 'manager_db'):
            self.manager_db = ProjectDatabase()
            self.manager_db.create_tables()
            
        if not hasattr(self, 'event_logic'):
            self.event_logic = ProjectEventLogic(self.manager_db)
            
        self._build_project_selector(main_container)
        
        create_item = self._create_accordion_header(main_container, "Создать проект")
        events_item = self._create_accordion_header(main_container, "Открыть для ввода событий")
        charts_item = self._create_accordion_header(main_container, "Построение диаграмм")
        print_item = self._create_accordion_header(main_container, "Настройка печати")
        
        # Наполнение секций
        self._fill_create_section(create_item["content"])
        self._fill_events_section(events_item["content"]) 
        
        # Панель кнопок внизу главного окна
        button_frame = ctk.CTkFrame(main_container)
        button_frame.pack(fill="x", pady=(10, 0), padx=5)
        
        close_btn = ctk.CTkButton(button_frame, text="Закрыть", width=120, command=self.destroy)
        close_btn.pack(side="right", padx=10, pady=10)
        
        # Вызываем наполнение секции графиков
        self._fill_charts_section(charts_item["content"])
        
        self._fill_print_section(print_item["content"])
        
        # Регистрация заголовков аккордеона (цикл должен быть в самом конце перед геометрией)
        for item in [create_item, events_item, charts_item, print_item]:
            btn = item["button"]
            content = item["content"]
            self.accordion_data[btn] = {"content": content}
            btn.configure(command=lambda b=btn, c=content: self.toggle_section(b, c))
            
        # Синхронизация размеров
        self.update_idletasks() 
        correct_geometry = self._calculate_window_geometry(collapsed=False)
        self.geometry(correct_geometry) 
        
        self.toggle_section(create_item["button"], create_item["content"])
        
        self.protocol("WM_DELETE_WINDOW", self.on_closing)
        self.after(50, self._enforce_onscreen_position)
        
    def _force_ui_sync(self):
        """
        Главная точка входа синхронизации. Вызывается при смене проекта или нажатии кнопки ОБНОВИТЬ ПРОЕКТ.
        Находит внутренний Code по отображаемому Cipher и обновляет контекст окна.
        """
        current_display_cipher = self.project_selector.get()
        
        if not current_display_cipher or current_display_cipher == "Проектов нет":
            print("[SYNC FORCE] Context cleared.")
            self.current_selected_project_code = ""
            self._update_control_buttons_state(is_enabled=False)
            return
        try:
            self.manager_db.close()
        except Exception:
            pass
        fresh_projects = self.manager_db.list_projects()
        selected_tuple = next((p for p in fresh_projects if p[3] == current_display_cipher), None)
        
        if selected_tuple:
            internal_code = selected_tuple[2]
            self.current_selected_project_code = internal_code
            
            print(f"[SYNC FORCE] Switched to '{current_display_cipher}' -> DB File: {internal_code}.db")
            
            # === ИСПРАВЛЕННЫЙ БЛОК ВЫЗОВА UI ===
            events_btn = None
            # 1. Находим кнопку заголовка "Открыть для ввода событий"
            for btn, data in self.accordion_data.items():
                if btn.cget("text") == "Открыть для ввода событий":
                    events_btn = btn
                    break
            
            # 2. Если кнопка найдена, получаем её контент-фрейм из словаря
            if events_btn and events_btn in self.accordion_data:
                content_frame = self.accordion_data[events_btn]["content"]
                
                # 3. Проходим по виджетам внутри этого фрейма (там лежит ProjectEventUI)
                for child in content_frame.winfo_children():
                    if child.__class__.__name__ == "ProjectEventUI":
                        print("[DIRECT CALL] Updating lists via accordion_data reference.")
                        child.update_project_dependent_lists(internal_code)
                        break
                        
            self._update_control_buttons_state(is_enabled=True)
        else:
            print(f"[SYNC FORCE ERROR] Project cipher '{current_display_cipher}' not found.")
            self.current_selected_project_code = ""
            self._update_control_buttons_state(is_enabled=False)
            
    def _force_refresh_event_ui(self):
        print("[UI SYNC] Manual refresh of lists triggered.")
        
        current_display_cipher = self.project_selector.get()
        
        if not current_display_cipher or current_display_cipher == "Проектов нет":
            print("[SYNC FORCE] Context cleared for manual list update.")
            return
        try:
            self.manager_db.close()
        except Exception:
            pass
        fresh_projects = self.manager_db.list_projects()
        selected_tuple = next((p for p in fresh_projects if p[3] == current_display_cipher), None)
        
        if selected_tuple:
            internal_code = selected_tuple[2]
            
            events_section_content = None
            for item in self.accordion_data.values():
                if item and "content" in item:
                    content_widget = item["content"]
                    for child in content_widget.winfo_children():
                        if child.__class__.__name__ == "ProjectEventUI":
                            events_section_content = child
                            break
            
            if events_section_content:
                print(f"[DIRECT CALL] Updating lists for project code: {internal_code}")
                events_section_content.update_project_dependent_lists(internal_code)
            else:
                print("[WARN] ProjectEventUI instance not found in accordion cache.")
                
    def _update_control_buttons_state(self, is_enabled: bool):
        """Меняет цвет кнопок ОБНОВИТЬ / ОБНОВИТЬ СПИСКИ вверху окна"""
        target_state = "normal" if is_enabled else "disabled"
        
        # Логика ТЗ: Оранжевый (#FF8C00) - если есть изменения/синхронизация нужна.
        # Серый (#A9A9A9) - если контекст пуст или заблокирован.
        text_color = "#FF8C00" if is_enabled else "#A9A9A9" 

        if hasattr(self, 'btn_sync_context'):
            self.btn_sync_context.configure(state=target_state, text_color=text_color)
        if hasattr(self, 'btn_sync_lists'):
            self.btn_sync_lists.configure(state=target_state, text_color=text_color)
            
        # НИЖНИЕ КНОПКИ СЕКЦИИ ГРАФИКОВ БОЛЬШЕ НЕ ТРОГАЕМ ЗДЕСЬ.
        # Они управляются своим локальным кодом в project_chart_ui.py
            
    def _build_title_bar(self, container):
        title_lbl = ctk.CTkLabel(container, text="Наполнение проекта", font=ctk.CTkFont(size=20, weight="bold"))
        title_lbl.pack(pady=(0, 15))
        
    def _build_project_selector(self, container):
        outer_frame = ctk.CTkFrame(container, fg_color="transparent")
        outer_frame.pack(fill="x", pady=(5, 10))
        
        btn_font = ("Arial", 11, "bold")
        
        top_frame = ctk.CTkFrame(outer_frame, fg_color="transparent")
        top_frame.pack(fill="x", pady=(0, 10))
        
        label_project = ctk.CTkLabel(top_frame, text="Выбрать проект:", anchor="w")
        label_project.pack(side="left", padx=(0, 10))
        
        self.project_selector = ctk.CTkComboBox(
            top_frame,
            values=[], 
            state="readonly",
            width=250
        )
        self.project_selector.pack(side="left", expand=True, fill="x")
        self.project_selector.configure(command=lambda *args: self._force_ui_sync())
        
        control_frame = ctk.CTkFrame(outer_frame, fg_color="transparent")
        control_frame.pack(fill="x")
        
        control_frame.grid_columnconfigure(0, weight=1)
        control_frame.grid_columnconfigure(1, weight=1)
        self.btn_sync_context = ctk.CTkButton(
            master=control_frame,
            text="ОБНОВИТЬ ПРОЕКТ",
            font=btn_font,
            fg_color="#000000", 
            hover_color="#2B2B2B",
            command=self._force_ui_sync,
            state="disabled"
        )
        self.btn_sync_context.grid(row=0, column=0, sticky="ew", padx=(0, 5))
        self.btn_sync_lists = ctk.CTkButton(
            master=control_frame,
            text="ОБНОВИТЬ СПИСКИ",
            font=btn_font,
            fg_color="#000000", 
            hover_color="#2B2B2B",
            command=self._force_refresh_event_ui,
            state="disabled"
        )
        self.btn_sync_lists.grid(row=0, column=1, sticky="ew", padx=(5, 0))
        
    def toggle_section(self, button, content):
        is_currently_open = (self.currently_open_button == button)
        if self.currently_open_button and not is_currently_open:
            prev_content = self.accordion_data[self.currently_open_button]["content"]
            prev_content.pack_forget()
            self.currently_open_button.configure(fg_color="#34495e")
            
        if is_currently_open:
            content.pack_forget()
            self.currently_open_button = None
            new_geo = self._calculate_window_geometry(collapsed=True)
        else:
            content.pack(fill="x", padx=5, pady=(0, 5), anchor="center")
            self.currently_open_button = button
            button.configure(fg_color="#2c3e50")
            new_geo = self._calculate_window_geometry(collapsed=False)
            
        self.geometry(new_geo)
        self.after(50, self._enforce_onscreen_position)

        # === ЛОГИКА ЗАПОЛНЕНИЯ ПРИ ОТКРЫТИИ ===
        title = button.cget("text")
        if title == "Построение диаграммы" and not is_currently_open:
            # Окно открылось, даем ему долю секунды отрисоваться
            self.after(100, lambda: self._load_chart_groups())

    def _load_chart_groups(self):
        """Загрузка групп при открытии секции графиков"""
        try:
            logic = ProjectEventLogic(self.manager_db)
            groups_raw, _ = logic.load_groups_and_indicators()
            items = []
            if groups_raw:
                try:
                    if hasattr(groups_raw[0], 'name'):
                        names = sorted([item.name for item in groups_raw])
                    elif isinstance(groups_raw[0], tuple):
                        names = sorted([item[1] for item in groups_raw])
                    else:
                        names = sorted(groups_raw)
                    items.extend(names)
                except Exception:
                    pass
            
            # Устанавливаем значения ТОЛЬКО если список существует
            if hasattr(self, 'cmb_group') and self.cmb_group:
                self.cmb_group.configure(values=items)
                if items:
                    self.cmb_group.set(items[0])
        except Exception as e:
            print(f"[UI LOAD ERROR]: {e}")
        
    def _enforce_onscreen_position(self):
        if not self.winfo_exists():
            return
            
        self.update_idletasks()
        
        win_w = self.winfo_width()
        win_h = self.winfo_height()
        screen_w = self.winfo_screenwidth()
        screen_h = self.winfo_screenheight()
        
        x = int((screen_w - win_w) / 2)
        
        bottom_edge = self.winfo_rooty() + win_h
        
        overflow = bottom_edge - screen_h + 50 
        if overflow > 0:
            y = max(40, self.winfo_rooty() - overflow)
        else:
            y = 40
            
        self.geometry(f"+{x}+{y}")
        
    def _calculate_window_geometry(self, collapsed=False):
        current_width = self.winfo_width()
        if current_width == 1:
            current_width = 650
            
        header_height = 50 * 4 + 30  
        button_panel_height = 70   
        
        if collapsed or not self.currently_open_button:
            target_height = header_height + 80 + button_panel_height
        else:
            data = self.accordion_data[self.currently_open_button]
            content_widget = data["content"]
            self.update_idletasks()
            open_content_height = content_widget.winfo_reqheight()
            target_height = header_height + open_content_height + button_panel_height
            
        max_h = self.winfo_screenheight() - 50
        final_height = min(target_height, max_h)
        min_h = 460 
        final_height = max(final_height, min_h)
        
        return f"{current_width}x{final_height}"
        
    def _create_accordion_header(self, parent, title):
        item_container = ctk.CTkFrame(parent, fg_color="#f0f2f5", border_color="#d1d8e0", border_width=1, corner_radius=8)
        item_container.pack(fill="x", pady=5)
        header_btn = ctk.CTkButton(
            item_container,
            text=title,
            height=45,
            font=ctk.CTkFont(weight="bold", size=16),
            anchor="w",
            fg_color="#34495e",
            hover_color="#3c5a72",
        )
        header_btn.pack(fill="x")
        content_frame = ctk.CTkFrame(item_container, fg_color="transparent", corner_radius=0)
        return {"button": header_btn, "content": content_frame}
        
    def _handle_project_deletion(self):
        current_display = self.project_selector.get()
        if not current_display or current_display == "Проектов нет":
            messagebox.showwarning("Ошибка", "Сначала выберите проект над аккордеоном.")
            return
        
        try:
            self.manager_db.close()
        except Exception:
            pass
            
        fresh_projects = self.manager_db.list_projects()
        code_to_delete = next((p for p in fresh_projects if p[3] == current_display), None)
        if not code_to_delete:
            messagebox.showerror("Ошибка", f"ID для '{current_display}' не найден.")
            return
        confirm = messagebox.askyesno("Внимание", 
                                      f"Удалить проект '{current_display}'?\n"
                                      f"Это действие нельзя отменить.\nФайл событий будет стерт.")
        if not confirm:
            return
        
        import os
        db_file_path = os.path.join(self.event_logic.project_dbs_folder, f"{code_to_delete}.db")
        file_was_deleted = False
        if os.path.exists(db_file_path):
            try:
                os.remove(db_file_path)
                file_was_deleted = True
            except PermissionError:
                messagebox.showerror("Доступ запрещен", "Закройте другие программы и попробуйте снова.")
                return
        success = False
        try:
            conn = self.manager_db.connect()
            cursor = conn.cursor()
            cursor.execute("DELETE FROM projects WHERE code = ?", (code_to_delete,))
            conn.commit()
            success = True
        except sqlite3.Error as e:
            conn.rollback()
            print(f"[DB DELETE ERROR]: {e}")
        finally:
            self.manager_db.close()
        if success and (not os.path.exists(db_file_path) or file_was_deleted):
            updated = self.manager_db.list_projects()
            names = [p[3] for p in updated]
            self.project_selector.configure(values=names)
            if names:
                self.project_selector.set(names[-1])
                self.current_selected_project_code = next((p[2] for p in updated if p[3] == names[-1]), "")
            else:
                self.project_selector.set("Проектов нет")
                self.current_selected_project_code = ""
                
            if hasattr(self, 'tree_projects'):
                tree = self.tree_projects
                for row in tree.get_children():
                    tree.delete(row)
                for proj in sorted(updated, key=lambda x: x[0]):
                    tree.insert("", "end", values=proj)
                    
            messagebox.showinfo("Готово", f"'{current_display}' удален.")
        else:
            messagebox.showerror("Ошибка", "Не удалось обновить реестр. Перезапустите программу.")
            
    def _fill_create_section(self, frame):
        from modules.data.project_database import ProjectDatabase
        from modules.project.project_create_logic import ProjectCreateLogic
        
        if not hasattr(self, 'manager_db'):
            self.manager_db = ProjectDatabase()
            self.manager_db.create_tables()
            self.event_logic = ProjectEventLogic(self.manager_db)
        
        logic = ProjectCreateLogic(self.manager_db)
        
        form_frame = ctk.CTkFrame(frame, fg_color="transparent")
        form_frame.pack(fill="x", expand=True, padx=10, pady=10)
        form_frame.grid_columnconfigure(1, weight=1)
        
        entries = {}
        fields = [
            ("Название:", "human_name"),
            ("Шифр:", "short_code"), 
            ("Код (ID):", "full_code"),
            ("Дата открытия:", "date"),
            ("Ответственный:", "responsible")
        ]
        
        for i, (label_text, key) in enumerate(fields):
            ttk.Label(form_frame, text=label_text).grid(row=i, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
            entry = ctk.CTkEntry(form_frame)
            entry.grid(row=i, column=1, sticky="ew", pady=(0, 5))
            entries[key] = entry
            if key == "date":
                today = datetime.datetime.now().strftime("%Y-%m-%d")
                entry.insert(0, today)
                
        action_frame = ctk.CTkFrame(form_frame, fg_color="transparent")
        action_frame.grid(row=len(fields), column=0, columnspan=2, sticky="ew", pady=(5, 0))
        for i in range(4): action_frame.grid_columnconfigure(i, weight=1)
        
        create_btn = ctk.CTkButton(
            action_frame, 
            text="Создать", 
            fg_color="#3B82F6", 
            hover_color="#2563EB",
            command=lambda n=entries['human_name'], sc=entries['short_code'], c=entries['full_code'], d=entries['date'], r=entries['responsible']: self._handle_project_creation(
                {
                    'human_name': n.get(),
                    'short_code': sc.get(),
                    'full_code': c.get(),
                    'description': "",
                    'date': d.get(),
                    'responsible': r.get()
                },
                logic,
                lambda: [widget.delete(0, "end") for widget in entries.values()]
            )
        )
        create_btn.grid(row=0, column=0, sticky="ew", padx=(0, 5))
        
        clear_btn = ctk.CTkButton(
            action_frame, 
            text="Очистить", 
            fg_color="#6B7280", 
            hover_color="#4B5563",
            command=lambda: [widget.delete(0, "end") for widget in entries.values()]
        )
        clear_btn.grid(row=0, column=1, sticky="ew", padx=5)
        
        delete_btn = ctk.CTkButton(
            action_frame, 
            text="Удалить", 
            fg_color="#EF4444", 
            hover_color="#B91C1C",
            command=self._handle_project_deletion
        )
        delete_btn.grid(row=0, column=2, sticky="ew", padx=5)
        
    def _handle_project_creation(self, data_dict: dict, logic_instance: ProjectCreateLogic, on_success_callback):
        success, message = logic_instance.create_project(data_dict)
        if not success:
            messagebox.showerror("Ошибка создания", message)
            return
        messagebox.showinfo("Проект создан", message)
        on_success_callback()
        
        try:
            self.manager_db.close()
        except Exception:
            pass
            
        fresh_tuples = self.manager_db.list_projects()
        fresh_names = [p[1] for p in fresh_tuples]
        
        # Безопасное пересоздание селектора проектов
        parent_widget = self.project_selector.master
        old_selector = self.project_selector
        old_selector.pack_forget()
        old_selector.destroy()
        
        self.project_selector = ctk.CTkComboBox(
            master=parent_widget,
            values=fresh_names,
            state="readonly",
            width=250
        )
        self.project_selector.pack(side="left")
        
        if fresh_names:
            selected_proj_tuple = next((p for p in fresh_tuples if p[1] == fresh_names[-1]), None)
            display_text = selected_proj_tuple[2] if selected_proj_tuple else fresh_names[-1]
            self.project_selector.set(display_text)
            self.current_selected_project_code = selected_proj_tuple[2] if selected_proj_tuple else ""
        else:
            self.project_selector.set("Проектов нет")
            self.current_selected_project_code = ""
            
        self.project_selector.configure(command=lambda *args: self._force_ui_sync())
        
        if hasattr(self, 'tree_projects'):
            tree = self.tree_projects
            for row in tree.get_children():
                tree.delete(row)
            for proj in sorted(fresh_tuples, key=lambda x: x[0]):
                tree.insert("", "end", values=proj)
                
        current_title = self.currently_open_button.cget("text") if self.currently_open_button else ""
        if current_title == "Открыть для ввода событий":
            for child in self.currently_open_content.winfo_children():
                if isinstance(child, ProjectEventUI):
                    try:
                        child.update_project_dependent_lists()
                    except Exception as update_err:
                        print(f"[LOG] Ошибка обновления UI событий: {update_err}")
                    break
                    
    def _fill_events_section(self, frame):
        from modules.project.project_event_ui import ProjectEventUI
        from modules.project.project_event_logic import ProjectEventLogic
        
        if not hasattr(self, 'manager_db'):
            self.manager_db = ProjectDatabase()
            self.manager_db.create_tables()
            
        if not hasattr(self, 'event_logic'):
            self.event_logic = ProjectEventLogic(self.manager_db)
            
        ui = ProjectEventUI(frame, self, self.event_logic)
        ui.pack(fill="both", expand=True)
        
        current_proj_code = getattr(self, 'current_selected_project_code', "")
        if current_proj_code:
            ui.after(100, lambda pc=current_proj_code: (
                ui.update_project_dependent_lists(pc),
                ui._refresh_event_table(pc)
            ))
            
    def _fill_charts_section(self, frame):
        from modules.project.project_chart_ui import build_charts_interface
        
        # Создаем жесткий контейнер фиксированной высоты
        charts_container = ctk.CTkFrame(frame, height=450) 
        charts_container.pack(fill="x", expand=False, padx=5, pady=5)
        
        # КРИТИЧЕСКИ ВАЖНО: запрещаем рамке расти по вертикали
        charts_container.pack_propagate(False) 
        
        build_charts_interface(charts_container, self)
    
    def _fill_print_section(self, frame):
        if DateEntry is None:
            error_lbl = ctk.CTkLabel(frame, text="Ошибка: установите библиотеку\npip install tkcalendar", text_color="red")
            error_lbl.pack(expand=True)
            return
            
    def _generate_word_report(self, group, report_date, orientation):
        if docx is None:
            messagebox.showerror("Библиотека не найдена", "Для экспорта нужна библиотека 'python-docx'.\nВыполните:\npip install python-docx")
            return
            
    def on_closing(self):
        try:
            self.after_cancel("all")
        except Exception as e:
            # Логируем ошибку отмены задач планировщика, но не прерываем закрытие
            print(f"[WARN] Failed to cancel scheduled events: {e}")
            
        for item in list(self.accordion_data.values()):
            try:
                content = item.get("content")
                if content:
                    content.destroy()
            except Exception as e:
                # Логируем ошибки уничтожения виджетов аккордеона
                print(f"[WARN] Failed to destroy accordion content: {e}")
                
        try:
            self.destroy()
        except Exception as e:
            # Логируем финальную ошибку закрытия корневого окна
            print(f"[CRITICAL] Failed to close ProjectWindow: {e}")