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


Промт для переделки выпадающего списка
Мне нужно переписать функцию формирования выпадающего списка (ComboBox) в моем Python-приложении. 
Исходные данные: показатели выгружаются из базы данных (поля: indicator_code, indicator_name). Сейчас они сортируются по алфавиту названия, это ломает логику. Мне нужна строгая сортировка по возрастанию поля indicator_code.
Задача:
1.	Объединить код и название в одну строку вида: [CODE]\t[NAME].
2.	Название показателя ([NAME]) должно быть разбито на несколько строк прямо внутри этой опции списка. Перенос должен происходить по словам (не разрывая слово пополам) каждые 60–70 символов. Для переносов использовать символ новой строки \n. Вторую и последующие строки нужно сдвинуть вправо (добавить отступы пробелами), чтобы текст стоял под текстом названия, а не под кодом.
3.	Для корректного выравнивания столбцов обязуй пользователя установить моноширинный шрифт (укажи в коде семейство вроде Courier New) для этого Combobox.
4.	Идентификация выбранного значения. Несмотря на то что ComboBox показывает объединенную строку, вся дальнейшая логика приложения должна работать ТОЛЬКО С ПОЛЕМ indicator_code. Реализуй скрытый маппинг (словарь), где ключом будет полная отображаемая строка, а значением — чистый код индикатора.
Вот мой текущий код [сюда вставьте свой код]. Переделай его согласно этим требованиям."
Отступы: Если табы \t будут выглядеть неровно из-за разницы в длине кодов, замените формирование строки на форматирование фиксированной ширины: f"{item['code']:<10} {'\n '.join(lines)}" (где <10 выделяет 10 символов под код).


Здесь еще логика нарушена в формировании кода события. Его код формирпуется из кода группы + порядковый номер показателя + порядковый номер события при данном показателе. Но в выпадающем списке берется не код уже созданного показателя, а формируется заново, и исходя из расположения по алфавиту. Главный и правильный код показателя в базе данных. Ниже высылаю файлы заполнения событий проекта, где создан неправильно выпадающий список для его правки. 

Кроме проблем со списком есть еще одно необходимое для правки место: Календарь.
Проблема в том, что когда создается событие и когда происходит правка события, а именно даты , то у них ставятся разные разделители числа, месяца и года. В одном случае это точки, а в другом это дефисы - надо привести одному знаменателю.

# =================================================================
# 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 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 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\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_window.py
# =========================================
import customtkinter as ctk
import tkinter as tk
from tkinter import ttk, messagebox  
from typing import List, Any  

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