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


Промт

Работаем с окном "Наполнение проекта" с заголовком аккордеона "Построить диаграмму". 

Текущая проблема: Отображение графиков с большим интервалом по времени (10 дней+), с цветами гистограмм, с размещением столбиков, с сохранением графика. 

Необходимо пересмотреть код и поправить под логику работы. 
Как по плану  должно это работать.

Если выбирается только одна запись из списка (не важно, группа или "Все группы"), то строится  только один, выбранный график. 
Если выбирается несколько групп, в том числе и опция "Все группы", то строится столько графиков на одном листе, сколько выбрано. 
Для чего это делается. "Все группы" - это объединенный график - все группы сведены в один график. Когда он один на листе - вопросов нет. Когда Строится несколько графиков, то это каждая группа в отдельности. Если еще добавляется и "Все группы" то видно, сколько событий в общей массе произошло из той или иной группы. Сейчас "Все группы" где то блокируются и их невозможно отобразить совместно с  другими группами. 

Когда в поле выбрано несколько групп, то строится только одна последняя. Если их выделить все  - то строится как "Все группы". Графиков должно быть столько - сколько их в поле для добавления.

Графики строятся от указанной пользователем даты до последней записи в базе данных с указанным шагом. Над столбиком гистаграммы или маркером графика  проставляется значение (количество событий в этом промежутке). Если это гистаграмма, то столбик должен стоять только на одну дату, а не растягиваться от даты к дате. В настоящий момент столбик может быть нанесен на несколько дней сразу. Кроме того имеется такой баг. Пример. Анализируется  последний месяц. Пользователь выбрал шаг в 15 дней. График строится от 1 числа до 15-го, а дальше не строится, потому что не хватает дней до полных 15. Необходимо, чтобы в таком случае график достраивался не по шагу, а по остатку. То есть на на 30-е число должна быть очередная засечка, а , например на 20- Если это крайний день наблюдения.

Каждый график или гистограмма должна иметь свой цвет. Здесь как минимум может использоваться 5 цветов: для "Все группы" и для каждой группы. Пока их 4. Предлагаю цвета расписать жестко для этих 5 значений, а если будут добавляться другие группы то их заливать различными оттенками серого. Цвета предлагаю такие:
"Все группы" - Черным, остальные 4 группы: Синим, Красным, Зеленым и Коричневым. 

2. Было организовано так, что сохраняется файл с графиком ххх.png в корень программы, где лежит запускающий файл. Необходимо, чтобы он сохранялся в папку dukora\events (папка в корне проекта).

3. Сейчас файл с графиком сохраняется с названием temp_chart.png. Необходимо, чтобы он сохранялся с названием файла График_24-08-2026_10-28.png (График+дата+время).

4. На данный момент консоль выдает следующую ошибку:
    Traceback (most recent call last):
  File "E:\Anaconda3\Lib\tkinter\__init__.py", line 2068, in __call__
    return self.func(*args)
           ~~~~~~~~~^^^^^^^
  File "E:\dozor_env\Lib\site-packages\customtkinter\windows\widgets\ctk_button.py", line 564, in _on_release
    self._command()
    ~~~~~~~~~~~~~^^
  File "E:\dukora\modules\project\chart_modal.py", line 155, in <lambda>
    full_path := os.path.join(os.getcwd(), f"График_{datetime.now().strftime('%d-%m-%Y_%H-%M')}.png"),
                                                     ^^^^^^^^^^^^
AttributeError: module 'datetime' has no attribute 'now'
    

Чтобы ты мог найти ошибку и исправить код, высылаю содержимое нескольких файлов. 

1. dukora/modules/project/project_event_logic.py
2. dukora/modules/project/chart_modal.py
3. dukora/modules/project/project_chart_ui.py
4. dukora/modules/project/project_window.py
5. dukora/modules/utils/resources.py


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

Файл project_window.py не меняем, он уже избыточен, все что необходимо добавлять в код - использовать файл dukora/modules/project/project_chart_ui.py  или chart_modal.py

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

Итогом работы должно стать построение графика, его запись в указанном месте с указанным названием. 

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




# ==========================================
# dukora\modules\project\project_window.py
# ==========================================
import customtkinter as ctk
import tkinter as tk
from tkinter import ttk, messagebox  
from typing import List, Any 

import sqlite3
import datetime
import os
from pathlib import Path
# Локальные импорты проекта (должны быть после стандартных)
try:
    from tkcalendar import DateEntry
except ImportError:
    DateEntry = None
try:
    import docx
except ImportError:
    docx = None
from modules.project.project_event_logic import ProjectEventLogic
from modules.project.project_event_ui import ProjectEventUI
from modules.project.project_create_logic import ProjectCreateLogic
from modules.data.project_database import ProjectDatabase
from modules.project.chart_modal import show_chart_modal


class ProjectWindow(ctk.CTkToplevel):
    def __init__(self, master):
        super().__init__(master)
        
        self.parent_main = master
        self.title("Журнал событий")
        
        # Инициализируем пустым, пока не выбрали проект
        self.current_selected_project_code = "" 
        
        # === ИНИЦИАЛИЗАЦИЯ ПЕРЕМЕННЫХ (ОДИН РАЗ) ===
        # === ИНИЦИАЛИЗАЦИЯ ПЕРЕМЕННЫХ (ОДИН РАЗ) ===
        self.accordion_data = {}
        self.currently_open_button = None
        
        # --- ПЕРЕМЕННЫЕ ДЛЯ ГРАФИКОВ ---
        self.var_group = ctk.StringVar(value="")
        import datetime
        self.var_date_from = ctk.StringVar(value=datetime.datetime.now().strftime('%d.%m.%Y'))
        self.var_period_str = ctk.StringVar(value="1") 
        self.var_chart_type = ctk.StringVar(value="Гистограмма")
        
        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)
        available_height = screen_height - initial_height
        y_cord = int(available_height / 2)
        self.geometry(f"{window_width}x{initial_height}+{x_cord}+{y_cord}")
        self.resizable(False, False) # ЗАПРЕЩАЕМ изменение размера
        self.grab_set() 
        self.transient(master)
        
        main_container = ctk.CTkFrame(self, corner_radius=10)
        main_container.pack(padx=10, pady=10, fill="both", expand=True)
        
        self._build_title_bar(main_container)
        
        if not hasattr(self, 'manager_db'):
            self.manager_db = ProjectDatabase()
            self.manager_db.create_tables()
            
        if not hasattr(self, 'event_logic'):
            self.event_logic = ProjectEventLogic(self.manager_db)
            
        self._build_project_selector(main_container)
        
        create_item = self._create_accordion_header(main_container, "Создать проект")
        events_item = self._create_accordion_header(main_container, "Открыть для ввода событий")
        charts_item = self._create_accordion_header(main_container, "Построение диаграмм")
        print_item = self._create_accordion_header(main_container, "Настройка печати")
        
        # Наполнение ВСЕХ секций сразу при создании окна
        self._fill_create_section(create_item["content"])
        self._fill_events_section(events_item["content"]) 
        self._fill_charts_section(charts_item["content"])  
        self._fill_print_section(print_item["content"])
        
        # Панель кнопок внизу главного окна
        button_frame = ctk.CTkFrame(main_container)
        button_frame.pack(fill="x", pady=(10, 0), padx=5)
        
        close_btn = ctk.CTkButton(button_frame, text="Закрыть", width=120, command=self.destroy)
        close_btn.pack(side="right", padx=10, pady=10)
        
        # Регистрация заголовков аккордеона
        for item in [create_item, events_item, charts_item, print_item]:
            btn = item["button"]
            content = item["content"]
            self.accordion_data[btn] = {"content": content}
            btn.configure(command=lambda b=btn, c=content: self.toggle_section(b, c))
            
        # Синхронизация размеров ДО открытия первой вкладки
        self.update_idletasks() 
        correct_geometry = self._calculate_window_geometry(collapsed=False)
        self.geometry(correct_geometry) 
        
        # Открываем первую вкладку
        self.toggle_section(create_item["button"], create_item["content"])
        
    def _force_ui_sync(self):
        """
        Главная точка входа синхронизации. Вызывается при смене проекта или нажатии кнопки ОБНОВИТЬ ПРОЕКТ.
        Находит внутренний Code по отображаемому Cipher и обновляет контекст окна.
        """
        current_display_cipher = self.project_selector.get()
        
        if not current_display_cipher or current_display_cipher == "Проектов нет":
            print("[SYNC FORCE] Context cleared.")
            self.current_selected_project_code = ""
            self._update_control_buttons_state(is_enabled=False)
            return
        try:
            self.manager_db.close()
        except Exception:
            pass
        fresh_projects = self.manager_db.list_projects()
        selected_tuple = next((p for p in fresh_projects if p[3] == current_display_cipher), None)
        
        if selected_tuple:
            internal_code = selected_tuple[2]
            self.current_selected_project_code = internal_code
            
            print(f"[SYNC FORCE] Switched to '{current_display_cipher}' -> DB File: {internal_code}.db")
            
            # === ИСПРАВЛЕННЫЙ БЛОК ВЫЗОВА UI ===
            events_btn = None
            # 1. Находим кнопку заголовка "Открыть для ввода событий"
            for btn, data in self.accordion_data.items():
                if btn.cget("text") == "Открыть для ввода событий":
                    events_btn = btn
                    break
            
            # 2. Если кнопка найдена, получаем её контент-фрейм из словаря
            if events_btn and events_btn in self.accordion_data:
                content_frame = self.accordion_data[events_btn]["content"]
                
                # 3. Проходим по виджетам внутри этого фрейма (там лежит ProjectEventUI)
                for child in content_frame.winfo_children():
                    if child.__class__.__name__ == "ProjectEventUI":
                        print("[DIRECT CALL] Updating lists via accordion_data reference.")
                        child.update_project_dependent_lists(internal_code)
                        break
                        
            self._update_control_buttons_state(is_enabled=True)
        else:
            print(f"[SYNC FORCE ERROR] Project cipher '{current_display_cipher}' not found.")
            self.current_selected_project_code = ""
            self._update_control_buttons_state(is_enabled=False)
            
    def _force_refresh_event_ui(self):
        """
        Принудительно обновляет списки групп/индикаторов/сотрудников 
        во вкладке "Открыть для ввода событий" и сбрасывает фильтры в графиках.
        Вызывается кнопкой "ОБНОВИТЬ СПИСКИ".
        """
        print("[UI SYNC] Manual refresh of lists triggered.")
        current_display_cipher = self.project_selector.get()
        if not current_display_cipher or current_display_cipher == "Проектов нет":
            print("[SYNC FORCE] Context cleared for manual list update.")
            
            # ЖЕСТКАЯ ОЧИСТКА ТИПОВ ДАННЫХ (решение проблемы TclError)
            if hasattr(self, 'var_period_str') and isinstance(self.var_period_str, tk.IntVar):
                self.var_period_str.set(0)
            if hasattr(self, 'var_group') and isinstance(self.var_group, tk.StringVar):
                self.var_group.set("")
                
            return
        try:
            self.manager_db.close()
        except Exception:
            pass
        fresh_projects = self.manager_db.list_projects()
        selected_tuple = next((p for p in fresh_projects if p[3] == current_display_cipher), None)
        if selected_tuple:
            internal_code = selected_tuple[2]
            events_section_content = None
            target_btn = None
            for btn, data in self.accordion_data.items():
                if btn.cget("text") == "Открыть для ввода событий":
                    target_btn = btn
                    break
                    
            if target_btn and target_btn in self.accordion_data:
                content_widget = self.accordion_data[target_btn]["content"]
                for child in content_widget.winfo_children():
                    if child.__class__.__name__ == "ProjectEventUI":
                        events_section_content = child
                        break
            if events_section_content:
                print(f"[DIRECT CALL] Updating lists for project code: {internal_code}")
                events_section_content.update_project_dependent_lists(internal_code)
                if hasattr(events_section_content, '_refresh_event_table'):
                    events_section_content._refresh_event_table(internal_code)
            else:
                print("[WARN] ProjectEventUI instance not found by direct tree traversal.")
            # Обновление секции графиков + жесткая очистка типов
            if hasattr(self, 'cmb_group') and self.cmb_group:
                try:
                    from modules.project.project_event_logic import ProjectEventLogic
                    logic = ProjectEventLogic(self.manager_db)
                    groups_raw, _ = logic.load_groups_and_indicators()
                    names = []
                    if groups_raw:
                        try:
                            if hasattr(groups_raw[0], 'name'):
                                names = sorted([item.name for item in groups_raw])
                            elif isinstance(groups_raw[0], tuple):
                                names = sorted([item[1] for item in groups_raw])
                            else:
                                names = sorted(groups_raw)
                        except Exception:
                            pass
                    
                    # Сначала меняем тип внутри Python-объекта
                    if hasattr(self, 'var_period_str') and isinstance(self.var_period_str, tk.IntVar):
                        self.var_period_str.set(0)
                    if hasattr(self, 'var_group') and isinstance(self.var_group, tk.StringVar):
                        self.var_group.set("")
                        
                    # Затем обновляем интерфейс
                    self.cmb_group.configure(values=names)
                    if names:
                        self.cmb_group.set(names[0])
                    else:
                        self.cmb_group.set("")
                        
                    if hasattr(self, 'listbox_groups') and self.listbox_groups:
                        self.listbox_groups.delete(0, tk.END)
                except Exception as e:
                    print(f"[CHART UI REFRESH ERROR]: {e}")
            self._update_control_buttons_state(is_enabled=True)
        else:
            print(f"[SYNC FORCE ERROR] Project cipher '{current_display_cipher}' not found.")
            self.current_selected_project_code = ""
            self._update_control_buttons_state(is_enabled=False)
                
    def _update_control_buttons_state(self, is_enabled: bool):
        target_state = "normal" if is_enabled else "disabled"
        text_color = "#FF8C00" if is_enabled else "#A9A9A9" 
        if hasattr(self, 'btn_sync_context'):
            self.btn_sync_context.configure(state=target_state, text_color=text_color)
        if hasattr(self, 'btn_sync_lists'):
            self.btn_sync_lists.configure(state=target_state, text_color=text_color)
        # Нижние кнопки удалены отсюда — ими управляет только проект_chart_ui.py
            
    def _build_title_bar(self, container):
        title_lbl = ctk.CTkLabel(container, text="Наполнение проекта", font=ctk.CTkFont(size=20, weight="bold"))
        title_lbl.pack(pady=(0, 15))
        
    def _build_project_selector(self, container):
        outer_frame = ctk.CTkFrame(container, fg_color="transparent")
        outer_frame.pack(fill="x", pady=(5, 10))
        
        btn_font = ("Arial", 11, "bold")
        
        top_frame = ctk.CTkFrame(outer_frame, fg_color="transparent")
        top_frame.pack(fill="x", pady=(0, 10))
        
        label_project = ctk.CTkLabel(top_frame, text="Выбрать проект:", anchor="w")
        label_project.pack(side="left", padx=(0, 10))
        
        self.project_selector = ctk.CTkComboBox(
            top_frame,
            values=[], 
            state="readonly",
            width=250
        )
        self.project_selector.pack(side="left", expand=True, fill="x")
        self.project_selector.configure(command=lambda *args: self._force_ui_sync())
        
        control_frame = ctk.CTkFrame(outer_frame, fg_color="transparent")
        control_frame.pack(fill="x")
        
        control_frame.grid_columnconfigure(0, weight=1)
        control_frame.grid_columnconfigure(1, weight=1)
        self.btn_sync_context = ctk.CTkButton(
            master=control_frame,
            text="ОБНОВИТЬ ПРОЕКТ",
            font=btn_font,
            fg_color="#000000", 
            hover_color="#2B2B2B",
            command=self._force_ui_sync,
            state="disabled"
        )
        self.btn_sync_context.grid(row=0, column=0, sticky="ew", padx=(0, 5))
        self.btn_sync_lists = ctk.CTkButton(
            master=control_frame,
            text="ОБНОВИТЬ СПИСКИ",
            font=btn_font,
            fg_color="#000000", 
            hover_color="#2B2B2B",
            command=self._force_refresh_event_ui,
            state="disabled"
        )
        self.btn_sync_lists.grid(row=0, column=1, sticky="ew", padx=(5, 0))
        
    def toggle_section(self, button, content):
        is_currently_open = (self.currently_open_button == button)
        
        if self.currently_open_button and not is_currently_open:
            prev_content = self.accordion_data[self.currently_open_button]["content"]
            prev_content.pack_forget()
            self.currently_open_button.configure(fg_color="#34495e")
            
        if is_currently_open:
            content.pack_forget()
            self.currently_open_button = None
            new_geo = self._calculate_window_geometry(collapsed=True)
        else:
            content.pack(fill="x", padx=5, pady=(0, 5), anchor="center")
            self.currently_open_button = button
            button.configure(fg_color="#2c3e50")
            new_geo = self._calculate_window_geometry(collapsed=False)
        
        self.geometry(new_geo)
        self.after(50, self._enforce_onscreen_position)
        
        title = button.cget("text")
        
        # ЛОГИКА ЗАПОЛНЕНИЯ ПРИ ОТКРЫТИИ
        if title == "Построение диаграмм" and not is_currently_open:
            sync_enabled = False
            if hasattr(self, 'btn_sync_context'):
                sync_state = self.btn_sync_context.cget("state")
                sync_enabled = (sync_state == "normal")
                
            if sync_enabled and self.current_selected_project_code:
                self.after(100, lambda: self._load_chart_groups())
            else:
                # Используем стандартный messagebox из проекта_window.py
                messagebox.showinfo(
                    "Выбор проекта", 
                    "Сначала выберите активный проект над этим окном."
                )
    def _load_chart_groups(self):
        """Загрузка групп при открытии секции графиков"""
        
        # Очищаем старые данные
        if hasattr(self, 'listbox_groups') and self.listbox_groups:
            self.listbox_groups.delete(0, tk.END)
            
        if hasattr(self, 'cmb_group') and self.cmb_group:
            self.cmb_group.configure(values=[])
            self.cmb_group.set("")
        # Если проект НЕ выбран
        if not self.current_selected_project_code:
            print("[CHART LOAD] Project code is empty.")
            # Оставляем интерфейс пустым, но рабочим для выбора "Из выпадающего списка"
            return
        # Если проект ВЫБРАН
        try:
            from modules.project.project_event_logic import ProjectEventLogic
            logic = ProjectEventLogic(self.manager_db)
            groups_raw, _ = logic.load_groups_and_indicators()
            names = []
            if groups_raw:
                try:
                    if hasattr(groups_raw[0], 'name'):
                        names = sorted([item.name for item in groups_raw])
                    elif isinstance(groups_raw[0], tuple):
                        names = sorted([item[1] for item in groups_raw])
                    else:
                        names = sorted(groups_raw)
                except Exception as e:
                    print(f"[GROUP PARSE ERROR]: {e}")
            
            if hasattr(self, 'cmb_group') and self.cmb_group:
                self.cmb_group.configure(values=names)
                if names:
                    self.cmb_group.set(names[0]) # Выбираем первую группу по умолчанию
                    
        except Exception as e:
            print(f"[UI LOAD ERROR]: {e}")
        
    def _enforce_onscreen_position(self):
        if not self.winfo_exists():
            return
            
        self.update_idletasks()
        
        win_w = self.winfo_width()
        win_h = self.winfo_height()
        screen_w = self.winfo_screenwidth()
        screen_h = self.winfo_screenheight()
        
        x = int((screen_w - win_w) / 2)
        
        bottom_edge = self.winfo_rooty() + win_h
        
        overflow = bottom_edge - screen_h + 50 
        if overflow > 0:
            y = max(40, self.winfo_rooty() - overflow)
        else:
            y = 40
            
        self.geometry(f"+{x}+{y}")
        
    def _calculate_window_geometry(self, collapsed=False):
        current_width = self.winfo_width()
        if current_width == 1:
            current_width = 650
            
        header_height = 50 * 4 + 30  
        button_panel_height = 70   
        
        if collapsed or not self.currently_open_button:
            target_height = header_height + 80 + button_panel_height
        else:
            data = self.accordion_data[self.currently_open_button]
            content_widget = data["content"]
            self.update_idletasks()
            open_content_height = content_widget.winfo_reqheight()
            target_height = header_height + open_content_height + button_panel_height
            
        max_h = self.winfo_screenheight() - 50
        final_height = min(target_height, max_h)
        min_h = 460 
        final_height = max(final_height, min_h)
        
        return f"{current_width}x{final_height}"
        
    def _create_accordion_header(self, parent, title):
        item_container = ctk.CTkFrame(parent, fg_color="#f0f2f5", border_color="#d1d8e0", border_width=1, corner_radius=8)
        item_container.pack(fill="x", pady=5)
        header_btn = ctk.CTkButton(
            item_container,
            text=title,
            height=45,
            font=ctk.CTkFont(weight="bold", size=16),
            anchor="w",
            fg_color="#34495e",
            hover_color="#3c5a72",
        )
        header_btn.pack(fill="x")
        content_frame = ctk.CTkFrame(item_container, fg_color="transparent", corner_radius=0)
        return {"button": header_btn, "content": content_frame}
        
    def _handle_project_deletion(self):
        current_display = self.project_selector.get()
        if not current_display or current_display == "Проектов нет":
            messagebox.showwarning("Ошибка", "Сначала выберите проект над аккордеоном.")
            return
        
        try:
            self.manager_db.close()
        except Exception:
            pass
            
        fresh_projects = self.manager_db.list_projects()
        code_to_delete = next((p for p in fresh_projects if p[3] == current_display), None)
        if not code_to_delete:
            messagebox.showerror("Ошибка", f"ID для '{current_display}' не найден.")
            return
        confirm = messagebox.askyesno("Внимание", 
                                      f"Удалить проект '{current_display}'?\n"
                                      f"Это действие нельзя отменить.\nФайл событий будет стерт.")
        if not confirm:
            return
        
        import os
        db_file_path = os.path.join(self.event_logic.project_dbs_folder, f"{code_to_delete}.db")
        file_was_deleted = False
        if os.path.exists(db_file_path):
            try:
                os.remove(db_file_path)
                file_was_deleted = True
            except PermissionError:
                messagebox.showerror("Доступ запрещен", "Закройте другие программы и попробуйте снова.")
                return
        success = False
        try:
            conn = self.manager_db.connect()
            cursor = conn.cursor()
            cursor.execute("DELETE FROM projects WHERE code = ?", (code_to_delete,))
            conn.commit()
            success = True
        except sqlite3.Error as e:
            conn.rollback()
            print(f"[DB DELETE ERROR]: {e}")
        finally:
            self.manager_db.close()
        if success and (not os.path.exists(db_file_path) or file_was_deleted):
            updated = self.manager_db.list_projects()
            names = [p[3] for p in updated]
            self.project_selector.configure(values=names)
            if names:
                self.project_selector.set(names[-1])
                self.current_selected_project_code = next((p[2] for p in updated if p[3] == names[-1]), "")
            else:
                self.project_selector.set("Проектов нет")
                self.current_selected_project_code = ""
                
            if hasattr(self, 'tree_projects'):
                tree = self.tree_projects
                for row in tree.get_children():
                    tree.delete(row)
                for proj in sorted(updated, key=lambda x: x[0]):
                    tree.insert("", "end", values=proj)
                    
            messagebox.showinfo("Готово", f"'{current_display}' удален.")
        else:
            messagebox.showerror("Ошибка", "Не удалось обновить реестр. Перезапустите программу.")
            
    def _fill_create_section(self, frame):
        from modules.data.project_database import ProjectDatabase
        from modules.project.project_create_logic import ProjectCreateLogic
        
        if not hasattr(self, 'manager_db'):
            self.manager_db = ProjectDatabase()
            self.manager_db.create_tables()
            self.event_logic = ProjectEventLogic(self.manager_db)
        
        logic = ProjectCreateLogic(self.manager_db)
        
        form_frame = ctk.CTkFrame(frame, fg_color="transparent")
        form_frame.pack(fill="x", expand=True, padx=10, pady=10)
        form_frame.grid_columnconfigure(1, weight=1)
        
        entries = {}
        fields = [
            ("Название:", "human_name"),
            ("Шифр:", "short_code"), 
            ("Код (ID):", "full_code"),
            ("Дата открытия:", "date"),
            ("Ответственный:", "responsible")
        ]
        
        for i, (label_text, key) in enumerate(fields):
            ttk.Label(form_frame, text=label_text).grid(row=i, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
            entry = ctk.CTkEntry(form_frame)
            entry.grid(row=i, column=1, sticky="ew", pady=(0, 5))
            entries[key] = entry
            if key == "date":
                today = datetime.datetime.now().strftime("%Y-%m-%d")
                entry.insert(0, today)
                
        action_frame = ctk.CTkFrame(form_frame, fg_color="transparent")
        action_frame.grid(row=len(fields), column=0, columnspan=2, sticky="ew", pady=(5, 0))
        for i in range(4): action_frame.grid_columnconfigure(i, weight=1)
        
        create_btn = ctk.CTkButton(
            action_frame, 
            text="Создать", 
            fg_color="#3B82F6", 
            hover_color="#2563EB",
            command=lambda n=entries['human_name'], sc=entries['short_code'], c=entries['full_code'], d=entries['date'], r=entries['responsible']: self._handle_project_creation(
                {
                    'human_name': n.get(),
                    'short_code': sc.get(),
                    'full_code': c.get(),
                    'description': "",
                    'date': d.get(),
                    'responsible': r.get()
                },
                logic,
                lambda: [widget.delete(0, "end") for widget in entries.values()]
            )
        )
        create_btn.grid(row=0, column=0, sticky="ew", padx=(0, 5))
        
        clear_btn = ctk.CTkButton(
            action_frame, 
            text="Очистить", 
            fg_color="#6B7280", 
            hover_color="#4B5563",
            command=lambda: [widget.delete(0, "end") for widget in entries.values()]
        )
        clear_btn.grid(row=0, column=1, sticky="ew", padx=5)
        
        delete_btn = ctk.CTkButton(
            action_frame, 
            text="Удалить", 
            fg_color="#EF4444", 
            hover_color="#B91C1C",
            command=self._handle_project_deletion
        )
        delete_btn.grid(row=0, column=2, sticky="ew", padx=5)
        
    def _handle_project_creation(self, data_dict: dict, logic_instance: ProjectCreateLogic, on_success_callback):
        success, message = logic_instance.create_project(data_dict)
        if not success:
            messagebox.showerror("Ошибка создания", message)
            return
        messagebox.showinfo("Проект создан", message)
        on_success_callback()
        
        try:
            self.manager_db.close()
        except Exception:
            pass
            
        fresh_tuples = self.manager_db.list_projects()
        fresh_names = [p[1] for p in fresh_tuples]
        
        # Безопасное пересоздание селектора проектов
        parent_widget = self.project_selector.master
        old_selector = self.project_selector
        old_selector.pack_forget()
        old_selector.destroy()
        
        self.project_selector = ctk.CTkComboBox(
            master=parent_widget,
            values=fresh_names,
            state="readonly",
            width=250
        )
        self.project_selector.pack(side="left")
        
        if fresh_names:
            selected_proj_tuple = next((p for p in fresh_tuples if p[1] == fresh_names[-1]), None)
            display_text = selected_proj_tuple[2] if selected_proj_tuple else fresh_names[-1]
            self.project_selector.set(display_text)
            self.current_selected_project_code = selected_proj_tuple[2] if selected_proj_tuple else ""
        else:
            self.project_selector.set("Проектов нет")
            self.current_selected_project_code = ""
            
        self.project_selector.configure(command=lambda *args: self._force_ui_sync())
        
        if hasattr(self, 'tree_projects'):
            tree = self.tree_projects
            for row in tree.get_children():
                tree.delete(row)
            for proj in sorted(fresh_tuples, key=lambda x: x[0]):
                tree.insert("", "end", values=proj)
                
        current_title = self.currently_open_button.cget("text") if self.currently_open_button else ""
        if current_title == "Открыть для ввода событий":
            for child in self.currently_open_content.winfo_children():
                if isinstance(child, ProjectEventUI):
                    try:
                        child.update_project_dependent_lists()
                    except Exception as update_err:
                        print(f"[LOG] Ошибка обновления UI событий: {update_err}")
                    break
                    
    def _fill_events_section(self, frame):
        from modules.project.project_event_ui import ProjectEventUI
        from modules.project.project_event_logic import ProjectEventLogic
        
        if not hasattr(self, 'manager_db'):
            self.manager_db = ProjectDatabase()
            self.manager_db.create_tables()
            
        if not hasattr(self, 'event_logic'):
            self.event_logic = ProjectEventLogic(self.manager_db)
            
        ui = ProjectEventUI(frame, self, self.event_logic)
        ui.pack(fill="both", expand=True)
        
        current_proj_code = getattr(self, 'current_selected_project_code', "")
        if current_proj_code:
            ui.after(100, lambda pc=current_proj_code: (
                ui.update_project_dependent_lists(pc),
                ui._refresh_event_table(pc)
            ))
            
    def _fill_charts_section(self, frame):
        from modules.project.project_chart_ui import build_charts_interface
        
        charts_container = ctk.CTkFrame(frame)
        charts_container.pack(fill="both", expand=True, padx=5, pady=5)
        
        # Передаем 'self', чтобы UI мог обращаться к вашим данным
        build_charts_interface(charts_container, self)
        
    def _try_build_chart(self):
        print("[DEBUG MAIN] КНОПКА НАЖАТА!")
        
        if not self.current_selected_project_code:
            messagebox.showinfo("Ошибка", "Сначала выберите проект над аккордеоном.")
            return

        # --- Считываем настройки из интерфейса ---
        try:
            # 1. Группы (если выбрано "Все" или пусто - берем весь список)
            selected_group = None
            if hasattr(self, 'cmb_group') and self.cmb_group.get() != "Все группы":
                selected_group = [self.cmb_group.get()]
            else:
                # Если комбобокс пустой или стоит "Все", используем полный список из logic
                groups_raw, _ = self.event_logic.load_groups_and_indicators()
                selected_group = sorted([item.name for item in groups_raw]) if groups_raw else []

            # 2. Дата начала (проверяем поле ввода)
            start_date_str = "01.08.2026" # Значение по умолчанию
            if hasattr(self, 'entry_date_from'):
                date_text = self.entry_date_from.get()
                # Здесь должна быть проверка формата строки dd.mm.yyyy
                start_date_str = date_text

            # 3. Тип графика
            chart_type = "Гистограмма"
            if hasattr(self, 'chart_type_var'):
                val = self.chart_type_var.get()
                if val == 1: chart_type = "Линия"
            
            step_days = 7 # Можно тоже сделать настраиваемым ползунком

        except Exception as e:
            print(f"[DEBUG MAIN] Ошибка чтения настроек UI: {e}")
            return

        print(f"[DEBUG MAIN] Параметры -> Проект: {self.current_selected_project_code}, Группы: {selected_group}, Дата: {start_date_str}")

        # --- Логика создания инстанса остается прежней ---
        try:
            from modules.project.project_event_logic import ProjectEventLogic
            logic_for_chart = ProjectEventLogic(self.manager_db) 
        except Exception as e:
            print(f"[DEBUG MAIN] Не удалось создать инстанс логики: {e}")
            return

        # --- Читаем данные ---
        try:
            from modules.project.chart_modal import show_chart_modal, fetch_real_chart_data
            data_df = fetch_real_chart_data(
                logic_instance=logic_for_chart,
                project_code=self.current_selected_project_code,
                group_names=selected_group,
                start_date=start_date_str,
                step_days=step_days
            )

            if data_df.empty or data_df['count'].sum() == 0:
                messagebox.showinfo("Данные", "За данный период событий не найдено.")
                return

        except Exception as e:
            print(f"[DEBUG MAIN] ОШИБКА ПРИ СБОРЕ ДАННЫХ: {e}")
            return

        # --- Открываем окно ---
        try:
            show_chart_modal(
                master=self,
                project_code=self.current_selected_project_code,
                group_names=selected_group,
                start_date=start_date_str,
                step_days=step_days,
                chart_type=chart_type
            )
        except Exception as e:
            print(f"[DEBUG MAIN] ОШИБКА ПРИ ОТКРЫТИИ ОКНА: {e}")
        
    def _fill_print_section(self, frame):
        if DateEntry is None:
            error_lbl = ctk.CTkLabel(frame, text="Ошибка: установите библиотеку\npip install tkcalendar", text_color="red")
            error_lbl.pack(expand=True)
            return
            
    def _generate_word_report(self, group, report_date, orientation):
        if docx is None:
            messagebox.showerror("Библиотека не найдена", "Для экспорта нужна библиотека 'python-docx'.\nВыполните:\npip install python-docx")
            return
            
    def on_closing(self):
        try:
            self.after_cancel("all")
        except Exception as e:
            # Логируем ошибку отмены задач планировщика, но не прерываем закрытие
            print(f"[WARN] Failed to cancel scheduled events: {e}")
            
        for item in list(self.accordion_data.values()):
            try:
                content = item.get("content")
                if content:
                    content.destroy()
            except Exception as e:
                # Логируем ошибки уничтожения виджетов аккордеона
                print(f"[WARN] Failed to destroy accordion content: {e}")
                
        try:
            self.destroy()
        except Exception as e:
            # Логируем финальную ошибку закрытия корневого окна
            print(f"[CRITICAL] Failed to close ProjectWindow: {e}")

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








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

# --- НАДЕЖНОЕ ОПРЕДЕЛЕНИЕ ПУТИ К БАЗАМ ---
try:
    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 [], {}
        
        conn = None
        try:
            conn = sqlite3.connect(DATABASE_DB_PATH)
            cursor = conn.cursor()
            
            # Запрашиваем только имена строкой
            cursor.execute("SELECT name FROM groups ORDER BY name ASC")
            rows = cursor.fetchall() 
            
            group_names = []
            if rows:
                for row in rows:
                    # Защита от любой структуры: берем первый доступный элемент строки
                    try:
                        group_names.append(str(row[0]))
                    except IndexError:
                        continue
            
            # Словарь для кодов нам сейчас не нужен, оставим пустым или заполним так же строго
            name_to_code_map = {}
            if group_names:
                 cursor.execute("SELECT code, name FROM groups ORDER BY name")
                 code_rows = cursor.fetchall()
                 for c_row in code_rows:
                     try:
                         name_to_code_map[c_row[1]] = c_row[0]
                     except IndexError:
                         continue

            print(f"[LOGIC DEBUG] Final list sent to UI: {group_names}")
            return group_names, name_to_code_map
            
        except Exception as e:
            print(f"[LOGIC ERROR] Failed to read from master DB: {e}")
            import traceback
            traceback.print_exc()
            return [], []
        finally:
            if conn:
                conn.close()

    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, code FROM indicators WHERE group_code = ? ORDER BY code", (group_code,)
            )
            indicators = cursor.fetchall()
            conn.close()
            return indicators
        except Exception as e:
            print(f"[LOGIC ERROR] Failed to read indicators: {e}")
            return []

    # --- ПОЛУЧЕНИЕ СОБЫТИЙ ДЛЯ ГРАФИКА ---
    def get_events_for_chart(
        self,
        project_code: str,
        group_names: List[str],
        date_from: datetime.date,
        date_to: datetime.date,
    ) -> List[Tuple]:

        db_path = Path(self.project_dbs_folder) / f"{project_code}.db"

        if not db_path.exists():
            print(f"[DEBUG] DB file does not exist: {db_path}")
            return []

        try:
            conn = sqlite3.connect(db_path)
            cursor = conn.cursor()

            query = f'''
                SELECT event_data, group_name, COUNT(*) as cnt
                FROM project_data 
                WHERE group_name IN ({','.join('?' for _ in group_names)})
                  AND date(event_data) >= date(?)
                  AND date(event_data) < date(?)
                GROUP BY date(event_data), group_name
                ORDER BY date(event_data) ASC
            '''
            params = [*group_names, date_from.strftime('%Y-%m-%d'), date_to.strftime('%Y-%m-%d')]

            print(f"[DEBUG QUERY] Executing on {db_path.name}")
            print(f"[DEBUG PARAMS] Groups: {group_names}, From: {params[-2]}, To: {params[-1]}")

            cursor.execute(query, params)
            events = cursor.fetchall()

            print(f"[DEBUG RESULTS] Found rows: {len(events)}")
            if events:
                print(f"[DEBUG SAMPLE] First row: {events[0]}")

            conn.close()
            return events

        except Exception as e:
            print(f"[LOGIC ERROR] Failed to read events for chart: {e}")
            return []

    # --- ЗАПИСЬ СОБЫТИЙ ---
    def add_event_to_db(self, selected_data: Dict, event_text: str, date_str: str, direction: int) -> tuple[bool, str]:
        db_path = os.path.join(self.project_dbs_folder, f"{selected_data['project_code']}.db")

        is_new_db = not os.path.exists(db_path)
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()

        # === КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ СТРУКТУРЫ ТАБЛИЦЫ ===
        cursor.execute("PRAGMA table_info(project_data)")
        columns = [col[1] for col in cursor.fetchall()]

        if 'indicator_code' not in columns or 'event_sign' not in columns:
            print("[DB FIX] Table structure outdated or missing. Recreating project_data...")
            cursor.execute("DROP TABLE IF EXISTS project_data")

            cursor.execute('''
                CREATE TABLE project_data (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    project_code TEXT NOT NULL,
                    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                    group_name TEXT,
                    group_code TEXT,
                    indicator_name TEXT,
                    indicator_code TEXT,
                    event_data DATE,
                    event_sign INTEGER,
                    event_code TEXT,
                    event_text_content TEXT
                )
            ''')
            conn.commit()

        cursor.execute(
            "SELECT COUNT(*) FROM project_data WHERE project_code = ? AND indicator_code = ?",
            (selected_data['project_code'], selected_data['indicator_code']),
        )
        count = cursor.fetchone()[0]

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

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









# =====================================
# dukora\modules\project\chart_modal.ру
# =====================================
import customtkinter as ctk
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.dates
import datetime
import os
from tkinter import messagebox
from typing import List
from modules.project.project_event_logic import ProjectEventLogic


def fetch_real_chart_data(logic_instance: ProjectEventLogic, project_code: str, group_names: List[str], start_date: str, step_days: int):
    data = []
    
    if not logic_instance or not project_code or not group_names:
        print("[CHART LOGIC] Missing context.")
        return pd.DataFrame()
        
    try:
        current_dt = datetime.datetime.strptime(start_date, '%d.%m.%Y')
        end_limit = datetime.date.today()
        
        # ИСПРАВЛЕНО: Проверяем начало периода, а не конец
        while current_dt.date() < end_limit: 
            
            period_end_dt = current_dt + datetime.timedelta(days=step_days)
            
            raw_events = logic_instance.get_events_for_chart(
                project_code=project_code,
                group_names=group_names,
                date_from=current_dt.date(),
                date_to=period_end_dt.date()
            )
            
            daily_count = sum(row[2] for row in raw_events)
            
            if daily_count > 0:
                label = "Все события" if len(group_names) > 1 else group_names[0]
                data.append({
                    'date': current_dt.date(), 
                    'group': label, 
                    'count': daily_count
                })
                
            # Переходим к следующему окну
            current_dt = period_end_dt
            
        df = pd.DataFrame(data)
        
        if not df.empty:
            df['date'] = pd.to_datetime(df['date'])
            
        return df
        
    except Exception as e:
        print(f"[LOGIC ERROR] Failed to read events: {e}")
        return pd.DataFrame()


def show_chart_modal(master, project_code: str, group_names: List[str], start_date: str, step_days: int, chart_type: str, passed_logic):
    df = fetch_real_chart_data(passed_logic, project_code, group_names, start_date, step_days)
    
    if df.empty:
        messagebox.showinfo("Нет данных", "За данный период событий не найдено.\nПроверьте дату начала или выберите другие группы.")
        return

    modal = ctk.CTkToplevel(master)
    title_name = ', '.join(group_names[:3]) + ("..." if len(group_names) > 3 else "")
    modal.title(f"Интенсивность событий: {title_name}")
    modal.geometry("850x650")
    modal.protocol("WM_DELETE_WINDOW", lambda: None) 
    
    master.update_idletasks()
    modal.update_idletasks()
    x = master.winfo_rootx() + (master.winfo_width() // 2) - (modal.winfo_width() // 2)
    y = master.winfo_rooty() + (master.winfo_height() // 2) - (modal.winfo_height() // 2)
    modal.geometry(f"+{x}+{y}")
    modal.grab_set()
    modal.transient(master)
    
    modal.grid_rowconfigure(0, weight=1)
    modal.grid_columnconfigure(0, weight=1)

    fig, ax = plt.subplots(figsize=(10, 6), dpi=100)
    dates = pd.to_datetime(df['date'])
    
    color = '#3B82F6'
    
    if chart_type == "Гистограмма":
        # Ширина занимает 70% от выбранного периода, оставляя 30% на белый зазор
        width = step_days * 0.7  
        
        bars = ax.bar(dates, df['count'], width=width, label=df['group'].iloc[0], alpha=0.9, color=color, align='center', edgecolor='black')
        
        for bar in bars:
            height = bar.get_height()
            if height > 0:
                ax.text(
                    bar.get_x() + bar.get_width() / 2, 
                    height, 
                    f'{int(height)}',
                    ha='center', va='bottom', 
                    fontsize=11, fontweight='bold', 
                    color='#FFFFFF',
                    bbox=dict(facecolor='black', edgecolor='none', pad=1, alpha=0.8)
                )
                    
    else:
        line, = ax.plot(dates, df['count'], marker='o', linewidth=2, markersize=6, label=df['group'].iloc[0], color=color)
        
        for i, v in enumerate(df['count']):
            if v > 0:
                ax.text(
                    dates[i], 
                    v,
                    str(int(v)),
                    ha='center', va='bottom', 
                    fontsize=10, fontweight='bold', 
                    color='#000000',
                    bbox=dict(facecolor='#FFFAF0', edgecolor='gray', boxstyle="round,pad=0.3", alpha=0.8)
                )

    ax.set_title(f'Активность событий\n(Период от {start_date})', fontsize=16)
    ax.set_xlabel('Дата (начало периода)', fontsize=14)
    ax.set_ylabel('Количество событий', fontsize=14)
    
    ax.legend(loc="upper left")
    ax.grid(True, linestyle='--', alpha=0.6)
    
    fig.autofmt_xdate()
    date_fmt = matplotlib.dates.DateFormatter('%d-%m')
    ax.xaxis.set_major_formatter(date_fmt)
    plt.setp(ax.xaxis.get_majorticklabels(), rotation=45, ha='right')

    canvas = FigureCanvasTkAgg(fig, master=modal)
    canvas.draw()
    canvas.get_tk_widget().pack(fill="both", expand=True, padx=10, pady=10)
    
    # === ПАНЕЛЬ КНОПОК (Создается строго один раз здесь) ===
    button_frame = ctk.CTkFrame(modal)
    button_frame.pack(fill="x", pady=(0, 10), padx=10)
    
    def close_window():
        try:
            plt.close(fig)
        except Exception:
            pass
        try:
            modal.destroy()
        except Exception:
            pass

    save_btn = ctk.CTkButton(button_frame, text="Сохранить", fg_color="#22C55E", command=lambda: (
        full_path := os.path.join(os.getcwd(), f"График_{datetime.now().strftime('%d-%m-%Y_%H-%M')}.png"),
        fig.savefig(full_path, dpi=300, bbox_inches='tight'),
        master.clipboard_append(f"График сохранен как {full_path}"),
        print(f"[MODAL] Chart saved to {full_path}")
    ))
    save_btn.pack(side="left", fill="x", expand=True, padx=5)

    close_btn = ctk.CTkButton(button_frame, text="Закрыть", fg_color="#3B82F6", command=close_window)
    close_btn.pack(side="right", fill="x", expand=True, padx=5)

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





#================================================
# 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
# =================================================================









# ==========================================
# dukora\modules\project\project_chart_ui.py
# ==========================================
import customtkinter as ctk
import tkinter as tk
from tkinter import ttk, messagebox
import datetime
from modules.project.chart_modal import show_chart_modal


def build_charts_interface(parent_frame: ctk.CTkFrame, window_context):
    # print(f"[UI BUILD] Context received. Logic object exists: {hasattr(window_context, 'event_logic')}")
    
    # Переменные UI
    var_group = ctk.StringVar(value="")
    var_date_from = ctk.StringVar(value=datetime.datetime.now().strftime('%d.%m.%Y'))
    var_period_str = ctk.StringVar(value="7") # Шаг по умолчанию неделя для агрегации
    var_chart_type = ctk.StringVar(value="Гистограмма")

    listbox_groups_ref = None
    cmb_group = None

    parent_frame.grid_rowconfigure(0, weight=0)
    parent_frame.grid_rowconfigure(1, weight=0)
    parent_frame.grid_rowconfigure(2, weight=0)
    parent_frame.grid_rowconfigure(3, weight=0)
    parent_frame.grid_rowconfigure(4, weight=1)
    parent_frame.grid_columnconfigure(0, weight=1)

    # --- БЛОК ГРУПП ---
    grp_f = ctk.CTkFrame(parent_frame, fg_color="transparent")
    grp_f.grid(row=0, column=0, sticky="ew", pady=(0, 5), padx=5)

    lbl_g = ctk.CTkLabel(grp_f, text="Группа:", anchor="w", width=80)
    lbl_g.grid(row=0, column=0, padx=(0, 5), pady=5, sticky="w")

    cmb_group = ctk.CTkComboBox(grp_f, state="readonly", width=150, variable=var_group)
    cmb_group.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
    cmb_group.set("")

    def add_current_selection():
        selected = var_group.get()
        if not selected or not selected.strip() or selected == "Все группы":
            return
        
        current_items = listbox_groups_ref.get(0, tk.END)
        if selected not in current_items:
            listbox_groups_ref.insert(tk.END, selected)
            # Автоматически выделяем то, что только что добавили
            last_index = listbox_groups_ref.size() - 1
            listbox_groups_ref.selection_clear(0, tk.END)
            listbox_groups_ref.selection_set(last_index)
            
        cmb_group.set("") # Сбрасываем комбобокс после добавления

    def remove_selected_item():
        sel = listbox_groups_ref.curselection()
        for index in reversed(sel):
            listbox_groups_ref.delete(index)

    btn_add = ctk.CTkButton(grp_f, text="Добавить в список", fg_color="#3B82F6", command=add_current_selection)
    btn_add.grid(row=0, column=2, padx=5, pady=5)

    btn_remove = ctk.CTkButton(
        grp_f, text="Удалить", fg_color="#DC2626", hover_color="#b91c1c", text_color="white", command=remove_selected_item
    )
    btn_remove.grid(row=0, column=3, padx=(5, 0), pady=5)

    # --- СПИСОК ВЫБРАННЫХ ГРУПП ---
    list_frame = ctk.CTkFrame(parent_frame)
    list_frame.grid(row=4, column=0, sticky="nsew", pady=(5, 15), padx=5)
    list_frame.grid_rowconfigure(0, weight=1)
    list_frame.grid_columnconfigure(0, weight=1)

    listbox_groups_ref = tk.Listbox(
        list_frame, height=4, selectmode="extended", bg="#2B2B2B", fg="white", selectbackground="#3C8EC7", selectforeground="white", highlightthickness=0
    )
    listbox_groups_ref.grid(row=0, column=0, sticky="nsew", padx=(0, 5))

    scrollbar = ctk.CTkScrollbar(list_frame, orientation="vertical", command=listbox_groups_ref.yview)
    scrollbar.grid(row=0, column=1, sticky="ns")
    listbox_groups_ref.config(yscrollcommand=scrollbar.set)

    # --- ДАТА И ПЕРИОД ---
    date_f = ctk.CTkFrame(parent_frame, fg_color="transparent")
    date_f.grid(row=1, column=0, sticky="ew", pady=5, padx=5)
    ttk.Label(date_f, text="Дата начала:", width=12).pack(side="left", padx=(0, 5))

    def _safe_pack_date_entry(master, text_var):
        today = datetime.datetime.now()
        try:
            from tkcalendar import DateEntry
            de = DateEntry(
                master=master,
                width=12,
                background='darkblue',
                foreground='white',
                bordercolor='gray',
                headersbackground='darkgray',
                normalbackground='white',
                normalforeground='black',
                weekendbackground='lightblue',
                weekendforeground='red',
                date_pattern='dd.MM.yyyy',
                textvariable=text_var
            )
            de.set_date(today)
            de.pack(side="left", padx=5)
        except ImportError:
            e_entry = ctk.CTkEntry(master, placeholder_text="ДД.ММ.ГГГГ", width=120, textvariable=text_var)
            e_entry.insert(0, today.strftime('%d.%m.%Y'))
            e_entry.pack(side="left", padx=5)

    _safe_pack_date_entry(date_f, var_date_from)

    per_f = ctk.CTkFrame(parent_frame, fg_color="transparent")
    per_f.grid(row=2, column=0, sticky="ew", pady=5, padx=5)
    ttk.Label(per_f, text="Шаг (дней):", width=12).pack(side="left", padx=(0, 5))

    def _validate_step_input(P):
        return P.isdigit() or P == ""

    vcmd = (parent_frame.register(_validate_step_input), '%P')
    ent_p = ctk.CTkEntry(per_f, width=60, justify="center", textvariable=var_period_str, validate="key", validatecommand=vcmd)
    ent_p.pack(side="left", padx=5)
    ttk.Label(per_f, text="(ось X)", foreground="#A9A9A9").pack(side="left", padx=(5, 0))

    # --- ТИП ГРАФИКА ---
    type_f = ctk.CTkFrame(parent_frame, fg_color="transparent")
    type_f.grid(row=3, column=0, sticky="ew", pady=5, padx=5)
    ttk.Label(type_f, text="Тип:", width=12).pack(side="left", padx=(0, 5))
    ctk.CTkComboBox(type_f, values=["Гистограмма", "График"], state="readonly", width=150, variable=var_chart_type).pack(side="left", padx=5)

    # --- КНОПКИ ДЕЙСТВИЙ ---
    action_f = ctk.CTkFrame(parent_frame)
    action_f.grid(row=5, column=0, sticky="ew", pady=(20, 0), padx=5)
    action_f.grid_columnconfigure(0, weight=1)
    action_f.grid_columnconfigure(1, weight=1)

    btn_reset = ctk.CTkButton(action_f, text="Сбросить настройки", fg_color="#EF4444", command=lambda: (
        var_group.set(""),
        var_date_from.set(datetime.datetime.now().strftime('%d.%m.%Y')),
        var_period_str.set("7"), # Сбрасываем на неделю
        var_chart_type.set("Гистограмма"),
        listbox_groups_ref.delete(0, tk.END),
        cmb_group.set("")
    ))
    btn_reset.grid(row=0, column=0, sticky="ew", padx=5)

    btn_build = ctk.CTkButton(action_f, text="Построить диаграмму", fg_color="#3B82F6", state="disabled")
    btn_build.grid(row=0, column=1, sticky="ew", padx=5)

    # === ЛОГИКА ПОСТРОЕНИЯ ===
    def _try_build_chart():
        groups_to_plot = []
        
        choice = var_group.get().strip()
        manual_list = [listbox_groups_ref.get(i) for i in listbox_groups_ref.curselection()]
        
        if manual_list:
            groups_to_plot = manual_list
        else:
            if not choice:
                messagebox.showwarning("Ошибка", "Выберите группу.")
                return
            
            if choice == "Все группы":
                all_raw, _ = window_context.event_logic.load_groups_and_indicators()
                
                names = []
                if all_raw:
                    try:
                        if isinstance(all_raw[0], tuple) and len(all_raw[0]) > 1:
                            names = sorted([item[1] for item in all_raw])
                        elif hasattr(all_raw[0], 'name'):
                            names = sorted([item.name for item in all_raw])
                        else:
                            names = sorted([str(x) for x in all_raw])
                    except Exception as e:
                        print(f"[CHART UI GROUP PARSE ERROR]: {e}")
                
                if names:
                    groups_to_plot = names
                else:
                    messagebox.showerror("Ошибка", "В справочнике датабаза нет ни одной группы.")
                    return
            else:
                groups_to_plot.append(choice)

        if not groups_to_plot:
            messagebox.showerror("Ошибка", "Список групп пустой.")
            return

        step_text = var_period_str.get().strip()
        try:
            step_days = int(step_text)
            if step_days < 1: raise ValueError
        except (ValueError, TypeError):
            step_days = 7
            var_period_str.set("7")

        start_date_str = var_date_from.get().strip()
        if not start_date_str:
            messagebox.showwarning("Ошибка", "Введите дату начала."); return
        try:
            selected_date = datetime.datetime.strptime(start_date_str, '%d.%m.%Y').date()
        except ValueError:
            messagebox.showwarning("Ошибка", "Неверный формат даты.\nДД.ММ.ГГГГ"); return

        today = datetime.date.today()
        if selected_date >= today:
            selected_date = today - datetime.timedelta(days=7)

        final_start_date_str = selected_date.strftime('%d.%m.%Y')
        
        # print(f"[DEBUG UI] Отправляю в график -> Проект: {window_context.current_selected_project_code}, Группы: {groups_to_plot}, Дата: {final_start_date_str}, Шаг: {step_days}")
        
        show_chart_modal(
            master=window_context,
            project_code=window_context.current_selected_project_code,
            group_names=groups_to_plot,
            start_date=final_start_date_str,
            step_days=step_days,
            chart_type=var_chart_type.get(),
            passed_logic=window_context.event_logic
        )

    btn_build.configure(command=_try_build_chart)

    # === ЗАГРУЗКА ДАННЫХ В COMBOBOX (в самый конец сборки UI) ===
    print("[UI BUILD] Loading groups into ComboBox...")
    names = []  # Инициализируем пустой список СРАЗУ, чтобы он существовал всегда
    
    try:
        all_raw, _ = window_context.event_logic.load_groups_and_indicators()
        print(f"[UI BUILD] Raw data from DB: {all_raw}")
        
        if all_raw:
            # Защита от разных структур БД (объекты NamedTuple или обычные кортежи)
            first_item = all_raw[0]
            
            if isinstance(first_item, tuple) and len(first_item) > 1:
                # Если это кортеж вида ('Код', 'Имя')
                names = sorted([item[1] for item in all_raw])
            elif hasattr(first_item, 'name'):
                # Если это объект с атрибутом .name
                names = sorted([item.name for item in all_raw])
            else:
                # Если это просто строка или число
                names = sorted([str(x) for x in all_raw])
                
    except Exception as e:
        # ВАЖНО: Отступ под print должен быть таким же, как у самого print
        print(f"[UI BUILD FATAL]: {e}")
        
    final_combo_values = ["Все группы"] + names
    cmb_group.configure(values=final_combo_values)
    
    if len(final_combo_values) > 1:
        cmb_group.set(final_combo_values[1]) 
        
    # Раскомментируйте эту строку, если нужно проверить список в консоли:
    # print(f"[UI BUILD] Finalized ComboBox items: {final_combo_values}")

    # === УПРАВЛЕНИЕ КНОПКОЙ BUILD ===
    def check_enable(*args):
        has_manual_sel = bool(listbox_groups_ref.curselection())
        has_cmb_choice = bool(var_group.get().strip())
        
        is_all_selected = (var_group.get() == "Все группы")
        
        step_ok = False
        txt = var_period_str.get().strip()
        if txt.isdigit() and int(txt) >= 1:
            step_ok = True

        date_ok = bool(var_date_from.get() and var_date_from.get().count('.') == 2)
        context_ok = bool(window_context.current_selected_project_code)

        if context_ok and date_ok and step_ok and (is_all_selected or has_manual_sel or has_cmb_choice):
            btn_build.configure(state="normal", fg_color="#3B82F6")
        else:
            btn_build.configure(state="disabled", fg_color="#A9A9A9")

    var_group.trace_add("write", check_enable)
    var_period_str.trace_add("write", check_enable)
    var_date_from.trace_add("write", check_enable)
    listbox_groups_ref.bind('<<ListboxSelect>>', check_enable)
    
    check_enable()
    
  # ================================================