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


Работаем с окном "Наполнение проекта" с заголовком аккордеона "Построить диаграмму". 

Цель: Заставить окно графика открыться.
Текущая проблема: Нажатие кнопки «Построить диаграмму» не вызывает никаких действий в консоли (print не срабатывает). Код до функции построения окна просто не доходит.

План действий:

Проверить файл интерфейса: кнопка должна создаваться до назначения ей команды .configure(command=...).
Заменить файлы на эталонные версии, где добавлены жесткие отладочные принты ([DEBUG] BUTTON CLICKED), чтобы точно видеть, где обрывается цепочка.
Если принты появились, но окна нет — чинить пути к базам данных или библиотеки визуализации.

Чтобы ты мог найти ошибку, высылаю содержимое трех файлов. 

1. dukora/modules/project/project_event_logic.py

(Самый важный файл) Ты увидишь:

Есть ли строчка self.project_dbs_folder = PROJECT_DBS_FOLDER.
Полное тело метода get_events_for_chart.

2. dukora/modules/project/chart_modal.py

(Файл самого окна) Ты увидишь:

Импорты в самом верху (особенно import datetime и from tkinter import messagebox).
Начало функции show_chart_modal. Там обязательно должен быть самый первый вызов: print("[DEBUG] show_chart_modal CALLED").

3. dukora/modules/project/project_window.py

(Главный контроллер) Ты увидишь:

Строку def _try_build_chart(): (она находится внутри файла project_chart_ui.py, который вызывается из project_window).
Как именно вызывается show_chart_modal(...) и передаются туда переменные.



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

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

Итогом работы должно стать полностью рабочее окно  "Построить диаграмму". 

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


# ==========================================
# dukora\modules\project\project_window.py
# ==========================================
import customtkinter as ctk
import tkinter as tk
from tkinter import ttk, messagebox  
from typing import List, Any 

import sqlite3
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_logic import ProjectEventLogic
from modules.project.project_event_ui import ProjectEventUI
from modules.project.project_create_logic import ProjectCreateLogic
from modules.data.project_database import ProjectDatabase
from modules.project.chart_modal import show_chart_modal


class ProjectWindow(ctk.CTkToplevel):
    def __init__(self, master):
        super().__init__(master)
        
        self.parent_main = master
        self.title("Журнал событий")
        
        # Инициализируем пустым, пока не выбрали проект
        self.current_selected_project_code = "" 
        
        # === ИНИЦИАЛИЗАЦИЯ ПЕРЕМЕННЫХ (ОДИН РАЗ) ===
        self.accordion_data = {}
        self.currently_open_button = None
        self.var_group = None 
        
        self.current_user = getattr(master, 'current_user', None)
        
        screen_height = self.winfo_screenheight()
        window_width = 650
        initial_height = 380
        screen_w = self.winfo_screenwidth()
        x_cord = int((screen_w - window_width) / 2)
        y_cord = int((screen_h := self.winfo_screenheight() - initial_height) / 2) # Центрируем Y тоже
        self.geometry(f"{window_width}x{initial_height}+{x_cord}+{y_cord}")
        self.resizable(False, False) # ЗАПРЕЩАЕМ изменение размера
        self.grab_set() 
        self.transient(master)
        
        main_container = ctk.CTkFrame(self, corner_radius=10)
        main_container.pack(padx=10, pady=10, fill="both", expand=True)
        
        self._build_title_bar(main_container)
        
        if not hasattr(self, 'manager_db'):
            self.manager_db = ProjectDatabase()
            self.manager_db.create_tables()
            
        if not hasattr(self, 'event_logic'):
            self.event_logic = ProjectEventLogic(self.manager_db)
            
        self._build_project_selector(main_container)
        
        create_item = self._create_accordion_header(main_container, "Создать проект")
        events_item = self._create_accordion_header(main_container, "Открыть для ввода событий")
        charts_item = self._create_accordion_header(main_container, "Построение диаграмм")
        print_item = self._create_accordion_header(main_container, "Настройка печати")
        
        # Наполнение ВСЕХ секций сразу при создании окна
        self._fill_create_section(create_item["content"])
        self._fill_events_section(events_item["content"]) 
        self._fill_charts_section(charts_item["content"])  
        self._fill_print_section(print_item["content"])
        
        # Панель кнопок внизу главного окна
        button_frame = ctk.CTkFrame(main_container)
        button_frame.pack(fill="x", pady=(10, 0), padx=5)
        
        close_btn = ctk.CTkButton(button_frame, text="Закрыть", width=120, command=self.destroy)
        close_btn.pack(side="right", padx=10, pady=10)
        
        # Регистрация заголовков аккордеона
        for item in [create_item, events_item, charts_item, print_item]:
            btn = item["button"]
            content = item["content"]
            self.accordion_data[btn] = {"content": content}
            btn.configure(command=lambda b=btn, c=content: self.toggle_section(b, c))
            
        # Синхронизация размеров ДО открытия первой вкладки
        self.update_idletasks() 
        correct_geometry = self._calculate_window_geometry(collapsed=False)
        self.geometry(correct_geometry) 
        
        # Открываем первую вкладку
        self.toggle_section(create_item["button"], create_item["content"])
        
    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.")
            
            # ЖЕСТКАЯ ОЧИСТКА ТИПОВ ДАННЫХ (решение проблемы TclError)
            if hasattr(self, 'var_period_str') and isinstance(self.var_period_str, tk.IntVar):
                self.var_period_str.set(0)
            if hasattr(self, 'var_group') and isinstance(self.var_group, tk.StringVar):
                self.var_group.set("")
                
            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
            target_btn = None
            for btn, data in self.accordion_data.items():
                if btn.cget("text") == "Открыть для ввода событий":
                    target_btn = btn
                    break
                    
            if target_btn and target_btn in self.accordion_data:
                content_widget = self.accordion_data[target_btn]["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)
                if hasattr(events_section_content, '_refresh_event_table'):
                    events_section_content._refresh_event_table(internal_code)
            else:
                print("[WARN] ProjectEventUI instance not found by direct tree traversal.")
            # Обновление секции графиков + жесткая очистка типов
            if hasattr(self, 'cmb_group') and self.cmb_group:
                try:
                    from modules.project.project_event_logic import ProjectEventLogic
                    logic = ProjectEventLogic(self.manager_db)
                    groups_raw, _ = logic.load_groups_and_indicators()
                    names = []
                    if groups_raw:
                        try:
                            if hasattr(groups_raw[0], 'name'):
                                names = sorted([item.name for item in groups_raw])
                            elif isinstance(groups_raw[0], tuple):
                                names = sorted([item[1] for item in groups_raw])
                            else:
                                names = sorted(groups_raw)
                        except Exception:
                            pass
                    
                    # Сначала меняем тип внутри Python-объекта
                    if hasattr(self, 'var_period_str') and isinstance(self.var_period_str, tk.IntVar):
                        self.var_period_str.set(0)
                    if hasattr(self, 'var_group') and isinstance(self.var_group, tk.StringVar):
                        self.var_group.set("")
                        
                    # Затем обновляем интерфейс
                    self.cmb_group.configure(values=names)
                    if names:
                        self.cmb_group.set(names[0])
                    else:
                        self.cmb_group.set("")
                        
                    if hasattr(self, 'listbox_groups') and self.listbox_groups:
                        self.listbox_groups.delete(0, tk.END)
                except Exception as e:
                    print(f"[CHART UI REFRESH ERROR]: {e}")
            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 _update_control_buttons_state(self, is_enabled: bool):
        target_state = "normal" if is_enabled else "disabled"
        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)
        # Нижние кнопки удалены отсюда — ими управляет только проект_chart_ui.py
            
    def _build_title_bar(self, container):
        title_lbl = ctk.CTkLabel(container, text="Наполнение проекта", font=ctk.CTkFont(size=20, weight="bold"))
        title_lbl.pack(pady=(0, 15))
        
    def _build_project_selector(self, container):
        outer_frame = ctk.CTkFrame(container, fg_color="transparent")
        outer_frame.pack(fill="x", pady=(5, 10))
        
        btn_font = ("Arial", 11, "bold")
        
        top_frame = ctk.CTkFrame(outer_frame, fg_color="transparent")
        top_frame.pack(fill="x", pady=(0, 10))
        
        label_project = ctk.CTkLabel(top_frame, text="Выбрать проект:", anchor="w")
        label_project.pack(side="left", padx=(0, 10))
        
        self.project_selector = ctk.CTkComboBox(
            top_frame,
            values=[], 
            state="readonly",
            width=250
        )
        self.project_selector.pack(side="left", expand=True, fill="x")
        self.project_selector.configure(command=lambda *args: self._force_ui_sync())
        
        control_frame = ctk.CTkFrame(outer_frame, fg_color="transparent")
        control_frame.pack(fill="x")
        
        control_frame.grid_columnconfigure(0, weight=1)
        control_frame.grid_columnconfigure(1, weight=1)
        self.btn_sync_context = ctk.CTkButton(
            master=control_frame,
            text="ОБНОВИТЬ ПРОЕКТ",
            font=btn_font,
            fg_color="#000000", 
            hover_color="#2B2B2B",
            command=self._force_ui_sync,
            state="disabled"
        )
        self.btn_sync_context.grid(row=0, column=0, sticky="ew", padx=(0, 5))
        self.btn_sync_lists = ctk.CTkButton(
            master=control_frame,
            text="ОБНОВИТЬ СПИСКИ",
            font=btn_font,
            fg_color="#000000", 
            hover_color="#2B2B2B",
            command=self._force_refresh_event_ui,
            state="disabled"
        )
        self.btn_sync_lists.grid(row=0, column=1, sticky="ew", padx=(5, 0))
        
    def toggle_section(self, button, content):
        is_currently_open = (self.currently_open_button == button)
        
        if self.currently_open_button and not is_currently_open:
            prev_content = self.accordion_data[self.currently_open_button]["content"]
            prev_content.pack_forget()
            self.currently_open_button.configure(fg_color="#34495e")
            
        if is_currently_open:
            content.pack_forget()
            self.currently_open_button = None
            new_geo = self._calculate_window_geometry(collapsed=True)
        else:
            content.pack(fill="x", padx=5, pady=(0, 5), anchor="center")
            self.currently_open_button = button
            button.configure(fg_color="#2c3e50")
            new_geo = self._calculate_window_geometry(collapsed=False)
        
        self.geometry(new_geo)
        self.after(50, self._enforce_onscreen_position)
        
        title = button.cget("text")
        
        # ЛОГИКА ЗАПОЛНЕНИЯ ПРИ ОТКРЫТИИ
        if title == "Построение диаграмм" and not is_currently_open:
            sync_enabled = False
            if hasattr(self, 'btn_sync_context'):
                sync_state = self.btn_sync_context.cget("state")
                sync_enabled = (sync_state == "normal")
                
            if sync_enabled and self.current_selected_project_code:
                self.after(100, lambda: self._load_chart_groups())
            else:
                # Используем стандартный messagebox из проекта_window.py
                messagebox.showinfo(
                    "Выбор проекта", 
                    "Сначала выберите активный проект над этим окном."
                )
    def _load_chart_groups(self):
        """Загрузка групп при открытии секции графиков"""
        
        # Очищаем старые данные
        if hasattr(self, 'listbox_groups') and self.listbox_groups:
            self.listbox_groups.delete(0, tk.END)
            
        if hasattr(self, 'cmb_group') and self.cmb_group:
            self.cmb_group.configure(values=[])
            self.cmb_group.set("")
        # Если проект НЕ выбран
        if not self.current_selected_project_code:
            print("[CHART LOAD] Project code is empty.")
            # Оставляем интерфейс пустым, но рабочим для выбора "Из выпадающего списка"
            return
        # Если проект ВЫБРАН
        try:
            from modules.project.project_event_logic import ProjectEventLogic
            logic = ProjectEventLogic(self.manager_db)
            groups_raw, _ = logic.load_groups_and_indicators()
            names = []
            if groups_raw:
                try:
                    if hasattr(groups_raw[0], 'name'):
                        names = sorted([item.name for item in groups_raw])
                    elif isinstance(groups_raw[0], tuple):
                        names = sorted([item[1] for item in groups_raw])
                    else:
                        names = sorted(groups_raw)
                except Exception as e:
                    print(f"[GROUP PARSE ERROR]: {e}")
            
            if hasattr(self, 'cmb_group') and self.cmb_group:
                self.cmb_group.configure(values=names)
                if names:
                    self.cmb_group.set(names[0]) # Выбираем первую группу по умолчанию
                    
        except Exception as e:
            print(f"[UI LOAD ERROR]: {e}")
        
    def _enforce_onscreen_position(self):
        if not self.winfo_exists():
            return
            
        self.update_idletasks()
        
        win_w = self.winfo_width()
        win_h = self.winfo_height()
        screen_w = self.winfo_screenwidth()
        screen_h = self.winfo_screenheight()
        
        x = int((screen_w - win_w) / 2)
        
        bottom_edge = self.winfo_rooty() + win_h
        
        overflow = bottom_edge - screen_h + 50 
        if overflow > 0:
            y = max(40, self.winfo_rooty() - overflow)
        else:
            y = 40
            
        self.geometry(f"+{x}+{y}")
        
    def _calculate_window_geometry(self, collapsed=False):
        current_width = self.winfo_width()
        if current_width == 1:
            current_width = 650
            
        header_height = 50 * 4 + 30  
        button_panel_height = 70   
        
        if collapsed or not self.currently_open_button:
            target_height = header_height + 80 + button_panel_height
        else:
            data = self.accordion_data[self.currently_open_button]
            content_widget = data["content"]
            self.update_idletasks()
            open_content_height = content_widget.winfo_reqheight()
            target_height = header_height + open_content_height + button_panel_height
            
        max_h = self.winfo_screenheight() - 50
        final_height = min(target_height, max_h)
        min_h = 460 
        final_height = max(final_height, min_h)
        
        return f"{current_width}x{final_height}"
        
    def _create_accordion_header(self, parent, title):
        item_container = ctk.CTkFrame(parent, fg_color="#f0f2f5", border_color="#d1d8e0", border_width=1, corner_radius=8)
        item_container.pack(fill="x", pady=5)
        header_btn = ctk.CTkButton(
            item_container,
            text=title,
            height=45,
            font=ctk.CTkFont(weight="bold", size=16),
            anchor="w",
            fg_color="#34495e",
            hover_color="#3c5a72",
        )
        header_btn.pack(fill="x")
        content_frame = ctk.CTkFrame(item_container, fg_color="transparent", corner_radius=0)
        return {"button": header_btn, "content": content_frame}
        
    def _handle_project_deletion(self):
        current_display = self.project_selector.get()
        if not current_display or current_display == "Проектов нет":
            messagebox.showwarning("Ошибка", "Сначала выберите проект над аккордеоном.")
            return
        
        try:
            self.manager_db.close()
        except Exception:
            pass
            
        fresh_projects = self.manager_db.list_projects()
        code_to_delete = next((p for p in fresh_projects if p[3] == current_display), None)
        if not code_to_delete:
            messagebox.showerror("Ошибка", f"ID для '{current_display}' не найден.")
            return
        confirm = messagebox.askyesno("Внимание", 
                                      f"Удалить проект '{current_display}'?\n"
                                      f"Это действие нельзя отменить.\nФайл событий будет стерт.")
        if not confirm:
            return
        
        import os
        db_file_path = os.path.join(self.event_logic.project_dbs_folder, f"{code_to_delete}.db")
        file_was_deleted = False
        if os.path.exists(db_file_path):
            try:
                os.remove(db_file_path)
                file_was_deleted = True
            except PermissionError:
                messagebox.showerror("Доступ запрещен", "Закройте другие программы и попробуйте снова.")
                return
        success = False
        try:
            conn = self.manager_db.connect()
            cursor = conn.cursor()
            cursor.execute("DELETE FROM projects WHERE code = ?", (code_to_delete,))
            conn.commit()
            success = True
        except sqlite3.Error as e:
            conn.rollback()
            print(f"[DB DELETE ERROR]: {e}")
        finally:
            self.manager_db.close()
        if success and (not os.path.exists(db_file_path) or file_was_deleted):
            updated = self.manager_db.list_projects()
            names = [p[3] for p in updated]
            self.project_selector.configure(values=names)
            if names:
                self.project_selector.set(names[-1])
                self.current_selected_project_code = next((p[2] for p in updated if p[3] == names[-1]), "")
            else:
                self.project_selector.set("Проектов нет")
                self.current_selected_project_code = ""
                
            if hasattr(self, 'tree_projects'):
                tree = self.tree_projects
                for row in tree.get_children():
                    tree.delete(row)
                for proj in sorted(updated, key=lambda x: x[0]):
                    tree.insert("", "end", values=proj)
                    
            messagebox.showinfo("Готово", f"'{current_display}' удален.")
        else:
            messagebox.showerror("Ошибка", "Не удалось обновить реестр. Перезапустите программу.")
            
    def _fill_create_section(self, frame):
        from modules.data.project_database import ProjectDatabase
        from modules.project.project_create_logic import ProjectCreateLogic
        
        if not hasattr(self, 'manager_db'):
            self.manager_db = ProjectDatabase()
            self.manager_db.create_tables()
            self.event_logic = ProjectEventLogic(self.manager_db)
        
        logic = ProjectCreateLogic(self.manager_db)
        
        form_frame = ctk.CTkFrame(frame, fg_color="transparent")
        form_frame.pack(fill="x", expand=True, padx=10, pady=10)
        form_frame.grid_columnconfigure(1, weight=1)
        
        entries = {}
        fields = [
            ("Название:", "human_name"),
            ("Шифр:", "short_code"), 
            ("Код (ID):", "full_code"),
            ("Дата открытия:", "date"),
            ("Ответственный:", "responsible")
        ]
        
        for i, (label_text, key) in enumerate(fields):
            ttk.Label(form_frame, text=label_text).grid(row=i, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
            entry = ctk.CTkEntry(form_frame)
            entry.grid(row=i, column=1, sticky="ew", pady=(0, 5))
            entries[key] = entry
            if key == "date":
                today = datetime.datetime.now().strftime("%Y-%m-%d")
                entry.insert(0, today)
                
        action_frame = ctk.CTkFrame(form_frame, fg_color="transparent")
        action_frame.grid(row=len(fields), column=0, columnspan=2, sticky="ew", pady=(5, 0))
        for i in range(4): action_frame.grid_columnconfigure(i, weight=1)
        
        create_btn = ctk.CTkButton(
            action_frame, 
            text="Создать", 
            fg_color="#3B82F6", 
            hover_color="#2563EB",
            command=lambda n=entries['human_name'], sc=entries['short_code'], c=entries['full_code'], d=entries['date'], r=entries['responsible']: self._handle_project_creation(
                {
                    'human_name': n.get(),
                    'short_code': sc.get(),
                    'full_code': c.get(),
                    'description': "",
                    'date': d.get(),
                    'responsible': r.get()
                },
                logic,
                lambda: [widget.delete(0, "end") for widget in entries.values()]
            )
        )
        create_btn.grid(row=0, column=0, sticky="ew", padx=(0, 5))
        
        clear_btn = ctk.CTkButton(
            action_frame, 
            text="Очистить", 
            fg_color="#6B7280", 
            hover_color="#4B5563",
            command=lambda: [widget.delete(0, "end") for widget in entries.values()]
        )
        clear_btn.grid(row=0, column=1, sticky="ew", padx=5)
        
        delete_btn = ctk.CTkButton(
            action_frame, 
            text="Удалить", 
            fg_color="#EF4444", 
            hover_color="#B91C1C",
            command=self._handle_project_deletion
        )
        delete_btn.grid(row=0, column=2, sticky="ew", padx=5)
        
    def _handle_project_creation(self, data_dict: dict, logic_instance: ProjectCreateLogic, on_success_callback):
        success, message = logic_instance.create_project(data_dict)
        if not success:
            messagebox.showerror("Ошибка создания", message)
            return
        messagebox.showinfo("Проект создан", message)
        on_success_callback()
        
        try:
            self.manager_db.close()
        except Exception:
            pass
            
        fresh_tuples = self.manager_db.list_projects()
        fresh_names = [p[1] for p in fresh_tuples]
        
        # Безопасное пересоздание селектора проектов
        parent_widget = self.project_selector.master
        old_selector = self.project_selector
        old_selector.pack_forget()
        old_selector.destroy()
        
        self.project_selector = ctk.CTkComboBox(
            master=parent_widget,
            values=fresh_names,
            state="readonly",
            width=250
        )
        self.project_selector.pack(side="left")
        
        if fresh_names:
            selected_proj_tuple = next((p for p in fresh_tuples if p[1] == fresh_names[-1]), None)
            display_text = selected_proj_tuple[2] if selected_proj_tuple else fresh_names[-1]
            self.project_selector.set(display_text)
            self.current_selected_project_code = selected_proj_tuple[2] if selected_proj_tuple else ""
        else:
            self.project_selector.set("Проектов нет")
            self.current_selected_project_code = ""
            
        self.project_selector.configure(command=lambda *args: self._force_ui_sync())
        
        if hasattr(self, 'tree_projects'):
            tree = self.tree_projects
            for row in tree.get_children():
                tree.delete(row)
            for proj in sorted(fresh_tuples, key=lambda x: x[0]):
                tree.insert("", "end", values=proj)
                
        current_title = self.currently_open_button.cget("text") if self.currently_open_button else ""
        if current_title == "Открыть для ввода событий":
            for child in self.currently_open_content.winfo_children():
                if isinstance(child, ProjectEventUI):
                    try:
                        child.update_project_dependent_lists()
                    except Exception as update_err:
                        print(f"[LOG] Ошибка обновления UI событий: {update_err}")
                    break
                    
    def _fill_events_section(self, frame):
        from modules.project.project_event_ui import ProjectEventUI
        from modules.project.project_event_logic import ProjectEventLogic
        
        if not hasattr(self, 'manager_db'):
            self.manager_db = ProjectDatabase()
            self.manager_db.create_tables()
            
        if not hasattr(self, 'event_logic'):
            self.event_logic = ProjectEventLogic(self.manager_db)
            
        ui = ProjectEventUI(frame, self, self.event_logic)
        ui.pack(fill="both", expand=True)
        
        current_proj_code = getattr(self, 'current_selected_project_code', "")
        if current_proj_code:
            ui.after(100, lambda pc=current_proj_code: (
                ui.update_project_dependent_lists(pc),
                ui._refresh_event_table(pc)
            ))
            
    def _fill_charts_section(self, frame):
        from modules.project.project_chart_ui import build_charts_interface
        
        charts_container = ctk.CTkFrame(frame)
        charts_container.pack(fill="both", expand=True, padx=5, pady=5)
        
        # Больше НЕ используем pack_propagate(False) здесь, 
        # так как project_chart_ui использует grid и рассчитывает свои размеры.
        build_charts_interface(charts_container, self)
    
    def _fill_print_section(self, frame):
        if DateEntry is None:
            error_lbl = ctk.CTkLabel(frame, text="Ошибка: установите библиотеку\npip install tkcalendar", text_color="red")
            error_lbl.pack(expand=True)
            return
            
    def _generate_word_report(self, group, report_date, orientation):
        if docx is None:
            messagebox.showerror("Библиотека не найдена", "Для экспорта нужна библиотека 'python-docx'.\nВыполните:\npip install python-docx")
            return
            
    def on_closing(self):
        try:
            self.after_cancel("all")
        except Exception as e:
            # Логируем ошибку отмены задач планировщика, но не прерываем закрытие
            print(f"[WARN] Failed to cancel scheduled events: {e}")
            
        for item in list(self.accordion_data.values()):
            try:
                content = item.get("content")
                if content:
                    content.destroy()
            except Exception as e:
                # Логируем ошибки уничтожения виджетов аккордеона
                print(f"[WARN] Failed to destroy accordion content: {e}")
                
        try:
            self.destroy()
        except Exception as e:
            # Логируем финальную ошибку закрытия корневого окна
            print(f"[CRITICAL] Failed to close ProjectWindow: {e}")
            
# =================================================================







# =================================================================
# dukora/modules/project/project_event_logic.py
# =================================================================
import os
import sys # <-- Добавьте эту строку, если её нет
import sqlite3
import datetime # <-- Обязательно должно быть здесь
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, code FROM indicators WHERE group_code = ? ORDER BY code", (group_code,))
            indicators = cursor.fetchall() 
            conn.close()
            return indicators
        except Exception as e:
            print(f"[LOGIC ERROR] Failed to read indicators: {e}")
            return []

    # --- ПОЛУЧЕНИЕ СОБЫТИЙ ДЛЯ ГРАФИКА ---
    def get_events_for_chart(
        self, 
        project_code: str, 
        group_names: List[str], 
        date_from: datetime.date, 
        date_to: datetime.date
    ) -> List[Tuple]:
        
        db_path = Path(self.project_dbs_folder) / f"{project_code}.db"
        
        if not db_path.exists():
            print(f"[DEBUG] DB file does not exist: {db_path}")
            return []
            
        try:
            conn = sqlite3.connect(db_path)
            cursor = conn.cursor()
            
            # Используем CAST или функции даты для надежности
            query = f'''
                SELECT event_data, group_name, COUNT(*) as cnt
                FROM project_data 
                WHERE group_name IN ({','.join('?' for _ in group_names)})
                  AND date(event_data) >= date(?)
                  AND date(event_data) < date(?)
                GROUP BY date(event_data), group_name
                ORDER BY date(event_data) ASC
            '''

            params = [*group_names, date_from.strftime('%Y-%m-%d'), date_to.strftime('%Y-%m-%d')]
            
            print(f"[DEBUG QUERY] Executing on {db_path.name}")
            print(f"[DEBUG PARAMS] Groups: {group_names}, From: {params[-2]}, To: {params[-1]}")
            
            cursor.execute(query, params)
            events = cursor.fetchall() 
            
            print(f"[DEBUG RESULTS] Found rows: {len(events)}")
            if events:
                print(f"[DEBUG SAMPLE] First row: {events[0]}")
                
            conn.close()
            return events
            
        except Exception as e:
            print(f"[LOGIC ERROR] Failed to read events for chart: {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\chart_modal.py
# ========================================
import customtkinter as ctk
import pandas as pd
import plotly.graph_objects as go
from io import BytesIO
import datetime
import sys
from typing import List, Any

# Импортируем вашу логику работы с событиями
try:
    from modules.project.project_event_logic import ProjectEventLogic
except ImportError:
    ProjectEventLogic = None


def fetch_real_chart_data(project_code: str, group_names: List[str], start_date: str, step_days: int):
    data = []
    
    if not ProjectEventLogic or not project_code:
        print("[CHART] Logic is missing.")
        return pd.DataFrame()
        
    try:
        # Создаем инстанс логики БЕЗ параметров, так как путь берется из глобальных констант модуля logic
        from modules.project.project_event_logic import ProjectEventLogic
        logic = ProjectEventLogic(None) 
        
        end_date_dt = datetime.datetime.strptime(start_date, '%d.%m.%Y') + datetime.timedelta(days=30)
        current_dt = datetime.datetime.strptime(start_date, '%d.%m.%Y')
        
        while current_dt <= end_date_dt:
            next_dt = current_dt + datetime.timedelta(days=step_days)
            
            # Вызываем ВАШ НОВЫЙ МЕТОД
            raw_events = logic.get_events_for_chart(
                project_code=project_code,
                group_names=group_names,
                date_from=current_dt.date(),
                date_to=next_dt.date()
            )
            
            # Агрегируем результат
            counts = {g: 0 for g in group_names}
            for row in raw_events:
                # row = (event_data, group_name, count)
                grp_name = row[1]
                cnt = row[2]
                if grp_name in counts:
                    counts[grp_name] += cnt
            
            for g_name, total_cnt in counts.items():
                data.append({
                    'date': current_dt.strftime('%Y-%m-%d'),
                    'group': g_name,
                    'count': total_cnt
                })
                
            current_dt = next_dt
            
        df = pd.DataFrame(data)
        return df
        
    except Exception as e:
        print(f"[LOGIC ERROR] Failed to read events for chart: {e}")
        return []


def show_chart_modal(master, project_code: str, group_names: List[str], start_date: str, step_days: int, chart_type: str):
    df = fetch_real_chart_data(project_code, group_names, start_date, step_days)
    
    # Проверка на пустоту данных
    if df.empty or df['count'].sum() == 0:
        ctk.CTkMessagebox.showinfo("Данные", "За данный период событий не найдено.")
        return

    modal = ctk.CTkToplevel(master)
    title_name = ', '.join(group_names[:3]) + ("..." if len(group_names) > 3 else "")
    modal.title(f"Интенсивность событий: {title_name}")
    modal.geometry("800x600")
    modal.grab_set()
    modal.transient(master)
    
    modal.grid_rowconfigure(0, weight=1)
    modal.grid_columnconfigure(0, weight=1)

    fig = go.Figure()
    
    dates = pd.to_datetime(df['date'])
    
    if chart_type == "Гистограмма":
        for name in group_names:
            subset = df[df['group'] == name]
            if not subset.empty:
                fig.add_trace(go.Bar(x=subset['date'], y=subset['count'], name=name))
    else:
        for name in group_names:
            subset = df[df['group'] == name]
            if not subset.empty:
                fig.add_trace(go.Scatter(x=subset['date'], y=subset['count'], mode='lines+markers', name=name))

    fig.update_layout(barmode='group' if chart_type == "Гистограмма" else 'overlay',
                      template="plotly_white", legend_title_text="Группы")

    canvas = tk.Canvas(modal, bg="#ffffff", highlightthickness=0)
    canvas.pack(fill="both", expand=True, padx=10, pady=10)

    try:
        import matplotlib.pyplot as plt
        from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
        
        img_bytes = fig.to_image(format="png", engine="kaleido", scale=2)
        buf = BytesIO(img_bytes)
        
        fig_mpl, ax = plt.subplots(figsize=(8, 6), dpi=100)
        ax.imshow(plt.imread(buf))
        ax.axis('off')
        
        agg_canvas = FigureCanvasTkAgg(fig_mpl, master=canvas)
        agg_canvas.draw()
        agg_canvas.get_tk_widget().pack(fill="both", expand=True)
        
    except ImportError:
        error_lbl = ctk.CTkLabel(modal, text="pip install matplotlib pillow kaleido", text_color="red")
        error_lbl.pack(expand=True)
        return

    button_frame = ctk.CTkFrame(modal)
    button_frame.pack(fill="x", pady=(0, 10), padx=10)
    
    def close_modal():
        modal.destroy()
        plt.close('all')

    copy_btn = ctk.CTkButton(button_frame, text="Копировать", fg_color="#22C55E", command=lambda: fig.write_image("temp_chart.png"))
    copy_btn.pack(side="left", fill="x", expand=True, padx=5)

    close_btn = ctk.CTkButton(button_frame, text="Закрыть", fg_color="#3B82F6", command=close_modal)
    close_btn.pack(side="right", fill="x", expand=True, padx=5)
# ========================================
    
 





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