Загрузка данных
Промпт: Завершение реализации окна аккордеона под заголовком "Открыть для ввода событий".
Цель: Полностью отладить функционал второго заголовка аккордеона ("Открыть для ввода событий").
Задачи по отладке окна
*1* В выпадающем списке не правильно настроен выбор Проекта. Поясняю как задумано в приложении.
При создании проекта пользователь делает три записи:
1 - Название проекта - это большое полное название. Оно нужно только во время печати документа.
2 - Шифр - это короткое сокращенное название проекта, которое должно отображаться в выпадающем списке.
3 - Код - это айди проекта и это же название файла базы данных проекта. По коду должен проект искаться, а в выпадающем списке должен отображаться его Шифр.
В настоящее время не понятно по каким параметрам все это работает. Сейчас в списке отображаются большие названия проектов вместо шифров.
Имеется путаница с id, Name, Code, Cipher.
===========================================================================
*2* При клике на событии в таблице данная строчка должна выделяться и всплывать полное название события. Пока мышка на строчке всплывающее событие находится на экране. При смещении мышки через секунду событие закрывается.
===========================================================================
*3* Редактирование событий. В данный момент в окне 4 основных кнопки. Необходимо сделать чтобы в окне было 3 основных кнопки - одна, в зависимости от режима должна быть скрыта. Стандартный режим: в окне кнопки: "Добавить", "Редактировать", "Удалить". При нажатии на "Удалить" кнопка "Редактировать" прячется, а на ее месте появляется кнопка "Сохранить". Все поля переходят в режим редактирования и могут быть изменены. После нажатия на кнопку "Сохранить" - происходит сохранение изменений кнопка "Сохранить" прячется и опять появляется кнопка "Редактировать" - окно выходит из режима редактирования. Изменять пользователь может только текст события, дату, направленность и удаление, остальные поля должны быть заблокированы для редактирования.
===========================================================================
*4* Поиск. Фактически эта опция предназначена для поиска события для дальнейшего редактирования или удаления. Чтобы найти событие пользователь должен набрать код события и нажать на кнопку "Искать". Найденное событие должно быть в видимой части экрана в таблице и код залит желтым цветом.
o Список представленных файлов
Чтобы провести отладку, даю актуальное содержимое следующих файлов целиком:
1. Интерфейс формы событий: modules/project/project_event_ui.py
2. Бизнес-логика чтения/записи: modules/project/project_event_logic.py
3. Главный координатор (аккордеон): modules/project/project_window.py
4. Менеджер БД проектов: modules/data/project_database.py
5. Единая база справочников: modules/data/database_module.py
6. Точка входа (проверка инициализации мастер-данных): gui/main_interface.py
7. Функция путей: dukora/modules/utils/resources.py
Я, вайб кодер - ты, профессиональный программист. Ты делаешь - я вставляю, заменяю.
Кусками не работаем - только целыми фрагментами - метод на метод, файл на файл, от комментария до комментария, от комментария до конца и т.д. - по такому принципу.
Делаем отладку в той последовательности, как в ТЗ. Начинаем с добавления строчек в выпадающие списки.
#==============================================================================
#======================================================
# dukora/modules/project/project_event_ui.py
#======================================================
import customtkinter as ctk
from tkinter import ttk, messagebox
import datetime
import tkinter as tk
import os
import sqlite3
from .project_event_logic import ProjectEventLogic
from tkcalendar import DateEntry
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
# FINAL FIX: Храним только чистые списки строк для надежной работы ComboBox
self.group_names_list = []
self.indicator_names_list = []
self.group_names_to_codes = {}
self.indicator_cache = {}
self._setup_widgets()
self._bind_global_events()
# Загружаем данные списков Группа/Показатель
self.update_project_dependent_lists()
# === ГАРАНТИРОВАННОЕ ОБНОВЛЕНИЕ ТАБЛИЦЫ ПОСЛЕ СОЗДАНИЯ ИНТЕРФЕЙСА ===
# Даем Tkinter долю секунды, чтобы комбобокс проекта получил текст от главного окна,
# и затем читаем его напрямую для заполнения таблицы.
self.after(50, self._initial_load)
def _initial_load(self):
"""Метод-диспетчер: берет текущий проект из родителя и загружает таблицу."""
try:
if not hasattr(self, 'window_ref') or not self.window_ref:
return
proj_text = self.window_ref.project_selector.get()
print(f"[INIT LOAD] Current project in selector is: '{proj_text}'")
if proj_text and proj_text != "Проектов нет":
# Вызываем наш рабочий метод загрузки таблицы
self._refresh_event_table()
else:
print("[INIT LOAD] Project not ready yet.")
except Exception as e:
print(f"[INIT LOAD ERROR]: {e}")
def _setup_widgets(self):
main_form = ctk.CTkFrame(self, fg_color="transparent")
main_form.pack(fill="both", expand=True, padx=10, pady=10)
main_form.grid_columnconfigure(1, weight=1)
# --- Блок Выбрать группу ---
lbl_group = ctk.CTkLabel(main_form, text="Выбрать группу:", anchor="w")
lbl_group.grid(row=0, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
self.cmb_group = ctk.CTkComboBox(
main_form,
state="readonly",
width=250,
command=self._on_group_change
)
self.cmb_group.grid(row=0, column=1, sticky="ew", pady=(0, 5))
# --- Блок Выбрать показатель ---
lbl_indicator = ctk.CTkLabel(main_form, text="Выбрать показатель:", anchor="w")
lbl_indicator.grid(row=1, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
# ФИКС: Обновляем таблицу ПРИ ВЫБОРЕ показателя (клик по пункту)
self.cmb_indicator = ctk.CTkComboBox(
main_form,
state="readonly",
width=250,
command=lambda choice: self._refresh_event_table(self.window_ref.current_selected_project_code)
)
self.cmb_indicator.grid(row=1, column=1, sticky="ew", pady=(0, 10))
# --- Поле Содержание события (в рамке) ---
lbl_event = ctk.CTkLabel(main_form, text="Содержание события:", anchor="w")
lbl_event.grid(row=2, column=0, sticky="nw", pady=(0, 5), padx=(0, 10))
event_container = ctk.CTkFrame(main_form, fg_color="#F0F0F0", border_width=1, border_color="#A9A9A9")
event_container.grid(row=2, column=1, sticky="ew", pady=(0, 5), ipadx=2, ipady=2)
self.txt_event = ctk.CTkTextbox(event_container, height=80, wrap="word", fg_color="white")
self.txt_event.pack(fill="both", expand=True, padx=2, pady=2)
paste_btn = ctk.CTkButton(main_form, text="Вставить", fg_color="#2B2B2B", hover_color="#3C3C3C", command=self._paste_from_clipboard)
paste_btn.grid(row=3, column=1, sticky="e", pady=(0, 10), ipadx=10)
# === ОБНОВЛЕННЫЙ БЛОК ДАТЫ И НАПРАВЛЕННОСТИ ===
meta_frame = ctk.CTkFrame(main_form, fg_color="transparent")
meta_frame.grid(row=4, column=0, columnspan=2, sticky="ew", pady=(0, 10))
meta_frame.grid_columnconfigure(1, weight=1)
ttk.Label(meta_frame, text="Дата события:").grid(row=0, column=0, sticky="w", padx=(0, 10))
self.date_entry = DateEntry(
master=meta_frame,
width=12,
background='darkblue',
foreground='white',
bordercolor='gray',
headersbackground='darkgray',
normalbackground='white',
normalforeground='black',
weekendbackground='lightblue',
weekendforeground='red',
date_pattern='dd.MM.yyyy'
)
today = datetime.datetime.now()
self.date_entry.set_date(today)
self.date_entry.grid(row=0, column=1, sticky="w")
ttk.Label(meta_frame, text="Направленность:").grid(row=0, column=2, sticky="w", padx=(20, 10))
self.dir_var = tk.IntVar(value=1)
rb_plus = ctk.CTkRadioButton(meta_frame, text="(+)", variable=self.dir_var, value=1)
rb_plus.grid(row=0, column=3, sticky="w")
rb_minus = ctk.CTkRadioButton(meta_frame, text="(-)", variable=self.dir_var, value=-1)
rb_minus.grid(row=0, column=4, sticky="w")
# --- Кнопки управления событием ---
ctrl_frame = ctk.CTkFrame(main_form, fg_color="transparent")
ctrl_frame.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(0, 10))
for i in range(4): ctrl_frame.grid_columnconfigure(i, weight=1)
self.btn_add = ctk.CTkButton(ctrl_frame, text="Добавить", fg_color="#3B82F6", command=self._add_event)
self.btn_add.grid(row=0, column=0, sticky="ew", padx=2)
self.btn_edit = ctk.CTkButton(ctrl_frame, text="Редактировать", fg_color="#10B981", command=self._enter_edit_mode)
self.btn_edit.grid(row=0, column=1, sticky="ew", padx=2)
self.btn_save = ctk.CTkButton(ctrl_frame, text="Сохранить", state="disabled", fg_color=self.EDIT_MODE_COLOR, command=self._save_edited_event)
self.btn_save.grid(row=0, column=2, sticky="ew", padx=2)
self.btn_del = ctk.CTkButton(ctrl_frame, text="Удалить", fg_color="#EF4444", command=self._delete_selected_event)
self.btn_del.grid(row=0, column=3, sticky="ew", padx=2)
# --- Таблица событий ---
tree_container = ctk.CTkFrame(main_form, fg_color="transparent")
tree_container.grid(row=6, column=0, columnspan=2, sticky="nsew", pady=(10, 0))
tree_container.grid_rowconfigure(0, weight=1); tree_container.grid_columnconfigure(0, weight=1)
style = ttk.Style()
theme_name = "clam" if "dark" in ctk.get_appearance_mode() else "default"
style.theme_use(theme_name)
# Порядок: (№п/п, Код, Дата, +/-, Событие)
columns = ("num", "code", "date", "sign", "event")
self.tree = ttk.Treeview(tree_container, columns=columns, show="headings", selectmode="browse", height=8)
# Заголовки
self.tree.heading("num", text="№п/п"); self.tree.column("num", width=40, anchor="center", stretch=False)
self.tree.heading("code", text="Код"); self.tree.column("code", width=100, anchor="center", stretch=False)
self.tree.heading("date", text="Дата"); self.tree.column("date", width=110, anchor="center", stretch=False)
self.tree.heading("sign", text="+/-"); self.tree.column("sign", width=50, anchor="center", stretch=False)
self.tree.heading("event", text="Событие"); self.tree.column("event", minwidth=200, stretch=True)
self.tree.pack(side="left", fill="both", expand=True)
scrollbar = ctk.CTkScrollbar(tree_container, orientation="vertical", command=self.tree.yview)
scrollbar.pack(side="right", fill="y")
self.tree.configure(yscrollcommand=scrollbar.set)
# --- Поиск по коду ---
search_frame = ctk.CTkFrame(main_form, fg_color="transparent")
search_frame.grid(row=7, column=0, columnspan=2, sticky="ew", pady=(10, 0))
search_frame.grid_columnconfigure(1, weight=1)
ttk.Label(search_frame, text="Наберите код:").grid(row=0, column=0, sticky="w", padx=(0, 5))
self.search_entry = ctk.CTkEntry(search_frame)
self.search_entry.grid(row=0, column=1, sticky="ew", padx=(0, 5))
find_btn = ctk.CTkButton(search_frame, text="Искать", fg_color="#3B82F6", command=self._search_by_code)
find_btn.grid(row=0, column=2, sticky="e")
def update_project_dependent_lists(self):
"""Глобальное обновление списков Группа/Показатель."""
try:
if not hasattr(self, 'window_ref') or not self.window_ref:
print("[UI ERROR] window_ref is missing.")
return
selected_project_name = self.window_ref.project_selector.get()
print(f"[UI SYNC] Updating for project: {selected_project_name}")
groups_raw, name_to_code_map = self.logic.load_groups_and_indicators()
if not groups_raw:
print("[UI WARNING] Groups list is empty from logic.")
group_names = ["Все группы"]
else:
# Сортируем реальные названия групп и ставим заглушку в начало
sorted_groups = sorted([str(g).strip() for g in groups_raw])
group_names = ["Все группы"] + sorted_groups
self.group_names_list = group_names
self.group_names_to_codes = {"Все группы": ""}
if name_to_code_map:
self.group_names_to_codes.update(name_to_code_map)
self.cmb_group.configure(values=group_names)
# ПО УМОЛЧАНИЮ ВЫБИРАЕМ ЗАГЛУШКИ (индекс 0)
self.cmb_group.set(group_names[0])
# Собираем ВСЕ показатели изо всех кэшированных групп для режима "Все показатели"
all_indicators = []
seen_names = set()
for code in self.indicator_cache.values():
cached_list = self.indicator_cache.get(code, [])
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])
except Exception as e:
print(f"[CRITICAL UI LOAD FAIL]: {e}")
self.cmb_group.configure(values=["Ошибка"])
self.cmb_group.set("Ошибка")
self.cmb_indicator.configure(values=[])
self.cmb_indicator.set("")
def _on_group_change(self, event=None):
group_name = self.cmb_group.get()
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
try:
self.cmb_indicator.configure(values=placeholder_list)
self.cmb_indicator.set("Все показатели")
try:
if self.cmb_indicator.dropdown is not None and self.cmb_indicator.dropdown.winfo_children():
listbox = self.cmb_indicator.dropdown.winfo_children()[0]
if isinstance(listbox, tk.Listbox):
listbox.configure(height=8)
except Exception:
pass
except Exception as ui_err:
print(f"[CRITICAL UI CONFIG FAIL]: {ui_err}")
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
try:
if self.cmb_indicator.dropdown is not None and self.cmb_indicator.dropdown.winfo_children():
listbox = self.cmb_indicator.dropdown.winfo_children()[0]
if isinstance(listbox, tk.Listbox):
listbox.configure(height=8)
except Exception:
pass
# ФИКС: Если есть реальные показатели, выбираем ПЕРВЫЙ из них (индекс 1)
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 group_code and indicator_name != "Все показатели":
try:
clean_name = indicator_name[0] if isinstance(indicator_name, tuple) else indicator_name
if clean_name in self.indicator_names_list:
count = self.indicator_names_list.index(clean_name)
indicator_code = f"{group_code}_{count}"
except Exception as gen_err:
print(f"[CODE GEN ERROR]: {gen_err}")
return {
"project_name": "",
"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._collect_current_selection()['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 hasattr(self, 'window_ref') or not self.window_ref:
print("[TABLE ERROR] window_ref is missing.")
return
try:
current_text = self.window_ref.project_selector.get()
if not current_text or current_text == "Проектов нет":
return
proj_code = current_text
db_path = os.path.join(self.logic.project_dbs_folder, f"{proj_code}.db")
if not os.path.exists(db_path):
return
conn = sqlite3.connect(db_path)
cur = conn.cursor()
current_group = self.cmb_group.get()
current_indicator = self.cmb_indicator.get()
query = "SELECT event_code, event_data, event_sign, event_text_content FROM project_data"
params = ()
conditions = []
if current_group and current_group != "Все группы":
conditions.append("group_name = ?")
params += (current_group,)
if current_indicator and current_indicator != "Все показатели":
conditions.append("indicator_name = ?")
params += (current_indicator,)
if conditions:
query += " WHERE " + " AND ".join(conditions)
# ВАЖНО: Последние события всегда сверху (id DESC)
query += " ORDER BY id DESC LIMIT 50"
cur.execute(query, params)
rows = cur.fetchall()
conn.close()
last_iid = None
# ФИКС: Нумеруем с 1 до len(rows) по порядку полученного списка
for idx, row in enumerate(rows, start=1):
sign_char = "+" if row[2] == 1 else "-"
iid = self.tree.insert(
"",
"end",
values=(idx, row[0], row[1], sign_char, row[3])
)
last_iid = iid
if last_iid:
self.tree.see(last_iid)
except Exception as e:
print(f"[CRITICAL TABLE FAIL]: {e}")
def _paste_from_clipboard(self):
try:
txt = self.clipboard_get()
self.txt_event.insert(tk.INSERT, txt)
except tk.TclError:
pass
def _enter_edit_mode(self):
sel = self.tree.selection()
if not sel:
messagebox.showwarning("Ошибка", "Выберите событие в таблице."); return
vals = self.tree.item(sel[0])["values"]
self.editing_event_code = vals[1]
full_text = vals[3]
self.txt_event.delete("1.0", "end")
self.txt_event.insert("1.0", full_text)
self.btn_add.configure(state="disabled")
self.btn_edit.configure(state="disabled")
self.btn_save.configure(state="normal")
self.btn_del.configure(state="disabled")
def _save_edited_event(self):
if not getattr(self, 'editing_event_code', None):
return
new_text = self.txt_event.get("1.0", "end").strip()
if not new_text:
messagebox.showwarning("Ошибка", "Текст не может быть пустым."); return
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 = ? WHERE event_code = ?",
(new_text, 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:
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"][0]
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 _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:
if self.tree.item(item)["values"][0] == target:
found = item
break
if found:
self.tree.selection_set(found)
self.tree.see(found)
self.tree.focus(found)
else:
messagebox.showinfo("Поиск", "Событие с таким кодом не найдено.")
def _reset_controls(self):
self.btn_add.configure(state="normal")
self.btn_edit.configure(state="normal")
self.btn_save.configure(state="disabled")
self.btn_del.configure(state="normal")
self.txt_event.delete("1.0", "end")
def _bind_global_events(self):
self.txt_event.bind("<Button-3>", lambda e: (self._paste_from_clipboard(), "break"))
#======================================================
#======================================================
# dukora/modules/project/project_event_logic.py
#======================================================
import os
import sqlite3
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 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()
if is_new_db:
cursor.execute('''
CREATE TABLE IF NOT EXISTS project_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_code TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
group_name TEXT,
group_code TEXT,
indicator_name TEXT,
indicator_code TEXT,
event_data DATE,
event_sign INTEGER,
event_code TEXT,
event_text_content TEXT
)
''')
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}"
try:
cursor.execute('''
INSERT INTO project_data (
project_code, group_name, group_code, indicator_name, indicator_code,
event_data, event_sign, event_code, event_text_content
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
selected_data['project_code'],
selected_data['group_name'],
selected_data['group_code'],
selected_data['indicator_name'],
selected_data['indicator_code'],
date_str,
direction,
event_code,
event_text
))
conn.commit()
return True, f"Данные добавлены. Код события: {event_code}"
except Exception as e:
conn.rollback()
return False, f"Ошибка записи в БД: {e}"
finally:
conn.close()
#======================================================
#=====================================================
# modules/project/project_window.py
#=====================================================
import customtkinter as ctk
from tkinter import ttk, messagebox
from modules.project.project_event_logic import ProjectEventLogic
from typing import List
import datetime
import os
# Попытка импорта внешних библиотек (оставляем ваши try/except)
try:
from tkcalendar import DateEntry
except ImportError:
DateEntry = None
try:
import docx
except ImportError:
docx = None
from modules.project.project_event_ui import ProjectEventUI
from modules.project.project_create_logic import ProjectCreateLogic
class ProjectWindow(ctk.CTkToplevel):
def __init__(self, master):
super().__init__(master)
self.parent_main = master
self.title("Журнал событий")
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)
taskbar_offset_top = 40
y_cord = taskbar_offset_top
self.geometry(f"{window_width}x{initial_height}+{x_cord}+{y_cord}")
self.resizable(True, 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)
self._build_project_selector(main_container)
accordion_frame = ctk.CTkFrame(main_container)
accordion_frame.pack(fill="both", expand=True, pady=(10, 0))
self.accordion_data = {}
self.currently_open_button = None
# === ЗАГОЛОВКИ АККОРДЕОНА (ТЕПЕРЬ ИХ 4) ===
create_item = self._create_accordion_header(accordion_frame, "Создать проект")
events_item = self._create_accordion_header(accordion_frame, "Открыть для ввода событий")
charts_item = self._create_accordion_header(accordion_frame, "Показать графики")
print_item = self._create_accordion_header(accordion_frame, "Настройка печати")
# Наполняем секции контентом (ваши методы наполнения остаются прежними)
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"])
# Привязка команд переключения
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.toggle_section(create_item["button"], create_item["content"])
# === ЗАМЕНА МЕТОДА: КНОПКИ ТЕПЕРЬ ОДНА ("ЗАКРЫТЬ") ===
button_frame = ctk.CTkFrame(main_container)
button_frame.pack(fill="x", pady=(10, 0))
close_btn = ctk.CTkButton(button_frame, text="Закрыть", width=120, command=self.destroy)
close_btn.pack(side="right", padx=10, pady=10)
self.protocol("WM_DELETE_WINDOW", self.on_closing)
# === НОВЫЙ МЕТОД ДЛЯ ШАПКИ НАД СЕЛЕКТОРОМ ПРОЕКТА ===
def _build_title_bar(self, container):
title_lbl = ctk.CTkLabel(container, text="Наполнение проекта", font=ctk.CTkFont(size=20, weight="bold"))
# Убран anchor="w". По умолчанию CTkLabel центрирует текст.
title_lbl.pack(pady=(0, 15))
def _build_project_selector(self, container):
"""
Создает блок выбора проекта над аккордеоном без использования placeholder_text.
Логика "Проектов нет" реализована через set() текстового поля.
"""
frame = ctk.CTkFrame(container)
frame.pack(pady=(0, 10))
label_project = ctk.CTkLabel(frame, text="Выбрать проект:", anchor="w")
label_project.pack(side="left", padx=(0, 10))
# Передаем пустой список значений
self.project_selector = ctk.CTkComboBox(
frame,
values=[],
state="readonly",
width=250
)
# Так как списка нет, пишем текст напрямую в поле ввода комбобокса
self.project_selector.set("Проектов нет")
self.project_selector.pack(side="left")
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 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()
button.configure(fg_color="#34495e")
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")
button.configure(fg_color="#2c3e50")
self.currently_open_button = button
new_geo = self._calculate_window_geometry(collapsed=False)
self.geometry(new_geo)
# Вызываем ПОСЛЕ установки geometry, чтобы считать актуальные размеры
self.after(50, self._enforce_onscreen_position)
# === ОБНОВЛЕННЫЙ РАСЧЕТ ГЕОМЕТРИИ (УЧИТЫВАЕТ 4 СЕКЦИИ И НИЖНЮЮ ПАНЕЛЬ) ===
def _calculate_window_geometry(self, collapsed=False):
current_width = self.winfo_width()
if current_width == 1:
current_width = 650
header_height = 50 * 4 + 30 # Высота шапок всех 4 секций
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)
# Увеличенный порог видимости всех заголовков (расчет выше показал ~453px)
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 _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)
# --- Поля ввода ---
ttk.Label(form_frame, text="Название:").grid(row=0, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
name_entry = ctk.CTkEntry(form_frame, placeholder_text="Введите полное название...")
name_entry.grid(row=0, column=1, sticky="ew", pady=(0, 5))
ttk.Label(form_frame, text="Шифр:").grid(row=1, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
short_code_entry = ctk.CTkEntry(form_frame, placeholder_text="Краткое имя (латиница)...")
short_code_entry.grid(row=1, column=1, sticky="ew", pady=(0, 5))
ttk.Label(form_frame, text="Код (ID):").grid(row=2, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
code_entry = ctk.CTkEntry(form_frame, placeholder_text="Уникальный идентификатор...")
code_entry.grid(row=2, column=1, sticky="ew", pady=(0, 5))
ttk.Label(form_frame, text="Дата открытия:").grid(row=3, column=0, sticky="w", pady=(0, 5), padx=(0, 10))
date_entry = ctk.CTkEntry(form_frame)
today = datetime.datetime.now().strftime("%Y-%m-%d")
date_entry.insert(0, today)
date_entry.grid(row=3, column=1, sticky="ew", pady=(0, 5))
ttk.Label(form_frame, text="Ответственный:").grid(row=4, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
resp_entry = ctk.CTkEntry(form_frame, placeholder_text="ФИО ответственного лица...")
resp_entry.grid(row=4, column=1, sticky="ew", pady=(0, 10))
# --- Кнопки действий ---
action_frame = ctk.CTkFrame(form_frame, fg_color="transparent")
action_frame.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(5, 0))
action_frame.grid_columnconfigure((0, 1), weight=1) # Равномерное распределение кнопок
create_btn = ctk.CTkButton(
action_frame,
text="Создать проект",
fg_color="#3B82F6",
hover_color="#2563EB",
command=lambda n=name_entry, sc=short_code_entry, c=code_entry, d=date_entry, r=resp_entry: 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: [n.delete(0, "end"), sc.delete(0, "end"), c.delete(0, "end"), r.delete(0, "end")] # Теперь очистка полей сработает
)
)
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: [name_entry.delete(0, "end"), short_code_entry.delete(0, "end"), code_entry.delete(0, "end"), resp_entry.delete(0, "end")]
)
clear_btn.grid(row=0, column=1, sticky="ew", padx=(5, 0))
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()
# --- ПРИНУДИТЕЛЬНАЯ СИНХРОНИЗАЦИЯ ЧЕРЕЗ ОСНОВНОЙ МЕНЕДЖЕР ---
# 1. Закрываем текущее соединение manager_db, чтобы сбросить кэш транзакций
try:
self.manager_db.close()
except Exception:
pass
# 2. Читаем свежие данные ИЗ ОТКРЫТОЙ БАЗЫ ОКНА
fresh_tuples = self.manager_db.list_projects()
print(f"[DEBUG HARD SYNC] Прямое чтение list_projects(): {fresh_tuples}")
# Извлекаем только имена для ComboBox
fresh_names = [p[1] for p in fresh_tuples]
# 3. Радикальное обновление UI (полная очистка + пересоздание с выводом ШИФРА)
parent_widget = self.project_selector.master
# Удаляем старый экземпляр полностью
self.project_selector.destroy()
# Создаем абсолютно новый чистый ComboBox
self.project_selector = ctk.CTkComboBox(
master=parent_widget,
values=fresh_names,
state="readonly",
width=250
)
self.project_selector.pack(side="left")
# TASK: Выводим шифр (код) проекта вместо названия
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)
else:
self.project_selector.set("Проектов нет")
# 4. Обновляем Treeview внизу формы создания
try:
for row in self.tree_projects.get_children():
self.tree_projects.delete(row)
for proj in sorted(fresh_tuples, key=lambda x: x[0]):
self.tree_projects.insert("", "end", values=proj)
except Exception as tree_err:
print(f"[UI ERROR] Treeview refresh failed: {tree_err}")
# 5. Вкладка событий (если открыта)
current_title = ""
if self.currently_open_button:
current_title = self.currently_open_button.cget("text")
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)
# === КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ ===
# После того как виджет отрисовался (idle), принудительно читаем текущий проект
# из селектора и обновляем таблицу.
def delayed_refresh():
print("[WINDOW ACTION] Triggering initial table refresh after window open.")
current_proj = self.project_selector.get()
if current_proj and current_proj != "Проектов нет":
ui._refresh_event_table(current_proj)
self.after(100, delayed_refresh) # Ждем 100 мс, чтобы все значения установились
def _fill_charts_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 _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:
pass
for item in list(self.accordion_data.values()):
try:
content = item.get("content")
if content:
content.destroy()
except Exception:
pass
try:
self.destroy()
except Exception:
pass
#=====================================================
#====================================================
# dukora/modules/data/project_database.py
#====================================================
import os
import sqlite3
from pathlib import Path
from typing import Optional, List, Tuple
from types import SimpleNamespace
class ProjectDatabase:
"""
Управляет общей базой данных 'projects.db', хранящей метаданные о проектах.
Создает файл БД в директории db относительно корня проекта.
"""
def __init__(self):
# Используем утилиту получения корня проекта
from modules.utils.resources import get_project_root
self.base_dir = get_project_root() / "db"
self.base_dir.mkdir(parents=True, exist_ok=True)
self.db_path = self.base_dir / "projects.db"
self.connection: Optional[sqlite3.Connection] = None
def connect(self) -> sqlite3.Connection:
if self.connection is None:
try:
self.connection = sqlite3.connect(self.db_path)
self.connection.row_factory = sqlite3.Row
except sqlite3.Error as e:
raise RuntimeError(f"Ошибка подключения к {self.db_path}: {e}")
return self.connection
def close(self):
if self.connection:
self.connection.close()
self.connection = None
def create_tables(self):
conn = self.connect()
cursor = conn.cursor()
# ДОБАВИЛИ колонку cipher TEXT NOT NULL UNIQUE
cursor.execute('''
CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
code TEXT NOT NULL UNIQUE,
cipher TEXT NOT NULL UNIQUE,
description TEXT DEFAULT '',
date_opened TEXT DEFAULT '',
responsible TEXT DEFAULT ''
)
''')
conn.commit()
def add_project(self, name: str, code: str, cipher: str, description: str, date_opened: str, responsible: str) -> bool:
"""Добавляет запись о проекте в общий список."""
try:
with self.connect() as conn:
conn.execute(
"INSERT INTO projects (name, code, cipher, description, date_opened, responsible) VALUES (?, ?, ?, ?, ?, ?)",
(name, code, cipher, description, date_opened, responsible)
)
return True
except sqlite3.IntegrityError:
return False
except Exception:
return False
def list_projects(self) -> List[Tuple]:
"""Возвращает список кортежей (id, name, code, cipher)."""
with self.connect() as conn:
cursor = conn.execute("SELECT id, name, code, cipher FROM projects ORDER BY name")
return cursor.fetchall()
def list_projects_names(self) -> List[str]:
"""Возвращает плоский список только названий проектов для ComboBox."""
with self.connect() as conn:
cursor = conn.execute("SELECT name FROM projects ORDER BY name")
return [row[0] for row in cursor.fetchall()]
def get_all_groups(self):
"""Читает общую таблицу групп."""
try:
with self.connect() as conn:
cursor = conn.cursor()
# Проверяем наличие таблицы перед запросом
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='groups';")
if not cursor.fetchone():
return []
cursor.execute("SELECT id, code, name FROM groups ORDER BY name")
rows = cursor.fetchall()
# Возвращаем объекты SimpleNamespace для совместимости с вашим старым кодом UI
from types import SimpleNamespace
return [SimpleNamespace(id=r[0], code=r[1], name=r[2]) for r in rows]
except Exception as e:
print(f"[DB ERROR] Groups load failed: {e}")
return []
def get_indicators_by_group(self, group_code: str):
"""Читает показатели конкретной группы из общей базы."""
try:
with self.connect() as conn:
cursor = conn.cursor()
# Проверяем наличие таблицы индикаторов
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='indicators';")
if not cursor.fetchone():
return []
cursor.execute("SELECT id, code, name, group_code FROM indicators WHERE group_code = ? ORDER BY name", (group_code,))
rows = cursor.fetchall()
from types import SimpleNamespace
return [SimpleNamespace(id=r[0], code=r[1], name=r[2], group_code=r[3]) for r in rows]
except Exception as e:
print(f"[DB ERROR] Indicators load failed: {e}")
return []
#====================================================
#====================================================
# dukora/modules/data/database_module.py
#====================================================
# dukora/modules/data/database_module.py
import sys
import sqlite3
from pathlib import Path
try:
# Определяем корень проекта надежно:
# Если мы находимся внутри подпапок dukora, идем вверх до корня репозитория.
# __file__ дает путь к текущему .py файлу.
current_file_path = Path(__file__).resolve()
# Ищем папку 'dukora' в родителях
try:
dukora_parent = current_file_path.parents[[p.name for p in current_file_path.parents].index('dukora')]
except ValueError:
# Если по какой-то причине не нашли (запуск из другой директории), берем CWD
dukora_parent = Path.cwd()
BASE_DIR = dukora_parent
DB_FOLDER = BASE_DIR / "db"
ACTIVE_FLAG_PATH = DB_FOLDER / "active.flag"
DEFAULT_DB_PATH = DB_FOLDER / "database.db"
MASTERDATA_PATH = DB_FOLDER / "masterdata.db"
except Exception as e:
print(f"[FATAL] Не удалось вычислить путь к базе данных: {e}")
sys.exit(1)
def _determine_active_path():
try:
if ACTIVE_FLAG_PATH.exists():
with open(ACTIVE_FLAG_PATH, 'r', encoding='utf-8') as f:
name = f.read().strip()
if name == "masterdata":
return MASTERDATA_PATH
except Exception:
pass
return DEFAULT_DB_PATH
DB_PATH = _determine_active_path()
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session as SqlAlchemySession, declarative_base
engine = create_engine(f"sqlite:///{DB_PATH}", echo=False, connect_args={"check_same_thread": False})
Base = declarative_base()
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db_session = SessionLocal()
try:
yield db_session
finally:
db_session.close()
def get_db_connection():
active_path = _determine_active_path()
global DB_PATH
DB_PATH = active_path
# === КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ ===
# Сначала проверяем наличие папки и создаем её ДО попытки открыть БД
if not DB_FOLDER.exists():
print(f"[ACTION] Создаю папку для базы: {DB_FOLDER}")
try:
DB_FOLDER.mkdir(parents=True, exist_ok=True)
except PermissionError as pe:
print(f"[FATAL] Нет прав на создание папки '{DB_FOLDER}'. Запустите терминал от имени Администратора.")
raise SystemExit(1)
conn = sqlite3.connect(active_path)
conn.execute("PRAGMA foreign_keys = ON;")
return conn
def _create_tables(cursor):
cursor.execute('''
CREATE TABLE IF NOT EXISTS groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
code TEXT NOT NULL UNIQUE,
gain_group REAL DEFAULT 0.0
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS indicators (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
code TEXT NOT NULL UNIQUE,
group_code TEXT NOT NULL,
FOREIGN KEY (group_code) REFERENCES groups(code) ON DELETE CASCADE
)
''')
def _populate_test_data(conn):
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM groups")
count = cursor.fetchone()[0]
if count == 0:
print("[INFO] База данных пуста. Заполняю тестовыми данными...")
groups_data = [("Клубы", "KB", 1.5), ("Фанаты", "FAN", 2.0)]
cursor.executemany("INSERT INTO groups (name, code, gain_group) VALUES (?, ?, ?)", groups_data)
indicators_data = [
("ЦСКА", "KB-1", "KB"), ("Спартак", "KB-2", "KB"), ("Зенит", "KB-3", "KB"),
("Мужчины", "FAN-1", "FAN"), ("Женщины", "FAN-2", "FAN"), ("Дети", "FAN-3", "FAN")
]
cursor.executemany("INSERT INTO indicators (name, code, group_code) VALUES (?, ?, ?)", indicators_data)
conn.commit()
print("[SUCCESS] Тестовые данные успешно записаны.")
else:
print("[INFO] База данных уже содержит данные. Пропускаю инициализацию.")
def init_database():
print("[INIT] Проверка наличия рабочих баз данных...")
conn = None
try:
conn = get_db_connection()
cursor = conn.cursor()
_create_tables(cursor)
_populate_test_data(conn)
except sqlite3.Error as e:
print(f"[ERROR] Ошибка при инициализации DATABASE.DB: {e}")
if conn:
conn.rollback()
finally:
if conn:
conn.close()
master_conn = None
try:
if not MASTERDATA_PATH.exists():
print(f"[ACTION] База мастер-данных не найдена. Создаю новую: {MASTERDATA_PATH.name}")
master_conn = sqlite3.connect(MASTERDATA_PATH)
master_conn.execute("PRAGMA foreign_keys = ON;")
master_cursor = master_conn.cursor()
_create_tables(master_cursor)
_populate_test_data(master_conn)
except sqlite3.Error as e:
print(f"[ERROR] Ошибка при инициализации MASTERDATA.DB: {e}")
if master_conn:
master_conn.rollback()
finally:
if master_conn:
master_conn.close()
class Session:
def __init__(self):
pass
# --- ФУНКЦИИ ДЛЯ ИМПОРТА (лежат строго вне класса Session) ---
def list_groups():
"""Функция-заглушка для прямого импорта"""
return [(1, "Клубы"), (2, "Фанаты")]
def get_all_indicators_by_group(group_code):
"""Функция-заглушка для прямого импорта"""
mock_data = {
"KB": [(1, "ЦСКА"), (2, "Спартак"), (3, "Зенит")],
"FAN": [(4, "Мужчины"), (5, "Женщины"), (6, "Дети")]
}
return mock_data.get(group_code, [])
if __name__ == "__main__":
init_database()
#====================================================
#====================================================
# dukora\gui\main_interface.py
#====================================================
import os
import customtkinter as ctk
from PIL import Image
import sys
from modules.manager.manager_window import ManagerWindow
from modules.experts.experts_window import ExpertsWindow
from modules.project.project_window import ProjectWindow
from modules.forecasting.forecast_window import ForecastWindow
from modules.utils.resources import resource_path
from typing import List, Any
class MainApp(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Dozor System")
try:
if sys.platform == "win32":
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except Exception:
pass
target_w, target_h = 422, 335
self.geometry(f"{target_w}x{target_h}")
self.resizable(False, False)
bg_color = "#F0F0F0"
self.configure(fg_color=bg_color)
main_bg = ctk.CTkFrame(self, fg_color=bg_color)
main_bg.pack(fill="both", expand=True, padx=1, pady=1)
# === СЕТКА ИНТЕРФЕЙСА ===
main_bg.grid_columnconfigure(0, minsize=210, weight=0)
main_bg.grid_columnconfigure(1, weight=1)
for i in range(6):
main_bg.grid_rowconfigure(i, weight=0)
main_bg.grid_rowconfigure(6, weight=1)
button_font = ("Arial", 13, "bold")
btn_height = 45
# === ЛЕВАЯ ПАНЕЛЬ МЕНЮ (КНОПКИ ВЫЗОВА) ===
sidebar = ctk.CTkFrame(main_bg, fg_color="#EEEEEE", width=210)
sidebar.grid(row=0, column=0, rowspan=7, sticky="nsew", padx=(5, 5), pady=5)
sidebar.grid_columnconfigure(0, weight=1)
actions = [
("Администратор", self.open_admin_module),
("Менеджер базы", self.open_manager_module),
("Эксперты", self.open_experts_module),
("Проект", self.open_project_module),
("Прогнозирование", self.open_forecasting_module)
]
# Создание физических кнопок на экране
for i, (text, command_func) in enumerate(actions):
btn = ctk.CTkButton(
master=sidebar,
text=text,
font=button_font,
height=btn_height,
command=command_func
)
btn.grid(row=i, column=0, padx=10, pady=5, sticky="ew")
# === ПРАВАЯ ЧАСТЬ ВИТРИНЫ ===
content_area = ctk.CTkFrame(main_bg, fg_color=bg_color)
content_area.grid(row=0, column=1, rowspan=7, sticky="nsew", padx=(0, 10), pady=10)
content_area.grid_columnconfigure(0, weight=1)
content_area.grid_rowconfigure(0, weight=0)
content_area.grid_rowconfigure(1, minsize=2, weight=0)
content_area.grid_rowconfigure(2, minsize=27, weight=0)
content_area.grid_rowconfigure(3, weight=0)
pic_container = ctk.CTkFrame(content_area, fg_color="#d9ead3", width=220, height=130)
pic_container.grid(row=0, column=0, pady=(5, 0))
pic_container.grid_propagate(False)
img_loaded = False
try:
img_path = resource_path('images/logo1.png')
pil_img = Image.open(img_path)
target_w_i, target_h_i = 210, 110
ratio = pil_img.width / pil_img.height
box_ratio = target_w_i / target_h_i
if ratio > box_ratio:
new_w, new_h = target_w_i, int(target_w_i / ratio)
else:
new_w, new_h = int(target_h_i * ratio), target_h_i
resized_img = pil_img.resize((new_w, new_h), resample=Image.LANCZOS)
tk_image = ctk.CTkImage(light_image=resized_img, size=(new_w, new_h))
lbl_pic = ctk.CTkLabel(pic_container, image=tk_image, text="", fg_color="#d9ead3")
lbl_pic.place(relx=0.5, rely=0.5, anchor='center', x=0, y=-3)
img_loaded = True
except Exception as e:
fail_lbl = ctk.CTkLabel(pic_container, text="Сова\nне найдена", font=("Arial", 14), fg_color="#d9ead3")
fail_lbl.place(relx=0.5, rely=0.5, anchor="center")
bottom_btns_frame = ctk.CTkFrame(content_area, fg_color=bg_color)
bottom_btns_frame.grid(row=3, column=0, sticky="ew", padx=0)
bottom_btns_frame.grid_columnconfigure(0, minsize=195, weight=0)
ctk.CTkButton(
bottom_btns_frame,
text="Руководство",
font=button_font,
height=btn_height,
command=self.open_guide
).grid(row=0, column=0, sticky="ew", pady=(0, 8))
ctk.CTkButton(
bottom_btns_frame,
text="О программе",
font=button_font,
height=btn_height,
command=self.show_about
).grid(row=1, column=0, sticky="ew")
footer_frame = ctk.CTkFrame(main_bg, fg_color="#0078D7", height=30)
footer_frame.grid(row=7, column=0, columnspan=2, sticky="ew", pady=(10, 0))
footer_label = ctk.CTkLabel(footer_frame,
text="Copyright © Минск, НИИ, 2027 год",
text_color="#FF8C00",
fg_color="#0078D7",
font=("Arial", 13, "bold"))
footer_label.pack(expand=True, ipady=4)
self.update_idletasks()
self.tk.call('wm', 'geometry', self._w, f"{target_w}x{target_h}")
screen_w = self.winfo_screenwidth()
# Высота верхней панели задач Windows обычно около 40 пикселей.
# Если панель скрыта или находится сбоку/снизу, отступ можно уменьшить до 0-10.
taskbar_offset_top = 40
pos_x = (screen_w - target_w) // 2
pos_y = taskbar_offset_top
# Устанавливаем размер И позицию одной командой через configure
self.geometry(f"{target_w}x{target_h}+{pos_x}+{pos_y}")
def _position_center_top(self, window_instance, width: int, height: int):
screen_w = window_instance.winfo_screenwidth()
pos_x = (screen_w - width) // 2
pos_y = 40 # тот же отступ сверху
window_instance.geometry(f"{width}x{height}+{pos_x}+{pos_y}")
# === ОБРАБОТЧИК КНОПКИ АДМИНИСТРАТОРА (с принудительным логом) ===
def open_admin_module(self):
if hasattr(self, '_admin_win') and self._admin_win.winfo_exists():
self._admin_win.lift()
return
try:
from modules.admin.admin_accordion_window import AdminAccordionWindow
self._admin_win = AdminAccordionWindow(self)
self.wait_window(self._admin_win)
except Exception as e:
messagebox.showerror("Фатальный сбой", f"Не удалось открыть раздел Администратора.\n\n{e}")
import traceback
traceback.print_exc()
def open_manager_module(self):
if hasattr(self, '_manager_win') and self._manager_win.winfo_exists():
self._manager_win.lift()
return
self._manager_win = ManagerWindow(master=self)
self.wait_window(self._manager_win)
def open_experts_module(self):
if hasattr(self, '_experts_win') and self._experts_win.winfo_exists():
self._experts_win.lift()
return
try:
from modules.experts.experts_window import ExpertsWindow
self._experts_win = ExpertsWindow(master=self)
self.wait_window(self._experts_win)
except Exception as e:
messagebox.showerror("Фатальный сбой", f"Не удалось открыть раздел Экспертов.\n\n{e}")
import traceback
traceback.print_exc()
def open_project_module(self):
"""Открывает окно управления проектами."""
if hasattr(self, '_project_win') and self._project_win.winfo_exists():
self._project_win.lift()
return
self._project_win = ProjectWindow(master=self)
try:
self._project_win.manager_db.close()
fresh_projects = self._project_win.manager_db.list_projects()
# Теперь кортежи имеют вид: (id, Name, Code, Cipher)
# Для отображения берем Шифр [3]
display_names = [p[3] for p in fresh_projects]
print(f"[DEBUG MAIN MENU] Projects loaded. Display: {display_names}")
if hasattr(self._project_win, 'project_selector'):
self._project_win.project_selector.configure(values=display_names)
if display_names:
selected_display = display_names[-1]
# Устанавливаем текстовое поле комбобокса (ВАЖНО: сначала values, потом set)
self._project_win.project_selector.set(selected_display)
# Сохраняем выбранное значение (Шифр) в атрибут окна для event_ui
self._project_win.current_selected_project_code = selected_display
else:
self._project_win.project_selector.set("Проектов нет")
self._project_win.current_selected_project_code = ""
if hasattr(self._project_win, 'tree_projects'):
tree = self._project_win.tree_projects
for row in tree.get_children():
tree.delete(row)
# В дереве оставляем полную информацию для наглядности
for proj in sorted(fresh_projects, key=lambda x: x[0]):
tree.insert("", "end", values=proj)
except Exception as e:
print(f"[ERROR] Failed to sync projects on module open: {e}")
if hasattr(self._project_win, 'project_selector'):
self._project_win.project_selector.configure(values=[])
self._project_win.project_selector.set("Проектов нет")
self._project_win.current_selected_project_code = ""
self._project_win.grab_set()
def open_forecasting_module(self):
if hasattr(self, '_forecast_win') and self._forecast_win.winfo_exists():
self._forecast_win.lift()
return
self._forecast_win = ForecastWindow(master=self)
self.wait_window(self._forecast_win)
def open_guide(self):
file_name = "manual.chm"
full_path = resource_path(file_name)
print(f"Пытаюсь открыть: {full_path}")
if os.path.exists(full_path):
try:
os.startfile(full_path)
except Exception as e:
print(f"Система отказалась открывать файл: {e}")
else:
print("Ошибка: ФАЙЛА НЕТ в этой папке!")
def show_about(self):
from tkinter import messagebox
info_text = (
"Система Dozor V1.0\n"
"Разработано: Минск, НИИ\n"
"Версия сборки: 31.07.2026\n\n"
"(c) Все права защищены."
)
messagebox.showinfo(title="О программе", message=info_text)
if __name__ == "__main__":
app = MainApp()
app.mainloop()
#====================================================
#===================================================
# 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
#===================================================