Загрузка данных
Промт для переделки выпадающего списка
Мне нужно переписать функцию формирования выпадающего списка (ComboBox) в моем Python-приложении.
Исходные данные: показатели выгружаются из базы данных (поля: indicator_code, indicator_name). Сейчас они сортируются по алфавиту названия, это ломает логику. Мне нужна строгая сортировка по возрастанию поля indicator_code.
Задача:
1. Объединить код и название в одну строку вида: [CODE]\t[NAME].
2. Название показателя ([NAME]) должно быть разбито на несколько строк прямо внутри этой опции списка. Перенос должен происходить по словам (не разрывая слово пополам) каждые 60–70 символов. Для переносов использовать символ новой строки \n. Вторую и последующие строки нужно сдвинуть вправо (добавить отступы пробелами), чтобы текст стоял под текстом названия, а не под кодом.
3. Для корректного выравнивания столбцов обязуй пользователя установить моноширинный шрифт (укажи в коде семейство вроде Courier New) для этого Combobox.
4. Идентификация выбранного значения. Несмотря на то что ComboBox показывает объединенную строку, вся дальнейшая логика приложения должна работать ТОЛЬКО С ПОЛЕМ indicator_code. Реализуй скрытый маппинг (словарь), где ключом будет полная отображаемая строка, а значением — чистый код индикатора.
Вот мой текущий код [сюда вставьте свой код]. Переделай его согласно этим требованиям."
Отступы: Если табы \t будут выглядеть неровно из-за разницы в длине кодов, замените формирование строки на форматирование фиксированной ширины: f"{item['code']:<10} {'\n '.join(lines)}" (где <10 выделяет 10 символов под код).
Здесь еще логика нарушена в формировании кода события. Его код формирпуется из кода группы + порядковый номер показателя + порядковый номер события при данном показателе. Но в выпадающем списке берется не код уже созданного показателя, а формируется заново, и исходя из расположения по алфавиту. Главный и правильный код показателя в базе данных. Ниже высылаю файлы заполнения событий проекта, где создан неправильно выпадающий список для его правки.
Кроме проблем со списком есть еще одно необходимое для правки место: Календарь.
Проблема в том, что когда создается событие и когда происходит правка события, а именно даты , то у них ставятся разные разделители числа, месяца и года. В одном случае это точки, а в другом это дефисы - надо привести одному знаменателю.
# =================================================================
# 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 FROM indicators WHERE group_code = ? ORDER BY name", (group_code,))
indicators = [(row[0], ) for row in cursor.fetchall()]
conn.close()
return indicators
except Exception as e:
print(f"[LOGIC ERROR] Failed to read indicators: {e}")
return []
# --- ПОЛУЧЕНИЕ СОБЫТИЙ ДЛЯ ГРАФИКА ---
def 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\project_event_ui.py
# ==========================================
import customtkinter as ctk
from tkinter import ttk
import tkinter as tk
import datetime
import os
import sqlite3
from .project_event_logic import ProjectEventLogic
from tkcalendar import DateEntry
from tkinter import messagebox
# ==========================================
# ОСНОВНОЙ КЛАСС ОКНА СОБЫТИЙ
# ==========================================
class ProjectEventUI(ctk.CTkFrame):
EDIT_MODE_COLOR = "#8B4513"
def __init__(self, master, window_ref, logic: ProjectEventLogic):
super().__init__(master, fg_color="transparent")
self.window_ref = window_ref
self.logic = logic
self.group_names_list = []
self.indicator_names_list = []
self.group_names_to_codes = {}
self.indicator_cache = {}
self.editing_event_code = None
self._setup_widgets()
self._bind_global_events()
self.update_project_dependent_lists()
self.after(50, self._initial_load)
def _initial_load(self):
print(f"[INIT LOAD] Waiting for UI sync...")
current_proj_code = getattr(self.window_ref, 'current_selected_project_code', "")
if current_proj_code and current_proj_code != "Проектов нет":
self._refresh_event_table(current_proj_code)
def _setup_widgets(self):
main_form = ctk.CTkFrame(self, fg_color="transparent")
main_form.pack(fill="both", expand=True, padx=10, pady=10)
main_form.grid_columnconfigure(1, weight=1)
lbl_group = ctk.CTkLabel(main_form, text="Выбрать группу:", anchor="w")
lbl_group.grid(row=0, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
self.cmb_group = ctk.CTkComboBox(
main_form,
state="readonly",
width=250,
command=self._on_group_change
)
self.cmb_group.grid(row=0, column=1, sticky="ew", pady=(0, 5))
lbl_indicator = ctk.CTkLabel(main_form, text="Выбрать показатель:", anchor="w")
lbl_indicator.grid(row=1, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
self.cmb_indicator = ctk.CTkComboBox(
main_form,
state="readonly",
width=250,
command=lambda choice: self._refresh_event_table(self.window_ref.current_selected_project_code)
)
self.cmb_indicator.grid(row=1, column=1, sticky="ew", pady=(0, 10))
lbl_event = ctk.CTkLabel(main_form, text="Содержание события:", anchor="w")
lbl_event.grid(row=2, column=0, sticky="nw", pady=(0, 5), padx=(0, 10))
event_container = ctk.CTkFrame(main_form, fg_color="#F0F0F0", border_width=1, border_color="#A9A9A9")
event_container.grid(row=2, column=1, sticky="ew", pady=(0, 5), ipadx=2, ipady=2)
self.txt_event = ctk.CTkTextbox(event_container, height=80, wrap="word", fg_color="white")
self.txt_event.pack(fill="both", expand=True, padx=2, pady=2)
paste_btn = ctk.CTkButton(main_form, text="Вставить", fg_color="#2B2B2B", hover_color="#3C3C3C", command=self._paste_from_clipboard)
paste_btn.grid(row=3, column=1, sticky="e", pady=(0, 10), ipadx=10)
meta_frame = ctk.CTkFrame(main_form, fg_color="transparent")
meta_frame.grid(row=4, column=0, columnspan=2, sticky="ew", pady=(0, 10))
meta_frame.grid_columnconfigure(1, weight=1)
ttk.Label(meta_frame, text="Дата события:").grid(row=0, column=0, sticky="w", padx=(0, 10))
self.date_entry = DateEntry(
master=meta_frame,
width=12,
background='darkblue',
foreground='white',
bordercolor='gray',
headersbackground='darkgray',
normalbackground='white',
normalforeground='black',
weekendbackground='lightblue',
weekendforeground='red',
date_pattern='dd.MM.yyyy'
)
today = datetime.datetime.now()
self.date_entry.set_date(today)
self.date_entry.grid(row=0, column=1, sticky="w")
ttk.Label(meta_frame, text="Направленность:").grid(row=0, column=2, sticky="w", padx=(20, 10))
self.dir_var = tk.IntVar(value=1)
rb_plus = ctk.CTkRadioButton(meta_frame, text="(+)", variable=self.dir_var, value=1)
rb_plus.grid(row=0, column=3, sticky="w")
rb_minus = ctk.CTkRadioButton(meta_frame, text="(-)", variable=self.dir_var, value=-1)
rb_minus.grid(row=0, column=4, sticky="w")
ctrl_frame = ctk.CTkFrame(main_form, fg_color="transparent")
ctrl_frame.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(0, 10))
for i in range(4): ctrl_frame.grid_columnconfigure(i, weight=1)
self.btn_add = ctk.CTkButton(ctrl_frame, text="Добавить", fg_color="#3B82F6", command=self._add_event)
self.btn_add.grid(row=0, column=0, sticky="ew", padx=2)
self.btn_edit = ctk.CTkButton(ctrl_frame, text="Редактировать", fg_color="#10B981", command=self._enter_edit_mode)
self.btn_edit.grid(row=0, column=1, sticky="ew", padx=2)
self.btn_save = ctk.CTkButton(ctrl_frame, text="Сохранить", state="disabled", fg_color=self.EDIT_MODE_COLOR, command=self._save_edited_event)
self.btn_save.grid(row=0, column=2, sticky="ew", padx=2)
self.btn_del = ctk.CTkButton(ctrl_frame, text="Удалить", fg_color="#EF4444", command=self._delete_selected_event)
self.btn_del.grid(row=0, column=3, sticky="ew", padx=2)
tree_container = ctk.CTkFrame(main_form, fg_color="transparent")
tree_container.grid(row=6, column=0, columnspan=2, sticky="nsew", pady=(10, 0))
tree_container.grid_rowconfigure(0, weight=1); tree_container.grid_columnconfigure(0, weight=1)
style = ttk.Style()
theme_name = "default" # Жесткая фиксация темы для избежания конфликтов CTk
style.theme_use(theme_name)
columns = ("num", "code", "date", "sign", "event")
self.tree = ttk.Treeview(tree_container, columns=columns, show="headings", selectmode="browse", height=8)
self.tree.heading("num", text="№п/п"); self.tree.column("num", width=40, anchor="center", stretch=False)
self.tree.heading("code", text="Код"); self.tree.column("code", width=100, anchor="center", stretch=False)
self.tree.heading("date", text="Дата"); self.tree.column("date", width=110, anchor="center", stretch=False)
self.tree.heading("sign", text="+/-"); self.tree.column("sign", width=50, anchor="center", stretch=False)
self.tree.heading("event", text="Событие"); self.tree.column("event", minwidth=200, stretch=True)
self.tree.pack(side="left", fill="both", expand=True)
scrollbar = ctk.CTkScrollbar(tree_container, orientation="vertical", command=self.tree.yview)
scrollbar.pack(side="right", fill="y")
self.tree.configure(yscrollcommand=scrollbar.set)
search_frame = ctk.CTkFrame(main_form, fg_color="transparent")
search_frame.grid(row=7, column=0, columnspan=2, sticky="ew", pady=(10, 0))
search_frame.grid_columnconfigure(1, weight=1)
ttk.Label(search_frame, text="Наберите код:").grid(row=0, column=0, sticky="w", padx=(0, 5))
self.search_entry = ctk.CTkEntry(search_frame)
self.search_entry.grid(row=0, column=1, sticky="ew", padx=(0, 5))
find_btn = ctk.CTkButton(search_frame, text="Искать", fg_color="#3B82F6", command=self._search_by_code)
find_btn.grid(row=0, column=2, sticky="e")
def update_project_dependent_lists(self, forced_project_code=None):
proj_code = forced_project_code or getattr(self.window_ref, 'current_selected_project_code', "")
if not proj_code:
placeholder_groups = ["Все группы"]
placeholder_inds = ["Все показатели"]
self.cmb_group.configure(values=placeholder_groups)
self.cmb_group.set(placeholder_groups[0])
self.cmb_indicator.configure(values=placeholder_inds)
self.cmb_indicator.set(placeholder_inds[0])
return
groups_raw, name_to_code_map = self.logic.load_groups_and_indicators()
safe_groups = []
if groups_raw is not None:
try:
if hasattr(groups_raw[0], 'name'):
safe_groups = sorted(list(set([item.name for item in groups_raw])))
elif isinstance(groups_raw[0], tuple) and len(groups_raw[0]) > 1:
safe_groups = sorted(list(set([item[1] for item in groups_raw])))
elif isinstance(groups_raw[0], str):
safe_groups = sorted(list(set(groups_raw)))
except Exception:
pass
final_group_list = ["Все группы"] + safe_groups if safe_groups else ["Все группы"]
self.group_names_list = final_group_list
self.group_names_to_codes = {"": ""}
if isinstance(name_to_code_map, dict):
self.group_names_to_codes.update(name_to_code_map)
self.cmb_group.configure(values=final_group_list)
self.cmb_group.set(final_group_list[0])
all_indicators = []
seen_names = set()
for cached_list in self.indicator_cache.values():
for item in cached_list:
name = item[0]
if name not in seen_names:
all_indicators.append(item)
seen_names.add(name)
all_indicators.sort(key=lambda x: x[0])
ind_names = ["Все показатели"] + [item[0] for item in all_indicators]
self.indicator_names_list = ind_names
self.cmb_indicator.configure(values=ind_names)
self.cmb_indicator.set(ind_names[0])
self.after_idle(lambda code=proj_code: (
self.window_ref._update_control_buttons_state(True),
self._refresh_event_table(code)
))
def _on_group_change(self, event=None):
group_name = self.cmb_group.get()
if group_name == "Все группы":
self.cmb_indicator.set("")
self.indicator_names_list = ["Все показатели"]
self.cmb_indicator.configure(values=["Все показатели"])
self._update_indicator_combobox(group_name)
def _update_indicator_combobox(self, group_name: str):
self.cmb_indicator.set("")
if not group_name or group_name == "Все группы":
placeholder_list = ["Все показатели"]
self.indicator_names_list = placeholder_list
self.cmb_indicator.configure(values=placeholder_list)
self.cmb_indicator.set("Все показатели")
return
group_code = self.group_names_to_codes.get(group_name)
if not group_code:
self.cmb_indicator.configure(values=["Все показатели"])
self.cmb_indicator.set("Все показатели")
return
indicators = self.logic.load_indicators_for_group(group_code)
ind_names = ["Все показатели"] + [item[0] for item in indicators]
self.indicator_names_list = ind_names
self.cmb_indicator.configure(values=ind_names)
self.indicator_cache[group_code] = indicators
if len(ind_names) > 1:
self.cmb_indicator.set(ind_names[1])
else:
self.cmb_indicator.set("Все показатели")
def _collect_current_selection(self) -> dict:
proj_code = self.window_ref.current_selected_project_code
group_name = self.cmb_group.get()
group_code = self.group_names_to_codes.get(group_name, "") if group_name else ""
indicator_name = self.cmb_indicator.get()
indicator_code = ""
if indicator_name and indicator_name != "Все показатели" and group_code:
indicators_list = self.indicator_cache.get(group_code, [])
try:
idx = [item[0] for item in indicators_list].index(indicator_name)
indicator_code = f"{group_code}_{idx + 1}"
except ValueError:
pass
return {
"project_code": proj_code,
"group_name": group_name,
"group_code": group_code,
"indicator_name": indicator_name,
"indicator_code": indicator_code
}
def _add_event(self):
data = self._collect_current_selection()
if not all([data['project_code'], data['group_code'], data['indicator_code']]):
msg_parts = []
if not data['project_code']: msg_parts.append("Проект")
if not data['group_code']: msg_parts.append("Группа")
if not data['indicator_code']: msg_parts.append("Показатель")
messagebox.showwarning("Ошибка", f"Заполнены не все обязательные поля.\nОтсутствуют: {', '.join(msg_parts)}")
return
text = self.txt_event.get("1.0", "end").strip()
if not text:
messagebox.showwarning("Ошибка", "Поле 'Содержание события' пустое.")
return
date_str = self.date_entry.get()
sign = self.dir_var.get()
success, msg = self.logic.add_event_to_db(data, text, date_str, sign)
messagebox.showinfo("Результат", msg)
if success:
self.txt_event.delete("1.0", "end")
proj_code = self.window_ref.current_selected_project_code
self.cmb_group.set("Все группы")
self.cmb_indicator.set("Все показатели")
self._refresh_event_table(proj_code)
def _refresh_event_table(self, project_code: str = None):
# Очистка таблицы
for row in self.tree.get_children():
self.tree.delete(row)
if not project_code:
return
db_path = os.path.join(self.logic.project_dbs_folder, f"{project_code}.db")
if not os.path.exists(db_path):
return
try:
conn = sqlite3.connect(db_path)
cur = conn.cursor()
query = "SELECT event_code, event_data, event_sign, event_text_content FROM project_data"
params = ()
conditions = []
current_group = self.cmb_group.get()
current_indicator = self.cmb_indicator.get()
if current_group and current_group != "Все группы":
group_code = self.group_names_to_codes.get(current_group, "")
if group_code:
conditions.append("group_code = ?")
params += (group_code,)
if current_indicator and current_indicator != "Все показатели":
conditions.append("indicator_name = ?")
params += (current_indicator,)
if conditions:
query += " WHERE " + " AND ".join(conditions)
query += " ORDER BY id DESC LIMIT 50"
print(f"[SQL EXEC] Query: {query} | Params: {params}")
cur.execute(query, params)
rows = cur.fetchall()
conn.close()
print(f"[TABLE DEBUG] Fetched {len(rows)} items from disk.")
# Предварительная очистка выделения перед заполнением новой таблицей
self.tree.selection_remove(self.tree.selection())
# Вставка строк с привязкой клика к полю txt_event
for idx, row in enumerate(rows, start=1):
sign_char = "+" if row[2] == 1 else "-"
full_text = row[3]
iid = f"row_{idx}"
self.tree.insert("", "end", iid=iid, values=(idx, row[0], row[1], sign_char, full_text))
except Exception as e:
print(f"[CRITICAL TABLE FAIL]: {e}")
def _on_tree_select(self, event):
"""
Срабатывает ПРИ ЛЮБОМ ВЫБОРЕ СТРОКИ.
Заполняет форму данными выбранной строки.
"""
selected = self.tree.selection()
if not selected:
return
item_id = selected[0]
vals = self.tree.item(item_id)["values"]
if len(vals) < 5:
return
text_content = vals[4]
# Блокируем модифицированные события, чтобы избежать зацикливания
self.txt_event.unbind("<<Modified>>")
# Очищаем и заполняем поле текстом из таблицы
self.txt_event.delete("1.0", "end")
self.txt_event.insert("1.0", text_content)
# Заполняем дату и знак (+/-)
try:
date_obj = datetime.datetime.strptime(str(vals[2]), '%Y-%m-%d')
self.date_entry.set_date(date_obj)
except Exception:
pass
sign_val = int(vals[3].replace('+', '1').replace('-', '-1'))
self.dir_var.set(sign_val)
# Устанавливаем режим редактирования для этой строки
self.editing_event_code = vals[1]
# Включаем визуальную индикацию (желтый фон удален согласно ТЗ по очистке NativeToolTip)
self._set_edit_mode_visuals(True)
# Возвращаем отслеживание изменений текста
self.after(100, lambda: self.txt_event.bind("<<Modified>>", self._on_text_modified))
def _set_edit_mode_visuals(self, is_active: bool):
"""Меняет цвет фона поля текста и состояние кнопок"""
color = "#FFF9C4" if is_active else "#F0F0F0" # Желтый или стандартный
self.txt_event.configure(background=color)
# Кнопки согласно ТЗ: Редактировать блокируется, Сохранить активна
self.btn_edit.configure(state="disabled")
self.btn_save.configure(state="normal")
# Разблокируем поля даты и знака для изменения
self.date_entry.config(state="normal")
def _get_text_by_iid(self, item_id: str) -> str:
vals = self.tree.item(item_id)["values"]
if len(vals) > 4:
return vals[4]
return ""
def _enter_edit_mode(self):
sel = self.tree.selection()
if not sel:
messagebox.showwarning("Ошибка", "Выберите событие в таблице."); return
self._enter_edit_mode_by_iid(sel[0])
def _enter_edit_mode_by_iid(self, item_id: str):
vals = self.tree.item(item_id)["values"]
if len(vals) < 5:
return
self.editing_event_code = vals[1]
# Заполняем поля формы данными выбранной строки
self.txt_event.delete("1.0", "end")
self.txt_event.insert("1.0", vals[4])
# Дата события
try:
date_obj = datetime.datetime.strptime(str(vals[2]), '%Y-%m-%d')
self.date_entry.set_date(date_obj)
except Exception:
pass
# Направленность (+/-)
sign_val = int(vals[3].replace('+', '1').replace('-', '-1'))
self.dir_var.set(sign_val)
# === РЕЖИМ РЕДАКТИРОВАНИЯ (КНОПКИ И ПОЛЯ) ===
# Блокируем неизменяемые фильтры
self.btn_add.configure(state="disabled")
self.btn_del.configure(state="disabled")
self.cmb_group.configure(state="disabled")
self.cmb_indicator.configure(state="disabled")
# Кнопка Редактировать становится серой (заблокирована)
self.btn_edit.configure(state="disabled")
# Кнопка Сохранить остается активной (оранжевой), как вы просили НЕ скрывать её
# Если она была disabled - включаем
self.btn_save.configure(state="normal")
# Разблокируем поля ввода для изменений (ТЗ п.3)
self.txt_event.configure(state="normal")
self.date_entry.config(state="normal")
# Радиокнопки dir_var обычно доступны всегда, если frame активен
def _save_edited_event(self):
if not getattr(self, 'editing_event_code', None):
return
new_text = self.txt_event.get("1.0", "end").strip()
if not new_text:
# Используем тот же messagebox, который мы только что импортировали
messagebox.showwarning("Ошибка", "Текст не может быть пустым.")
return
try:
date_str = self.date_entry.get_date().strftime('%Y-%m-%d')
except Exception:
date_str = datetime.datetime.now().strftime('%Y-%m-%d')
sign_val = self.dir_var.get()
db_path = os.path.join(self.logic.project_dbs_folder, f"{self._collect_current_selection()['project_code']}.db")
try:
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute(
'''UPDATE project_data
SET event_text_content = ?, event_data = ?, event_sign = ?
WHERE event_code = ?''',
(new_text, date_str, sign_val, self.editing_event_code)
)
conn.commit()
conn.close()
# Если дошли до сюда без ошибок — показываем успех
messagebox.showinfo("Готово", "Изменения сохранены.")
self._reset_controls()
proj_code = self._collect_current_selection()['project_code']
self._refresh_event_table(proj_code)
except Exception as e:
# Если упало подключение к БД или SQL-запрос — показываем ошибку
messagebox.showerror("Ошибка записи", f"Не удалось сохранить изменения: {e}")
def _delete_selected_event(self):
sel = self.tree.selection()
if not sel: return
if not messagebox.askyesno("Подтверждение", "Удалить выбранное событие?"): return
event_code = self.tree.item(sel[0])["values"][1]
db_path = os.path.join(self.logic.project_dbs_folder, f"{self._collect_current_selection()['project_code']}.db")
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute("DELETE FROM project_data WHERE event_code = ?", (event_code,))
conn.commit()
conn.close()
self._refresh_event_table(self._collect_current_selection()['project_code'])
def _reset_controls(self):
self.editing_event_code = None
# Возвращаем базовые состояния кнопок
self.btn_add.configure(state="normal")
self.btn_del.configure(state="normal")
self.cmb_group.configure(state="readonly")
self.cmb_indicator.configure(state="readonly")
# Кнопка Редактировать снова зеленая и активная
self.btn_edit.configure(state="normal")
# Кнопка Сохранить гаснет (становится недоступной)
self.btn_save.configure(state="disabled")
# Очищаем поле текста
self.txt_event.delete("1.0", "end")
def _paste_from_clipboard(self):
try:
txt = self.clipboard_get()
self.txt_event.insert(tk.INSERT, txt)
except tk.TclError:
pass
def _bind_global_events(self):
self.txt_event.bind("<Button-3>", lambda e: (self._paste_from_clipboard(), "break"))
def _search_by_code(self):
target = self.search_entry.get().strip()
if not target:
messagebox.showwarning("Поиск", "Введите код."); return
items = self.tree.get_children()
found = None
for item in items:
vals = self.tree.item(item)["values"]
if len(vals) > 1 and vals[1] == target:
found = item
break
if found:
self.tree.selection_set(found)
self.tree.see(found)
self.tree.focus(found)
else:
messagebox.showinfo("Поиск", "Событие с таким кодом не найдено.")
# =========================================
# =========================================
# 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 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 = None
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)
y_cord = int((screen_h := self.winfo_screenheight() - initial_height) / 2) # Центрируем Y тоже
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)
# Больше НЕ используем pack_propagate(False) здесь,
# так как project_chart_ui использует grid и рассчитывает свои размеры.
build_charts_interface(charts_container, self)
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}")