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


Промпт: Завершение реализации окна аккордеона под заголовком "Открыть для ввода событий".
Цель: Полностью отладить функционал второго заголовка аккордеона "Открыть для ввода событий".

Задачи по отладке окна 

*1* Выпадающий список при переключении обновляется обновляется с помощьюнажатия кнопок Обновить проекты и Обновить показатели. Сейчас надпись в кнопкахменяется с белого на серый и обратно. Необходимо поменять цвет - когда надо обновить -надпись оранжевая, когда обновлено - белая. Серый слабо виден. 

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

*2* При подведении мыши к событию (клике) в таблице данная строчка должна выделяться и всплывать полное название события. Пока мышка на строчке всплывающее событие находится на экране. При смещении мышки через секунду событие закрывается.

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

*3* Редактирование событий. В данный момент в окне 4 основных кнопки. Необходимо сделать чтобы в окне было постоянно 3 основных кнопки - одна, в зависимости от режима должна быть скрыта. Стандартный режим: в окне 3 кнопки: "Добавить", "Редактировать", "Удалить". При нажатии на "Редактировать" кнопка "Редактировать" прячется, а на ее месте появляется кнопка "Сохранить". Все поля переходят в режим редактирования и могут быть изменены. После нажатия на кнопку "Сохранить" - происходит сохранение изменений кнопка "Сохранить" прячется и опять появляется кнопка "Редактировать" - окно выходит из режима редактирования. Изменять пользователь может только текст события, дату, направленность и удаление, остальные поля должны быть заблокированы для редактирования.

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

*4* Поиск. Фактически эта опция предназначена для поиска события для дальнейшего редактирования или удаления. Чтобы найти событие пользователь должен набрать код события и нажать на кнопку "Искать". Найденное событие должно быть в видимой части экрана в таблице и код залит желтым цветом. 

o Список представленных файлов
Чтобы провести отладку, даю актуальное содержимое следующих файлов целиком:
1.	Интерфейс формы событий: modules/project/project_event_ui.py
2.	Бизнес-логика чтения/записи: modules/project/project_event_logic.py
3.	Главный координатор (аккордеон): modules/project/project_window.py 
4.	Менеджер БД проектов: modules/data/project_database.py
5.	Единая база справочников: modules/data/database_module.py
6.	Точка входа (проверка инициализации мастер-данных):	gui/main_interface.py
7.  Функция путей: dukora/modules/utils/resources.py


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

Делаем отладку в той последовательности, как в ТЗ. 

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






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


# ==========================================
# КЛАСС ДЛЯ ВСПЛЫВАЮЩИХ ПОДСКАЗОК (TOOLTIP)
# ==========================================
class ToolTip(object):
    def __init__(self, widget):
        self.widget = widget
        self.tipwindow = None
        self.id = None
        self.x = self.y = 0
        self.delay = 1000  

    def showtip(self, text):
        if text.strip() == "": return
        
        self.hidetip()
        
        self.x, self.y = self.widget.winfo_pointerx(), self.widget.winfo_pointery() + 20
        
        self.tipwindow = tw = tk.Toplevel(self.widget)
        tw.wm_overrideredirect(True) 
        tw.wm_transient(self.widget.winfo_toplevel())
        
        try:
            tw.attributes('-topmost', True)
            tw.attributes('-alpha', 0.95)
        except Exception:
            pass

        tw.wm_geometry(f"+{self.x}+{self.y}")
        
        label = tk.Label(tw, text=text, justify=tk.LEFT,
                         background="#ffffe0", relief=tk.SOLID, borderwidth=1,
                         font=("tahoma", "8", "normal"))
        label.pack(ipadx=4, ipady=2)
        
        self.schedule_destroy()

    def hidetip(self):
        if self.tipwindow:
            self.tipwindow.destroy()
            self.tipwindow = None
            
        if self.id:
            self.widget.after_cancel(self.id)
            self.id = None

    def schedule_destroy(self):
        if self.id:
            self.widget.after_cancel(self.id)
        self.id = self.widget.after(self.delay, self.hidetip)


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._setup_widgets()
        self._bind_global_events()
        
        self.update_project_dependent_lists()
        
        self.after(50, self._initial_load)

    def _initial_load(self):
        try:
            current_text = self.window_ref.project_selector.get()
            actual_code = getattr(self.window_ref, 'current_selected_project_code', "")
            
            print(f"[INIT LOAD] Selector text: '{current_text}'. Internal code: '{actual_code}'")
            
            if actual_code and current_text != "Проектов нет":
                self._refresh_event_table(actual_code) 
                
        except Exception as e:
            print(f"[INIT LOAD ERROR]: {e}")

    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 = "clam" if "dark" in ctk.get_appearance_mode() else "default"
        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):
        """
        Глобальное обновление списков Группа/Показатель.
        Возвращает вашу предыдущую стабильную логику + защита от None.
        """
        proj_code = forced_project_code or getattr(self.window_ref, 'current_selected_project_code', "")
        
        if not proj_code:
            print("[UI SYNC] No active project code provided to lists updater.")
            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])
            self._refresh_event_table(proj_code)
            return

        print(f"[UI SYNC] Synchronous load for DB: {proj_code}.db")

        # Читаем сырые данные из бизнес-логики
        groups_raw, name_to_code_map = self.logic.load_groups_and_indicators()

        # === ЛЕЧЕНИЕ ОШИБОК ТИПОВ ДАННЫХ ===
        safe_groups = []
        if groups_raw is not None:  # Защита, если БД вернулась пустой (None)
            try:
                for item in groups_raw:
                    # Извлекаем строку, будь то кортеж ('Название',) или готовый объект
                    val = item[0] if isinstance(item, tuple) else str(item)
                    clean_str = str(val).strip()
                    if clean_str and clean_str != "": 
                        safe_groups.append(clean_str)
            except Exception as e:
                print(f"[ERROR PARSING GROUPS]: {e}")
        
        final_group_list = ["Все группы"] + sorted(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)
            
        print(f"[UI CONFIG] Setting ComboBox values to: {final_group_list}")
        self.cmb_group.configure(values=final_group_list)
        self.cmb_group.set("Все группы")

        # --- Собираем ВСЕ показатели изо всех кэшированных групп ---
        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._refresh_event_table(code))


    def _on_group_change(self, event=None):
        group_name = self.cmb_group.get()
        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:
                print(f"[WARN] Indicator '{indicator_name}' not found in cache for group {group_code}")
                
        return {
            "project_name": "",
            "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:
            print("[TABLE] No project code provided.")
            return
            
        db_path = os.path.join(self.logic.project_dbs_folder, f"{project_code}.db")
        if not os.path.exists(db_path): 
            print(f"[TABLE WARN] Physical DB missing: {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.")
            last_iid = None
            
            for idx, row in enumerate(rows, start=1):
                sign_char = "+" if row[2] == 1 else "-"
                
                iid = self.tree.insert(
                    "", 
                    "end", 
                    values=(idx, row[0], row[1], sign_char, row[3])
                )
                
                self.tree.tag_bind(iid, "<Enter>", lambda e, txt=row[3]: self._show_tooltip_for_item(e, txt))
                
                last_iid = iid
                
            if last_iid: 
                self.tree.see(last_iid)
                
        except Exception as e:
            print(f"[CRITICAL TABLE FAIL]: {e}")

    def _paste_from_clipboard(self):
        try:
            txt = self.clipboard_get()
            self.txt_event.insert(tk.INSERT, txt)
        except tk.TclError:
            pass

    def _enter_edit_mode(self):
        sel = self.tree.selection()
        if not sel: 
            messagebox.showwarning("Ошибка", "Выберите событие в таблице."); return
            
        vals = self.tree.item(sel[0])["values"]
        self.editing_event_code = vals[1]
        full_text = vals[4]
        self.txt_event.delete("1.0", "end")
        self.txt_event.insert("1.0", full_text)
        
        self.btn_add.configure(state="disabled")
        self.btn_edit.configure(state="disabled")
        self.btn_save.configure(state="normal")
        self.btn_del.configure(state="disabled")

    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.showwarning("Ошибка", "Текст не может быть пустым."); return
            
        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 = ? WHERE event_code = ?", 
                (new_text, 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:
            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 _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:
            if self.tree.item(item)["values"][1] == target:
                found = item
                break
        if found:
            self.tree.selection_set(found)
            self.tree.see(found)
            self.tree.focus(found)
        else:
            messagebox.showinfo("Поиск", "Событие с таким кодом не найдено.")

    def _reset_controls(self):
        self.btn_add.configure(state="normal")
        self.btn_edit.configure(state="normal")
        self.btn_save.configure(state="disabled")
        self.btn_del.configure(state="normal")
        self.txt_event.delete("1.0", "end")

    def _bind_global_events(self):
        self.txt_event.bind("<Button-3>", lambda e: (self._paste_from_clipboard(), "break"))
        
    def _show_tooltip_for_item(self, event, text):
        self.tooltip.showtip(text)

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






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

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

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

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

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

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


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

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

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

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






# =============================================
# modules/project/project_window.py
# =============================================
import customtkinter as ctk
from tkinter import ttk, messagebox
from modules.project.project_event_logic import ProjectEventLogic
from typing import List
import datetime
import os
from pathlib import Path

# Попытка импорта внешних библиотек
try:
    from tkcalendar import DateEntry
except ImportError:
    DateEntry = None
try:
    import docx
except ImportError:
    docx = None
from modules.project.project_event_ui import ProjectEventUI
from modules.project.project_create_logic import ProjectCreateLogic
from modules.data.project_database import ProjectDatabase


class ProjectWindow(ctk.CTkToplevel):
    def __init__(self, master):
        super().__init__(master)
               
        self.parent_main = master
        
        self.title("Журнал событий")
        self.current_user = getattr(master, 'current_user', None)
        
        # --- Настройка размеров ---
        screen_height = self.winfo_screenheight()
        window_width = 650
        initial_height = 380
        screen_w = self.winfo_screenwidth()
        x_cord = int((screen_w - window_width) / 2)
        taskbar_offset_top = 40 
        y_cord = taskbar_offset_top
        self.geometry(f"{window_width}x{initial_height}+{x_cord}+{y_cord}")
        
        self.resizable(True, False)
        self.grab_set() 
        self.transient(master)
        
        main_container = ctk.CTkFrame(self, corner_radius=10)
        main_container.pack(padx=10, pady=10, fill="both", expand=True)
        
        # === Заголовок ===
        self._build_title_bar(main_container)
        
        # Инициализация БД один раз для всего окна
        if not hasattr(self, 'manager_db'):
            self.manager_db = ProjectDatabase()
            self.manager_db.create_tables()
            
        if not hasattr(self, 'event_logic'):
            self.event_logic = ProjectEventLogic(self.manager_db)

        self.accordion_data = {}
        self.currently_open_button = None

        # ##################################################################
        # ### ПАНЕЛЬ УПРАВЛЕНИЯ ПРОЕКТОМ (САМЫЙ ВЕРХ ОКНА НАД АККОРДЕОНОМ) ###
        # ##################################################################
        self._build_project_selector(main_container)

        # === ЗАГОЛОВКИ АККОРДЕОНА ===
        create_item = self._create_accordion_header(main_container, "Создать проект")
        events_item = self._create_accordion_header(main_container, "Открыть для ввода событий")
        charts_item = self._create_accordion_header(main_container, "Показать графики")
        print_item = self._create_accordion_header(main_container, "Настройка печати")
        
        # Наполняем секции контентом
        self._fill_create_section(create_item["content"])
        self._fill_events_section(events_item["content"]) # Кнопок здесь больше нет
        self._fill_charts_section(charts_item["content"])
        self._fill_print_section(print_item["content"])
        
        # Привязка команд переключения
        for item in [create_item, events_item, charts_item, print_item]:
            btn = item["button"]
            content = item["content"]
            self.accordion_data[btn] = {"content": content}
            btn.configure(command=lambda b=btn, c=content: self.toggle_section(b, c))
            
        # Открываем первую секцию по умолчанию
        self.toggle_section(create_item["button"], create_item["content"])
            
        # Кнопка закрытия внизу
        button_frame = ctk.CTkFrame(main_container)
        button_frame.pack(fill="x", pady=(10, 0))
        
        close_btn = ctk.CTkButton(button_frame, text="Закрыть", width=120, command=self.destroy)
        close_btn.pack(side="right", padx=10, pady=10)
        
        self.protocol("WM_DELETE_WINDOW", self.on_closing)

        # Центрируем окно после сборки интерфейса
        self.update_idletasks()
        screen_w = self.winfo_screenwidth()
        pos_x = (screen_w - window_width) // 2
        pos_y = taskbar_offset_top
        self.geometry(f"{window_width}x{initial_height}+{pos_x}+{pos_y}")

    ###############################################################################
    # === СЕКЦИЯ 1: УПРАВЛЕНИЕ КОНТЕКСТОМ ПРОЕКТА И КНОПКАМИ ===
    ###############################################################################
    
    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")
            
            # Обновляем открытую вкладку событий мгновенно
            if hasattr(self, 'currently_open_content'):
                for child in self.currently_open_content.winfo_children():
                    if child.__class__.__name__ == "ProjectEventUI":
                        child.update_project_dependent_lists()
                        proj_db_file_name = f"{current_display_cipher}.db"
                        child._refresh_event_table(proj_db_file_name)
                        
            # Включаем кнопки управления
            self._update_control_buttons_state(is_enabled=True)
        else:
            print(f"[SYNC FORCE ERROR] Project cipher '{current_display_cipher}' not found.")
            self._update_control_buttons_state(is_enabled=False)

    def _force_refresh_event_ui(self):
        """
        Принудительно обновляет списки групп/показателей во вкладке событий.
        Вызывается кнопкой "ОБНОВИТЬ СПИСКИ".
        Теперь работает НАПРЯМУЮ без поиска дочерних виджетов.
        """
        print("[UI SYNC] Manual refresh of lists triggered.")
        
        # Получаем текущий выбранный проект (Шифр)
        current_display_cipher = self.project_selector.get()
        
        if not current_display_cipher or current_display_cipher == "Проектов нет":
            print("[SYNC FORCE] Context cleared for manual list update.")
            return

        try:
            self.manager_db.close()
        except Exception:
            pass

        fresh_projects = self.manager_db.list_projects()
        selected_tuple = next((p for p in fresh_projects if p[3] == current_display_cipher), None)
        
        if selected_tuple:
            internal_code = selected_tuple[2]
            
            # === ПРИНУДИТЕЛЬНЫЙ ВЫЗОВ МЕТОДА UI ===
            # Мы точно знаем, что экземпляр ProjectEventUI создан первым в аккордеоне.
            # Находим его среди детей первого элемента аккордеона.
            
            events_section_content = None
            for item in self.accordion_data.values():
                if item and "content" in item:
                    content_widget = item["content"]
                    # Ищем наш UI по имени класса внутри этой секции
                    for child in content_widget.winfo_children():
                        if child.__class__.__name__ == "ProjectEventUI":
                            events_section_content = child
                            break
            
            if events_section_content:
                print(f"[DIRECT CALL] Updating lists for project code: {internal_code}")
                events_section_content.update_project_dependent_lists(internal_code)
            else:
                print("[WARN] ProjectEventUI instance not found in accordion cache.")


    def _update_control_buttons_state(self, is_enabled: bool):
        """Включает или выключает кнопки управления под селектором."""
        target_state = "normal" if is_enabled else "disabled"
        
        if hasattr(self, 'btn_sync_context'):
            self.btn_sync_context.configure(state=target_state)
        if hasattr(self, 'btn_sync_lists'):
            self.btn_sync_lists.configure(state=target_state)

    ###############################################################################
    # === СЕКЦИЯ 2: ПОСТРОЕНИЕ ИНТЕРФЕЙСА (Layout) ===
    ###############################################################################

    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")
        
        # --- РЯД 0: ВЫБОР ПРОЕКТА ---
        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())

        # --- РЯД 1: КНОПКИ УПРАВЛЕНИЯ ---
        control_frame = ctk.CTkFrame(outer_frame, fg_color="transparent")
        control_frame.pack(fill="x")
        
        control_frame.grid_columnconfigure(0, weight=1)
        control_frame.grid_columnconfigure(1, weight=1)

        self.btn_sync_context = ctk.CTkButton(
            master=control_frame,
            text="ОБНОВИТЬ ПРОЕКТ",
            font=btn_font,
            fg_color="#000000", 
            hover_color="#2B2B2B",
            command=self._force_ui_sync,
            state="disabled"
        )
        self.btn_sync_context.grid(row=0, column=0, sticky="ew", padx=(0, 5))

        self.btn_sync_lists = ctk.CTkButton(
            master=control_frame,
            text="ОБНОВИТЬ СПИСКИ",
            font=btn_font,
            fg_color="#000000", 
            hover_color="#2B2B2B",
            command=self._force_refresh_event_ui,
            state="disabled"
        )
        self.btn_sync_lists.grid(row=0, column=1, sticky="ew", padx=(5, 0))

    def toggle_section(self, button, content):
        is_currently_open = (self.currently_open_button == button)
        if self.currently_open_button and not is_currently_open:
            prev_content = self.accordion_data[self.currently_open_button]["content"]
            prev_content.pack_forget()
            self.currently_open_button.configure(fg_color="#34495e")
        if is_currently_open:
            content.pack_forget()
            button.configure(fg_color="#34495e")
            self.currently_open_button = None
            new_geo = self._calculate_window_geometry(collapsed=True)
        else:
            content.pack(fill="x", padx=5, pady=(0, 5), anchor="center")
            button.configure(fg_color="#2c3e50")
            self.currently_open_button = button
            new_geo = self._calculate_window_geometry(collapsed=False)
        
        self.geometry(new_geo)
        self.after(50, self._enforce_onscreen_position)

    def _enforce_onscreen_position(self):
        """Гарантирует, что нижняя часть окна не уйдет за пределы экрана."""
        if not self.winfo_exists():
            return
            
        self.update_idletasks()
        
        win_w = self.winfo_width()
        win_h = self.winfo_height()
        screen_w = self.winfo_screenwidth()
        screen_h = self.winfo_screenheight()
        
        x = int((screen_w - win_w) / 2)
        
        bottom_edge = self.winfo_rooty() + win_h
        
        overflow = bottom_edge - screen_h + 50 
        if overflow > 0:
            y = max(40, self.winfo_rooty() - overflow)
        else:
            y = 40
            
        self.geometry(f"+{x}+{y}")

    def _calculate_window_geometry(self, collapsed=False):
        current_width = self.winfo_width()
        if current_width == 1:
            current_width = 650
            
        header_height = 50 * 4 + 30  
        button_panel_height = 70   
        
        if collapsed or not self.currently_open_button:
            target_height = header_height + 80 + button_panel_height
        else:
            data = self.accordion_data[self.currently_open_button]
            content_widget = data["content"]
            self.update_idletasks()
            open_content_height = content_widget.winfo_reqheight()
            target_height = header_height + open_content_height + button_panel_height
            
        max_h = self.winfo_screenheight() - 50
        final_height = min(target_height, max_h)
        min_h = 460 
        final_height = max(final_height, min_h)
        
        return f"{current_width}x{final_height}"

    def _create_accordion_header(self, parent, title):
        item_container = ctk.CTkFrame(parent, fg_color="#f0f2f5", border_color="#d1d8e0", border_width=1, corner_radius=8)
        item_container.pack(fill="x", pady=5)
        header_btn = ctk.CTkButton(
            item_container,
            text=title,
            height=45,
            font=ctk.CTkFont(weight="bold", size=16),
            anchor="w",
            fg_color="#34495e",
            hover_color="#3c5a72",
        )
        header_btn.pack(fill="x")
        content_frame = ctk.CTkFrame(item_container, fg_color="transparent", corner_radius=0)
        return {"button": header_btn, "content": content_frame}

    def _handle_project_deletion(self):
        current_display = self.project_selector.get()
        if not current_display or current_display == "Проектов нет":
            messagebox.showwarning("Ошибка", "Сначала выберите проект над аккордеоном.")
            return
        
        try:
            self.manager_db.close()
        except Exception:
            pass
            
        fresh_projects = self.manager_db.list_projects()
        code_to_delete = next((p for p in fresh_projects if p[3] == current_display), None)
        if not code_to_delete:
            messagebox.showerror("Ошибка", f"ID для '{current_display}' не найден.")
            return

        confirm = messagebox.askyesno("Внимание", 
                                      f"Удалить проект '{current_display}'?\n"
                                      f"Это действие нельзя отменить.\nФайл событий будет стерт.")
        if not confirm:
            return
        
        import os
        db_file_path = os.path.join(self.event_logic.project_dbs_folder, f"{code_to_delete}.db")
        file_was_deleted = False
        if os.path.exists(db_file_path):
            try:
                os.remove(db_file_path)
                file_was_deleted = True
            except PermissionError:
                messagebox.showerror("Доступ запрещен", "Закройте другие программы и попробуйте снова.")
                return

        success = False
        try:
            conn = self.manager_db.connect()
            cursor = conn.cursor()
            cursor.execute("DELETE FROM projects WHERE code = ?", (code_to_delete,))
            conn.commit()
            success = True
        except sqlite3.Error as e:
            conn.rollback()
            print(f"[DB DELETE ERROR]: {e}")
        finally:
            self.manager_db.close()

        if success and (not os.path.exists(db_file_path) or file_was_deleted):
            updated = self.manager_db.list_projects()
            names = [p[3] for p in updated]
            self.project_selector.configure(values=names)
            if names:
                self.project_selector.set(names[-1])
                self.current_selected_project_code = next((p[2] for p in updated if p[3] == names[-1]), "")
            else:
                self.project_selector.set("Проектов нет")
                self.current_selected_project_code = ""
                
            if hasattr(self, 'tree_projects'):
                tree = self.tree_projects
                for row in tree.get_children():
                    tree.delete(row)
                for proj in sorted(updated, key=lambda x: x[0]):
                    tree.insert("", "end", values=proj)
                    
            messagebox.showinfo("Готово", f"'{current_display}' удален.")
        else:
            messagebox.showerror("Ошибка", "Не удалось обновить реестр. Перезапустите программу.")

    ###############################################################################
    # === СЕКЦИЯ 3: НАПОЛНЕНИЕ ВКЛАДОК АККОРДЕОНА ===
    ###############################################################################

    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
        self.project_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):
        """
        ВАЖНО: Здесь остались ТОЛЬКО инициализация логики и создание виджета ProjectEventUI.
        Все кнопки "Обновить" вынесены в _build_project_selector (наверх).
        """
        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.update_project_dependent_lists()
            ui._refresh_event_table(current_proj_code)

    def _fill_charts_section(self, frame):
        if DateEntry is None:
            error_lbl = ctk.CTkLabel(frame, text="Ошибка: установите библиотеку\npip install tkcalendar", text_color="red")
            error_lbl.pack(expand=True)
            return
    
    def _fill_print_section(self, frame):
        if DateEntry is None:
            error_lbl = ctk.CTkLabel(frame, text="Ошибка: установите библиотеку\npip install tkcalendar", text_color="red")
            error_lbl.pack(expand=True)
            return
        
    def _generate_word_report(self, group, report_date, orientation):
        if docx is None:
            messagebox.showerror("Библиотека не найдена", "Для экспорта нужна библиотека 'python-docx'.\nВыполните:\npip install python-docx")
            return
        
    def on_closing(self):
        try:
            self.after_cancel("all")
        except Exception:
            pass
        for item in list(self.accordion_data.values()):
            try:
                content = item.get("content")
                if content:
                    content.destroy()
            except Exception:
                pass
        try:
            self.destroy()
        except Exception:
            pass
            
#===============================================






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

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

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

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

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


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


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


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

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

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

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





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

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

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

DB_PATH = _determine_active_path()

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

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

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

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

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

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

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

class Session:
    def __init__(self):
        pass

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

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

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






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


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

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

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

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

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

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

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

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

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


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

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






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

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

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

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