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


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

# ==========================================
# ОСНОВНОЙ КЛАСС ОКНА СОБЫТИЙ
# ==========================================
class ProjectEventUI(ctk.CTkFrame):
    EDIT_MODE_COLOR = "#8B4513"

    def _build_indicator_choices(self, indicators):
        """
        Принимает сырой список индикаторов [(name, ), ...] или [{'name': ..., 'code': ...}, ...]
        Возвращает два объекта:
        1. choices_list - то, что увидит пользователь в списке (с \n и отступами)
        2. display_to_code_map - скрытый словарь {строка_из_списка: чистый_db_code}
        """
        import textwrap

        # Сначала достаем данные из БД уже отсортированными ПО КОДУ (это мы поправим в логике позже)
        # Пока считаем, что indicators это просто список имен строкой
        
        # Если на вход пришел список объектов/кортежей, вытащим имена
        if indicators and not isinstance(indicators[0], str):
            names = []
            codes = []
            for item in indicators:
                # Проверяем структуру данных
                if isinstance(item, dict):
                    names.append(item.get('name', ''))
                    codes.append(item.get('code', ''))
                elif isinstance(item, tuple) and len(item) >= 2:
                    names.append(item[0])
                    codes.append(item[1]) # Предполагаем, что второй элемент это code
                else:
                    names.append(str(item[0]))
                    codes.append("") # Кода нет
        else:
            names = indicators if indicators else []
            codes = [""] * len(names)

        # Сортируем их вместе по названию (или по коду, если коды есть), чтобы порядок был стабильным
        # ВАЖНО: Логика в load_indicators_for_group должна вернуть сортировку по CODE!
        combined = sorted(zip(names, codes), key=lambda x: x[0])
        
        choices_list = ["Все показатели"]
        display_to_code_map = {"": ""} # Пустая строка для "Все показатели"

        wrapper = textwrap.TextWrapper(width=65, subsequent_indent='          ') # 10 пробелов под код + 1 пробел

        for name, code in combined:
            if not name.strip():
                continue
            
            lines = wrapper.wrap(name)
            formatted_text = f"{code:<10} {' \\n '.join(lines)}"
            
            choices_list.append(formatted_text)
            display_to_code_map[formatted_text] = code
            
        return choices_list, display_to_code_map
    
    def __init__(self, master, window_ref, logic: ProjectEventLogic):
        super().__init__(master, fg_color="transparent")
        self.window_ref = window_ref
        self.logic = logic
        
        self.group_names_list = []
        self.indicator_names_list = []
        
        self.group_names_to_codes = {}
        self.indicator_cache = {}  
        
        self.editing_event_code = None
        
        self._setup_widgets()
        self._bind_global_events()
        
        self.update_project_dependent_lists()
        
        self.after(50, self._initial_load)
        
    def _initial_load(self):
        print(f"[INIT LOAD] Waiting for UI sync...")
        current_proj_code = getattr(self.window_ref, 'current_selected_project_code', "")
        if current_proj_code and current_proj_code != "Проектов нет":
            self._refresh_event_table(current_proj_code)
            
    def _setup_widgets(self):
        main_form = ctk.CTkFrame(self, fg_color="transparent")
        main_form.pack(fill="both", expand=True, padx=10, pady=10)
        main_form.grid_columnconfigure(1, weight=1)
        
        lbl_group = ctk.CTkLabel(main_form, text="Выбрать группу:", anchor="w")
        lbl_group.grid(row=0, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
        
        self.cmb_group = ctk.CTkComboBox(
            main_form, 
            state="readonly", 
            width=250, 
            command=self._on_group_change
        )
        self.cmb_group.grid(row=0, column=1, sticky="ew", pady=(0, 5))
        
        lbl_indicator = ctk.CTkLabel(main_form, text="Выбрать показатель:", anchor="w")
        lbl_indicator.grid(row=1, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
        
        # --- КНОПКА ВЫБОРА ПОКАЗАТЕЛЯ ---
        self.btn_show_indicators = ctk.CTkButton(
            main_form, 
            text="Выбрать показатель...", 
            width=250,
            anchor="w",
            fg_color="#F0F0F0",
            hover_color="#E0E0E0",
            text_color="black",
            command=self._toggle_indicator_list
        )
        self.btn_show_indicators.grid(row=1, column=1, sticky="ew", pady=(0, 10))
        
        mono_font = ctk.CTkFont(family="Courier New", size=12)
        self.btn_show_indicators.configure(font=mono_font)

        # --- СКРЫТОЕ ОКНО СО СПИСКОМ (создаем как атрибут объекта) ---
        self.indicator_popup_frame = ctk.CTkFrame(
            main_form, 
            width=300, 
            height=200, 
            border_width=1, 
            border_color="gray"
        )
        
        # Создаем таблицу с фиксированной шириной первого (и единственного) столбца
        self.tree_indicators = ttk.Treeview(
            self.indicator_popup_frame, 
            columns=("indicator"), 
            show="headings", 
            height=8,
            selectmode="browse"
        )
        
        # Явно задаем ширину для колонки "indicator" (280 пикселей под текст + место под скролл)
        self.tree_indicators.column("indicator", width=280, minwidth=280, stretch=False, anchor="w")
        self.tree_indicators.pack(fill="both", expand=True, side="left")

        # Прокрутка для таблицы
        scrollbar_i = ctk.CTkScrollbar(
            self.indicator_popup_frame, 
            orientation="vertical", 
            command=self.tree_indicators.yview
        )
        scrollbar_i.pack(side="right", fill="y")
        self.tree_indicators.configure(yscrollcommand=scrollbar_i.set)

        # Событие клика по строке списка
        self.tree_indicators.bind("<ButtonRelease-1>", self._on_indicator_select)    
        
        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()
        
        # Берем текущую тему CustomTkinter, чтобы цвета скроллбаров совпадали
        theme_name = style.theme_use() 
        
        # Конфигурируем стиль. rowheight — это высота одной строки в пикселях.
        # Для шрифта 12px значение 35-40 позволит вместить переносы текста без наложения.
        style.configure("Custom.Treeview", 
                        font=("Courier New", 11), 
                        rowheight=38) 
                        
        # Применяем наш кастомный стиль к таблице
        self.tree = ttk.Treeview(
            tree_container, 
            columns=columns, 
            show="headings", 
            selectmode="browse", 
            height=8,
            style="Custom.Treeview" # Подключаем увеличенную высоту
        )
        
        self.tree.heading("num", text="№п/п"); self.tree.column("num", width=40, anchor="center", stretch=False)
        self.tree.heading("code", text="Код"); self.tree.column("code", width=100, anchor="center", stretch=False)
        self.tree.heading("date", text="Дата"); self.tree.column("date", width=110, anchor="center", stretch=False)
        self.tree.heading("sign", text="+/-"); self.tree.column("sign", width=50, anchor="center", stretch=False)
        self.tree.heading("event", text="Событие"); self.tree.column("event", minwidth=200, stretch=True)
        self.tree.pack(side="left", fill="both", expand=True)
        
        scrollbar = ctk.CTkScrollbar(tree_container, orientation="vertical", command=self.tree.yview)
        scrollbar.pack(side="right", fill="y")
        self.tree.configure(yscrollcommand=scrollbar.set)
        
        search_frame = ctk.CTkFrame(main_form, fg_color="transparent")
        search_frame.grid(row=7, column=0, columnspan=2, sticky="ew", pady=(10, 0))
        search_frame.grid_columnconfigure(1, weight=1)
        ttk.Label(search_frame, text="Наберите код:").grid(row=0, column=0, sticky="w", padx=(0, 5))
        self.search_entry = ctk.CTkEntry(search_frame)
        self.search_entry.grid(row=0, column=1, sticky="ew", padx=(0, 5))
        find_btn = ctk.CTkButton(search_frame, text="Искать", fg_color="#3B82F6", command=self._search_by_code)
        find_btn.grid(row=0, column=2, sticky="e")
        
    def update_project_dependent_lists(self, forced_project_code=None):
        proj_code = forced_project_code or getattr(self.window_ref, 'current_selected_project_code', "")

        if not proj_code:
            placeholder_groups = ["Все группы"]
            
            self.cmb_group.configure(values=placeholder_groups)
            self.cmb_group.set(placeholder_groups[0])
            
            # Вместо cmb_indicator меняем ТЕКСТ КНОПКИ
            self.btn_show_indicators.configure(text="Выбрать показатель...")
            return

        groups_raw, name_to_code_map = self.logic.load_groups_and_indicators()

        safe_groups = []
        if groups_raw is not None:
            try:
                if hasattr(groups_raw[0], 'name'):
                    safe_groups = sorted(list(set([item.name for item in groups_raw])))
                elif isinstance(groups_raw[0], tuple) and len(groups_raw[0]) > 1:
                    safe_groups = sorted(list(set([item[1] for item in groups_raw])))
                elif isinstance(groups_raw[0], str):
                    safe_groups = sorted(list(set(groups_raw)))
            except Exception:
                pass

        final_group_list = ["Все группы"] + safe_groups if safe_groups else ["Все группы"]

        self.group_names_list = final_group_list
        self.group_names_to_codes = {"": ""}
        if isinstance(name_to_code_map, dict):
            self.group_names_to_codes.update(name_to_code_map)

        self.cmb_group.configure(values=final_group_list)
        self.cmb_group.set(final_group_list[0])
        
        # Загрузка индикаторов для первой выбранной группы или "Все"
        all_indicators = []
        seen_names = set()
        for cached_list in self.indicator_cache.values():
            for item in cached_list:
                name = item[0]
                if name not in seen_names:
                    all_indicators.append(item)
                    seen_names.add(name)

        all_indicators.sort(key=lambda x: x[0])
        ind_names = ["Все показатели"] + [item[0] for item in all_indicators]

        self.indicator_names_list = ind_names
        
        # Обновляем наши новые виджеты вместо старых combobox
        choices_list, mapping = self._build_indicator_choices(all_indicators)
        self.populate_indicator_tree(choices_list)
        
        if len(ind_names) > 1:
            self.btn_show_indicators.configure(text=ind_names[1])
        else:
            self.btn_show_indicators.configure(text="Выбрать показатель...")
            
        self.after_idle(lambda code=proj_code: (
            self.window_ref._update_control_buttons_state(True),
            self._refresh_event_table(code)
        ))
        
    def _on_group_change(self, event=None):
        group_name = self.cmb_group.get()
        
        if group_name == "Все группы":
            # Сбрасываем кнопку индикаторов на заглушку
            self.btn_show_indicators.configure(text="Выбрать показатель...")
            # Очищаем дерево (список) внутри всплывающего окна
            self.populate_indicator_tree(["Все показатели"])
            
            # Сохраняем пустой кэш для этой группы, чтобы не было ошибок при поиске кода
            self.indicator_cache[group_name] = ([], {})
            return

        group_code = self.group_names_to_codes.get(group_name)
        if not group_code:
            self.btn_show_indicators.configure(text="Выбрать показатель...")
            self.populate_indicator_tree(["Все показатели"])
            self.indicator_cache[group_code] = ([], {})
            return
            
        raw_indicators = self.logic.load_indicators_for_group(group_code)
        
        final_list, mapping = self._build_indicator_choices(raw_indicators)
        
        # Обновляем интерфейс: заполняем дерево и текст кнопки
        self.populate_indicator_tree(final_list)
        
        if len(final_list) > 1:
            self.btn_show_indicators.configure(text=final_list[1])
        else:
            self.btn_show_indicators.configure(text="Выбрать показатель...")
            
        # Сохраняем данные в кэш
        self.indicator_cache[group_code] = (raw_indicators, mapping)
        
    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("Все показатели")
            self.indicator_cache[group_name] = ([], {})
            return
            
        group_code = self.group_names_to_codes.get(group_name)
        if not group_code:
            self.cmb_indicator.configure(values=["Все показатели"])
            self.cmb_indicator.set("Все показатели")
            self.indicator_cache[group_code] = ([], {})
            return
            
        raw_indicators = self.logic.load_indicators_for_group(group_code)
        
        final_list, mapping = self._build_indicator_choices(raw_indicators)
        
        # Сохраняем маппинг глобально для доступа в других методах
        self.indicator_cache[group_code] = (raw_indicators, mapping)
        
        # Обновляем текст кнопки по умолчанию
        if len(final_list) > 1:
            self.btn_show_indicators.configure(text=final_list[1])
        else:
            self.btn_show_indicators.configure(text="Выбрать показатель...")
            
        # Наполняем наше новое древовидное окно
        self.populate_indicator_tree(final_list)
            
    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 ""
        full_display_text = self.btn_show_indicators.cget("text")
        
        # Если выбрано "Все показатели" или заглушка
        if full_display_text == "Выбрать показатель..." or full_display_text == "Все показатели":
            indicator_name = ""
            indicator_code = ""
        else:
            # Берем сохраненный словарь
            _, mapping = self.indicator_cache.get(group_code, ([], {}))
            indicator_code = mapping.get(full_display_text, "")
            # Имя берем без кода и переносов для записи в БД
            clean_name = full_display_text.split('\n')[0].replace(f"{indicator_code:<10}", "").strip()
            indicator_name = clean_name
        
        selected_display = self.cmb_indicator.get()
        
        # Если выбран конкретный показатель (не заглушка)
        if selected_display and selected_display != "Все показатели" and group_code:
            # Берем сохраненный ранее словарь для этой группы
            _, mapping = self.indicator_cache.get(group_code, ([], {}))
            # Достаем чистый код базы данных через отображаемую строку
            indicator_code = mapping.get(selected_display, "")
            # Фикс названия для БД (убираем лишние переносы, если они туда попадут)
            clean_name = selected_display.split('\n')[0].replace(f"{indicator_code:<10}", "").strip()
            indicator_name = clean_name
        
        return {
            "project_code": proj_code,
            "group_name": group_name,
            "group_code": group_code,
            "indicator_name": indicator_name,
            "indicator_code": indicator_code
        }
        
    def _add_event(self):
        data = self._collect_current_selection()
        if not all([data['project_code'], data['group_code'], data['indicator_code']]):
            msg_parts = []
            if not data['project_code']: msg_parts.append("Проект")
            if not data['group_code']: msg_parts.append("Группа")
            if not data['indicator_code']: msg_parts.append("Показатель")
            
            messagebox.showwarning("Ошибка", f"Заполнены не все обязательные поля.\nОтсутствуют: {', '.join(msg_parts)}")
            return
            
        text = self.txt_event.get("1.0", "end").strip()
        if not text:
            messagebox.showwarning("Ошибка", "Поле 'Содержание события' пустое.")
            return
            
        try:
            date_str = self.date_entry.get_date().strftime('%Y-%m-%d') # Принудительно YYYY-MM-DD
        except Exception:
            date_str = datetime.datetime.now().strftime('%Y-%m-%d')
        sign = self.dir_var.get()
        success, msg = self.logic.add_event_to_db(data, text, date_str, sign)
        messagebox.showinfo("Результат", msg)
        
        if success:
            self.txt_event.delete("1.0", "end")
            proj_code = self.window_ref.current_selected_project_code
            self.cmb_group.set("Все группы")
            self.cmb_indicator.set("Все показатели")
            self._refresh_event_table(proj_code)
            
    def _refresh_event_table(self, project_code: str = None):
        # Очистка таблицы
        for row in self.tree.get_children(): 
            self.tree.delete(row)
            
        if not project_code:
            return
            
        db_path = os.path.join(self.logic.project_dbs_folder, f"{project_code}.db")
        if not os.path.exists(db_path): 
            return
            
        try:
            conn = sqlite3.connect(db_path)
            cur = conn.cursor()
            
            query = "SELECT event_code, event_data, event_sign, event_text_content FROM project_data"
            params = ()
            conditions = []
            
            current_group = self.cmb_group.get()
            current_indicator = self.cmb_indicator.get()
            
            if current_group and current_group != "Все группы":
                group_code = self.group_names_to_codes.get(current_group, "")
                if group_code:
                    conditions.append("group_code = ?")
                    params += (group_code,)
                    
            if current_indicator and current_indicator != "Все показатели":
                conditions.append("indicator_name = ?")
                params += (current_indicator,)
                
            if conditions:
                query += " WHERE " + " AND ".join(conditions)
                
            query += " ORDER BY id DESC LIMIT 50"
            
            print(f"[SQL EXEC] Query: {query} | Params: {params}")
            cur.execute(query, params)
            rows = cur.fetchall()
            conn.close()
            
            print(f"[TABLE DEBUG] Fetched {len(rows)} items from disk.")
            
            # Предварительная очистка выделения перед заполнением новой таблицей
            self.tree.selection_remove(self.tree.selection())
            
            # Вставка строк с привязкой клика к полю txt_event
            for idx, row in enumerate(rows, start=1):
                sign_char = "+" if row[2] == 1 else "-"
                full_text = row[3]
                
                iid = f"row_{idx}"
                self.tree.insert("", "end", iid=iid, values=(idx, row[0], row[1], sign_char, full_text))
                
        except Exception as e:
            print(f"[CRITICAL TABLE FAIL]: {e}")
            
    def _on_tree_select(self, event):
        """
        Срабатывает ПРИ ЛЮБОМ ВЫБОРЕ СТРОКИ.
        Заполняет форму данными выбранной строки.
        """
        selected = self.tree.selection()
        if not selected:
            return
        item_id = selected[0]
        vals = self.tree.item(item_id)["values"]
        
        if len(vals) < 5:
            return
        text_content = vals[4]
        
        # Блокируем модифицированные события, чтобы избежать зацикливания
        self.txt_event.unbind("<<Modified>>")
        
        # Очищаем и заполняем поле текстом из таблицы
        self.txt_event.delete("1.0", "end")
        self.txt_event.insert("1.0", text_content)
        
        # Заполняем дату и знак (+/-)
        try:
            date_obj = datetime.datetime.strptime(str(vals[2]), '%Y-%m-%d')
            self.date_entry.set_date(date_obj)
        except ValueError:
            # Если вдруг формат пришел другой (например dd.mm.YYYY)
            try:
                date_obj = datetime.datetime.strptime(str(vals[2]), '%d.%m.%Y')
                self.date_entry.set_date(date_obj)
            except Exception:
                pass
            
        sign_val = int(vals[3].replace('+', '1').replace('-', '-1'))
        self.dir_var.set(sign_val)
        
        # Устанавливаем режим редактирования для этой строки
        self.editing_event_code = vals[1]
        
        # Включаем визуальную индикацию (желтый фон удален согласно ТЗ по очистке NativeToolTip)
        self._set_edit_mode_visuals(True)
        
        # Возвращаем отслеживание изменений текста
        self.after(100, lambda: self.txt_event.bind("<<Modified>>", self._on_text_modified))
        
    def _set_edit_mode_visuals(self, is_active: bool):
        """Меняет цвет фона поля текста и состояние кнопок"""
        color = "#FFF9C4" if is_active else "#F0F0F0" # Желтый или стандартный
        self.txt_event.configure(background=color)
        
        # Кнопки согласно ТЗ: Редактировать блокируется, Сохранить активна
        self.btn_edit.configure(state="disabled")   
        self.btn_save.configure(state="normal")     
        
        # Разблокируем поля даты и знака для изменения
        self.date_entry.config(state="normal")
        
    def _get_text_by_iid(self, item_id: str) -> str:
        vals = self.tree.item(item_id)["values"]
        if len(vals) > 4:
            return vals[4]
        return ""
        
    def _enter_edit_mode(self):
        sel = self.tree.selection()
        if not sel: 
            messagebox.showwarning("Ошибка", "Выберите событие в таблице."); return
        self._enter_edit_mode_by_iid(sel[0])
        
    def _enter_edit_mode_by_iid(self, item_id: str):
        vals = self.tree.item(item_id)["values"]
        if len(vals) < 5:
            return
            
        self.editing_event_code = vals[1]
        
        # Заполняем поля формы данными выбранной строки
        self.txt_event.delete("1.0", "end")
        self.txt_event.insert("1.0", vals[4])
        
        # Дата события
        try:
            date_obj = datetime.datetime.strptime(str(vals[2]), '%Y-%m-%d')
            self.date_entry.set_date(date_obj)
        except Exception:
            pass
            
        # Направленность (+/-)
        sign_val = int(vals[3].replace('+', '1').replace('-', '-1'))
        self.dir_var.set(sign_val)
        
        # === РЕЖИМ РЕДАКТИРОВАНИЯ (КНОПКИ И ПОЛЯ) ===
        # Блокируем неизменяемые фильтры
        self.btn_add.configure(state="disabled")
        self.btn_del.configure(state="disabled")
        self.cmb_group.configure(state="disabled")
        self.cmb_indicator.configure(state="disabled")
        
        # Кнопка Редактировать становится серой (заблокирована)
        self.btn_edit.configure(state="disabled")   
        
        # Кнопка Сохранить остается активной (оранжевой), как вы просили НЕ скрывать её
        # Если она была disabled - включаем
        self.btn_save.configure(state="normal")     
        
        # Разблокируем поля ввода для изменений (ТЗ п.3)
        self.txt_event.configure(state="normal")
        self.date_entry.config(state="normal")
        # Радиокнопки dir_var обычно доступны всегда, если frame активен
        
    def _save_edited_event(self):
        if not getattr(self, 'editing_event_code', None):
            return
            
        new_text = self.txt_event.get("1.0", "end").strip()
        if not new_text:
            # Используем тот же messagebox, который мы только что импортировали
            messagebox.showwarning("Ошибка", "Текст не может быть пустым.")
            return
            
        try:
            date_str = self.date_entry.get_date().strftime('%Y-%m-%d')
        except Exception:
            date_str = datetime.datetime.now().strftime('%Y-%m-%d')
            
        sign_val = self.dir_var.get()
        
        db_path = os.path.join(self.logic.project_dbs_folder, f"{self._collect_current_selection()['project_code']}.db")
        
        try:
            conn = sqlite3.connect(db_path)
            cur = conn.cursor()
            cur.execute(
                '''UPDATE project_data 
                   SET event_text_content = ?, event_data = ?, event_sign = ? 
                   WHERE event_code = ?''', 
                (new_text, date_str, sign_val, self.editing_event_code)
            )
            conn.commit()
            conn.close()
            
            # Если дошли до сюда без ошибок — показываем успех
            messagebox.showinfo("Готово", "Изменения сохранены.")
            self._reset_controls()
            
            proj_code = self._collect_current_selection()['project_code']
            self._refresh_event_table(proj_code)
                
        except Exception as e:
            # Если упало подключение к БД или SQL-запрос — показываем ошибку
            messagebox.showerror("Ошибка записи", f"Не удалось сохранить изменения: {e}")
            
    def _delete_selected_event(self):
        sel = self.tree.selection()
        if not sel: return
        if not messagebox.askyesno("Подтверждение", "Удалить выбранное событие?"): return
        
        event_code = self.tree.item(sel[0])["values"][1]
        db_path = os.path.join(self.logic.project_dbs_folder, f"{self._collect_current_selection()['project_code']}.db")
        conn = sqlite3.connect(db_path)
        cur = conn.cursor()
        cur.execute("DELETE FROM project_data WHERE event_code = ?", (event_code,))
        conn.commit()
        conn.close()
        
        self._refresh_event_table(self._collect_current_selection()['project_code'])
        
    def _reset_controls(self):
        self.editing_event_code = None
        
        # Возвращаем базовые состояния кнопок
        self.btn_add.configure(state="normal")
        self.btn_del.configure(state="normal")
        self.cmb_group.configure(state="readonly")
        
        # Вместо cmb_indicator разблокируем нашу кнопку выбора (состояние normal у кнопки уже есть по умолчанию)
        # Если кнопка была заблокирована в режиме редактирования, включаем её
        if self.btn_show_indicators.cget("state") == "disabled":
            self.btn_show_indicators.configure(state="normal")
        
        # Кнопка Редактировать снова зеленая и активная
        self.btn_edit.configure(state="normal")     
        
        # Кнопка Сохранить гаснет (становится недоступной)
        self.btn_save.configure(state="disabled")   
        
        # Очищаем поле текста
        self.txt_event.delete("1.0", "end")
        
        # Сбрасываем текст кнопки индикаторов на подсказку
        self.btn_show_indicators.configure(text="Выбрать показатель...")
        
    def _paste_from_clipboard(self):
        try:
            txt = self.clipboard_get()
            self.txt_event.insert(tk.INSERT, txt)
        except tk.TclError:
            pass
            
    def _bind_global_events(self):
        self.txt_event.bind("<Button-3>", lambda e: (self._paste_from_clipboard(), "break"))
        
    def _search_by_code(self):
        target = self.search_entry.get().strip()
        if not target:
            messagebox.showwarning("Поиск", "Введите код."); return
            
        items = self.tree.get_children()
        found = None
        for item in items:
            vals = self.tree.item(item)["values"]
            if len(vals) > 1 and vals[1] == target:
                found = item
                break
                
        if found:
            self.tree.selection_set(found)
            self.tree.see(found)
            self.tree.focus(found)
        else:
            messagebox.showinfo("Поиск", "Событие с таким кодом не найдено.")
            
    def _toggle_indicator_list(self, event=None):
        """Показывает или прячет всплывающее окно со списком"""
        if self.indicator_popup_frame.winfo_viewable():
            self.indicator_popup_frame.place_forget()
        else:
            x = self.btn_show_indicators.winfo_rootx() - self.winfo_rootx()
            y = self.btn_show_indicators.winfo_rooty() - self.winfo_rooty() + self.btn_show_indicators.winfo_height()
            
            # Поднимаем окно НАД всеми остальными виджетами ДО размещения
            self.indicator_popup_frame.lift()
            
            # Позиционируем панель строго под кнопкой
            self.indicator_popup_frame.place(
                in_=self.btn_show_indicators, 
                x=x, 
                y=y, 
                relwidth=1.0,
                bordermode="outside" # Чтобы рамка окна считалась от края кнопки
            )
            
            self.focus_set()

    def _on_indicator_select(self, event):
        """Срабатывает при выборе показателя в кастомном списке"""
        selected = self.tree_indicators.selection()
        if not selected:
            return
            
        item_id = selected[0]
        display_text = self.tree_indicators.item(item_id)["values"][0]
        
        # Обновляем текст на кнопке
        self.btn_show_indicators.configure(text=display_text)
        
        # Прячем окно
        self.indicator_popup_frame.place_forget()
        
        # Принудительно обновляем таблицу событий, так как сменился фильтр
        proj_code = self.window_ref.current_selected_project_code
        if proj_code:
            self._refresh_event_table(proj_code)

    def populate_indicator_tree(self, choices_list):
        """Очищает и заполняет дерево индикаторов новыми данными"""
        # Очистка
        for row in self.tree_indicators.get_children():
            self.tree_indicators.delete(row)
            
        # Заполнение (пропускаем первый элемент "Все показатели" для дерева, оставим его только текстом кнопки)
        start_index = 1 if len(choices_list) > 1 and choices_list[0] == "Все показатели" else 0
        
        for i, text in enumerate(choices_list[start_index:], start=1):
            self.tree_indicators.insert("", "end", iid=f"ind_{i}", values=(text,))