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


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

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

*1* В выпадающем списке не правильно настроен выбор Проекта. Поясняю как задумано в приложении.
При создании проекта пользователь делает три записи:
1 - Название проекта - это большое полное название. Оно нужно только во время печати документа.
2 - Шифр - это короткое сокращенное название проекта, которое должно отображаться в выпадающем списке.
3 - Код - это айди проекта и это же название файла базы данных проекта. По коду должен проект искаться, а в выпадающем списке должен отображаться его Шифр.
В настоящее время не понятно по каким параметрам все это работает. Сейчас в списке отображаются названия проектов вместо шифров. 
Имеется путаница с id, Name, Code, Cipher. 
Есть ощущение, что в файл базы проджект записываются данные баз самих создаваемых проектов, а в созданных этих базах данных нет. Поэтому и выдает в консоли ччто проектов нет. Еще этому подтверждение - когда я базы проектов удаляю - их данные остаются,как будто кэш. Но они пропадают,когда удаляю файл проджект.дб.

Нижние задачи пока не решаем - только эту первую. Ниже пойдем только если первая будет решена.

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

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

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

*3* Редактирование событий. В данный момент в окне 4 основных кнопки. Необходимо сделать чтобы в окне было 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

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
        
        # FINAL FIX: Храним только чистые списки строк для надежной работы ComboBox
        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()
        
        # === ГАРАНТИРОВАННОЕ ОБНОВЛЕНИЕ ТАБЛИЦЫ ПОСЛЕ СОЗДАНИЯ ИНТЕРФЕЙСА ===
        # Даем Tkinter долю секунды, чтобы комбобокс проекта получил текст от главного окна,
        # и затем читаем его напрямую для заполнения таблицы.
        self.after(50, self._initial_load)

    def _initial_load(self):
        """Метод-диспетчер: берет текущий проект из родителя и загружает таблицу."""
        try:
            if not hasattr(self, 'window_ref') or not self.window_ref:
                return
                
            proj_text = self.window_ref.project_selector.get()
            
            print(f"[INIT LOAD] Current project in selector is: '{proj_text}'")
            
            if proj_text and proj_text != "Проектов нет":
                # Вызываем наш рабочий метод загрузки таблицы
                self._refresh_event_table() 
            else:
                print("[INIT LOAD] Project not ready yet.")
                
        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):
        """Глобальное обновление списков Группа/Показатель."""
        try:
            if not hasattr(self, 'window_ref') or not self.window_ref:
                print("[UI ERROR] window_ref is missing.")
                return
            
            selected_project_name = self.window_ref.project_selector.get()
            print(f"[UI SYNC] Updating for project: {selected_project_name}")
            
            groups_raw, name_to_code_map = self.logic.load_groups_and_indicators()
            
            if not groups_raw:
                print("[UI WARNING] Groups list is empty from logic.")
                group_names = ["Все группы"]
            else:
                # Сортируем реальные названия групп и ставим заглушку в начало
                sorted_groups = sorted([str(g).strip() for g in groups_raw])
                group_names = ["Все группы"] + sorted_groups
                
            self.group_names_list = group_names
            self.group_names_to_codes = {"Все группы": ""}
            if name_to_code_map:
                self.group_names_to_codes.update(name_to_code_map)
                
            self.cmb_group.configure(values=group_names)
            
            # ПО УМОЛЧАНИЮ ВЫБИРАЕМ ЗАГЛУШКИ (индекс 0)
            self.cmb_group.set(group_names[0]) 
            
            # Собираем ВСЕ показатели изо всех кэшированных групп для режима "Все показатели"
            all_indicators = []
            seen_names = set()
            for code in self.indicator_cache.values():
                cached_list = self.indicator_cache.get(code, [])
                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]) 
                
        except Exception as e:
            print(f"[CRITICAL UI LOAD FAIL]: {e}")
            self.cmb_group.configure(values=["Ошибка"])
            self.cmb_group.set("Ошибка")
            self.cmb_indicator.configure(values=[])
            self.cmb_indicator.set("")

    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
            
            try:
                self.cmb_indicator.configure(values=placeholder_list)
                self.cmb_indicator.set("Все показатели")
                
                try:
                    if self.cmb_indicator.dropdown is not None and self.cmb_indicator.dropdown.winfo_children():
                        listbox = self.cmb_indicator.dropdown.winfo_children()[0]
                        if isinstance(listbox, tk.Listbox):
                            listbox.configure(height=8)
                except Exception:
                    pass
                    
            except Exception as ui_err:
                print(f"[CRITICAL UI CONFIG FAIL]: {ui_err}")
                
            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

        try:
            if self.cmb_indicator.dropdown is not None and self.cmb_indicator.dropdown.winfo_children():
                listbox = self.cmb_indicator.dropdown.winfo_children()[0]
                if isinstance(listbox, tk.Listbox):
                    listbox.configure(height=8)
        except Exception:
            pass

        # ФИКС: Если есть реальные показатели, выбираем ПЕРВЫЙ из них (индекс 1)
        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 group_code and indicator_name != "Все показатели":
            try:
                clean_name = indicator_name[0] if isinstance(indicator_name, tuple) else indicator_name
                if clean_name in self.indicator_names_list:
                    count = self.indicator_names_list.index(clean_name)
                    indicator_code = f"{group_code}_{count}"
            except Exception as gen_err:
                print(f"[CODE GEN ERROR]: {gen_err}")
                
        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._collect_current_selection()['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 hasattr(self, 'window_ref') or not self.window_ref:
            print("[TABLE ERROR] window_ref is missing.")
            return

        try:
            current_text = self.window_ref.project_selector.get()
            
            if not current_text or current_text == "Проектов нет":
                return
                
            proj_code = current_text
            
            db_path = os.path.join(self.logic.project_dbs_folder, f"{proj_code}.db")
            
            if not os.path.exists(db_path): 
                return
                
            conn = sqlite3.connect(db_path)
            cur = conn.cursor()
            
            current_group = self.cmb_group.get()
            current_indicator = self.cmb_indicator.get()
            
            query = "SELECT event_code, event_data, event_sign, event_text_content FROM project_data"
            params = ()
            
            conditions = []
            if current_group and current_group != "Все группы":
                conditions.append("group_name = ?")
                params += (current_group,)
            if current_indicator and current_indicator != "Все показатели":
                conditions.append("indicator_name = ?")
                params += (current_indicator,)
                
            if conditions:
                query += " WHERE " + " AND ".join(conditions)
                
            # ВАЖНО: Последние события всегда сверху (id DESC)
            query += " ORDER BY id DESC LIMIT 50"
            
            cur.execute(query, params)
            rows = cur.fetchall()
            conn.close()
            
            last_iid = None
            
            # ФИКС: Нумеруем с 1 до len(rows) по порядку полученного списка
            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])
                )
                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[3]
        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"][0]
        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"][0] == 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"))
        
#======================================================






#======================================================
# 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()
        
        if is_new_db:
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS project_data (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    project_code TEXT NOT NULL,
                    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                    group_name TEXT,
                    group_code TEXT,
                    indicator_name TEXT,
                    indicator_code TEXT,
                    event_data DATE,
                    event_sign INTEGER,
                    event_code TEXT,
                    event_text_content TEXT
                )
            ''')

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

        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,
                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

# Попытка импорта внешних библиотек (оставляем ваши try/except)
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

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)
        
        self._build_project_selector(main_container)
        
        accordion_frame = ctk.CTkFrame(main_container)
        accordion_frame.pack(fill="both", expand=True, pady=(10, 0))
        
        self.accordion_data = {}
        self.currently_open_button = None
        
        # === ЗАГОЛОВКИ АККОРДЕОНА (ТЕПЕРЬ ИХ 4) ===
        create_item = self._create_accordion_header(accordion_frame, "Создать проект")
        events_item = self._create_accordion_header(accordion_frame, "Открыть для ввода событий")
        charts_item = self._create_accordion_header(accordion_frame, "Показать графики")
        print_item = self._create_accordion_header(accordion_frame, "Настройка печати")
        
        # Наполняем секции контентом (ваши методы наполнения остаются прежними)
        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)

    # === НОВЫЙ МЕТОД ДЛЯ ШАПКИ НАД СЕЛЕКТОРОМ ПРОЕКТА ===
    def _build_title_bar(self, container):
        title_lbl = ctk.CTkLabel(container, text="Наполнение проекта", font=ctk.CTkFont(size=20, weight="bold"))
        # Убран anchor="w". По умолчанию CTkLabel центрирует текст.
        title_lbl.pack(pady=(0, 15)) 

    def _build_project_selector(self, container):
        """
        Создает блок выбора проекта над аккордеоном без использования placeholder_text.
        Логика "Проектов нет" реализована через set() текстового поля.
        """
        frame = ctk.CTkFrame(container)
        frame.pack(pady=(0, 10)) 
        
        label_project = ctk.CTkLabel(frame, text="Выбрать проект:", anchor="w")
        label_project.pack(side="left", padx=(0, 10))
        
       # Передаем пустой список значений
        self.project_selector = ctk.CTkComboBox(
            frame,
            values=[], 
            state="readonly",
            width=250
        )

        # Так как списка нет, пишем текст напрямую в поле ввода комбобокса
        self.project_selector.set("Проектов нет") 
        
        self.project_selector.pack(side="left")
        
    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 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)
        
        # Вызываем ПОСЛЕ установки geometry, чтобы считать актуальные размеры
        self.after(50, self._enforce_onscreen_position)

    # === ОБНОВЛЕННЫЙ РАСЧЕТ ГЕОМЕТРИИ (УЧИТЫВАЕТ 4 СЕКЦИИ И НИЖНЮЮ ПАНЕЛЬ) ===
    def _calculate_window_geometry(self, collapsed=False):
        current_width = self.winfo_width()
        if current_width == 1:
            current_width = 650
            
        header_height = 50 * 4 + 30  # Высота шапок всех 4 секций
        
        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)
        
        # Увеличенный порог видимости всех заголовков (расчет выше показал ~453px)
        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 _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)

        # --- Поля ввода ---
        ttk.Label(form_frame, text="Название:").grid(row=0, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
        name_entry = ctk.CTkEntry(form_frame, placeholder_text="Введите полное название...")
        name_entry.grid(row=0, column=1, sticky="ew", pady=(0, 5))

        ttk.Label(form_frame, text="Шифр:").grid(row=1, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
        short_code_entry = ctk.CTkEntry(form_frame, placeholder_text="Краткое имя (латиница)...")
        short_code_entry.grid(row=1, column=1, sticky="ew", pady=(0, 5))

        ttk.Label(form_frame, text="Код (ID):").grid(row=2, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
        code_entry = ctk.CTkEntry(form_frame, placeholder_text="Уникальный идентификатор...")
        code_entry.grid(row=2, column=1, sticky="ew", pady=(0, 5))

        ttk.Label(form_frame, text="Дата открытия:").grid(row=3, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
        date_entry = ctk.CTkEntry(form_frame)
        today = datetime.datetime.now().strftime("%Y-%m-%d")
        date_entry.insert(0, today)
        date_entry.grid(row=3, column=1, sticky="ew", pady=(0, 5))

        ttk.Label(form_frame, text="Ответственный:").grid(row=4, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
        resp_entry = ctk.CTkEntry(form_frame, placeholder_text="ФИО ответственного лица...")
        resp_entry.grid(row=4, column=1, sticky="ew", pady=(0, 10))

        # --- Кнопки действий ---
        action_frame = ctk.CTkFrame(form_frame, fg_color="transparent")
        action_frame.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(5, 0))
        action_frame.grid_columnconfigure((0, 1), weight=1) # Равномерное распределение кнопок

        create_btn = ctk.CTkButton(
            action_frame, 
            text="Создать проект", 
            fg_color="#3B82F6", 
            hover_color="#2563EB",
            command=lambda n=name_entry, sc=short_code_entry, c=code_entry, d=date_entry, r=resp_entry: 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: [n.delete(0, "end"), sc.delete(0, "end"), c.delete(0, "end"), r.delete(0, "end")] # Теперь очистка полей сработает
            )
        )
        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: [name_entry.delete(0, "end"), short_code_entry.delete(0, "end"), code_entry.delete(0, "end"), resp_entry.delete(0, "end")]
        )
        clear_btn.grid(row=0, column=1, sticky="ew", padx=(5, 0))
        
    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()

        # --- ПРИНУДИТЕЛЬНАЯ СИНХРОНИЗАЦИЯ ЧЕРЕЗ ОСНОВНОЙ МЕНЕДЖЕР ---
        
        # 1. Закрываем текущее соединение manager_db, чтобы сбросить кэш транзакций
        try:
            self.manager_db.close()
        except Exception:
            pass

        # 2. Читаем свежие данные ИЗ ОТКРЫТОЙ БАЗЫ ОКНА
        fresh_tuples = self.manager_db.list_projects() 
        print(f"[DEBUG HARD SYNC] Прямое чтение list_projects(): {fresh_tuples}")

        # Извлекаем только имена для ComboBox
        fresh_names = [p[1] for p in fresh_tuples]

        # 3. Радикальное обновление UI (полная очистка + пересоздание с выводом ШИФРА)
        parent_widget = self.project_selector.master
        
        # Удаляем старый экземпляр полностью
        self.project_selector.destroy()
        
        # Создаем абсолютно новый чистый ComboBox
        self.project_selector = ctk.CTkComboBox(
            master=parent_widget,
            values=fresh_names,
            state="readonly",
            width=250
        )
        self.project_selector.pack(side="left")
        
        # TASK: Выводим шифр (код) проекта вместо названия
        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)
        else:
            self.project_selector.set("Проектов нет")


        # 4. Обновляем Treeview внизу формы создания
        try:
            for row in self.tree_projects.get_children(): 
                self.tree_projects.delete(row)
            
            for proj in sorted(fresh_tuples, key=lambda x: x[0]):
                self.tree_projects.insert("", "end", values=proj)
        except Exception as tree_err:
            print(f"[UI ERROR] Treeview refresh failed: {tree_err}")

        # 5. Вкладка событий (если открыта)
        current_title = ""
        if self.currently_open_button:
            current_title = self.currently_open_button.cget("text")

        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)
        
        # === КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ ===
        # После того как виджет отрисовался (idle), принудительно читаем текущий проект 
        # из селектора и обновляем таблицу.
        def delayed_refresh():
            print("[WINDOW ACTION] Triggering initial table refresh after window open.")
            current_proj = self.project_selector.get()
            if current_proj and current_proj != "Проектов нет":
                ui._refresh_event_table(current_proj)
                
        self.after(100, delayed_refresh)  # Ждем 100 мс, чтобы все значения установились

        
    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
#====================================================
# 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()
            
            fresh_projects = self._project_win.manager_db.list_projects()
            # Теперь кортежи имеют вид: (id, Name, Code, Cipher)
            
            # Для отображения берем Шифр [3]
            display_names = [p[3] for p in fresh_projects] 
            
            print(f"[DEBUG MAIN MENU] Projects loaded. Display: {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]
                    
                    # Устанавливаем текстовое поле комбобокса (ВАЖНО: сначала values, потом set)
                    self._project_win.project_selector.set(selected_display) 
                    
                    # Сохраняем выбранное значение (Шифр) в атрибут окна для event_ui
                    self._project_win.current_selected_project_code = selected_display 
                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

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