Загрузка данных
'''
День добрый!
Я, вайб кодер - ты, профессионалный программист. Ты делаешь - я вставляю, заменяю.
Кусками не работаем - только целыми фрагментами - метод на метод, файл на файл, от комментария до комментария, от комментария до конца - по такому принципу.
- Файл main_interface.ру принципиально не менять, это только витрина и кнопки.
- Программа будет собираться руинсталлером, поэтому это надо иметь ввиду.
- В программе уже реализованы авторизация и первая кнопка главного меню АДМИНИСТРАТОР. На сейчас - компиляция проходит без ошибок.
- Необходимо подключить в "аккордеоне" вторую кнопку главного меню МЕНЕДЖЕР БАЗЫ. Ниже будут представлены файлы-доноры для этой кнопки. Они реализованы в ткинкере. Мы делаем рефакторинг и все должно стать в кастомткинкере. Логика в файлах рабочая. По образцу этих файлов и надо организовать - один заголовок Аккордеона - 2 файла, из которых один для интерфейса, а второй с функционалом.
- все файлы для работы МЕНЕДЖЕРА БАЗЫ должны лежать по адресу dukora\modules\manager\.
- база расположена по адресу dukora\db\ - с адресом базы будь осторожен, она должна быть прописана как в файле users_tab.ру база users.db. Баз будет две - основная - masterdata, и рабочая пользовательская database.
- файл для работы с базами database_module.ру расположен по адресу dukora\modules\data\.
- если какие то вопросы есть - задавай.
Ниже представляю файлы, которые могут пригодиться как доноры, либо уже присутствующие как реалные файлы приложения.
dukora\main.py
dukora\gui\main_interface.py
dukora\modules\admin\users_tab.py
dukora\modules\admin\admin_accordion_window.py
dukora\modules\manager\manager_window.py
dukora\modules\data\database_module.py
manager_group_inter.ру - файл – донор
manager_group_work.ру - файл – донор
manager_metrics_inter.py - файл – донор
manager_metrics_work.py - файл - донор
==================================================================
Итогом работы должна стать полностью рабочая кнопка МЕНЕДЖЕР БАЗЫ - открываться в аккордеоне два окна оформленных в новом интерфейсе кастомткинкера с рабочим функционалом.
Новый интерфейс это и кастомткинкер и расположение элементов управления.
===================================================================
МЕНЕДЖЕР ГРУППЫ:
Вверху как бы в таблице из двух колонок расположить
Слева:
Название: поле для названия
Код: поле для кода
Усиление: поле для усиления
Справа 3 кнопки, прижатые к правой стороне окна все одного размера:
Добавить (синего цвета)
Редактировать (зеленого цвета)
Удалить (красного цвета)
Ниже расположена таблица с тремя столбцами:
Код (фиксированной ширины в 12 знаков)
Усиление (фиксированной ширины в 7 знаков)
Название (резиновая)
При клике на сохраненную группу она выделяется в строке и имеется возможность ее удалить, редактировать.
===================================================================
МЕНЕДЖЕР ПОКАЗАТЕЛЕЙ:
Новый интерфейс это и кастомткинкер и расположение элементов управления.
Вверху как бы в таблице из двух колонок расположить
Слева в "таблице":
- Группа: выпадающий список
- Приблизительно на пол окна заголовок Название показателя: и кнопка Вставить (черного цвета),а ниже текстовое поле на 4 переносимые строки и скрол для вставки текста из буфера обмена сочетанием клавишь и кнопкой Вставить
Справа в "таблице" кнопки, прижатые к правой стороне окна все одного размера:
Добавить (синего цвета)
Редактировать (зеленого цвета)
Удалить (красного цвета)
Ниже расположена таблица с тремя столбцами:
№ п/п (фиксированной ширины в 5 знаков), в таблице это для красоты, при удалении пересчитывается по порядку
Код (фиксированной ширины в 15 знаков) не меняется
Название (резиновая).
При клике на сохраненный показатель он выделяется в строке и имеется возможность его удалить, редактировать.
Таблица должна быть не высокая по высоте, строк на 10. Но каждый добавляемый новый показатель должен добавляться и сразу быть на виду в окне.
Ниже таблицы расположить кнопку Экспорт в WORD (черного цвета)
======================================================================
'''
#================================================
#================================================
# dukora\main.py - реальный запускающий файл
#================================================
import customtkinter as ctk
from gui.main_interface import MainApp
from auth_window import AuthWindow
def start_application():
app = MainApp()
# Окно авторизации поверх главного
auth_win = AuthWindow(app)
app.wait_window(auth_win)
app.mainloop()
if __name__ == "__main__":
try:
ctk.set_appearance_mode("System")
ctk.set_default_color_theme("blue")
except Exception:
pass
start_application()
#===================================================
#====================================================
# 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
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()
screen_h = self.winfo_screenheight()
pos_x = (screen_w - target_w) // 2
pos_y = (screen_h - target_h) // 2
self.geometry(f"+{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
self._experts_win = ExpertsWindow(master=self)
self.wait_window(self._experts_win)
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)
self.wait_window(self._project_win)
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\admin\users_tab.py - реальный файл,
# для примера прописи адресов
#====================================================
import customtkinter as ctk
import tkinter as tk
from tkinter import ttk, messagebox
import sqlite3
from hashlib import sha256
from pathlib import Path
# Импорт нашей утилиты путей
from modules.utils.resources import resource_path
class AdminUsersPanel(ctk.CTkFrame):
def __init__(self, master):
super().__init__(master)
container = ctk.CTkFrame(self)
container.pack(fill="both", expand=True, padx=10, pady=10)
style = ttk.Style()
style.theme_use("default")
style.configure("mystyle.Treeview", background="#2b2b2b", fieldbackground="#2b2b2b", foreground="white", borderwidth=0, font=("Arial", 10))
style.map('mystyle.Treeview', background=[('selected', '#4a4a4a')])
table_frame = ctk.CTkFrame(container)
table_frame.pack(fill="both", expand=True, side="top")
self.tree = ttk.Treeview(table_frame, columns=("ID", "Логин", "ФИО", "Роль"), show="headings", style="mystyle.Treeview")
self.tree.heading("ID", text="ID")
self.tree.heading("Логин", text="Логин")
self.tree.heading("ФИО", text="ФИО")
self.tree.heading("Роль", text="Роль")
self.tree.column("ID", width=40)
self.tree.column("Логин", width=100)
self.tree.column("ФИО", width=180)
self.tree.column("Роль", width=80)
self.tree.pack(side="left", fill="both", expand=True)
scrollbar = ctk.CTkScrollbar(table_frame, command=self.tree.yview)
scrollbar.pack(side="right", fill="y")
self.tree.configure(yscrollcommand=scrollbar.set)
form_frame = ctk.CTkFrame(container)
form_frame.pack(fill="x", pady=(10, 0))
self.login_entry = ctk.CTkEntry(form_frame, placeholder_text="Логин", width=100)
self.login_entry.grid(row=0, column=0, padx=5, pady=5)
self.fio_entry = ctk.CTkEntry(form_frame, placeholder_text="ФИО", width=180)
self.fio_entry.grid(row=0, column=1, padx=5, pady=5)
self.pass_entry = ctk.CTkEntry(form_frame, placeholder_text="Пароль", width=120, show="*")
self.pass_entry.grid(row=0, column=2, padx=5, pady=5)
self.role_cb = ctk.CTkComboBox(form_frame, values=["Админ", "Эксперт", "Пользователь"], width=100)
self.role_cb.grid(row=0, column=3, padx=5, pady=5)
self.role_cb.set("Эксперт")
btn_frame = ctk.CTkFrame(container)
btn_frame.pack(pady=10)
add_btn = ctk.CTkButton(btn_frame, text="Добавить", command=self.add_user)
add_btn.pack(side="left", padx=5)
edit_btn = ctk.CTkButton(btn_frame, text="Изменить", command=self.edit_user)
edit_btn.pack(side="left", padx=5)
del_btn = ctk.CTkButton(btn_frame, text="Удалить", command=self.delete_user, fg_color="#d9534f", hover_color="#c9302c")
del_btn.pack(side="left", padx=5)
# Загружаем базу сразу при создании панели
self.load_users()
def load_users(self):
self.tree.delete(*self.tree.get_children())
db_path: Path = resource_path('db/users.db')
try:
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
fio TEXT NOT NULL,
role TEXT NOT NULL
)
''')
conn.commit()
cursor.execute('SELECT id, login, fio, role FROM users ORDER BY id')
rows = cursor.fetchall()
for index, row in enumerate(rows, start=1):
self.tree.insert("", "end", values=(index, row[1], row[2], row[3]), tags=(str(row[0]),))
except Exception as e:
messagebox.showerror("Ошибка БД", f"Не удалось загрузить пользователей:\n{e}\nПуть: {db_path}")
finally:
if 'conn' in locals():
conn.close()
def get_form_data(self):
return {
"login": self.login_entry.get(),
"fio": self.fio_entry.get(),
"password": self.pass_entry.get(),
"role": self.role_cb.get()
}
def clear_form(self):
self.login_entry.delete(0, "end")
self.fio_entry.delete(0, "end")
self.pass_entry.delete(0, "end")
self.role_cb.set("Эксперт")
def add_user(self):
data = self.get_form_data()
if not all([data["login"], data["fio"], data["password"]]):
messagebox.showwarning("Ошибка", "Заполните Логин, ФИО и Пароль")
return
password_hash = sha256(data["password"].encode()).hexdigest()
# === ВЫЗОВ РЕСУРСА ===
db_path: Path = resource_path('db/users.db')
try:
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
cursor.execute('INSERT INTO users (login, password_hash, fio, role) VALUES (?, ?, ?, ?)', (data["login"], password_hash, data["fio"], data["role"]))
conn.commit()
messagebox.showinfo("Успех", f"Пользователь {data['login']} добавлен")
self.clear_form()
self.load_users()
except sqlite3.IntegrityError:
messagebox.showwarning("Ошибка", "Пользователь с таким логином уже существует.")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось сохранить:\n{e}")
finally:
if 'conn' in locals():
conn.close()
def _get_selected_db_id(self):
selected = self.tree.selection()
if not selected:
return None
item = self.tree.item(selected)
db_id_str = item['tags'][0]
return int(db_id_str)
def delete_user(self):
db_id = self._get_selected_db_id()
login = self.tree.item(self.tree.selection())['values'][1]
if not db_id:
messagebox.showwarning("Ошибка", "Выберите строку")
return
# === ВЫЗОВ РЕСУРСА ===
db_path: Path = resource_path('db/users.db')
try:
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
cursor.execute("DELETE FROM users WHERE id=?", (db_id,))
conn.commit()
messagebox.showinfo("Успех", f"Пользователь '{login}' удален")
self.clear_form()
self.load_users()
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось удалить:\n{e}")
finally:
if 'conn' in locals():
conn.close()
def edit_user(self):
db_id = self._get_selected_db_id()
if not db_id:
messagebox.showwarning("Ошибка", "Выберите строку")
return
# === ВЫЗОВ РЕСУРСА ===
db_path: Path = resource_path('db/users.db')
try:
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
cursor.execute('SELECT login, fio, role FROM users WHERE id=?', (db_id,))
user_data = cursor.fetchone()
if user_data:
self.login_entry.delete(0, "end")
self.login_entry.insert(0, user_data[0])
self.login_entry.configure(state="disabled")
self.fio_entry.delete(0, "end")
self.fio_entry.insert(0, user_data[1])
self.role_cb.set(user_data[2])
self.pass_entry.delete(0, "end")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось прочитать данные:\n{e}")
finally:
if 'conn' in locals():
conn.close()
#===================================================
#====================================================
# dukora\modules\admin\admin_accordion_window.py - реальный файл,
# для примера
#====================================================
import customtkinter as ctk
from tkinter import ttk, messagebox
from .users_tab import AdminUsersPanel
from .db_manager_panel import DBManagerPanel
from .migration_panel import MigrationPanel
class AdminAccordionWindow(ctk.CTkToplevel):
def __init__(self, master):
super().__init__(master)
self.parent_main = master
self.title("Администрирование")
# --- 1. БАЗОВЫЕ ГАБАРИТЫ ОКНА ---
window_width = 650
initial_height = 380
screen_w = self.winfo_screenwidth()
screen_h = self.winfo_screenheight()
x_cord = int((screen_w - window_width) / 2)
y_cord = int((screen_h - initial_height) / 2)
self.geometry(f"{window_width}x{initial_height}+{x_cord}+{y_cord}")
# === 2. ЖЕСТКИЕ ОГРАНИЧЕНИЯ РАЗМЕРА ===
self.minsize(width=window_width, height=250)
self.maxsize(width=900, height=screen_h - 50)
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)
title_lbl = ctk.CTkLabel(main_container, text="Панель администратора", font=ctk.CTkFont(size=20, weight="bold"))
title_lbl.pack(pady=(0, 15))
accordion_frame = ctk.CTkFrame(main_container)
accordion_frame.pack(fill="both", expand=True, pady=(10, 0))
self.accordion_data = {}
self.currently_open_button = None
# === ЗАГОЛОВКИ АККОРДЕОНА ===
users_item = self._create_accordion_header(accordion_frame, "Управление пользователями")
security_item = self._create_accordion_header(accordion_frame, "Безопасность")
migration_item = self._create_accordion_header(accordion_frame, "Миграция")
# Наполняем секции контентом
self._fill_users_section(users_item["content"])
self._fill_security_section(security_item["content"])
self._fill_migration_section(migration_item["content"])
# Привязка команд переключения
for item in [users_item, security_item, migration_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(users_item["button"], users_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)
# === 3. ЦЕНТРИРОВАНИЕ ПОСЛЕ ПОЛНОЙ ОТРИСТОВКИ ===
self.after(100, self._center_over_parent)
def _center_over_parent(self):
if not self.parent_main or not self.parent_main.winfo_exists():
return
self.update_idletasks()
w = self.winfo_width()
h = self.winfo_height()
root_win = self.parent_main
p_x = root_win.winfo_rootx()
p_y = root_win.winfo_rooty()
final_x = int(p_x + (root_win.winfo_width() - w) / 2)
final_y = int(p_y + (root_win.winfo_height() - h) / 2)
self.geometry(f"+{final_x}+{final_y}")
# === МЕТОД toggle_section (твой оригинальный) ===
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)
# === РАСЧЕТ ГЕОМЕТРИИ (Исправлено!) ===
def _calculate_window_geometry(self, collapsed=False):
current_width = self.winfo_width()
# Если ширина еще не посчиталась (например, при первом запуске)
if current_width == 1:
current_width = 650
header_height = 50 * 3 + 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
min_h = 250
final_height = min(target_height, max_h) # Не выше экрана
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_users_section(self, frame):
panel = AdminUsersPanel(frame)
panel.pack(fill="both", expand=True, padx=10, pady=10)
def _fill_security_section(self, frame):
panel = DBManagerPanel(frame)
panel.pack(fill="both", expand=True, padx=10, pady=10)
def _fill_migration_section(self, frame):
panel = MigrationPanel(frame)
panel.pack(fill="both", expand=True, padx=10, pady=10)
def on_closing(self):
try:
self.after_cancel("all")
except Exception:
pass
try:
self.destroy()
except Exception:
pass
#===================================================
#====================================================
# dukora\modules\manager\manager_window.py - реальный файл
#====================================================
import customtkinter as ctk
from tkinter import ttk
class ManagerWindow(ctk.CTkToplevel):
def __init__(self, master):
super().__init__(master)
self.title("Менеджер базы")
window_width = 650
initial_height = 355
x_cord = int((self.winfo_screenwidth() / 2) - (window_width / 2))
y_cord = int((self.winfo_screenheight() / 2) - (initial_height / 2))
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)
title = ctk.CTkLabel(main_container, text="Управление базой данных", font=ctk.CTkFont(size=20, weight="bold"))
title.pack(pady=(0, 15))
accordion_frame = ctk.CTkFrame(main_container)
accordion_frame.pack(fill="both", expand=True, pady=(10, 0))
# ФРЕЙМ ДЛЯ КНОПКИ (теперь он внутри общей структуры четко)
button_frame = ctk.CTkFrame(main_container, fg_color="transparent")
self.accordion_data = {}
self.currently_open_button = None
group_item = self._create_accordion_header(accordion_frame, "Менеджер группы")
kpi_item = self._create_accordion_header(accordion_frame, "Менеджер показателей")
self._fill_group_section(group_item["content"])
self._fill_kpi_section(kpi_item["content"])
for item in [group_item, kpi_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))
# ВАЖНО: Сначала пакуем кнопку, потом задаем её параметры
button_frame.pack(fill="x", side="bottom") # Привязка к низу
close_btn = ctk.CTkButton(button_frame, text="Закрыть", width=120, command=self.destroy)
close_btn.pack(side="right", padx=10, pady=10) # Отступы внутри фрейма кнопки
# === МЕТОДЫ АККОРДЕОНА ===
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.after_idle(lambda g=new_geo: self.geometry(g))
def _calculate_window_geometry(self, collapsed=False):
current_width = self.winfo_width()
# Высота заголовков + разделителей между ними
header_height = (50 * len(self.accordion_data)) + 30
BOTTOM_RESERVED_SPACE = 70
if collapsed or not self.currently_open_button:
# ЖЕСТКАЯ ФИКСАЦИЯ: если всё закрыто - всегда стартовый размер
target_height = 355
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 + 120 + BOTTOM_RESERVED_SPACE
max_h = self.winfo_screenheight() - 50
final_height = min(target_height, max_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_group_section(self, frame):
placeholder = ctk.CTkLabel(frame, text="Здесь будет управление группами.", font=("Arial", 12))
placeholder.pack(expand=True)
def _fill_kpi_section(self, frame):
placeholder = ctk.CTkLabel(frame, text="Здесь будут показатели эффективности.", font=("Arial", 12))
placeholder.pack(expand=True)
#===================================================
#====================================================
# dukora\modules\data\database_module.py - реальный файл,
# для работы с базой данных
#====================================================
# -*- coding: utf-8 -*-
import sys
import sqlite3
from pathlib import Path
try:
potential_base = Path.cwd()
if "Dozor" not in str(potential_base):
BASE_DIR = Path(__file__).resolve().parents[4]
else:
BASE_DIR = potential_base
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') 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}")
DB_FOLDER.mkdir(parents=True, exist_ok=True)
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] Проверка наличия рабочих баз данных...")
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
# Эти функции объявлены СТАТИЧЕСКИМИ и НА УРОВНЕ МОДУЛЯ ниже
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()
#===================================================
Ниже идут файлы - доноры
#====================================================
# manager_group_inter.ру - файл - донор
#====================================================
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, messagebox
from modules.manager.manager_group_work import list_groups, add_group, delete_group
from modules.utils.window_behavior import LevelThreeBehavior
from modules.utils.app_state import disable_app, enable_app
class ManagerGroupInter(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.master = parent
self.transient(parent)
self.title("Менеджер групп")
self.geometry("600x450")
behavior = LevelThreeBehavior()
behavior.setup_level_three(self)
header_frame = tk.Frame(self, bg="#2c3e50", height=30)
header_frame.pack(fill="x")
header_frame._name = "title_bar"
close_btn = ttk.Button(header_frame, text="X", width=3, command=self.destroy)
close_btn.pack(side="right", padx=5, pady=2)
ttk.Label(header_frame, text="Менеджер групп", foreground="white", background="#2c3e50").pack(side="left", padx=10)
ttk.Separator(self, orient='horizontal').pack(fill='x')
top_frame = tk.Frame(self)
top_frame.pack(fill="x", padx=10, pady=(5, 10))
# --- НАЗВАНИЕ ---
ttk.Label(top_frame, text="Название:").grid(row=0, column=0, sticky="w")
self.name_entry = ttk.Entry(top_frame, width=30)
self.name_entry.grid(row=0, column=1, padx=5)
# --- КОД (Только чтение для редактирования) ---
ttk.Label(top_frame, text="Код:").grid(row=1, column=0, sticky="w")
self.code_var = tk.StringVar()
self.code_label = ttk.Label(top_frame, textvariable=self.code_var, relief="sunken", width=30)
self.code_label.grid(row=1, column=1, padx=5, pady=5)
frame_gain = tk.Frame(top_frame)
frame_gain.grid(row=2, column=1, sticky="w", padx=5)
ttk.Label(frame_gain, text="Усиление:").pack(side="left")
btn_minus = ttk.Button(frame_gain, text="-", command=lambda: self._change_gain(-1), width=3)
btn_minus.pack(side="left", padx=2)
self.gain_var = tk.StringVar(value="1")
gain_entry = ttk.Entry(frame_gain, textvariable=self.gain_var, width=5, justify="center")
gain_entry.pack(side="left", padx=2)
btn_plus = ttk.Button(frame_gain, text="+", command=lambda: self._change_gain(1), width=3)
btn_plus.pack(side="left", padx=2)
action_frame = tk.Frame(top_frame)
action_frame.grid(row=3, column=0, columnspan=2, pady=10)
# КНОПКА ДОБАВЛЕНИЯ БУДЕТ ВЫЗЫВАТЬ ЭТОТ МЕТОД
ttk.Button(action_frame, text="Добавить", command=self.add_group).pack(side="left", padx=5)
ttk.Button(action_frame, text="Редактировать", command=self.edit_group).pack(side="left", padx=5)
ttk.Button(action_frame, text="Удалить", command=self.delete_group).pack(side="left", padx=5)
tree_frame = tk.Frame(self)
tree_frame.pack(fill="both", expand=True, padx=10, pady=5)
columns = ("code", "name", "gain")
self.tree = ttk.Treeview(tree_frame, columns=columns, show="headings")
self.tree.heading("code", text="Код"); self.tree.column("code", width=100)
self.tree.heading("name", text="Название"); self.tree.column("name", width=200)
self.tree.heading("gain", text="Усиление"); self.tree.column("gain", width=80, anchor="center")
self.tree.pack(fill="both", expand=True, side="left")
scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
scrollbar.pack(side="right", fill="y")
self.tree.configure(yscrollcommand=scrollbar.set)
self.update_tree()
self.update_idletasks()
screen_width = self.winfo_screenwidth()
screen_height = self.winfo_screenheight()
size = tuple(int(_) for _ in self.geometry().split('+')[0].split('x'))
x = screen_width // 2 - size[0] // 2
y = screen_height // 2 - size[1] // 2
self.geometry(f"+{x}+{y}")
def update_tree(self):
for item in self.tree.get_children():
self.tree.delete(item)
groups = list_groups()
for group in groups:
gain_text = f"{group.gain_group:.1f}" if group.gain_group is not None else ""
self.tree.insert("", "end", iid=str(group.id), values=(group.code, group.name, gain_text))
def get_selected(self):
selected = self.tree.selection()
if not selected:
return None
code, name, gain = self.tree.item(selected[0])["values"]
db_id_str = selected[0]
try:
db_id = int(db_id_str)
except ValueError:
return None
return {"id": db_id, "code": code, "name": name}
# === ВОТ ЭТОТ МЕТОД БЫЛ ПРОПУЩЕН РАНЬШЕ ===
def add_group(self):
name = self.name_entry.get().strip()
# Для добавления временно используем поле ввода кода, если оно есть, иначе генерируем заглушку
code = ""
# Проверим, не является ли интерфейс сейчас режимом "редактирование" по наличию лейбла
if hasattr(self, 'code_label') and self.code_label.cget("text"):
code = self.code_label.cget("text")
gain_raw = self.gain_var.get().strip()
if not name or not code:
messagebox.showwarning("Ошибка", "Заполните название и код.")
return
try:
gain_val = float(gain_raw.replace(',', '.')) if gain_raw else 0.0
except ValueError:
messagebox.showwarning("Ошибка", "Усиление должно быть числом.")
return
existing = list_groups()
for g in existing:
if g.name == name or g.code == code:
messagebox.showerror("Ошибка", "Группа с таким названием или кодом уже существует.")
return
try:
add_group(name, code, gain_val)
messagebox.showinfo("Успех", "Группа добавлена.")
self.update_tree()
self._clear_form()
except Exception as e:
messagebox.showerror("Ошибка БД", str(e))
def edit_group(self):
data = self.get_selected()
if not data:
messagebox.showwarning("Ошибка", "Выберите группу.")
return
old_code = data["code"]
old_name = data["name"]
# Берем текущее усиление прямо сейчас (то, которое выбрал юзер)
current_gain_str = self.gain_var.get().strip()
# Заполняем форму для наглядности
self.name_entry.delete(0, tk.END)
self.name_entry.insert(0, old_name)
self.code_var.set(old_code)
disable_app(self.master)
edit_win = tk.Toplevel(self.master)
edit_win.title("Подтверждение")
edit_win.geometry("+{}+{}".format(self.winfo_pointerx(), self.winfo_pointery()))
msg = f"Изменить группу '{old_code}'?"
ttk.Label(edit_win, text=msg, wraplength=300).pack(pady=20)
btn_frame = tk.Frame(edit_win)
btn_frame.pack(pady=10)
# ПЕРЕДАЕМ ЗНАЧЕНИЕ НАПРЯМУЮ В ЛЯМБДУ
ttk.Button(btn_frame, text="Да",
command=lambda db_id=data["id"], name=old_name, gain=current_gain_str:
self._apply_edit(db_id, name, gain, edit_win)).pack(side="left", padx=10)
ttk.Button(btn_frame, text="Отмена", command=edit_win.destroy).pack(side="left", padx=10)
def _apply_edit(self, db_id, name, gain_str, win):
try:
final_gain = int(float(gain_str))
from modules.data.database_module import get_db_connection
conn = get_db_connection()
cursor = conn.cursor()
print(f"[DEBUG] SQL Update -> ID:{db_id} | Name:'{name}' | Gain:{final_gain}")
cursor.execute(
"UPDATE groups SET name = ?, gain_group = ? WHERE id = ?",
(name, final_gain, db_id)
)
rows_affected = cursor.rowcount
conn.commit()
conn.close()
if rows_affected > 0:
messagebox.showinfo("Готово", "Группа обновлена.")
win.destroy()
self.after_idle(self.update_tree)
enable_app(self.master)
else:
messagebox.showwarning("Внимание", "Запрос прошел, но строка не изменилась.")
win.destroy()
enable_app(self.master)
except Exception as e:
print(f"[FATAL DB ERROR RAW SQL] {e}")
messagebox.showerror("Ошибка БД", str(e))
enable_app(self.master)
def delete_group(self):
data = self.get_selected()
if not data:
messagebox.showwarning("Ошибка", "Выберите группу.")
return
if messagebox.askyesno("Подтверждение", f"Удалить группу {data['name']}?"):
try:
delete_group(data["id"])
messagebox.showinfo("Успех", "Группа удалена.")
self.update_tree()
self._clear_form()
except Exception as e:
messagebox.showerror("Ошибка БД", str(e))
def _clear_form(self):
if hasattr(self, 'name_entry'):
self.name_entry.delete(0, tk.END)
if hasattr(self, 'code_var'):
self.code_var.set("")
self.gain_var.set("1.0")
def on_close(self):
self.destroy()
def _change_gain(self, delta):
try:
val = int(self.gain_var.get())
new_val = max(0, val + delta)
self.gain_var.set(str(new_val))
except ValueError:
self.gain_var.set("1")
#===================================================
Ниже идут файлы - доноры
#====================================================
# manager_group_work.ру - файл - донор
#====================================================
# -*- coding: utf-8 -*-
import sqlite3
from typing import List
from contextlib import contextmanager
from modules.data.database_module import get_db_connection
class GroupDTO:
def __init__(self, id: int, code: str, name: str, gain_group: float = None):
self.id = id
self.code = code
self.name = name
self.gain_group = gain_group
@contextmanager
def _get_cursor():
conn = get_db_connection()
try:
cursor = conn.cursor()
yield cursor
# Принудительная фиксация СРАЗУ ЖЕ
conn.commit()
except Exception:
conn.rollback()
raise
finally:
# Принудительное закрытие соединения, чтобы сбросить все локи
conn.close()
def list_groups() -> List[GroupDTO]:
with _get_cursor() as 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("SELECT id, code, name, gain_group FROM groups ORDER BY name")
rows = cursor.fetchall()
return [GroupDTO(id=row[0], code=row[1], name=row[2], gain_group=row[3]) for row in rows]
def add_group(name: str, code: str, gain_group: float = 0.0) -> GroupDTO:
with _get_cursor() as cursor:
cursor.execute(
"INSERT INTO groups (name, code, gain_group) VALUES (?, ?, ?)",
(name, code, gain_group)
)
new_id = cursor.lastrowid
return GroupDTO(id=new_id, code=code, name=name, gain_group=gain_group)
def edit_group(group_id: int, new_name: str, new_code: str, new_gain: float):
conn = get_db_connection()
cursor = conn.cursor()
try:
cursor.execute(
"UPDATE groups SET name = ?, code = ?, gain_group = ? WHERE id = ?",
(new_name, new_code, new_gain, group_id)
)
# Проверяем, затронута ли хоть одна строка
if cursor.rowcount == 0:
raise Exception("Запись не обновлена. Проверьте уникальность кода/названия.")
# Принудительная фиксация прямо здесь
conn.commit()
print("[WORK LOGIC] Группа успешно обновлена в БД.")
except sqlite3.IntegrityError as e:
conn.rollback()
raise Exception(f"Нарушение целостности данных: {e}")
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
def delete_group(group_id: int):
with _get_cursor() as cursor:
cursor.execute("DELETE FROM groups WHERE id = ?", (group_id,))
#===================================================
Ниже идут файлы - доноры
#====================================================
# manager_metrics_inter.py - файл - донор
#====================================================
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import pyperclip # <-- ДОБАВИЛИ БИБЛИОТЕКУ
from docx import Document
from modules.manager.manager_metrics_work import (
get_all_indicators_by_group,
insert_indicator,
delete_indicator_by_code,
IndicatorDTO,
generate_unique_code,
save_docx_template
)
from modules.manager.manager_group_work import list_groups as group_list
from modules.utils.window_behavior import LevelTwoBehavior
class ManagerMetricsInter(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.parent_main = parent
self.group_names_to_codes = {}
behavior = LevelTwoBehavior()
behavior.setup_level_two(self)
self.title("Менеджер показателей")
self.geometry("600x450")
if not hasattr(self.parent_main, 'current_user') or not self.parent_main.current_user.get("login"):
messagebox.showwarning("Ошибка", "Сессия пользователя не найдена.")
self.destroy()
return
self.create_widgets()
self.update_group_combobox()
self.update_indicators_list()
self.update_idletasks()
screen_width = self.winfo_screenwidth()
screen_height = self.winfo_screenheight()
size = tuple(int(_) for _ in self.geometry().split('+')[0].split('x'))
x = screen_width // 2 - size[0] // 2
y = screen_height // 2 - size[1] // 2
self.geometry(f"+{x}+{y}")
# === СТРОКУ НИЖЕ МЫ УДАЛИЛИ! Теперь окно не крадёт фокус само ===
# self.name_entry.focus_set() <-- ЭТОЙ СТРОКИ БОЛЬШЕ НЕТ
# Оставляем глобальный бинд как страховку, но он теперь не мешает
self._setup_global_paste_hack()
def create_widgets(self):
top_panel = tk.Frame(self)
top_panel.pack(padx=10, pady=10, fill="x")
ttk.Label(top_panel, text="Группа:").pack(anchor="w")
self.group_var = tk.StringVar()
self.group_combobox = ttk.Combobox(top_panel, textvariable=self.group_var, state="readonly", width=30)
self.group_combobox.pack(fill="x", pady=5)
self.group_combobox.bind("<<ComboboxSelected>>", self.update_indicators_list)
ttk.Label(top_panel, text="Название показателя:").pack(anchor="w")
self.name_entry = tk.Text(top_panel, height=3, width=40)
self.name_entry.pack(fill="x", pady=5)
paste_button = tk.Button(top_panel, text="Вставить", command=self.paste_from_clipboard)
paste_button.pack()
# --- ДОБАВЬ ЭТУ СТРОКУ, ЕСЛИ ЕЁ НЕТ ---
self.paste_button = paste_button
buttons_frame = tk.Frame(top_panel)
buttons_frame.pack(fill="x", pady=5)
ttk.Button(buttons_frame, text="Добавить", command=self.add_indicator).pack(side="left", padx=2)
ttk.Button(buttons_frame, text="Удалить", command=self.delete_indicator).pack(side="left", padx=2)
ttk.Button(buttons_frame, text="Экспорт в Word", command=self.print_to_docx).pack(side="left", padx=5, pady=5)
middle_panel = tk.Frame(self)
middle_panel.pack(fill="both", expand=True, padx=10, pady=5)
scrollbar = ttk.Scrollbar(middle_panel)
scrollbar.pack(side="right", fill="y")
columns = ("number", "name", "code")
self.indicators_tree = ttk.Treeview(
middle_panel, columns=columns, show="headings", yscrollcommand=scrollbar.set, selectmode="browse"
)
self.indicators_tree.heading("number", text="№"); self.indicators_tree.column("number", width=50, anchor="center")
self.indicators_tree.heading("name", text="Название"); self.indicators_tree.column("name", width=300)
self.indicators_tree.heading("code", text="Код"); self.indicators_tree.column("code", width=150)
self.indicators_tree.pack(fill="both", expand=True, side="left")
scrollbar.config(command=self.indicators_tree.yview)
def update_group_combobox(self):
groups = group_list()
self.group_names_to_codes.clear()
for group in groups:
self.group_names_to_codes[group.name] = getattr(group, 'code', '')
group_names = [g.name for g in groups]
self.group_combobox["values"] = group_names
if group_names:
self.group_combobox.current(0)
self.update_indicators_list()
def update_indicators_list(self, event=None):
selected_group_name = self.group_var.get()
selected_group_code = self.group_names_to_codes.get(selected_group_name)
for item in self.indicators_tree.get_children():
self.indicators_tree.delete(item)
indicators = []
if selected_group_code:
try:
indicators = get_all_indicators_by_group(selected_group_code)
except Exception as e:
print(f"[METRICS WORK] Заглушка данных: {e}")
indicators = [IndicatorDTO(code=f"{selected_group_code}-TEST", name="Тестовый показатель")]
for idx, ind in enumerate(indicators, start=1):
val_name = getattr(ind, 'name', '--')
val_code = getattr(ind, 'code', '--')
self.indicators_tree.insert("", "end", iid=val_code, values=(f"{idx}.", val_name, val_code))
self._keep_on_top()
# --- СТАРЫЙ МЕТОД КНОПКИ ОСТАВЛЯЕМ ДЛЯ ЗАПАСА ---
def paste_from_clipboard(self):
try:
clipboard_text = self.clipboard_get()
self.name_entry.insert(tk.END, clipboard_text)
except tk.TclError:
pass
# --- НОВЫЙ ГЛАВНЫЙ МЕТОД ВСТАВКИ ЧЕРЕЗ PYPЕРCLIP ---
def _paste_via_pyperclip(self):
"""Универсальный метод вставки текста из буфера."""
try:
text = pyperclip.paste()
if text:
self.name_entry.insert(tk.END, text)
except pyperclip.PyperclipException:
messagebox.showerror("Ошибка доступа", "Не удалось получить доступ к буферу обмена.\nПопробуйте закрыть другие программы, использующие его.")
except Exception as e:
print(f"[DEBUG] Unexpected error in paste: {e}")
def add_indicator(self):
group_name = self.group_var.get()
group_code = self.group_names_to_codes.get(group_name)
indicator_name = self.name_entry.get("1.0", tk.END).rstrip("\n").strip()
if not group_code or not indicator_name:
messagebox.showwarning("Ошибка", "Выберите группу и введите название!")
return
new_code = generate_unique_code(group_code)
try:
insert_indicator(new_code, indicator_name, group_code)
messagebox.showinfo("Готово", "Индикатор добавлен.")
self.update_indicators_list()
self.name_entry.delete("1.0", tk.END)
except Exception as e:
messagebox.showerror("Ошибка", str(e))
self._keep_on_top()
def edit_indicator(self, event):
selected_items = self.indicators_tree.selection()
if not selected_items:
return
code = selected_items[0]
current_name = ""
item_values = self.indicators_tree.item(code)["values"]
if len(item_values) > 1:
current_name = item_values[1]
edit_win = tk.Toplevel(self)
edit_win.title("Редактировать")
edit_win.geometry("350x150")
x = self.winfo_pointerx()
y = self.winfo_pointery()
edit_win.geometry(f"+{x}+{y}")
ttk.Label(edit_win, text="Новое название:").pack(pady=5)
new_name_entry = tk.Text(edit_win, height=2, width=40)
new_name_entry.insert(tk.END, current_name)
new_name_entry.pack(pady=5)
ttk.Button(edit_win, text="Сохранить",
command=lambda: self.save_changes(code, new_name_entry.get("1.0", tk.END), edit_win)).pack(pady=5)
def save_changes(self, code, new_name_raw, edit_win):
new_name = new_name_raw.rstrip("\n").strip()
if not new_name:
messagebox.showwarning("Ошибка", "Название не может быть пустым.")
return
messagebox.showinfo("Готово", "Изменения сохранены (заглушка).")
self.update_indicators_list()
edit_win.destroy()
self._keep_on_top()
def delete_indicator(self):
selected_items = self.indicators_tree.selection()
if not selected_items:
messagebox.showwarning("Ошибка", "Выберите индикатор для удаления.")
return
code = selected_items[0]
if messagebox.askyesno("Подтверждение", f"Удалить индикатор?"):
try:
delete_indicator_by_code(code)
messagebox.showinfo("Готово", "Удалено успешно.")
self.update_indicators_list()
except Exception as e:
messagebox.showerror("Ошибка", str(e))
self._keep_on_top()
def print_to_docx(self):
"""
Экспортирует показатели в Word с автоматическим именем файла:
[НазваниеГруппы]_[Год-Месяц-День].docx
"""
group_name = self.group_var.get()
if not group_name:
messagebox.showwarning("Ошибка", "Выберите группу.")
return
group_code = self.group_names_to_codes.get(group_name)
indicators = get_all_indicators_by_group(group_code)
if not indicators:
messagebox.showwarning("Ошибка", "В группе нет показателей для экспорта.")
return
# --- ФОРМИРОВАНИЕ ИМЕНИ ФАЙЛА ---
from datetime import date
# Получаем текущую дату
today = date.today()
# Форматируем дату как ГГГГ-ММ-ДД (например, 2026-07-25)
date_str = today.strftime("%Y-%m-%d")
# Убираем пробелы из названия группы, чтобы не было проблем с путями
safe_group_name = group_name.replace(' ', '_')
# Собираем финальное имя файла
filename = f"{safe_group_name}_{date_str}.docx"
path = None
try:
# Открываем диалог сохранения С АВТОЗАПОЛНЕННЫМ именем
path = filedialog.asksaveasfilename(
initialfile=filename,
defaultextension=".docx",
filetypes=[("Word files", "*.docx"), ("All files", "*.*")]
)
# Если пользователь нажал "Отмена" — путь будет пустым ('')
if not path:
return
save_docx_template(group_name, indicators, path)
messagebox.showinfo("Успех", f"Файл успешно сохранён:\\n{path}")
except Exception as e:
messagebox.showerror("Ошибка записи", f"Не удалось сохранить файл:\\n{e}")
def on_close(self):
self.destroy()
def _change_gain(self, delta):
try:
val = int(self.gain_var.get())
new_val = max(0, val + delta)
self.gain_var.set(str(new_val))
except ValueError:
self.gain_var.set("1.0")
def _keep_on_top(self):
self.lift()
self.focus_force()
def _setup_global_paste_hack(self):
"""
Метод-сторож: постоянно следит за буфером обмена.
Если текст появился, пока активно наше поле — вставляем его.
"""
self._clipboard_last_check = "" # Храним последнее значение
def check_clipboard():
try:
current_text = pyperclip.paste()
# Проверяем три условия:
# 1. Текст в буфере изменился (мы что-то скопировали)
# 2. Текст непустой
# 3. Наше поле name_entry СЕЙЧАС имеет фокус
if (current_text != self._clipboard_last_check and
current_text and
self.focus_get() == self.name_entry):
# Вставляем текст напрямую
self.name_entry.insert(tk.END, current_text)
# Обновляем "память", чтобы не вставлять одно и то же дважды
self._clipboard_last_check = current_text
except Exception:
pass # Игнорируем ошибки доступа к буферу
# === ВАЖНО: запускаем проверку снова через 100 миллисекунд ===
self.after(100, check_clipboard)
# Запускаем первую итерацию цикла при создании окна
check_clipboard()
#===================================================
Ниже идут файлы - доноры
#====================================================
# manager_metrics_work.py - файл - донор
#====================================================
# -*- coding: utf-8 -*-
import os
import sqlite3
from typing import List
from contextlib import contextmanager
from modules.data.database_module import get_db_connection
class IndicatorDTO:
def __init__(self, id: int, code: str, name: str, group_code: str):
self.id = id
self.code = code
self.name = name
self.group_code = group_code
@contextmanager
def _get_cursor():
conn = get_db_connection()
try:
cursor = conn.cursor()
yield cursor
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def insert_indicator(code: str, name: str, group_code: str) -> IndicatorDTO:
print(f"[WORK LOGIC] INSERT | Код: {code}, Имя: {name}")
with _get_cursor() as cursor:
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
)
''')
cursor.execute("INSERT INTO indicators (code, name, group_code) VALUES (?, ?, ?)",
(code, name, group_code))
new_id = cursor.lastrowid
return IndicatorDTO(id=new_id, code=code, name=name, group_code=group_code)
def delete_indicator_by_code(code: str):
print(f"[WORK LOGIC] DELETE | Код: {code}")
with _get_cursor() as cursor:
cursor.execute("DELETE FROM indicators WHERE code = ?", (code,))
def get_all_indicators_by_group(group_code: str) -> List[IndicatorDTO]:
print(f"[WORK LOGIC] SELECT | Группа: {group_code}")
with _get_cursor() as cursor:
cursor.execute("SELECT id, code, name, group_code FROM indicators WHERE group_code = ?", (group_code,))
rows = cursor.fetchall()
return [IndicatorDTO(id=row[0], code=row[1], name=row[2], group_code=row[3]) for row in rows]
def generate_unique_code(group_code: str) -> str:
"""
Генерирует уникальный код показателя внутри группы по шаблону ГРУППА-N.
Например: Эко-1, Эко-2, ИТ-1.
"""
with _get_cursor() as cursor:
cursor.execute("""
SELECT MAX(CAST(substr(code, instr(code, '-') + 1) AS INTEGER))
FROM indicators WHERE code LIKE ?
""", (f"{group_code}-%",))
row = cursor.fetchone()[0]
if row is None:
next_num = 1
else:
next_num = row + 1
return f"{group_code}-{next_num}"
def save_docx_template(group_name: str, indicators: List[IndicatorDTO], path: str) -> bool:
"""
Альбомный лист (А4), поля 3см | 1см.
Шрифт таблицы: Times New Roman, 11пт, обычный.
Ширина колонок: 3см | 3см | 19.7см.
"""
print(f"[WORK LOGIC] Вызов: save_docx_template | Путь: {path} | Группа: {group_name}")
if not indicators:
return False
try:
import win32com.client as win32
word = win32.gencache.EnsureDispatch('Word.Application')
word.Visible = False
word.DisplayAlerts = False
doc = word.Documents.Add()
# === 1. АЛЬБОМНЫЙ ФОРМАТ И ПОЛЯ ===
section = doc.Sections(1)
# Меняем ориентацию на 1 (wdOrientLandscape)
section.PageSetup.Orientation = 1
# Поля: Лево 3см, Право 1см (~28.35 пунктов)
section.PageSetup.LeftMargin = 85
section.PageSetup.RightMargin = 28
section.PageSetup.TopMargin = 57
section.PageSetup.BottomMargin = 57
# Заголовок документа
para = doc.Paragraphs.Add()
para.Range.Text = f'Показатели группы: {group_name}'
para.Range.Font.Name = 'Times New Roman'
para.Range.Font.Size = 16
para.Range.Bold = 1
para.Alignment = 1 # По центру
para.Range.InsertParagraphAfter()
# Таблица
table = doc.Tables.Add(para.Range, len(indicators) + 1, 3)
# === 2. НОВЫЕ РАЗМЕРЫ КОЛОНОК ДЛЯ АЛЬБОМА ===
table.Columns(1).SetWidth(CentimetersToPoints(3), 0)
table.Columns(2).SetWidth(CentimetersToPoints(3), 0)
table.Columns(3).SetWidth(CentimetersToPoints(19.7), 0) # ~19.7 см остаток
# === 3. СТИЛЬ ТАБЛИЦЫ (Шрифт 11, без Жирного) ===
hdr_cells = table.Rows(1)
# Заголовки (оставим их жирными для акцента, если нужно убрать везде - поставь Bold=0)
titles = ['№ п/п', 'Код', 'Наименование']
for i in range(1, 4):
cell = hdr_cells.Cells(i).Range
cell.Text = titles[i-1]
cell.Font.Name = 'Times New Roman'
cell.Font.Size = 11
cell.Font.Bold = 1 # Оставил 1 для заголовков, поменяй на 0, если не надо
cell.ParagraphFormat.Alignment = 1 # Центр
# Данные (строго 11pt, НЕ жирные)
for idx, ind in enumerate(indicators, start=1):
row = table.Rows(idx + 1)
# Колонка №
c1 = row.Cells(1).Range
c1.Text = str(idx)
c1.Font.Name = 'Times New Roman'
c1.Font.Size = 11
c1.Font.Bold = 0 # Обычный шрифт
c1.ParagraphFormat.Alignment = 1 # Центр
# Колонка Код
c2 = row.Cells(2).Range
c2.Text = ind.code
c2.Font.Name = 'Times New Roman'
c2.Font.Size = 11
c2.Font.Bold = 0 # Обычный шрифт
c2.ParagraphFormat.Alignment = 1 # Центр
# Колонка Наименование
c3 = row.Cells(3).Range
c3.Text = ind.name
c3.Font.Name = 'Times New Roman'
c3.Font.Size = 11
c3.Font.Bold = 0 # Обычный шрифт
c3.ParagraphFormat.Alignment = 0 # Слева
doc.SaveAs(path)
doc.Close(SaveChanges=False)
word.Quit()
return True
except Exception as e:
print(f"[WORK LOGIC FATAL] Ошибка при работе с живым Word: {repr(e)}")
return False
# Вспомогательная функция-конвертер (остаётся прежней)
def CentimetersToPoints(cm):
"""Конвертирует сантиметры в пункты (единицы измерения Word)."""
return cm * 28.35