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


вот первый файл
import customtkinter as ctk
from tkinter import ttk, messagebox
import datetime
import tkinter as tk
import os
import sqlite3
from .project_event_logic import ProjectEventLogic
from tkcalendar import DateEntry

# ==========================================
# КЛАСС ДЛЯ ВСПЛЫВАЮЩИХ ПОДСКАЗОК (TOOLTIP)
# ==========================================
class NativeToolTip(object):
    def __init__(self, master_widget):
        # Принимаем главное окно (root или self в ProjectEventUI)
        self.master = master_widget 
        self.tipwindow = None

    def show(self, text):
        if not text or len(text.strip()) == 0:
            return

        self.hidetip() # Закрываем предыдущее, если открыто

        # Позиционируем строго по центру главного окна
        win_w = self.master.winfo_width()
        win_h = self.master.winfo_height()
        win_x = self.master.winfo_rootx()
        win_y = self.master.winfo_rooty()
        
        pos_x = win_x + int(win_w / 2) - 150 # Сдвиг влево от центра
        pos_y = win_y + int(win_h / 2) - 50  # Сдвиг вверх от центра

        self.tipwindow = tw = tk.Toplevel(self.master)
        tw.wm_overrideredirect(True) # Без рамок Windows
        
        label = tk.Label(tw, text=text, justify=tk.LEFT,
                         background="#ffffe0", relief=tk.SOLID, borderwidth=1,
                         font=("tahoma", "9", "normal"), wraplength=300)
        label.pack(ipadx=8, ipady=6)
        
        tw.wm_geometry(f"+{pos_x}+{pos_y}")
        
        # Автозакрытие через 4 секунды
        self.tipwindow.after(4000, self.hidetip)

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



# ==========================================
# ОСНОВНОЙ КЛАСС ОКНА СОБЫТИЙ
# ==========================================
class ProjectEventUI(ctk.CTkFrame):
    EDIT_MODE_COLOR = "#8B4513"
    
    def __init__(self, master, window_ref, logic: ProjectEventLogic):
        super().__init__(master, fg_color="transparent")
        self.window_ref = window_ref
        self.logic = logic
        
        self.group_names_list = []
        self.indicator_names_list = []
        
        self.group_names_to_codes = {}
        self.indicator_cache = {}  
        
        self.editing_event_code = None
        
        self._setup_widgets()
        self._bind_global_events()
        
        self.update_project_dependent_lists()
        
        self.after(50, self._initial_load)

    def _initial_load(self):
        print(f"[INIT LOAD] Waiting for UI sync...")
        current_proj_code = getattr(self.window_ref, 'current_selected_project_code', "")
        if current_proj_code and current_proj_code != "Проектов нет":
            self._refresh_event_table(current_proj_code)

    def _setup_widgets(self):
        main_form = ctk.CTkFrame(self, fg_color="transparent")
        main_form.pack(fill="both", expand=True, padx=10, pady=10)
        main_form.grid_columnconfigure(1, weight=1)
        
        lbl_group = ctk.CTkLabel(main_form, text="Выбрать группу:", anchor="w")
        lbl_group.grid(row=0, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
        
        self.cmb_group = ctk.CTkComboBox(
            main_form, 
            state="readonly", 
            width=250, 
            command=self._on_group_change
        )
        self.cmb_group.grid(row=0, column=1, sticky="ew", pady=(0, 5))
        
        lbl_indicator = ctk.CTkLabel(main_form, text="Выбрать показатель:", anchor="w")
        lbl_indicator.grid(row=1, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
        
        self.cmb_indicator = ctk.CTkComboBox(
            main_form, 
            state="readonly", 
            width=250,
            command=lambda choice: self._refresh_event_table(self.window_ref.current_selected_project_code)
        )
        self.cmb_indicator.grid(row=1, column=1, sticky="ew", pady=(0, 10))
        
        lbl_event = ctk.CTkLabel(main_form, text="Содержание события:", anchor="w")
        lbl_event.grid(row=2, column=0, sticky="nw", pady=(0, 5), padx=(0, 10))
        event_container = ctk.CTkFrame(main_form, fg_color="#F0F0F0", border_width=1, border_color="#A9A9A9")
        event_container.grid(row=2, column=1, sticky="ew", pady=(0, 5), ipadx=2, ipady=2)
        self.txt_event = ctk.CTkTextbox(event_container, height=80, wrap="word", fg_color="white")
        self.txt_event.pack(fill="both", expand=True, padx=2, pady=2)
        paste_btn = ctk.CTkButton(main_form, text="Вставить", fg_color="#2B2B2B", hover_color="#3C3C3C", command=self._paste_from_clipboard)
        paste_btn.grid(row=3, column=1, sticky="e", pady=(0, 10), ipadx=10)
        
        meta_frame = ctk.CTkFrame(main_form, fg_color="transparent")
        meta_frame.grid(row=4, column=0, columnspan=2, sticky="ew", pady=(0, 10))
        meta_frame.grid_columnconfigure(1, weight=1)
        
        ttk.Label(meta_frame, text="Дата события:").grid(row=0, column=0, sticky="w", padx=(0, 10))
        
        self.date_entry = DateEntry(
            master=meta_frame, 
            width=12,
            background='darkblue',
            foreground='white',           
            bordercolor='gray',           
            headersbackground='darkgray', 
            normalbackground='white',
            normalforeground='black',
            weekendbackground='lightblue',
            weekendforeground='red',
            date_pattern='dd.MM.yyyy'
        )
        
        today = datetime.datetime.now()
        self.date_entry.set_date(today)
        self.date_entry.grid(row=0, column=1, sticky="w")
        
        ttk.Label(meta_frame, text="Направленность:").grid(row=0, column=2, sticky="w", padx=(20, 10))
        self.dir_var = tk.IntVar(value=1)
        rb_plus = ctk.CTkRadioButton(meta_frame, text="(+)", variable=self.dir_var, value=1)
        rb_plus.grid(row=0, column=3, sticky="w")
        rb_minus = ctk.CTkRadioButton(meta_frame, text="(-)", variable=self.dir_var, value=-1)
        rb_minus.grid(row=0, column=4, sticky="w")
        
        ctrl_frame = ctk.CTkFrame(main_form, fg_color="transparent")
        ctrl_frame.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(0, 10))
        for i in range(4): ctrl_frame.grid_columnconfigure(i, weight=1)
        
        self.btn_add = ctk.CTkButton(ctrl_frame, text="Добавить", fg_color="#3B82F6", command=self._add_event)
        self.btn_add.grid(row=0, column=0, sticky="ew", padx=2)
        self.btn_edit = ctk.CTkButton(ctrl_frame, text="Редактировать", fg_color="#10B981", command=self._enter_edit_mode)
        self.btn_edit.grid(row=0, column=1, sticky="ew", padx=2)
        self.btn_save = ctk.CTkButton(ctrl_frame, text="Сохранить", state="disabled", fg_color=self.EDIT_MODE_COLOR, command=self._save_edited_event)
        self.btn_save.grid(row=0, column=2, sticky="ew", padx=2)
        self.btn_del = ctk.CTkButton(ctrl_frame, text="Удалить", fg_color="#EF4444", command=self._delete_selected_event)
        self.btn_del.grid(row=0, column=3, sticky="ew", padx=2)
        
        tree_container = ctk.CTkFrame(main_form, fg_color="transparent")
        tree_container.grid(row=6, column=0, columnspan=2, sticky="nsew", pady=(10, 0))
        tree_container.grid_rowconfigure(0, weight=1); tree_container.grid_columnconfigure(0, weight=1)
        
        style = ttk.Style()
        theme_name = "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")
        
        # В конце метода _setup_widgets, после создания таблицы:
        self.tooltip = NativeToolTip(self) # Передаем ссылку на экземпляр UI


    def update_project_dependent_lists(self, forced_project_code=None):
        proj_code = forced_project_code or getattr(self.window_ref, 'current_selected_project_code', "")
        
        if not proj_code:
            placeholder_groups = ["Все группы"]
            placeholder_inds = ["Все показатели"]
            
            self.cmb_group.configure(values=placeholder_groups)
            self.cmb_group.set(placeholder_groups[0])
            self.cmb_indicator.configure(values=placeholder_inds)
            self.cmb_indicator.set(placeholder_inds[0])
            return
            
        groups_raw, name_to_code_map = self.logic.load_groups_and_indicators()
        
        safe_groups = []
        if groups_raw is not None:
            try:
                if hasattr(groups_raw[0], 'name'):
                    safe_groups = sorted(list(set([item.name for item in groups_raw])))
                elif isinstance(groups_raw[0], tuple) and len(groups_raw[0]) > 1:
                    safe_groups = sorted(list(set([item[1] for item in groups_raw])))
                elif isinstance(groups_raw[0], str):
                    safe_groups = sorted(list(set(groups_raw)))
            except Exception:
                pass
                
        final_group_list = ["Все группы"] + safe_groups if safe_groups else ["Все группы"]
        
        self.group_names_list = final_group_list
        self.group_names_to_codes = {"": ""}
        if isinstance(name_to_code_map, dict):
            self.group_names_to_codes.update(name_to_code_map)
            
        self.cmb_group.configure(values=final_group_list)
        self.cmb_group.set(final_group_list[0])
        
        all_indicators = []
        seen_names = set()
        for cached_list in self.indicator_cache.values():
            for item in cached_list:
                name = item[0]
                if name not in seen_names:
                    all_indicators.append(item)
                    seen_names.add(name)
                    
        all_indicators.sort(key=lambda x: x[0])
        ind_names = ["Все показатели"] + [item[0] for item in all_indicators]
        
        self.indicator_names_list = ind_names
        self.cmb_indicator.configure(values=ind_names)
        self.cmb_indicator.set(ind_names[0])
        
        self.after_idle(lambda code=proj_code: (
            self.window_ref._update_control_buttons_state(True),
            self._refresh_event_table(code)
        ))

    def _on_group_change(self, event=None):
        group_name = self.cmb_group.get()
        if group_name == "Все группы":
            self.cmb_indicator.set("")
            self.indicator_names_list = ["Все показатели"]
            self.cmb_indicator.configure(values=["Все показатели"])
        
        self._update_indicator_combobox(group_name)

    def _update_indicator_combobox(self, group_name: str):
        self.cmb_indicator.set("")
        if not group_name or group_name == "Все группы":
            placeholder_list = ["Все показатели"]
            self.indicator_names_list = placeholder_list
            self.cmb_indicator.configure(values=placeholder_list)
            self.cmb_indicator.set("Все показатели")
            return
            
        group_code = self.group_names_to_codes.get(group_name)
        if not group_code:
            self.cmb_indicator.configure(values=["Все показатели"])
            self.cmb_indicator.set("Все показатели")
            return
            
        indicators = self.logic.load_indicators_for_group(group_code)
        
        ind_names = ["Все показатели"] + [item[0] for item in indicators]
        self.indicator_names_list = ind_names
        self.cmb_indicator.configure(values=ind_names)
        
        self.indicator_cache[group_code] = indicators
        
        if len(ind_names) > 1:
            self.cmb_indicator.set(ind_names[1])
        else:
            self.cmb_indicator.set("Все показатели")

    def _collect_current_selection(self) -> dict:
        proj_code = self.window_ref.current_selected_project_code
        group_name = self.cmb_group.get()
        group_code = self.group_names_to_codes.get(group_name, "") if group_name else ""
        indicator_name = self.cmb_indicator.get()
        indicator_code = ""
        
        if indicator_name and indicator_name != "Все показатели" and group_code:
            indicators_list = self.indicator_cache.get(group_code, [])
            try:
                idx = [item[0] for item in indicators_list].index(indicator_name)
                indicator_code = f"{group_code}_{idx + 1}"
            except ValueError:
                pass
                
        return {
            "project_code": proj_code,
            "group_name": group_name,
            "group_code": group_code,
            "indicator_name": indicator_name,
            "indicator_code": indicator_code
        }

    def _add_event(self):
        data = self._collect_current_selection()
        if not all([data['project_code'], data['group_code'], data['indicator_code']]):
            msg_parts = []
            if not data['project_code']: msg_parts.append("Проект")
            if not data['group_code']: msg_parts.append("Группа")
            if not data['indicator_code']: msg_parts.append("Показатель")
            
            messagebox.showwarning("Ошибка", f"Заполнены не все обязательные поля.\nОтсутствуют: {', '.join(msg_parts)}")
            return
            
        text = self.txt_event.get("1.0", "end").strip()
        if not text:
            messagebox.showwarning("Ошибка", "Поле 'Содержание события' пустое.")
            return
            
        date_str = self.date_entry.get()
        sign = self.dir_var.get()
        success, msg = self.logic.add_event_to_db(data, text, date_str, sign)
        messagebox.showinfo("Результат", msg)
        
        if success:
            self.txt_event.delete("1.0", "end")
            proj_code = self.window_ref.current_selected_project_code
            self.cmb_group.set("Все группы")
            self.cmb_indicator.set("Все показатели")
            self._refresh_event_table(proj_code)

    def _refresh_event_table(self, project_code: str = None):
        # Очистка таблицы
        for row in self.tree.get_children(): 
            self.tree.delete(row)
            
        if not project_code:
            return
            
        db_path = os.path.join(self.logic.project_dbs_folder, f"{project_code}.db")
        if not os.path.exists(db_path): 
            return
            
        try:
            conn = sqlite3.connect(db_path)
            cur = conn.cursor()
            
            query = "SELECT event_code, event_data, event_sign, event_text_content FROM project_data"
            params = ()
            conditions = []
            
            current_group = self.cmb_group.get()
            current_indicator = self.cmb_indicator.get()
            
            if current_group and current_group != "Все группы":
                group_code = self.group_names_to_codes.get(current_group, "")
                if group_code:
                    conditions.append("group_code = ?")
                    params += (group_code,)
                    
            if current_indicator and current_indicator != "Все показатели":
                conditions.append("indicator_name = ?")
                params += (current_indicator,)
                
            if conditions:
                query += " WHERE " + " AND ".join(conditions)
                
            query += " ORDER BY id DESC LIMIT 50"
            
            print(f"[SQL EXEC] Query: {query} | Params: {params}")
            cur.execute(query, params)
            rows = cur.fetchall()
            conn.close()
            
            print(f"[TABLE DEBUG] Fetched {len(rows)} items from disk.")
            
            # Вставка строк с привязкой клика к полю txt_event
            for idx, row in enumerate(rows, start=1):
                sign_char = "+" if row[2] == 1 else "-"
                full_text = row[3]
                
                iid = f"row_{idx}"
                self.tree.insert("", "end", iid=iid, values=(idx, row[0], row[1], sign_char, full_text))
                
        except Exception as e:
            print(f"[CRITICAL TABLE FAIL]: {e}")

    def _on_tree_select(self, event):
        """
        Срабатывает ПРИ ЛЮБОМ ВЫБОРЕ СТРОКИ.
        Заполняет форму данными выбранной строки.
        """
        selected = self.tree.selection()
        if not selected:
            return

        item_id = selected[0]
        vals = self.tree.item(item_id)["values"]
        
        if len(vals) < 5:
            return

        text_content = vals[4]
        
        # Блокируем модифицированные события, чтобы избежать зацикливания
        self.txt_event.unbind("<<Modified>>")
        
        # Очищаем и заполняем поле текстом из таблицы
        self.txt_event.delete("1.0", "end")
        self.txt_event.insert("1.0", text_content)
        
        # Заполняем дату и знак (+/-)
        try:
            date_obj = datetime.datetime.strptime(str(vals[2]), '%Y-%m-%d')
            self.date_entry.set_date(date_obj)
        except Exception:
            pass
            
        sign_val = int(vals[3].replace('+', '1').replace('-', '-1'))
        self.dir_var.set(sign_val)
        
        # Устанавливаем режим редактирования для этой строки
        self.editing_event_code = vals[1]
        
        # Включаем визуальную индикацию (желтый фон)
        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.showwarning("Ошибка", "Текст не может быть пустым."); return

        # Собираем дату из DateEntry
        try:
            date_str = self.date_entry.get_date().strftime('%Y-%m-%d')
        except Exception:
            date_str = datetime.datetime.now().strftime('%Y-%m-%d')
            
        # Собираем знак (+/-) из IntVar
        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() # Сброс UI после успеха
            
            proj_code = self._collect_current_selection()['project_code']
            self._refresh_event_table(proj_code)
            
        except Exception as e:
            messagebox.showerror("Ошибка записи", f"Не удалось сохранить изменения: {e}")

    def _delete_selected_event(self):
        sel = self.tree.selection()
        if not sel: return
        if not messagebox.askyesno("Подтверждение", "Удалить выбранное событие?"): return
        
        event_code = self.tree.item(sel[0])["values"][1]
        db_path = os.path.join(self.logic.project_dbs_folder, f"{self._collect_current_selection()['project_code']}.db")
        conn = sqlite3.connect(db_path)
        cur = conn.cursor()
        cur.execute("DELETE FROM project_data WHERE event_code = ?", (event_code,))
        conn.commit()
        conn.close()
        
        self._refresh_event_table(self._collect_current_selection()['project_code'])

    def _reset_controls(self):
        self.editing_event_code = None
        
        # Возвращаем базовые состояния кнопок
        self.btn_add.configure(state="normal")
        self.btn_del.configure(state="normal")
        self.cmb_group.configure(state="readonly")
        self.cmb_indicator.configure(state="readonly")
        
        # Кнопка Редактировать снова зеленая и активная
        self.btn_edit.configure(state="normal")     
        
        # Кнопка Сохранить гаснет (становится недоступной)
        self.btn_save.configure(state="disabled")   
        
        # Очищаем поле текста
        self.txt_event.delete("1.0", "end")

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

    def _bind_global_events(self):
        self.txt_event.bind("<Button-3>", lambda e: (self._paste_from_clipboard(), "break"))
        
    def _search_by_code(self):
        target = self.search_entry.get().strip()
        if not target:
            messagebox.showwarning("Поиск", "Введите код."); return
            
        items = self.tree.get_children()
        found = None
        for item in items:
            vals = self.tree.item(item)["values"]
            if len(vals) > 1 and vals[1] == target:
                found = item
                break
                
        if found:
            self.tree.selection_set(found)
            self.tree.see(found)
            self.tree.focus(found)
        else:
            messagebox.showinfo("Поиск", "Событие с таким кодом не найдено.")