Загрузка данных
# dukora/modules/manager/manager_metrics_panel.py
import customtkinter as ctk
from tkinter import ttk, messagebox, filedialog
import tkinter as tk
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
class ManagerMetricsPanel(ctk.CTkFrame):
def __init__(self, master):
super().__init__(master)
self.parent_main = master
self.group_names_to_codes = {}
# === ГЛАВНАЯ СЕТКА ОКНА ===
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1)
# Уменьшена доля высоты таблицы
self.grid_rowconfigure(0, weight=0) # Группа
self.grid_rowconfigure(1, weight=2) # Поле ввода (базовая единица высоты)
self.grid_rowconfigure(2, weight=0) # Кнопки действий
self.grid_rowconfigure(3, weight=1) # Таблица (теперь имеет такой же вес, но она сжата)
self.grid_rowconfigure(4, weight=0) # Нижние кнопки
# === 1. СТРОКА: ГРУППА ===
ctk.CTkLabel(self, text="Группа:").grid(row=0, column=0, sticky="w", padx=(10, 5), pady=(10, 0))
self.group_var = ctk.StringVar(value="")
self.group_combobox = ctk.CTkComboBox(self)
self.group_combobox.grid(row=0, column=0, columnspan=2, sticky="ew", padx=10, pady=(10, 0))
self.group_combobox.configure(state="readonly")
# === 2. СТРОКА: НАЗВАНИЕ ПОКАЗАТЕЛЯ ===
input_frame = ctk.CTkFrame(self)
# Увеличено пространство снизу для отделения от кнопок
input_frame.grid(row=1, column=0, columnspan=2, sticky="ew", padx=10, pady=(5, 10))
input_frame.grid_columnconfigure(0, weight=1)
input_frame.grid_columnconfigure(1, weight=0)
ctk.CTkLabel(input_frame, text="Название показателя:", anchor="w").grid(row=0, column=0, sticky="w")
# Было height=3, стало height=4 (на 1 строчку выше)
self.name_entry = ctk.CTkTextbox(input_frame, height=3)
self.name_entry.grid(row=0, column=0, sticky="ew", padx=(0, 5), pady=5)
paste_btn = ctk.CTkButton(input_frame, text="Вставить", fg_color="#2B2B2B", hover_color="#3C3C3C")
paste_btn.grid(row=0, column=1, sticky="nsew", padx=(5, 0), pady=5)
self.name_entry.bind("<Button-3>", self._paste_via_pyperclip)
# === 3. СТРОКА: КНОПКИ ДЕЙСТВИЙ ===
action_frame = ctk.CTkFrame(self)
action_frame.grid(row=2, column=0, columnspan=2, sticky="ew", padx=10, pady=5)
for i in range(3):
action_frame.grid_columnconfigure(i, weight=1, uniform="actions")
ctk.CTkButton(action_frame, text="Добавить", fg_color="#3B82F6", command=self.add_indicator).pack(side="left", fill="both", expand=True, padx=2, pady=2)
ctk.CTkButton(action_frame, text="Редактировать", fg_color="#10B981", command=self.edit_indicator).pack(side="left", fill="both", expand=True, padx=2, pady=2)
ctk.CTkButton(action_frame, text="Удалить", fg_color="#EF4444", command=self.delete_indicator).pack(side="left", fill="both", expand=True, padx=2, pady=2)
# === 4. СТРОКА: ТАБЛИЦА ===
tree_container = ctk.CTkFrame(self)
tree_container.grid(row=3, column=0, columnspan=2, sticky="nsew", padx=10, pady=(0, 10))
tree_container.grid_columnconfigure(0, weight=1)
tree_container.grid_rowconfigure(0, weight=1)
columns = ("number", "name", "code")
self.indicators_tree = ttk.Treeview(tree_container, columns=columns, show="headings", selectmode="browse")
self.indicators_tree.pack(fill="both", expand=True, side="left")
scrollbar = tk.Scrollbar(tree_container, orient="vertical", command=self.indicators_tree.yview)
scrollbar.pack(side="right", fill="y")
self.indicators_tree.configure(yscrollcommand=scrollbar.set)
# ПЕРЕНОС И ВЫСОТА СТРОК:
self.indicators_tree.heading("number", text="№"); self.indicators_tree.column("number", width=60, anchor="center", stretch=False)
self.indicators_tree.heading("name", text="Название"); self.indicators_tree.column("name", anchor="w")
self.indicators_tree.heading("code", text="Код"); self.indicators_tree.column("code", width=self._get_pixels_for_chars(10), anchor="w", stretch=False)
# === 5. СТРОКА: НИЖНИЕ КНОПКИ ===
bottom_frame = ctk.CTkFrame(self)
bottom_frame.grid(row=4, column=0, columnspan=2, sticky="e", padx=10, pady=(0, 10))
ctk.CTkButton(bottom_frame, text="Экспорт в WORD", fg_color="#2B2B2B", hover_color="#3C3C3C", command=self.print_to_docx).pack(side="right", padx=5)
# Инициализация данных
self.update_group_combobox()
self.update_indicators_list()
def _get_pixels_for_chars(self, char_count: int) -> int:
return char_count * 8 + 20
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.configure(values=group_names)
if group_names:
self.group_combobox.set(group_names[0])
self.update_indicators_list()
def update_indicators_list(self, event=None):
selected_group_name = self.group_combobox.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:
indicators = get_all_indicators_by_group(selected_group_code)
for idx, ind in enumerate(indicators, start=1):
val_name = getattr(ind, 'name', '--')
val_code = getattr(ind, 'code', '--')
# Перенос каждые 20 знаков (вместо 30), чтобы лучше влезать в половину ширины
wrapped_name = '\n'.join([val_name[i:i+20] for i in range(0, len(val_name), 20)])
self.indicators_tree.insert("", "end", iid=val_code, values=(f"{idx}.", wrapped_name, val_code))
def _paste_via_pyperclip(self, event=None):
try:
clipboard_text = pyperclip.paste()
if clipboard_text:
self.name_entry.insert("insert", clipboard_text)
except Exception:
pass
return "break"
def add_indicator(self):
group_name = self.group_combobox.get()
group_code = self.group_names_to_codes.get(group_name)
indicator_name = self.name_entry.get("1.0", "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", "end")
except Exception as e:
messagebox.showerror("Ошибка", str(e))
def edit_indicator(self):
selected = self.indicators_tree.selection()
if not selected:
messagebox.showwarning("Ошибка", "Выберите показатель."); return
code = selected[0]
item = self.indicators_tree.item(code)
current_name = item["values"][1].replace('\n', ' ')
self.name_entry.delete("1.0", "end")
self.name_entry.insert("1.0", current_name)
def delete_indicator(self):
selected = self.indicators_tree.selection()
if not selected:
messagebox.showwarning("Ошибка", "Выберите показатель."); return
code = selected[0]
if messagebox.askyesno("Подтверждение", f"Удалить показатель?"):
try:
delete_indicator_by_code(code)
messagebox.showinfo("Готово", "Удалено успешно.")
self.update_indicators_list()
self.name_entry.delete("1.0", "end")
except Exception as e:
messagebox.showerror("Ошибка", str(e))
def print_to_docx(self):
group_name = self.group_combobox.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
safe_group_name = group_name.replace(' ', '_')
filename = f"{safe_group_name}_{date.today().strftime('%Y-%m-%d')}.docx"
path = filedialog.asksaveasfilename(
initialfile=filename,
defaultextension=".docx",
filetypes=[("Word files", "*.docx"), ("All files", "*.*")]
)
if not path: return
try:
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()