Загрузка данных
Промт
Работаем с окном "Наполнение проекта" с заголовком аккордеона "Построить диаграмму".
Текущая проблема: Нажатие кнопки «Построить диаграмму» выдает ошибку "Список групп пустой". Как развивались события при написании кода.
1. Сначала в выпадающем списке были из справочника подключенные к базе группы ( в группах показатели, в показателях события). График строился как положено - сколько групп столько графиков (столбиков гистаграммы). Но в выпадающем списке не хватало записи "Все группы". Добавили эту запись - пропал список самих групп.
Как по плану это должно работать.
Если выбирается только одна запись из списка (не важно, группа или "Все группы"), то строится только один, выбранный график. Если выбирается несколько групп, то строится столько , сколько выбрано.
Когда строится график по "Все группы", то это объединение всех данных из групп в один общий график.
Графики строятся от указанной пользователем даты до последней записи в базе данных с указанным шагом. Над столбиком гистаграммы или маркером графика желательно поставить значение (количество событий в этом промежутке).
2. Сейчас организовано так, что сохраняется файл с графиком ххх.png в корень программы, где лежит запускающий файл. Необходимо, чтобы он сохранялся в папку dukora\events (папка в корне проекта).
3. Сейчас файл с графиком сохраняется с названием temp_chart.png. Необходимо, чтобы он сохранялся с названием файла График_24-08-2026_10-28.png (График+дата+время).
Чтобы ты мог найти ошибку и исправить код, высылаю содержимое нескольких файлов.
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:
# Используем нашу рабочую функцию из resources.py
from modules.utils.resources import get_project_root
ROOT_DIR = get_project_root()
DB_FOLDER = ROOT_DIR / "db"
DATABASE_DB_PATH = DB_FOLDER / "database.db"
PROJECT_DBS_FOLDER = ROOT_DIR / "project_dbs"
except Exception as e:
print(f"[FATAL] Не удалось определить корень проекта для путей к БД: {e}")
sys.exit(1)
class ProjectEventLogic:
def __init__(self, manager_db: ProjectDatabase):
self.manager_db = manager_db # Теперь снова можем брать список проектов
# Пути берем из глобальных констант модуля (которые мы настроили ранее)
self.project_dbs_folder = PROJECT_DBS_FOLDER
os.makedirs(self.project_dbs_folder, exist_ok=True)
# --- СПИСОК ПРОЕКТОВ (из projects.db) ---
def load_project_list(self) -> List[Tuple]:
return self.manager_db.list_projects()
# --- ГРУППЫ И ПОКАЗАТЕЛИ (из database.db - без изменений) ---
def load_groups_and_indicators(self) -> Tuple[List[str], Dict[str, str]]:
if not DATABASE_DB_PATH.exists():
return [], {}
try:
conn = sqlite3.connect(DATABASE_DB_PATH)
cursor = conn.cursor()
# Группы - просто список имен
cursor.execute("SELECT name FROM groups ORDER BY name")
group_names = [row[0] for row in cursor.fetchall()]
name_to_code_map = {}
if group_names:
cursor.execute("SELECT code, name FROM groups ORDER BY name")
rows = cursor.fetchall()
name_to_code_map = {name: code for code, name in rows}
conn.close()
return group_names, name_to_code_map
except Exception as e:
print(f"[LOGIC ERROR] Failed to read from master DB: {e}")
return [], {}
def load_indicators_for_group(self, group_code: str):
if not DATABASE_DB_PATH.exists():
return []
try:
conn = sqlite3.connect(DATABASE_DB_PATH)
cursor = conn.cursor()
# Индикаторы - просто список имен
cursor.execute("SELECT name, 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()
# Используем CAST или функции даты для надежности
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, -- ВАЖНО: именно Integer, как ожидает ваша логика (+/-)
event_code TEXT,
event_text_content TEXT
)
''')
conn.commit()
# Теперь запрос COUNT точно найдет колонку indicator_code
cursor.execute(
"SELECT COUNT(*) FROM project_data WHERE project_code = ? AND indicator_code = ?",
(selected_data['project_code'], selected_data['indicator_code'])
)
count = cursor.fetchone()[0]
# Код события формируется только если выбран индикатор
event_code = f"{selected_data['indicator_code']}_{count + 1}" if selected_data['indicator_code'] else f"EVENT_{count + 1}"
try:
cursor.execute('''
INSERT INTO project_data (
project_code, group_name, group_code, indicator_name, indicator_code,
event_data, event_sign, event_code, event_text_content
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
selected_data['project_code'],
selected_data['group_name'],
selected_data['group_code'],
selected_data['indicator_name'],
selected_data['indicator_code'],
date_str,
direction, # Передаем число 1 или -1 напрямую
event_code,
event_text
))
conn.commit()
return True, f"Данные добавлены. Код события: {event_code}"
except Exception as e:
conn.rollback()
return False, f"Ошибка записи в БД: {e}"
finally:
conn.close()
# ========================================
# =====================================
# 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
from io import BytesIO
import datetime
import sys
from typing import List, Any
import tkinter as tk
from tkinter import messagebox
# Глобальный импорт логики событий
try:
from modules.project.project_event_logic import ProjectEventLogic
except ImportError:
print("[CHART MODAL] Не удалось импортировать ProjectEventLogic")
ProjectEventLogic = None
def fetch_real_chart_data(logic_instance, project_code: str, group_names: List[str], start_date: str, step_days: int):
data = []
# Используем переданную логику, если она есть
active_logic = logic_instance
# Если внешняя логика не пришла (старая ошибка), создаем свою локально
if not active_logic:
try:
from modules.project.project_event_logic import ProjectEventLogic
active_logic = ProjectEventLogic(None)
except Exception as e:
print(f"[CHART LOGIC FATAL]: {e}")
return pd.DataFrame()
try:
end_date_dt = datetime.datetime.strptime(start_date, '%d.%m.%Y') + datetime.timedelta(days=30)
current_dt = datetime.datetime.strptime(start_date, '%d.%m.%Y')
while current_dt <= end_date_dt:
next_dt = current_dt + datetime.timedelta(days=step_days)
raw_events = active_logic.get_events_for_chart( # <- используем переменную
project_code=project_code,
group_names=group_names,
date_from=current_dt.date(),
date_to=next_dt.date()
)
for row in raw_events:
data.append({
'date': row[0],
'group': row[1],
'count': row[2]
})
current_dt = next_dt
df = pd.DataFrame(data)
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(
logic_instance=passed_logic, # Передаем рабочую логику сюда
project_code=project_code,
group_names=group_names,
start_date=start_date,
step_days=step_days
)
if df.empty or df['count'].sum() == 0:
from tkinter import messagebox
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")
# --- КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ ---
# Отключаем стандартное закрытие по кнопке [X] в углу.
# Теперь закрыть окно можно ТОЛЬКО нашей кнопкой ниже.
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)
# --- СОЗДАНИЕ ГРАФИКА MATPLOTLIB ---
fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
dates = pd.to_datetime(df['date'])
if chart_type == "Гистограмма":
for name in group_names:
subset = df[df['group'] == name]
if not subset.empty:
ax.bar(subset['date'], subset['count'], label=name, alpha=0.7)
else:
for name in group_names:
subset = df[df['group'] == name]
if not subset.empty:
ax.plot(subset['date'], subset['count'], marker='o', linewidth=2, markersize=6, label=name)
ax.set_title(f'Активность событий\n(Период от {start_date})', fontsize=14)
ax.set_xlabel('Дата', fontsize=12)
ax.set_ylabel('Количество событий', fontsize=12)
ax.legend(title="Группы", loc="upper left")
ax.grid(True, linestyle='--', alpha=0.6)
fig.autofmt_xdate()
# --- ВСТРАИВАНИЕ В ОКНО ---
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: (
fig.savefig("temp_chart.png", dpi=300, bbox_inches='tight'),
master.clipboard_append("График сохранен как temp_chart.png"),
print("[MODAL] Chart saved to temp_chart.png")
))
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)
def close_window():
"""Безопасное закрытие окна и очистка памяти без ошибок Tkinter."""
try:
plt.close(fig)
except Exception:
pass # Игнорируем ошибку, если фигура уже закрыта системой
try:
modal.destroy()
except Exception:
pass # Игнорируем ошибку, если окно уничтожено принудительно
def save_chart():
try:
fig.savefig("temp_chart.png", dpi=300, bbox_inches='tight')
master.clipboard_append("График успешно сохранен как temp_chart.png")
except Exception:
messagebox.showerror("Ошибка", "Не удалось сохранить график.\nВозможно, папка защищена от записи.")
# Кнопка Сохранить
save_btn = ctk.CTkButton(button_frame, text="Сохранить", fg_color="#22C55E", command=save_chart)
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
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)
def build_charts_interface(parent_frame: ctk.CTkFrame, window_context):
print(f"[UI BUILD] Context received. Logic object exists: {hasattr(window_context, 'event_logic')}")
print(f"[UI BUILD] ID of context: {id(window_context)}")
# Здесь остальной твой код этой функции...
var_group = ctk.StringVar(value="")
var_date_from = ctk.StringVar(value=datetime.datetime.now().strftime('%d.%m.%Y'))
var_period_str = ctk.StringVar(value="1")
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 selected and selected.strip():
current_items = listbox_groups_ref.get(0, tk.END)
if selected not in current_items:
listbox_groups_ref.insert(tk.END, selected)
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))
_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("1"),
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)
def _try_build_chart():
groups_to_plot = []
selected_indices = listbox_groups_ref.curselection()
if selected_indices:
for i in selected_indices:
item_text = listbox_groups_ref.get(i)
if item_text and item_text != "Все группы":
groups_to_plot.append(item_text)
else:
current_choice = var_group.get().strip()
if not current_choice:
messagebox.showwarning("Ошибка", "Выберите группу.")
return
if current_choice == "Все группы":
all_raw, _ = window_context.event_logic.load_groups_and_indicators()
if all_raw:
if hasattr(all_raw[0], 'name'):
groups_to_plot = sorted([item.name for item in all_raw])
elif isinstance(all_raw[0], tuple):
groups_to_plot = sorted([item[1] for item in all_raw])
else:
groups_to_plot.append(current_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 = 1
var_period_str.set("1")
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')
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 = ctk.CTkButton(action_f, text="Построить диаграмму", fg_color="#3B82F6", state="disabled", command=_try_build_chart)
btn_build.grid(row=0, column=1, sticky="ew", padx=5)
# === ЗАГРУЗКА ДАННЫХ В COMBOBOX ===
print("[UI BUILD] Loading groups into ComboBox...") # <-- ПРИНТ №2
names = []
try:
all_raw, _ = window_context.event_logic.load_groups_and_indicators()
print(f"[UI BUILD] Raw data from DB: {all_raw}") # <-- ПРИНТ №3 (Самое важное!)
if all_raw:
if hasattr(all_raw[0], 'name'):
names = sorted([item.name for item in all_raw])
elif isinstance(all_raw[0], tuple):
names = sorted([item[1] for item in all_raw])
except Exception as e:
print(f"[UI BUILD FATAL]: {e}")
final_combo_values = ["Все группы"] + names
cmb_group.configure(values=final_combo_values)
initial_index = 1 if len(final_combo_values) > 1 else 0
cmb_group.set(final_combo_values[initial_index])
print(f"[UI BUILD] Finalized ComboBox items: {final_combo_values}") # <-- ПРИНТ №4
def check_enable(*args):
has_group = bool(listbox_groups_ref.curselection()) or bool(var_group.get().strip())
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)
if window_context.current_selected_project_code and has_group and step_ok and date_ok:
btn_build.configure(state="normal")
else:
btn_build.configure(state="disabled")
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()
print("[UI BUILD] Interface ready.") # <-- ПРИНТ №5
# ================================================