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


Работаем с окном "Наполнение проекта" с заголовком аккордеона "Построить диаграмму". Начинаем работу с переименования окна "Показать графики" в "Построить диаграмму".
Это новое окно, поэтому его реализацию начинаем с чистого листа. 
Все элементы управления в этом окне должны работать с проектом, подключенным в верху окна аккордеона  "Наполнение проекта" в выпадающем списке. С выбором в нем другого проекта все элементы  в "Построить диаграмму" должны обновляться и переключаться для работы с новым проектом.
Окно организовать на кастомткинкере (СТК). Работаем по схеме - один заголовок - 2 файла: один для интерфейса, другой для функционала. Название файлов этого окна должны начинаться с префикса project_ , все пути импортов организовать по модулю resources.ру. Ниже расписываю что должно быть в окне "Построить диаграмму".

1. Блок "Выбрать группу": рядом выпадающий список с группами, рядом кнопка синего цвета "Добавить в список" - все в одну строчку. По этому выбору будет в дальнейшем строиться график/гистограмма событий либо выбранной группы, либо  во всех группах вместе. Использоваться, видимо, будет база данных database.db или masterdata.db (справочная база).

2. Блок "Выбрать дату": рядом выпадающий список с Календарем. Выбирается с какого срока создавать график/гистаграмму. Привязывается к событиям базы конкретного проекта.

3. Блок "Выбрать периодичность": рядом поле для указания промежутков, рядом в скобках (дней). Пользователем выбирается шаг на оси.

4. Блок "Выбрать диаграмму": рядом выпадающий список. В списке 2 значения: "Гистограмма" и "График".

Все четыре блока разместить строго по вертикальной линии друг под другом. Блоки должны быть выровнены: заголовки по левому краю, поля по вертикальной линии.

5. Ниже всех блоков две кнопки: красная "Сбросить настройки" и синяя "Построить диаграмму". Кнопки одинакового размера, рзмещаются на всю ширину окна в одну строчку.

6. При нажатии на кнопку "Построить диаграмму" открывается модальное окно (его заголовок по выбранной группе "Интенсивность событий в группе N...") с диаграммой (либо гистограмма, либо график). Размер окна приблизительно А5. Диаграмма строится:
- по оси Y - частота событий, вверху маркера/столбика - число с количеством.
- по оси X - дата.

7. В модальном окне диаграммы в самом низу разместить 2 кнопки:
- зеленую - "Копировать" ( происходит копирование диаграммы в буфер обмена).
- синюю - "Закрыть" (закрывается модальное окно). 

Я, вайб кодер - ты, профессиональный программист. Ты делаешь - я вставляю, заменяю. 
Кусками не работаем - только целыми фрагментами - метод на метод, файл на файл, от комментария до комментария, от комментария до конца и т.д. - по такому принципу. 

- Файл main_interface.ру принципиально не менять, это только витрина и кнопки.
- Программа будет собираться руинсталлером, поэтому это надо иметь ввиду.
- все файлы для работы с окном  должны лежать по адресу dukora\modules\project\.
- файл для работы с базами database_module.ру расположен по адресу dukora\modules\data\ (справочниками).
- файл для работы с базами project_database.ру расположен по адресу dukora\modules\data\ (с созданными проектами).
- сами базы расположены по адресу: dukora\db - это и projects, и database, и masterdata.
- базы, создаваемых пользователем проектов лежат по адресу: dukora\project_dbs
- если какие то вопросы есть - задавай.


==================================================================

Итогом работы должно стать полностью рабочее окно  "Построить диаграмму". Изначально, чтобы не выдавало ошибки создать  можно часть элементов заблокировать через pass. Затем постепенно шаг за шагом будем подключать элемент за элементом.

===================================================================

Есть такой файл - project_window.py. Однако он уже избыточен. Поэтому его использовать нельзя - необходимо создать другой такой файл, например project_window_2.py


# =================================================================
# 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.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.accordion_data = {}
        self.currently_open_button = None
        
        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"]) 
        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))
            
        # === КРИТИЧЕСКИ ВАЖНЫЙ БЛОК СИНХРОНИЗАЦИИ РАЗМЕРОВ ===
        
        # 1. Заставляем Tkinter измерить реальную высоту всех созданных фреймов (включая тяжелую таблицу событий)
        self.update_idletasks() 
        
        # 2. Теперь рассчитываем правильную высоту окна на основе реальных размеров контента
        correct_geometry = self._calculate_window_geometry(collapsed=False)
        self.geometry(correct_geometry) 
        
        # 3. Открываем нужную секцию ПОСЛЕ установки размера (чтобы не было визуального прыжка collapse -> expand)
        self.toggle_section(create_item["button"], create_item["content"])
        
        # 4. Панель кнопок должна быть создана ДО финальной проверки границ экрана
        button_frame = ctk.CTkFrame(main_container)
        button_frame.pack(fill="x", pady=(10, 0))
        close_btn = ctk.CTkButton(button_frame, text="Закрыть", width=120, command=self.destroy)
        close_btn.pack(side="right", padx=10, pady=10)
        
        self.protocol("WM_DELETE_WINDOW", self.on_closing)

        # 5. Финальная проверка: если из-за таскбара окно все равно обрезано, сдвигаем его вверх
        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"
        # Пункт 1 ТЗ: активный цвет - оранжевый вместо белого/светло-серого
        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)
            
    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()
            button.configure(fg_color="#34495e")
            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")
            button.configure(fg_color="#2c3e50")
            self.currently_open_button = button
            new_geo = self._calculate_window_geometry(collapsed=False)
        
        self.geometry(new_geo)
        self.after(50, self._enforce_onscreen_position)
        
    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):
        if DateEntry is None:
            error_lbl = ctk.CTkLabel(frame, text="Ошибка: установите библиотеку\npip install tkcalendar", text_color="red")
            error_lbl.pack(expand=True)
            return
    
    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}")
            
# =================================================================


Возможно могут понадобиться для понимания эти файлы внизу:




# ==========================================
# dukora\modules\project\project_event_ui.py
# ==========================================
import customtkinter as ctk
from tkinter import ttk
import tkinter as tk  
import datetime
import os
import sqlite3
from .project_event_logic import ProjectEventLogic
from tkcalendar import DateEntry 
from tkinter import messagebox

# ==========================================
# ОСНОВНОЙ КЛАСС ОКНА СОБЫТИЙ
# ==========================================
class ProjectEventUI(ctk.CTkFrame):
    EDIT_MODE_COLOR = "#8B4513"
    
    def __init__(self, master, window_ref, logic: ProjectEventLogic):
        super().__init__(master, fg_color="transparent")
        self.window_ref = window_ref
        self.logic = logic
        
        self.group_names_list = []
        self.indicator_names_list = []
        
        self.group_names_to_codes = {}
        self.indicator_cache = {}  
        
        self.editing_event_code = None
        
        self._setup_widgets()
        self._bind_global_events()
        
        self.update_project_dependent_lists()
        
        self.after(50, self._initial_load)
        
    def _initial_load(self):
        print(f"[INIT LOAD] Waiting for UI sync...")
        current_proj_code = getattr(self.window_ref, 'current_selected_project_code', "")
        if current_proj_code and current_proj_code != "Проектов нет":
            self._refresh_event_table(current_proj_code)
            
    def _setup_widgets(self):
        main_form = ctk.CTkFrame(self, fg_color="transparent")
        main_form.pack(fill="both", expand=True, padx=10, pady=10)
        main_form.grid_columnconfigure(1, weight=1)
        
        lbl_group = ctk.CTkLabel(main_form, text="Выбрать группу:", anchor="w")
        lbl_group.grid(row=0, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
        
        self.cmb_group = ctk.CTkComboBox(
            main_form, 
            state="readonly", 
            width=250, 
            command=self._on_group_change
        )
        self.cmb_group.grid(row=0, column=1, sticky="ew", pady=(0, 5))
        
        lbl_indicator = ctk.CTkLabel(main_form, text="Выбрать показатель:", anchor="w")
        lbl_indicator.grid(row=1, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
        
        self.cmb_indicator = ctk.CTkComboBox(
            main_form, 
            state="readonly", 
            width=250,
            command=lambda choice: self._refresh_event_table(self.window_ref.current_selected_project_code)
        )
        self.cmb_indicator.grid(row=1, column=1, sticky="ew", pady=(0, 10))
        
        lbl_event = ctk.CTkLabel(main_form, text="Содержание события:", anchor="w")
        lbl_event.grid(row=2, column=0, sticky="nw", pady=(0, 5), padx=(0, 10))
        event_container = ctk.CTkFrame(main_form, fg_color="#F0F0F0", border_width=1, border_color="#A9A9A9")
        event_container.grid(row=2, column=1, sticky="ew", pady=(0, 5), ipadx=2, ipady=2)
        self.txt_event = ctk.CTkTextbox(event_container, height=80, wrap="word", fg_color="white")
        self.txt_event.pack(fill="both", expand=True, padx=2, pady=2)
        paste_btn = ctk.CTkButton(main_form, text="Вставить", fg_color="#2B2B2B", hover_color="#3C3C3C", command=self._paste_from_clipboard)
        paste_btn.grid(row=3, column=1, sticky="e", pady=(0, 10), ipadx=10)
        
        meta_frame = ctk.CTkFrame(main_form, fg_color="transparent")
        meta_frame.grid(row=4, column=0, columnspan=2, sticky="ew", pady=(0, 10))
        meta_frame.grid_columnconfigure(1, weight=1)
        
        ttk.Label(meta_frame, text="Дата события:").grid(row=0, column=0, sticky="w", padx=(0, 10))
        
        self.date_entry = DateEntry(
            master=meta_frame, 
            width=12,
            background='darkblue',
            foreground='white',           
            bordercolor='gray',           
            headersbackground='darkgray', 
            normalbackground='white',
            normalforeground='black',
            weekendbackground='lightblue',
            weekendforeground='red',
            date_pattern='dd.MM.yyyy'
        )
        
        today = datetime.datetime.now()
        self.date_entry.set_date(today)
        self.date_entry.grid(row=0, column=1, sticky="w")
        
        ttk.Label(meta_frame, text="Направленность:").grid(row=0, column=2, sticky="w", padx=(20, 10))
        self.dir_var = tk.IntVar(value=1)
        rb_plus = ctk.CTkRadioButton(meta_frame, text="(+)", variable=self.dir_var, value=1)
        rb_plus.grid(row=0, column=3, sticky="w")
        rb_minus = ctk.CTkRadioButton(meta_frame, text="(-)", variable=self.dir_var, value=-1)
        rb_minus.grid(row=0, column=4, sticky="w")
        
        ctrl_frame = ctk.CTkFrame(main_form, fg_color="transparent")
        ctrl_frame.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(0, 10))
        for i in range(4): ctrl_frame.grid_columnconfigure(i, weight=1)
        
        self.btn_add = ctk.CTkButton(ctrl_frame, text="Добавить", fg_color="#3B82F6", command=self._add_event)
        self.btn_add.grid(row=0, column=0, sticky="ew", padx=2)
        self.btn_edit = ctk.CTkButton(ctrl_frame, text="Редактировать", fg_color="#10B981", command=self._enter_edit_mode)
        self.btn_edit.grid(row=0, column=1, sticky="ew", padx=2)
        self.btn_save = ctk.CTkButton(ctrl_frame, text="Сохранить", state="disabled", fg_color=self.EDIT_MODE_COLOR, command=self._save_edited_event)
        self.btn_save.grid(row=0, column=2, sticky="ew", padx=2)
        self.btn_del = ctk.CTkButton(ctrl_frame, text="Удалить", fg_color="#EF4444", command=self._delete_selected_event)
        self.btn_del.grid(row=0, column=3, sticky="ew", padx=2)
        
        tree_container = ctk.CTkFrame(main_form, fg_color="transparent")
        tree_container.grid(row=6, column=0, columnspan=2, sticky="nsew", pady=(10, 0))
        tree_container.grid_rowconfigure(0, weight=1); tree_container.grid_columnconfigure(0, weight=1)
        
        style = ttk.Style()
        theme_name = "default" # Жесткая фиксация темы для избежания конфликтов CTk
        style.theme_use(theme_name)
        
        columns = ("num", "code", "date", "sign", "event") 
        self.tree = ttk.Treeview(tree_container, columns=columns, show="headings", selectmode="browse", height=8)
        
        self.tree.heading("num", text="№п/п"); self.tree.column("num", width=40, anchor="center", stretch=False)
        self.tree.heading("code", text="Код"); self.tree.column("code", width=100, anchor="center", stretch=False)
        self.tree.heading("date", text="Дата"); self.tree.column("date", width=110, anchor="center", stretch=False)
        self.tree.heading("sign", text="+/-"); self.tree.column("sign", width=50, anchor="center", stretch=False)
        self.tree.heading("event", text="Событие"); self.tree.column("event", minwidth=200, stretch=True)
        self.tree.pack(side="left", fill="both", expand=True)
        
        scrollbar = ctk.CTkScrollbar(tree_container, orientation="vertical", command=self.tree.yview)
        scrollbar.pack(side="right", fill="y")
        self.tree.configure(yscrollcommand=scrollbar.set)
        
        search_frame = ctk.CTkFrame(main_form, fg_color="transparent")
        search_frame.grid(row=7, column=0, columnspan=2, sticky="ew", pady=(10, 0))
        search_frame.grid_columnconfigure(1, weight=1)
        ttk.Label(search_frame, text="Наберите код:").grid(row=0, column=0, sticky="w", padx=(0, 5))
        self.search_entry = ctk.CTkEntry(search_frame)
        self.search_entry.grid(row=0, column=1, sticky="ew", padx=(0, 5))
        find_btn = ctk.CTkButton(search_frame, text="Искать", fg_color="#3B82F6", command=self._search_by_code)
        find_btn.grid(row=0, column=2, sticky="e")
        
    def update_project_dependent_lists(self, forced_project_code=None):
        proj_code = forced_project_code or getattr(self.window_ref, 'current_selected_project_code', "")
        
        if not proj_code:
            placeholder_groups = ["Все группы"]
            placeholder_inds = ["Все показатели"]
            
            self.cmb_group.configure(values=placeholder_groups)
            self.cmb_group.set(placeholder_groups[0])
            self.cmb_indicator.configure(values=placeholder_inds)
            self.cmb_indicator.set(placeholder_inds[0])
            return
            
        groups_raw, name_to_code_map = self.logic.load_groups_and_indicators()
        
        safe_groups = []
        if groups_raw is not None:
            try:
                if hasattr(groups_raw[0], 'name'):
                    safe_groups = sorted(list(set([item.name for item in groups_raw])))
                elif isinstance(groups_raw[0], tuple) and len(groups_raw[0]) > 1:
                    safe_groups = sorted(list(set([item[1] for item in groups_raw])))
                elif isinstance(groups_raw[0], str):
                    safe_groups = sorted(list(set(groups_raw)))
            except Exception:
                pass
                
        final_group_list = ["Все группы"] + safe_groups if safe_groups else ["Все группы"]
        
        self.group_names_list = final_group_list
        self.group_names_to_codes = {"": ""}
        if isinstance(name_to_code_map, dict):
            self.group_names_to_codes.update(name_to_code_map)
            
        self.cmb_group.configure(values=final_group_list)
        self.cmb_group.set(final_group_list[0])
        
        all_indicators = []
        seen_names = set()
        for cached_list in self.indicator_cache.values():
            for item in cached_list:
                name = item[0]
                if name not in seen_names:
                    all_indicators.append(item)
                    seen_names.add(name)
                    
        all_indicators.sort(key=lambda x: x[0])
        ind_names = ["Все показатели"] + [item[0] for item in all_indicators]
        
        self.indicator_names_list = ind_names
        self.cmb_indicator.configure(values=ind_names)
        self.cmb_indicator.set(ind_names[0])
        
        self.after_idle(lambda code=proj_code: (
            self.window_ref._update_control_buttons_state(True),
            self._refresh_event_table(code)
        ))
        
    def _on_group_change(self, event=None):
        group_name = self.cmb_group.get()
        if group_name == "Все группы":
            self.cmb_indicator.set("")
            self.indicator_names_list = ["Все показатели"]
            self.cmb_indicator.configure(values=["Все показатели"])
        
        self._update_indicator_combobox(group_name)
        
    def _update_indicator_combobox(self, group_name: str):
        self.cmb_indicator.set("")
        if not group_name or group_name == "Все группы":
            placeholder_list = ["Все показатели"]
            self.indicator_names_list = placeholder_list
            self.cmb_indicator.configure(values=placeholder_list)
            self.cmb_indicator.set("Все показатели")
            return
            
        group_code = self.group_names_to_codes.get(group_name)
        if not group_code:
            self.cmb_indicator.configure(values=["Все показатели"])
            self.cmb_indicator.set("Все показатели")
            return
            
        indicators = self.logic.load_indicators_for_group(group_code)
        
        ind_names = ["Все показатели"] + [item[0] for item in indicators]
        self.indicator_names_list = ind_names
        self.cmb_indicator.configure(values=ind_names)
        
        self.indicator_cache[group_code] = indicators
        
        if len(ind_names) > 1:
            self.cmb_indicator.set(ind_names[1])
        else:
            self.cmb_indicator.set("Все показатели")
            
    def _collect_current_selection(self) -> dict:
        proj_code = self.window_ref.current_selected_project_code
        group_name = self.cmb_group.get()
        group_code = self.group_names_to_codes.get(group_name, "") if group_name else ""
        indicator_name = self.cmb_indicator.get()
        indicator_code = ""
        
        if indicator_name and indicator_name != "Все показатели" and group_code:
            indicators_list = self.indicator_cache.get(group_code, [])
            try:
                idx = [item[0] for item in indicators_list].index(indicator_name)
                indicator_code = f"{group_code}_{idx + 1}"
            except ValueError:
                pass
                
        return {
            "project_code": proj_code,
            "group_name": group_name,
            "group_code": group_code,
            "indicator_name": indicator_name,
            "indicator_code": indicator_code
        }
        
    def _add_event(self):
        data = self._collect_current_selection()
        if not all([data['project_code'], data['group_code'], data['indicator_code']]):
            msg_parts = []
            if not data['project_code']: msg_parts.append("Проект")
            if not data['group_code']: msg_parts.append("Группа")
            if not data['indicator_code']: msg_parts.append("Показатель")
            
            messagebox.showwarning("Ошибка", f"Заполнены не все обязательные поля.\nОтсутствуют: {', '.join(msg_parts)}")
            return
            
        text = self.txt_event.get("1.0", "end").strip()
        if not text:
            messagebox.showwarning("Ошибка", "Поле 'Содержание события' пустое.")
            return
            
        date_str = self.date_entry.get()
        sign = self.dir_var.get()
        success, msg = self.logic.add_event_to_db(data, text, date_str, sign)
        messagebox.showinfo("Результат", msg)
        
        if success:
            self.txt_event.delete("1.0", "end")
            proj_code = self.window_ref.current_selected_project_code
            self.cmb_group.set("Все группы")
            self.cmb_indicator.set("Все показатели")
            self._refresh_event_table(proj_code)
            
    def _refresh_event_table(self, project_code: str = None):
        # Очистка таблицы
        for row in self.tree.get_children(): 
            self.tree.delete(row)
            
        if not project_code:
            return
            
        db_path = os.path.join(self.logic.project_dbs_folder, f"{project_code}.db")
        if not os.path.exists(db_path): 
            return
            
        try:
            conn = sqlite3.connect(db_path)
            cur = conn.cursor()
            
            query = "SELECT event_code, event_data, event_sign, event_text_content FROM project_data"
            params = ()
            conditions = []
            
            current_group = self.cmb_group.get()
            current_indicator = self.cmb_indicator.get()
            
            if current_group and current_group != "Все группы":
                group_code = self.group_names_to_codes.get(current_group, "")
                if group_code:
                    conditions.append("group_code = ?")
                    params += (group_code,)
                    
            if current_indicator and current_indicator != "Все показатели":
                conditions.append("indicator_name = ?")
                params += (current_indicator,)
                
            if conditions:
                query += " WHERE " + " AND ".join(conditions)
                
            query += " ORDER BY id DESC LIMIT 50"
            
            print(f"[SQL EXEC] Query: {query} | Params: {params}")
            cur.execute(query, params)
            rows = cur.fetchall()
            conn.close()
            
            print(f"[TABLE DEBUG] Fetched {len(rows)} items from disk.")
            
            # Предварительная очистка выделения перед заполнением новой таблицей
            self.tree.selection_remove(self.tree.selection())
            
            # Вставка строк с привязкой клика к полю txt_event
            for idx, row in enumerate(rows, start=1):
                sign_char = "+" if row[2] == 1 else "-"
                full_text = row[3]
                
                iid = f"row_{idx}"
                self.tree.insert("", "end", iid=iid, values=(idx, row[0], row[1], sign_char, full_text))
                
        except Exception as e:
            print(f"[CRITICAL TABLE FAIL]: {e}")
            
    def _on_tree_select(self, event):
        """
        Срабатывает ПРИ ЛЮБОМ ВЫБОРЕ СТРОКИ.
        Заполняет форму данными выбранной строки.
        """
        selected = self.tree.selection()
        if not selected:
            return
        item_id = selected[0]
        vals = self.tree.item(item_id)["values"]
        
        if len(vals) < 5:
            return
        text_content = vals[4]
        
        # Блокируем модифицированные события, чтобы избежать зацикливания
        self.txt_event.unbind("<<Modified>>")
        
        # Очищаем и заполняем поле текстом из таблицы
        self.txt_event.delete("1.0", "end")
        self.txt_event.insert("1.0", text_content)
        
        # Заполняем дату и знак (+/-)
        try:
            date_obj = datetime.datetime.strptime(str(vals[2]), '%Y-%m-%d')
            self.date_entry.set_date(date_obj)
        except Exception:
            pass
            
        sign_val = int(vals[3].replace('+', '1').replace('-', '-1'))
        self.dir_var.set(sign_val)
        
        # Устанавливаем режим редактирования для этой строки
        self.editing_event_code = vals[1]
        
        # Включаем визуальную индикацию (желтый фон удален согласно ТЗ по очистке NativeToolTip)
        self._set_edit_mode_visuals(True)
        
        # Возвращаем отслеживание изменений текста
        self.after(100, lambda: self.txt_event.bind("<<Modified>>", self._on_text_modified))
        
    def _set_edit_mode_visuals(self, is_active: bool):
        """Меняет цвет фона поля текста и состояние кнопок"""
        color = "#FFF9C4" if is_active else "#F0F0F0" # Желтый или стандартный
        self.txt_event.configure(background=color)
        
        # Кнопки согласно ТЗ: Редактировать блокируется, Сохранить активна
        self.btn_edit.configure(state="disabled")   
        self.btn_save.configure(state="normal")     
        
        # Разблокируем поля даты и знака для изменения
        self.date_entry.config(state="normal")
        
    def _get_text_by_iid(self, item_id: str) -> str:
        vals = self.tree.item(item_id)["values"]
        if len(vals) > 4:
            return vals[4]
        return ""
        
    def _enter_edit_mode(self):
        sel = self.tree.selection()
        if not sel: 
            messagebox.showwarning("Ошибка", "Выберите событие в таблице."); return
        self._enter_edit_mode_by_iid(sel[0])
        
    def _enter_edit_mode_by_iid(self, item_id: str):
        vals = self.tree.item(item_id)["values"]
        if len(vals) < 5:
            return
            
        self.editing_event_code = vals[1]
        
        # Заполняем поля формы данными выбранной строки
        self.txt_event.delete("1.0", "end")
        self.txt_event.insert("1.0", vals[4])
        
        # Дата события
        try:
            date_obj = datetime.datetime.strptime(str(vals[2]), '%Y-%m-%d')
            self.date_entry.set_date(date_obj)
        except Exception:
            pass
            
        # Направленность (+/-)
        sign_val = int(vals[3].replace('+', '1').replace('-', '-1'))
        self.dir_var.set(sign_val)
        
        # === РЕЖИМ РЕДАКТИРОВАНИЯ (КНОПКИ И ПОЛЯ) ===
        # Блокируем неизменяемые фильтры
        self.btn_add.configure(state="disabled")
        self.btn_del.configure(state="disabled")
        self.cmb_group.configure(state="disabled")
        self.cmb_indicator.configure(state="disabled")
        
        # Кнопка Редактировать становится серой (заблокирована)
        self.btn_edit.configure(state="disabled")   
        
        # Кнопка Сохранить остается активной (оранжевой), как вы просили НЕ скрывать её
        # Если она была disabled - включаем
        self.btn_save.configure(state="normal")     
        
        # Разблокируем поля ввода для изменений (ТЗ п.3)
        self.txt_event.configure(state="normal")
        self.date_entry.config(state="normal")
        # Радиокнопки dir_var обычно доступны всегда, если frame активен
        
    def _save_edited_event(self):
        if not getattr(self, 'editing_event_code', None):
            return
            
        new_text = self.txt_event.get("1.0", "end").strip()
        if not new_text:
            # Используем тот же messagebox, который мы только что импортировали
            messagebox.showwarning("Ошибка", "Текст не может быть пустым.")
            return
            
        try:
            date_str = self.date_entry.get_date().strftime('%Y-%m-%d')
        except Exception:
            date_str = datetime.datetime.now().strftime('%Y-%m-%d')
            
        sign_val = self.dir_var.get()
        
        db_path = os.path.join(self.logic.project_dbs_folder, f"{self._collect_current_selection()['project_code']}.db")
        
        try:
            conn = sqlite3.connect(db_path)
            cur = conn.cursor()
            cur.execute(
                '''UPDATE project_data 
                   SET event_text_content = ?, event_data = ?, event_sign = ? 
                   WHERE event_code = ?''', 
                (new_text, date_str, sign_val, self.editing_event_code)
            )
            conn.commit()
            conn.close()
            
            # Если дошли до сюда без ошибок — показываем успех
            messagebox.showinfo("Готово", "Изменения сохранены.")
            self._reset_controls()
            
            proj_code = self._collect_current_selection()['project_code']
            self._refresh_event_table(proj_code)
                
        except Exception as e:
            # Если упало подключение к БД или SQL-запрос — показываем ошибку
            messagebox.showerror("Ошибка записи", f"Не удалось сохранить изменения: {e}")
            
    def _delete_selected_event(self):
        sel = self.tree.selection()
        if not sel: return
        if not messagebox.askyesno("Подтверждение", "Удалить выбранное событие?"): return
        
        event_code = self.tree.item(sel[0])["values"][1]
        db_path = os.path.join(self.logic.project_dbs_folder, f"{self._collect_current_selection()['project_code']}.db")
        conn = sqlite3.connect(db_path)
        cur = conn.cursor()
        cur.execute("DELETE FROM project_data WHERE event_code = ?", (event_code,))
        conn.commit()
        conn.close()
        
        self._refresh_event_table(self._collect_current_selection()['project_code'])
        
    def _reset_controls(self):
        self.editing_event_code = None
        
        # Возвращаем базовые состояния кнопок
        self.btn_add.configure(state="normal")
        self.btn_del.configure(state="normal")
        self.cmb_group.configure(state="readonly")
        self.cmb_indicator.configure(state="readonly")
        
        # Кнопка Редактировать снова зеленая и активная
        self.btn_edit.configure(state="normal")     
        
        # Кнопка Сохранить гаснет (становится недоступной)
        self.btn_save.configure(state="disabled")   
        
        # Очищаем поле текста
        self.txt_event.delete("1.0", "end")
        
    def _paste_from_clipboard(self):
        try:
            txt = self.clipboard_get()
            self.txt_event.insert(tk.INSERT, txt)
        except tk.TclError:
            pass
            
    def _bind_global_events(self):
        self.txt_event.bind("<Button-3>", lambda e: (self._paste_from_clipboard(), "break"))
        
    def _search_by_code(self):
        target = self.search_entry.get().strip()
        if not target:
            messagebox.showwarning("Поиск", "Введите код."); return
            
        items = self.tree.get_children()
        found = None
        for item in items:
            vals = self.tree.item(item)["values"]
            if len(vals) > 1 and vals[1] == target:
                found = item
                break
                
        if found:
            self.tree.selection_set(found)
            self.tree.see(found)
            self.tree.focus(found)
        else:
            messagebox.showinfo("Поиск", "Событие с таким кодом не найдено.")

# =================================================================




# =================================================================
# dukora/modules/project/project_event_logic.py
# =================================================================
import os
import sqlite3
from typing import List, Tuple, Dict
from pathlib import Path
from modules.data.project_database import ProjectDatabase

# --- НАДЕЖНОЕ ОПРЕДЕЛЕНИЕ ПУТИ К БАЗАМ ---
try:
    # Используем нашу рабочую функцию из resources.py
    from modules.utils.resources import get_project_root
    
    ROOT_DIR = get_project_root()
    DB_FOLDER = ROOT_DIR / "db"
    DATABASE_DB_PATH = DB_FOLDER / "database.db"
    PROJECT_DBS_FOLDER = ROOT_DIR / "project_dbs"
except Exception as e:
    print(f"[FATAL] Не удалось определить корень проекта для путей к БД: {e}")
    sys.exit(1)

class ProjectEventLogic:
    def __init__(self, manager_db: ProjectDatabase):
        self.manager_db = manager_db # Теперь снова можем брать список проектов
        
        # Пути берем из глобальных констант модуля (которые мы настроили ранее)
        self.project_dbs_folder = PROJECT_DBS_FOLDER 
        os.makedirs(self.project_dbs_folder, exist_ok=True)

    # --- СПИСОК ПРОЕКТОВ (из projects.db) ---
    def load_project_list(self) -> List[Tuple]:
        return self.manager_db.list_projects()

    # --- ГРУППЫ И ПОКАЗАТЕЛИ (из database.db - без изменений) ---
    def load_groups_and_indicators(self) -> Tuple[List[str], Dict[str, str]]:
        if not DATABASE_DB_PATH.exists():
            return [], {}
        
        try:
            conn = sqlite3.connect(DATABASE_DB_PATH)
            cursor = conn.cursor()
            
            # Группы - просто список имен
            cursor.execute("SELECT name FROM groups ORDER BY name")
            group_names = [row[0] for row in cursor.fetchall()]
            
            name_to_code_map = {}
            if group_names:
                cursor.execute("SELECT code, name FROM groups ORDER BY name")
                rows = cursor.fetchall()
                name_to_code_map = {name: code for code, name in rows}
                
            conn.close()
            return group_names, name_to_code_map
        except Exception as e:
            print(f"[LOGIC ERROR] Failed to read from master DB: {e}")
            return [], {}

    def load_indicators_for_group(self, group_code: str):
        if not DATABASE_DB_PATH.exists():
            return []
        
        try:
            conn = sqlite3.connect(DATABASE_DB_PATH)
            cursor = conn.cursor()
            # Индикаторы - просто список имен
            cursor.execute("SELECT name FROM indicators WHERE group_code = ? ORDER BY name", (group_code,))
            indicators = [(row[0], ) for row in cursor.fetchall()] 
            conn.close()
            return indicators
        except Exception as e:
            print(f"[LOGIC ERROR] Failed to read indicators: {e}")
            return []


    # --- ЗАПИСЬ СОБЫТИЙ (без изменений) ---
    def add_event_to_db(self, selected_data: Dict, event_text: str, date_str: str, direction: int) -> tuple[bool, str]:
        db_path = os.path.join(self.project_dbs_folder, f"{selected_data['project_code']}.db")
        
        is_new_db = not os.path.exists(db_path)
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()

        # === КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ СТРУКТУРЫ ТАБЛИЦЫ ===
        # Проверяем, есть ли нужная колонка
        cursor.execute("PRAGMA table_info(project_data)")
        columns = [col[1] for col in cursor.fetchall()]
        
        if 'indicator_code' not in columns or 'event_sign' not in columns:
            print("[DB FIX] Table structure outdated or missing. Recreating project_data...")
            # Удаляем старую кривую таблицу
            cursor.execute("DROP TABLE IF EXISTS project_data")
            
            # Создаем новую со всеми правильными полями
            cursor.execute('''
                CREATE TABLE project_data (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    project_code TEXT NOT NULL,
                    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                    group_name TEXT,
                    group_code TEXT,
                    indicator_name TEXT,
                    indicator_code TEXT,
                    event_data DATE,
                    event_sign INTEGER, -- ВАЖНО: именно Integer, как ожидает ваша логика (+/-)
                    event_code TEXT,
                    event_text_content TEXT
                )
            ''')
            conn.commit()

        # Теперь запрос COUNT точно найдет колонку indicator_code
        cursor.execute(
            "SELECT COUNT(*) FROM project_data WHERE project_code = ? AND indicator_code = ?", 
            (selected_data['project_code'], selected_data['indicator_code'])
        )
        count = cursor.fetchone()[0]
        
        # Код события формируется только если выбран индикатор
        event_code = f"{selected_data['indicator_code']}_{count + 1}" if selected_data['indicator_code'] else f"EVENT_{count + 1}"

        try:
            cursor.execute('''
                INSERT INTO project_data (
                    project_code, group_name, group_code, indicator_name, indicator_code,
                    event_data, event_sign, event_code, event_text_content
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
            ''', (
                selected_data['project_code'],
                selected_data['group_name'],
                selected_data['group_code'],
                selected_data['indicator_name'],
                selected_data['indicator_code'],
                date_str,
                direction,  # Передаем число 1 или -1 напрямую
                event_code,
                event_text
            ))
            conn.commit()
            return True, f"Данные добавлены. Код события: {event_code}"
        except Exception as e:
            conn.rollback()
            return False, f"Ошибка записи в БД: {e}"
        finally:
            conn.close()
# =================================================================








# ===================================================
# dukora/modules/project/project_create_ui.py
# ===================================================
import customtkinter as ctk
from tkinter import ttk
import datetime
from project_create_logic import ProjectCreateLogic

class ProjectCreateUI(ctk.CTkFrame):
    EDIT_MODE_COLOR = "#8B4513" # Коричневый для кнопки Сохранить
    VIEW_MODE_EDIT_BTN_COLOR = "#10B981" # Зеленый для Редактировать

    def __init__(self, master, logic: ProjectCreateLogic):
        super().__init__(master)
        self.logic = logic
        self._current_editing_id = None
        self._setup_widgets()
        self._load_projects()
        self._set_view_mode()

    def _setup_widgets(self):
        padding = {"padx": 10, "pady": 5}
        
        # Поля ввода (Шапка)
        entry_frame = ctk.CTkFrame(self)
        entry_frame.pack(fill="x", **padding)
        
        self.name_entry = ctk.CTkEntry(entry_frame, placeholder_text="Название проекта:")
        self.name_entry.grid(row=0, column=0, sticky="ew", **padding)
        
        self.short_code_entry = ctk.CTkEntry(entry_frame, placeholder_text="Шифр (проекта):")
        self.short_code_entry.grid(row=0, column=1, sticky="ew", **padding)
        
        self.code_entry = ctk.CTkEntry(entry_frame, placeholder_text="Код (ID):")
        self.code_entry.grid(row=0, column=2, sticky="ew", **padding)
        
        self.date_entry = ctk.CTkEntry(entry_frame, placeholder_text="Дата открытия:")
        today = datetime.datetime.now().strftime("%Y-%m-%d")
        self.date_entry.insert(0, today)
        self.date_entry.grid(row=1, column=0, sticky="ew", **padding)
        
        self.resp_entry = ctk.CTkEntry(entry_frame, placeholder_text="Ответственный:")
        self.resp_entry.grid(row=1, column=1, sticky="ew", **padding)

        # Кнопки действий над шапкой
        action_frame = ctk.CTkFrame(self)
        action_frame.pack(fill="x", **padding)
        
        self.edit_btn = ctk.CTkButton(action_frame, text="Редактировать", fg_color=self.VIEW_MODE_EDIT_BTN_COLOR, command=self._enter_edit_mode)
        self.edit_btn.pack(side="left", expand=True, fill="x", padx=(0, 5))
        
        self.save_btn = ctk.CTkButton(action_frame, text="Сохранить", state="disabled", fg_color=self.EDIT_MODE_COLOR, command=self._save_changes)
        self.save_btn.pack(side="left", expand=True, fill="x", padx=(5, 0))

        # Список существующих проектов
        tree_frame = ctk.CTkFrame(self)
        tree_frame.pack(fill="both", expand=True, **padding)
        
        columns = ("id", "name", "code", "desc", "date", "resp")
        self.tree = ttk.Treeview(tree_frame, columns=columns, show="headings", height=6)
        self.tree.heading("name", text="Название"); self.tree.column("name", width=150)
        self.tree.heading("code", text="Код"); self.tree.column("code", width=80)
        self.tree.heading("date", text="Дата"); self.tree.column("date", width=80)
        self.tree.pack(fill="both", expand=True, side="left")
        
        scrollbar = ctk.CTkScrollbar(tree_frame, orientation="vertical", command=self.tree.yview)
        scrollbar.pack(side="right", fill="y")
        self.tree.configure(yscrollcommand=scrollbar.set)

    def _load_projects(self):
        for row in self.tree.get_children(): self.tree.delete(row)
        for proj in self.logic.manager_db.list_projects():
            # Получаем полные данные для отображения в таблице
            with self.logic.manager_db.connect() as conn:
                cur = conn.execute("SELECT * FROM projects WHERE id = ?", (proj[0],))
                full_row = cur.fetchone()
                if full_row:
                    self.tree.insert("", "end", values=(
                        full_row["id"], full_row["name"], full_row["code"], 
                        full_row["description"], full_row["date_opened"], full_row["responsible"]
                    ))

    def _clear_entries(self):
        widgets = [self.name_entry, self.short_code_entry, self.code_entry, self.date_entry, self.resp_entry]
        for w in widgets: w.delete(0, "end")

    def _set_view_mode(self):
        widgets = [self.name_entry, self.short_code_entry, self.code_entry, self.date_entry, self.resp_entry]
        for w in widgets: w.configure(state="disabled")
        self.edit_btn.configure(state="normal")
        self.save_btn.configure(state="disabled")

    def _enter_edit_mode(self):
        selected = self.tree.selection()
        if selected:
            item = self.tree.item(selected[0])
            vals = item["values"]
            self._current_editing_id = vals[0]
            self.name_entry.configure(state="normal"); self.name_entry.delete(0, "end"); self.name_entry.insert(0, vals[1])
            self.short_code_entry.configure(state="normal"); self.short_code_entry.delete(0, "end"); self.short_code_entry.insert(0, vals[2])
            # Код проекта менять нельзя
            self.code_entry.configure(state="disabled"); self.code_entry.delete(0, "end"); self.code_entry.insert(0, vals[2])
            self.date_entry.configure(state="normal"); self.date_entry.delete(0, "end"); self.date_entry.insert(0, vals[4])
            self.resp_entry.configure(state="normal"); self.resp_entry.delete(0, "end"); self.resp_entry.insert(0, vals[5])
        else:
            self._current_editing_id = None
            for w in [self.name_entry, self.short_code_entry, self.code_entry, self.date_entry, self.resp_entry]:
                w.configure(state="normal")
                w.delete(0, "end")

        self.edit_btn.configure(state="disabled")
        self.save_btn.configure(state="normal")

    def _save_changes(self):
        data = {
            'human_name': self.name_entry.get(),
            'short_code': self.short_code_entry.get(),
            'full_code': self.code_entry.get(),
            'description': "", # В ТЗ нет отдельного поля описания в UI, оставляем пустым
            'date': self.date_entry.get(),
            'responsible': self.resp_entry.get()
        }
        
        if self._current_editing_id:
            # Логика обновления существующей записи (если потребуется расширить функционал)
            pass 
        else:
            success, msg = self.logic.create_project(data)
            messagebox.showinfo("Успех", msg) if success else messagebox.showerror("Ошибка", msg)
            
        self._load_projects()
        self._clear_entries()
        self._set_view_mode()
# =================================================================





# =====================================================
# dukora/modules/project/project_create_logic.py
# =====================================================
import datetime
import os
import sys
import sqlite3
from tkinter import messagebox
from modules.data.project_database import ProjectDatabase
from typing import Optional, Tuple, Dict, List

class ProjectCreateLogic:
    MIN_CODE_LENGTH = 3
    
    def __init__(self, manager_db: ProjectDatabase):
        self.manager_db = manager_db
        self.project_dbs_folder = os.path.join(os.getcwd(), "project_dbs")
        os.makedirs(self.project_dbs_folder, exist_ok=True)

    def validate_inputs(self, human_name: str, short_code: str, full_code: str) -> tuple[bool, str]:
        if not human_name or not short_code or not full_code:
            return False, "Все поля шапки обязательны для заполнения."
        if len(full_code) < self.MIN_CODE_LENGTH:
            return False, f"Код проекта должен быть длиннее {self.MIN_CODE_LENGTH} символов."
        return True, ""

    def check_project_exists(self, code: str) -> bool:
        """Проверяет существование записи в общем списке И файла индивидуальной БД."""
        exists_in_list = any(p[2] == code for p in self.manager_db.list_projects())
        file_exists = os.path.exists(os.path.join(self.project_dbs_folder, f"{code}.db"))
        return exists_in_list or file_exists

    def suppress_stdout(func):
        """Декоратор для подавления вывода print при инициализации ProjectDatabase."""
        def wrapper(*args, **kwargs):
            old_stdout = sys.stdout
            sys.stdout = open(os.devnull, 'w')
            try:
                return func(*args, **kwargs)
            finally:
                sys.stdout.close()
                sys.stdout = old_stdout
        return wrapper

    @suppress_stdout
    def initialize_individual_db(self, code: str) -> bool:
        """Создает пустую индивидуальную базу для конкретного проекта."""
        db_path = os.path.join(self.project_dbs_folder, f"{code}.db")
        if os.path.exists(db_path):
            return False
            
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS project_data (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                project_code TEXT NOT NULL,
                created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                group_name TEXT,
                indicator_name TEXT,
                event_data DATE,
                event_sign INTEGER,
                event_code TEXT,
                event_text_content TEXT
            )
        ''')
        conn.commit()
        conn.close()
        return True

    def create_project(self, data: dict) -> tuple[bool, str]:
        """
        :param data: {'human_name': ..., 'short_code': ..., 'full_code': ..., 'description': ..., 'date': ..., 'responsible': ...}
        """
        is_valid, error_msg = self.validate_inputs(
            data['human_name'], data['short_code'], data['full_code']
        )
        if not is_valid:
            return False, error_msg

        if self.check_project_exists(data['full_code']):
            return False, f"База данных или проект с кодом '{data['full_code']}' уже существует."

        success = self.manager_db.add_project(
            name=data['human_name'],
            code=data['full_code'],
            cipher=data['short_code'],   # <--- ДОБАВИЛИ ПЕРЕДАЧУ ШИФРА
            description=data['description'],
            date_opened=data['date'],
            responsible=data['responsible']
        )

        if not success:
            return False, "Не удалось сохранить данные проекта в общий реестр."

        init_success = self.initialize_individual_db(data['full_code'])
        if not init_success:
            # Если создание индивид. БД провалилось, удаляем запись из общего списка для консистентности
            with self.manager_db.connect() as conn:
                conn.execute("DELETE FROM projects WHERE code = ?", (data['full_code'],))
            return False, "Проект создан в реестре, но не удалось создать рабочую базу данных."

        return True, f"Проект '{data['human_name']}' успешно создан!"





# ========================================
# dukora/modules/data/project_database.py
# ========================================
import os
import sqlite3
from pathlib import Path
from typing import Optional, List, Tuple
from types import SimpleNamespace

class ProjectDatabase:
    """
    Управляет общей базой данных 'projects.db', хранящей метаданные о проектах.
    Создает файл БД в директории db относительно корня проекта.
    """
    def __init__(self):
        # Используем утилиту получения корня проекта
        from modules.utils.resources import get_project_root
        
        self.base_dir = get_project_root() / "db"
        self.base_dir.mkdir(parents=True, exist_ok=True)
        
        self.db_path = self.base_dir / "projects.db"
        self.connection: Optional[sqlite3.Connection] = None

    def connect(self) -> sqlite3.Connection:
        if self.connection is None:
            try:
                self.connection = sqlite3.connect(self.db_path)
                self.connection.row_factory = sqlite3.Row
            except sqlite3.Error as e:
                raise RuntimeError(f"Ошибка подключения к {self.db_path}: {e}")
        return self.connection

    def close(self):
        if self.connection:
            self.connection.close()
            self.connection = None

    def create_tables(self):
        conn = self.connect()
        cursor = conn.cursor()
        # ДОБАВИЛИ колонку cipher TEXT NOT NULL UNIQUE
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS projects (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL UNIQUE,
                code TEXT NOT NULL UNIQUE,
                cipher TEXT NOT NULL UNIQUE, 
                description TEXT DEFAULT '',
                date_opened TEXT DEFAULT '',
                responsible TEXT DEFAULT ''
            )
        ''')
        conn.commit()


    def add_project(self, name: str, code: str, cipher: str, description: str, date_opened: str, responsible: str) -> bool:
        """Добавляет запись о проекте в общий список."""
        try:
            with self.connect() as conn:
                conn.execute(
                    "INSERT INTO projects (name, code, cipher, description, date_opened, responsible) VALUES (?, ?, ?, ?, ?, ?)",
                    (name, code, cipher, description, date_opened, responsible)
                )
            return True
        except sqlite3.IntegrityError:
            return False
        except Exception:
            return False


    def list_projects(self) -> List[Tuple]:
        """Возвращает список кортежей (id, name, code, cipher)."""
        with self.connect() as conn:
            cursor = conn.execute("SELECT id, name, code, cipher FROM projects ORDER BY name")
            return cursor.fetchall()


    def list_projects_names(self) -> List[str]:
        """Возвращает плоский список только названий проектов для ComboBox."""
        with self.connect() as conn:
            cursor = conn.execute("SELECT name FROM projects ORDER BY name")
            return [row[0] for row in cursor.fetchall()]

    def get_all_groups(self):
        """Читает общую таблицу групп."""
        try:
            with self.connect() as conn:
                cursor = conn.cursor()
                # Проверяем наличие таблицы перед запросом
                cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='groups';")
                if not cursor.fetchone():
                    return []
                
                cursor.execute("SELECT id, code, name FROM groups ORDER BY name")
                rows = cursor.fetchall()
                # Возвращаем объекты SimpleNamespace для совместимости с вашим старым кодом UI
                from types import SimpleNamespace
                return [SimpleNamespace(id=r[0], code=r[1], name=r[2]) for r in rows]
        except Exception as e:
            print(f"[DB ERROR] Groups load failed: {e}")
            return []

    def get_indicators_by_group(self, group_code: str):
        """Читает показатели конкретной группы из общей базы."""
        try:
            with self.connect() as conn:
                cursor = conn.cursor()
                # Проверяем наличие таблицы индикаторов
                cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='indicators';")
                if not cursor.fetchone():
                    return []

                cursor.execute("SELECT id, code, name, group_code FROM indicators WHERE group_code = ? ORDER BY name", (group_code,))
                rows = cursor.fetchall()
                from types import SimpleNamespace
                return [SimpleNamespace(id=r[0], code=r[1], name=r[2], group_code=r[3]) for r in rows]
        except Exception as e:
            print(f"[DB ERROR] Indicators load failed: {e}")
            return []
# =================================================================






# ============================================
# dukora/modules/data/database_module.py
# ============================================
import sys
import sqlite3
from pathlib import Path

try:
    # Определяем корень проекта надежно:
    # Если мы находимся внутри подпапок dukora, идем вверх до корня репозитория.
    # __file__ дает путь к текущему .py файлу.
    current_file_path = Path(__file__).resolve()
    
    # Ищем папку 'dukora' в родителях
    try:
        dukora_parent = current_file_path.parents[[p.name for p in current_file_path.parents].index('dukora')]
    except ValueError:
        # Если по какой-то причине не нашли (запуск из другой директории), берем CWD
        dukora_parent = Path.cwd()
        
    BASE_DIR = dukora_parent
    
    DB_FOLDER = BASE_DIR / "db"
    ACTIVE_FLAG_PATH = DB_FOLDER / "active.flag"
    
    DEFAULT_DB_PATH = DB_FOLDER / "database.db"
    MASTERDATA_PATH = DB_FOLDER / "masterdata.db"
except Exception as e:
    print(f"[FATAL] Не удалось вычислить путь к базе данных: {e}")
    sys.exit(1)

def _determine_active_path():
    try:
        if ACTIVE_FLAG_PATH.exists():
            with open(ACTIVE_FLAG_PATH, 'r', encoding='utf-8') as f:
                name = f.read().strip()
                if name == "masterdata":
                    return MASTERDATA_PATH
    except Exception:
        pass
    return DEFAULT_DB_PATH

DB_PATH = _determine_active_path()

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session as SqlAlchemySession, declarative_base

engine = create_engine(f"sqlite:///{DB_PATH}", echo=False, connect_args={"check_same_thread": False})
Base = declarative_base()
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def get_db():
    db_session = SessionLocal()
    try:
        yield db_session
    finally:
        db_session.close()

def get_db_connection():
    active_path = _determine_active_path()
    global DB_PATH
    DB_PATH = active_path
    
    # === КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ ===
    # Сначала проверяем наличие папки и создаем её ДО попытки открыть БД
    if not DB_FOLDER.exists():
        print(f"[ACTION] Создаю папку для базы: {DB_FOLDER}")
        try:
            DB_FOLDER.mkdir(parents=True, exist_ok=True)
        except PermissionError as pe:
            print(f"[FATAL] Нет прав на создание папки '{DB_FOLDER}'. Запустите терминал от имени Администратора.")
            raise SystemExit(1)
            
    conn = sqlite3.connect(active_path)
    conn.execute("PRAGMA foreign_keys = ON;")
    return conn

def _create_tables(cursor):
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS groups (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL UNIQUE,
    code TEXT NOT NULL UNIQUE,
    gain_group REAL DEFAULT 0.0
    )
    ''')
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS indicators (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL UNIQUE,
    code TEXT NOT NULL UNIQUE,
    group_code TEXT NOT NULL,
    FOREIGN KEY (group_code) REFERENCES groups(code) ON DELETE CASCADE
    )
    ''')

def _populate_test_data(conn):
    cursor = conn.cursor()
    cursor.execute("SELECT COUNT(*) FROM groups")
    count = cursor.fetchone()[0]
    if count == 0:
        print("[INFO] База данных пуста. Заполняю тестовыми данными...")
        groups_data = [("Клубы", "KB", 1.5), ("Фанаты", "FAN", 2.0)]
        cursor.executemany("INSERT INTO groups (name, code, gain_group) VALUES (?, ?, ?)", groups_data)
        indicators_data = [
            ("ЦСКА", "KB-1", "KB"), ("Спартак", "KB-2", "KB"), ("Зенит", "KB-3", "KB"),
            ("Мужчины", "FAN-1", "FAN"), ("Женщины", "FAN-2", "FAN"), ("Дети", "FAN-3", "FAN")
        ]
        cursor.executemany("INSERT INTO indicators (name, code, group_code) VALUES (?, ?, ?)", indicators_data)
        conn.commit()
        print("[SUCCESS] Тестовые данные успешно записаны.")
    else:
        print("[INFO] База данных уже содержит данные. Пропускаю инициализацию.")

def init_database():
    print("[INIT] Проверка наличия рабочих баз данных...")
    conn = None
    try:
        conn = get_db_connection()
        cursor = conn.cursor()
        _create_tables(cursor)
        _populate_test_data(conn)
    except sqlite3.Error as e:
        print(f"[ERROR] Ошибка при инициализации DATABASE.DB: {e}")
        if conn:
            conn.rollback()
    finally:
        if conn:
            conn.close()
            
    master_conn = None
    try:
        if not MASTERDATA_PATH.exists():
            print(f"[ACTION] База мастер-данных не найдена. Создаю новую: {MASTERDATA_PATH.name}")
            master_conn = sqlite3.connect(MASTERDATA_PATH)
            master_conn.execute("PRAGMA foreign_keys = ON;")
            master_cursor = master_conn.cursor()
            _create_tables(master_cursor)
            _populate_test_data(master_conn)
    except sqlite3.Error as e:
        print(f"[ERROR] Ошибка при инициализации MASTERDATA.DB: {e}")
        if master_conn:
            master_conn.rollback()
    finally:
        if master_conn:
            master_conn.close()

class Session:
    def __init__(self):
        pass

# --- ФУНКЦИИ ДЛЯ ИМПОРТА (лежат строго вне класса Session) ---
def list_groups():
    """Функция-заглушка для прямого импорта"""
    return [(1, "Клубы"), (2, "Фанаты")]

def get_all_indicators_by_group(group_code):
    """Функция-заглушка для прямого импорта"""
    mock_data = {
        "KB": [(1, "ЦСКА"), (2, "Спартак"), (3, "Зенит")],
        "FAN": [(4, "Мужчины"), (5, "Женщины"), (6, "Дети")]
    }
    return mock_data.get(group_code, [])

if __name__ == "__main__":
    init_database()
# =================================================================




#====================================================
# dukora\gui\main_interface.py 
#====================================================
import os
import customtkinter as ctk
from PIL import Image
import sys
from modules.manager.manager_window import ManagerWindow
from modules.experts.experts_window import ExpertsWindow
from modules.project.project_window import ProjectWindow
from modules.forecasting.forecast_window import ForecastWindow
from modules.utils.resources import resource_path 
from typing import List, Any


class MainApp(ctk.CTk):
    def __init__(self):
        super().__init__()
        
        self.title("Dozor System")
        
        try:
            if sys.platform == "win32":
                from ctypes import windll
                windll.shcore.SetProcessDpiAwareness(1) 
        except Exception:
            pass
            
        target_w, target_h = 422, 335
        self.geometry(f"{target_w}x{target_h}") 
        self.resizable(False, False)
        bg_color = "#F0F0F0"
        self.configure(fg_color=bg_color)
        
        main_bg = ctk.CTkFrame(self, fg_color=bg_color)
        main_bg.pack(fill="both", expand=True, padx=1, pady=1)
        
        # === СЕТКА ИНТЕРФЕЙСА ===
        main_bg.grid_columnconfigure(0, minsize=210, weight=0) 
        main_bg.grid_columnconfigure(1, weight=1) 
        
        for i in range(6): 
            main_bg.grid_rowconfigure(i, weight=0)
        main_bg.grid_rowconfigure(6, weight=1) 
        
        button_font = ("Arial", 13, "bold")
        btn_height = 45
        
        # === ЛЕВАЯ ПАНЕЛЬ МЕНЮ (КНОПКИ ВЫЗОВА) ===
        sidebar = ctk.CTkFrame(main_bg, fg_color="#EEEEEE", width=210)
        sidebar.grid(row=0, column=0, rowspan=7, sticky="nsew", padx=(5, 5), pady=5)
        sidebar.grid_columnconfigure(0, weight=1)
        
        actions = [
            ("Администратор", self.open_admin_module),
            ("Менеджер базы", self.open_manager_module), 
            ("Эксперты", self.open_experts_module),
            ("Проект", self.open_project_module),
            ("Прогнозирование", self.open_forecasting_module)
        ]
        
        # Создание физических кнопок на экране
        for i, (text, command_func) in enumerate(actions):
            btn = ctk.CTkButton(
                master=sidebar,
                text=text,
                font=button_font,
                height=btn_height,
                command=command_func
            )
            btn.grid(row=i, column=0, padx=10, pady=5, sticky="ew")
            
        # === ПРАВАЯ ЧАСТЬ ВИТРИНЫ ===
        content_area = ctk.CTkFrame(main_bg, fg_color=bg_color)
        content_area.grid(row=0, column=1, rowspan=7, sticky="nsew", padx=(0, 10), pady=10)
        
        content_area.grid_columnconfigure(0, weight=1)
        content_area.grid_rowconfigure(0, weight=0) 
        content_area.grid_rowconfigure(1, minsize=2, weight=0) 
        content_area.grid_rowconfigure(2, minsize=27, weight=0) 
        content_area.grid_rowconfigure(3, weight=0) 
        
        pic_container = ctk.CTkFrame(content_area, fg_color="#d9ead3", width=220, height=130)
        pic_container.grid(row=0, column=0, pady=(5, 0)) 
        pic_container.grid_propagate(False)
        
        img_loaded = False
        try:
            img_path = resource_path('images/logo1.png')
            pil_img = Image.open(img_path)
            
            target_w_i, target_h_i = 210, 110
            ratio = pil_img.width / pil_img.height
            box_ratio = target_w_i / target_h_i
            
            if ratio > box_ratio:
                new_w, new_h = target_w_i, int(target_w_i / ratio)
            else:
                new_w, new_h = int(target_h_i * ratio), target_h_i
                
            resized_img = pil_img.resize((new_w, new_h), resample=Image.LANCZOS)
            tk_image = ctk.CTkImage(light_image=resized_img, size=(new_w, new_h))
            
            lbl_pic = ctk.CTkLabel(pic_container, image=tk_image, text="", fg_color="#d9ead3")
            lbl_pic.place(relx=0.5, rely=0.5, anchor='center', x=0, y=-3)
            img_loaded = True
            
        except Exception as e:
            fail_lbl = ctk.CTkLabel(pic_container, text="Сова\nне найдена", font=("Arial", 14), fg_color="#d9ead3")
            fail_lbl.place(relx=0.5, rely=0.5, anchor="center")
            
        bottom_btns_frame = ctk.CTkFrame(content_area, fg_color=bg_color)
        bottom_btns_frame.grid(row=3, column=0, sticky="ew", padx=0)
        
        bottom_btns_frame.grid_columnconfigure(0, minsize=195, weight=0) 
        ctk.CTkButton(
            bottom_btns_frame, 
            text="Руководство", 
            font=button_font, 
            height=btn_height,
            command=self.open_guide
        ).grid(row=0, column=0, sticky="ew", pady=(0, 8))
        
        ctk.CTkButton(
            bottom_btns_frame, 
            text="О программе", 
            font=button_font, 
            height=btn_height,
            command=self.show_about
        ).grid(row=1, column=0, sticky="ew")
        
        footer_frame = ctk.CTkFrame(main_bg, fg_color="#0078D7", height=30)
        footer_frame.grid(row=7, column=0, columnspan=2, sticky="ew", pady=(10, 0))
        
        footer_label = ctk.CTkLabel(footer_frame, 
                                    text="Copyright © Минск, НИИ, 2027 год", 
                                    text_color="#FF8C00",
                                    fg_color="#0078D7", 
                                    font=("Arial", 13, "bold"))
        footer_label.pack(expand=True, ipady=4)
        
        self.update_idletasks()
        self.tk.call('wm', 'geometry', self._w, f"{target_w}x{target_h}")
        
        screen_w = self.winfo_screenwidth()
        # Высота верхней панели задач Windows обычно около 40 пикселей. 
        # Если панель скрыта или находится сбоку/снизу, отступ можно уменьшить до 0-10.
        taskbar_offset_top = 40 

        pos_x = (screen_w - target_w) // 2
        pos_y = taskbar_offset_top

        # Устанавливаем размер И позицию одной командой через configure
        self.geometry(f"{target_w}x{target_h}+{pos_x}+{pos_y}")

    def _position_center_top(self, window_instance, width: int, height: int):
        screen_w = window_instance.winfo_screenwidth()
        pos_x = (screen_w - width) // 2
        pos_y = 40 # тот же отступ сверху
        window_instance.geometry(f"{width}x{height}+{pos_x}+{pos_y}")

    # === ОБРАБОТЧИК КНОПКИ АДМИНИСТРАТОРА (с принудительным логом) ===
    def open_admin_module(self):
        if hasattr(self, '_admin_win') and self._admin_win.winfo_exists():
            self._admin_win.lift()
            return

        try:
            from modules.admin.admin_accordion_window import AdminAccordionWindow
            self._admin_win = AdminAccordionWindow(self)
            self.wait_window(self._admin_win)
        except Exception as e:
            messagebox.showerror("Фатальный сбой", f"Не удалось открыть раздел Администратора.\n\n{e}")
            import traceback
            traceback.print_exc()

    def open_manager_module(self):
        if hasattr(self, '_manager_win') and self._manager_win.winfo_exists():
            self._manager_win.lift()
            return
        self._manager_win = ManagerWindow(master=self)
        self.wait_window(self._manager_win)
        
    def open_experts_module(self):
        if hasattr(self, '_experts_win') and self._experts_win.winfo_exists():
            self._experts_win.lift()
            return
        try:
            from modules.experts.experts_window import ExpertsWindow
            self._experts_win = ExpertsWindow(master=self)
            self.wait_window(self._experts_win)
        except Exception as e:
            messagebox.showerror("Фатальный сбой", f"Не удалось открыть раздел Экспертов.\n\n{e}")
            import traceback
            traceback.print_exc()
        
    def open_project_module(self):
        """Открывает окно управления проектами."""
        if hasattr(self, '_project_win') and self._project_win.winfo_exists():
            self._project_win.lift()
            return

        self._project_win = ProjectWindow(master=self)
        
        try:
            # Сбрасываем кэш транзакций
            self._project_win.manager_db.close()
            
            # Читаем ВСЕ проекты из мастер-базы
            # Кортеж имеет вид: (id, Name, Code, Cipher)
            fresh_projects = self._project_win.manager_db.list_projects()

            # === ИСПРАВЛЕНИЕ 1: БЕРЕМ ДЛЯ ОТОБРАЖЕНИЯ ШИФР (индекс 3) ===
            display_names = [p[3] for p in fresh_projects]
            
            print(f"[DEBUG MAIN MENU] Final display list: {display_names}")
            
            if hasattr(self._project_win, 'project_selector'):
                self._project_win.project_selector.configure(values=display_names)
                
                if display_names:
                    selected_display = display_names[-1]
                    
                    # Устанавливаем текст в поле (пользователь видит Шифр)
                    self._project_win.project_selector.set(selected_display)
                    
                    # === ИСПРАВЛЕНИЕ 2: НАХОДИМ КОД ПО ПРАВИЛЬНОМУ ШИФРУ ===
                    real_code = next((p[2] for p in fresh_projects if p[3] == selected_display), "")
                    self._project_win.current_selected_project_code = real_code
                    
                    print(f"[SYNC] UI shows '{selected_display}', internal code set to '{real_code}'")
                    
                else:
                    self._project_win.project_selector.set("Проектов нет")
                    self._project_win.current_selected_project_code = ""
                    
            # Обновляем таблицу-отладку внизу окна (оставляем там всё для наглядности)
            if hasattr(self._project_win, 'tree_projects'):
                tree = self._project_win.tree_projects
                for row in tree.get_children(): 
                    tree.delete(row)
                
                for proj in sorted(fresh_projects, key=lambda x: x[0]):
                    tree.insert("", "end", values=proj)
                    
        except Exception as e:
            print(f"[ERROR] Failed to sync projects on module open: {e}")
            if hasattr(self._project_win, 'project_selector'):
                self._project_win.project_selector.configure(values=[])
                self._project_win.project_selector.set("Проектов нет")
                self._project_win.current_selected_project_code = ""
                
        self._project_win.grab_set()


        
    def open_forecasting_module(self):
        if hasattr(self, '_forecast_win') and self._forecast_win.winfo_exists():
            self._forecast_win.lift()
            return
        self._forecast_win = ForecastWindow(master=self)
        self.wait_window(self._forecast_win)
        
    def open_guide(self):
        file_name = "manual.chm" 
        full_path = resource_path(file_name)
        print(f"Пытаюсь открыть: {full_path}") 
        if os.path.exists(full_path):
            try:
                os.startfile(full_path)
            except Exception as e:
                print(f"Система отказалась открывать файл: {e}")
        else:
            print("Ошибка: ФАЙЛА НЕТ в этой папке!")
            
    def show_about(self):
        from tkinter import messagebox
        info_text = (
            "Система Dozor V1.0\n"
            "Разработано: Минск, НИИ\n"
            "Версия сборки: 31.07.2026\n\n"
            "(c) Все права защищены."
        )
        messagebox.showinfo(title="О программе", message=info_text)

if __name__ == "__main__":
    app = MainApp()
    app.mainloop()
# =================================================================




#================================================
# dukora/modules/utils/resources.py
#================================================
import sys
import os
from pathlib import Path

def get_project_root() -> Path:
    """
    Определяет корень проекта один раз.
    Работает и при запуске .py, и внутри замороженного .exe.
    """
    if getattr(sys, 'frozen', False):
        # Если приложение запущено как bundle (PyInstaller)
        return Path(sys.executable).parent
    else:
        # Если запущено как скрипт
        return Path(__file__).resolve().parents[2]

def resource_path(relative_path: str) -> Path:
    try:
        # Режим работы внутри скомпилированного EXE-файла
        base_path = Path(sys._MEIPASS)
    except Exception:
        # Режим разработки (.py): Идем строго от этого файла (__file__)
        # Поднимаемся на 2 уровня вверх до корня проекта 'dukora'
        base_path = Path(__file__).resolve().parents[2]
    
    return base_path / relative_path
# =================================================================