Загрузка данных
# Dozor/gui/main_interface.py
import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageTk
import os
from modules.administrator.admin_ui import AdminPanel
class MainInterface(tk.Tk):
def __init__(self):
super().__init__()
self.current_user = {"login": None}
self.geometry("458x360")
self.resizable(False, False)
bg_color = "#EEEEEE"
self.configure(bg=bg_color)
main_frame = tk.Frame(self, bg=bg_color)
main_frame.pack(padx=1, pady=1, fill='both', expand=True)
button_width = 20
button_height = 2
button_font = ("Arial", 12, "bold")
for i in range(7):
main_frame.grid_rowconfigure(i, weight=0)
main_frame.grid_columnconfigure(0, minsize=210, weight=0)
main_frame.grid_columnconfigure(1, minsize=235, weight=0)
# --- ЛЕВАЯ ПАНЕЛЬ ---
left_buttons = [
("Администратор", self._show_admin_menu),
("Менеджер базы", self._show_manager_menu),
("Эксперты", self._show_experts_menu),
("Проект", self._show_project_menu),
("Прогнозирование", self._show_forecast_menu)
]
for row_index, (btn_text, command_func) in enumerate(left_buttons):
btn = tk.Button(
main_frame,
text=btn_text,
font=button_font,
width=button_width,
height=button_height,
anchor="w",
command=command_func
)
btn.grid(row=row_index, column=0, padx=5, pady=5, sticky="ew")
# --- ПРАВАЯ ПАНЕЛЬ (СОВА) ---
right_box = tk.Frame(main_frame, bg=bg_color, width=235, height=300)
right_box.grid(row=0, column=1, rowspan=6, pady=5, sticky="nsew", padx=(0, 5))
right_box.grid_propagate(False)
right_box.columnconfigure(0, weight=1)
picture_frame = tk.Frame(right_box, bg="#d9ead3", highlightbackground="gray",
highlightthickness=1, width=225, height=160)
picture_frame.place(relx=0.5, y=5, anchor="n")
picture_frame.grid_propagate(False)
current_dir = os.path.dirname(os.path.abspath(__file__))
image_path = os.path.join(current_dir, "images", "Сова1.png")
try:
original_img = Image.open(image_path)
target_w, target_h = 215, 130
img_ratio = original_img.width / original_img.height
box_ratio = target_w / target_h
if img_ratio > box_ratio:
new_w, new_h = target_w, int(target_w / img_ratio)
else:
new_w, new_h = int(target_h * img_ratio), target_h
resized_img = original_img.resize((new_w, new_h), resample=Image.LANCZOS)
self.tk_image = ImageTk.PhotoImage(resized_img)
label = tk.Label(picture_frame, image=self.tk_image, bg="#d9ead3", borderwidth=0)
label.image = self.tk_image
label.place(relx=0.5, rely=0.5, anchor='center', width=new_w, height=new_h)
except Exception as e:
print(f"[FATAL IMAGE ERROR]: {e}")
canvas = tk.Canvas(picture_frame, width=225, height=160, bg="red", highlightthickness=0)
canvas.create_text(112, 80, text="IMG\nFAIL", fill="white")
canvas.place(x=0, y=0, relwidth=1, relheight=1)
spacer = tk.Frame(right_box, bg=bg_color, height=1)
spacer.grid(row=0, column=0, pady=(170, 10), sticky="ew")
upper_padding = tk.Frame(right_box, bg=bg_color, height=3)
upper_padding.grid(row=1, column=0)
btn6 = tk.Button(right_box, text="Руководство", font=button_font, width=button_width, height=button_height)
btn6.grid(row=2, column=0, ipadx=10, pady=(0, 5), sticky="ew")
btn7 = tk.Button(right_box, text="О программе", font=button_font, width=button_width, height=button_height)
btn7.grid(row=3, column=0, ipadx=10, pady=(5, 15), sticky="ew")
footer_frame = tk.Frame(main_frame, bg="#0078D7")
footer_frame.grid(row=6, column=0, columnspan=2, sticky="ew", pady=(10, 0))
footer_label = tk.Label(footer_frame, text="Copyright © Минск, НИИ, 2027 год", fg="#FF8C00",
bg="#0078D7", font=("Arial", 12, "bold"))
footer_label.pack(expand=True, ipady=4)
# --- КОНТЕЙНЕРЫ ДЛЯ ПОДМЕНЮ ---
self.menu_container = tk.Frame(main_frame, bg=bg_color)
self.menu_container.grid(row=0, column=0, rowspan=6, columnspan=2, sticky="nsew")
self.menu_container.grid_rowconfigure(0, weight=1)
self.menu_container.grid_columnconfigure(0, weight=1)
# Убираем старые заглушки, оставляем только Админа и Менеджера
self.admin_panel = None
self.manager_panel = None
self._current_menu_visible = None
# --- МЕТОД ВОЗВРАТА НА ГЛАВНЫЙ ЭКРАН ---
def _show_main_screen(self):
self._hide_current_menu()
self.menu_container.grid_remove()
self.grid_columnconfigure(0, minsize=210, weight=0)
self.grid_columnconfigure(1, minsize=235, weight=0)
def _hide_current_menu(self):
print("[LOG] Скрываем текущее меню...")
# Добавили удаление фасада Менеджера базы
if hasattr(self, 'manager_panel') and self.manager_panel is not None:
self.manager_panel.destroy()
self.manager_panel = None
# Оставили удаление фасада Администратора (он уже был)
if hasattr(self, 'admin_panel') and self.admin_panel is not None:
self.admin_panel.destroy()
self.admin_panel = None
# Очистка контейнера с серыми заглушками (на всякий случай)
for child in self.menu_container.winfo_children():
child.destroy()
self._current_menu_visible = None
def _show_admin_menu(self):
print("[LOG] Нажата кнопка 'Администратор'.")
self._hide_current_menu()
try:
from modules.administrator.admin_ui import AdminPanel
self.admin_panel = AdminPanel(self, current_user=self.current_user)
self.admin_panel.place(relx=0, rely=0, relwidth=1, relheight=1)
self.admin_panel.lift()
self.update_idletasks()
self._current_menu_visible = 'admin'
except Exception as e:
print(f"[FATAL ERROR] Не удалось создать панель администратора: {e}")
def _show_manager_menu(self):
print("[LOG] Нажата кнопка 'Менеджер базы'.")
# Прячем всё старое одним махом
self._hide_current_menu()
try:
from modules.manager.manager_ui import ManagerPanel
# Создаем панель ПРЯМО В ГЛАВНОМ ОКНЕ (self), а не в menu_container
self.manager_panel = ManagerPanel(self, current_user=self.current_user)
# Кладем её НАД ВСЕМ (поверх левой панели, правой панели и картинки)
self.manager_panel.place(relx=0, rely=0, relwidth=1, relheight=1)
# Принудительно просим поднять Z-index именно этой панели
self.manager_panel.lift()
# Заставляем Tkinter перерисоваться СЕЙЧАС
self.update_idletasks()
self._current_menu_visible = 'manager'
print("[LOG] Фасад Менеджера успешно размещен.")
except Exception as e:
# Теперь мы точно увидим ошибку, если она есть
print(f"[FATAL CRASH IN MANAGER MENU] {e}")
# Остальные кнопки пока просто закрывают оверлей (возвращают к сове)
def _show_experts_menu(self):
self._show_main_screen()
def _show_project_menu(self):
self._show_main_screen()
def _show_forecast_menu(self):
self._show_main_screen()
def _show_guide_menu(self):
self._show_main_screen()
def _show_about_menu(self):
self._show_main_screen()
def switch_user_radical(self):
root_win = self.winfo_toplevel()
from modules.utils.app_state import disable_app
disable_app(root_win)
if hasattr(self, 'current_user'):
self.current_user = {"login": None}
if hasattr(self, '_child_windows'):
self._child_windows.clear()
try:
root_win.destroy()
except Exception:
pass
import sys, subprocess, os, shutil
script_path = os.path.abspath("main.py")
gui_python = shutil.which("pythonw.exe")
if not gui_python:
current_dir = os.path.dirname(sys.executable)
potential_path = os.path.join(current_dir, "pythonw.exe")
gui_python = potential_path if os.path.exists(potential_path) else sys.executable
try:
subprocess.Popen([gui_python, script_path], shell=True, creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
except Exception as e:
print(f"[FATAL] Не удалось запустить новую копию Dozor: {e}")
sys.exit(0)
# Dozor/modules/utils/window_behavior.py
import tkinter as tk
class LevelOneBehavior:
"""
Поведение Главного окна (Уровень 1). Применяется к объекту класса tk.Tk.
Движение работает ТОЛЬКО по панели заголовка (title_bar).
"""
def setup_level_one(self, target_window):
target_window.resizable(False, False)
# Ищем панель заголовка для перетаскивания главного окна
try:
self.drag_widget = target_window.children['title_bar']
except KeyError:
self.drag_widget = target_window
self.drag_widget.bind("<ButtonPress-1>", self.save_offset)
self.drag_widget.bind("<B1-Motion>", self.drag_window)
target_window.bind("<Double-Button-1>", lambda e: None)
if not hasattr(target_window, '_child_windows'):
target_window._child_windows = []
self._drag_start_x = 0
self._drag_start_y = 0
def save_offset(self, event):
self._drag_start_x = event.x_root - self.winfo_x()
self._drag_start_y = event.y_root - self.winfo_y()
def drag_window(self, event):
x = event.x_root - self._drag_start_x
y = event.y_root - self._drag_start_y
self.geometry(f"+{x}+{y}")
if hasattr(self, '_update_child_positions'):
self._update_child_positions(x, y)
def register_child_window(self, window, offset_x=0, offset_y=0):
self._child_windows.append((window, offset_x, offset_y))
def do_nothing(self, event=None):
pass
class LevelTwoBehavior:
"""
Поведение окон второго уровня (привязаны к родителю).
Окно центрируется над главным и едет строго вместе с ним.
"""
def setup_level_two(self, target_window):
width, height = int(target_window.winfo_reqwidth()), int(target_window.winfo_reqheight())
target_window.minsize(width, height)
target_window.resizable(True, True)
parent = target_window.master
p_x = parent.winfo_rootx()
p_y = parent.winfo_rooty()
p_w = parent.winfo_width()
p_h = parent.winfo_height()
win_w = target_window.winfo_reqwidth()
win_h = target_window.winfo_reqheight()
x = p_x + (p_w - win_w) // 2
y = p_y + (p_h - win_h) // 2
target_window.geometry(f"+{x}+{y}")
target_window.update_idletasks()
offset_x = target_window.winfo_rootx() - p_x
offset_y = target_window.winfo_rooty() - p_y
if not hasattr(parent, '_child_windows'):
parent._child_windows = []
parent.register_child_window(target_window, offset_x, offset_y)
target_window.protocol("WM_MINIMIZE_WINDOW", self.do_nothing)
target_window.protocol("WM_DELETE_WINDOW", target_window.destroy)
target_window.bind("<ButtonPress-1>", lambda e, tw=target_window: self.save_offset(e, tw))
target_window.bind("<B1-Motion>", lambda e, tw=target_window: self.drag_with_parent(e, tw, parent))
def do_nothing(self, event=None):
pass
def save_offset(self, event, window):
self._start_screen_x = event.x_root
self._start_screen_y = event.y_root
self._start_win_x = window.winfo_rootx()
self._start_win_y = window.winfo_rooty()
parent = window.master
self._start_parent_x = parent.winfo_rootx()
self._start_parent_y = parent.winfo_rooty()
def drag_with_parent(self, event, child_win, parent_win):
delta_x = event.x_root - self._start_screen_x
delta_y = event.y_root - self._start_screen_y
new_parent_x = self._start_parent_x + delta_x
new_parent_y = self._start_parent_y + delta_y
parent_win.geometry(f"+{new_parent_x}+{new_parent_y}")
offset_x = self._start_win_x - self._start_parent_x
offset_y = self._start_win_y - self._start_parent_y
target_child_x = new_parent_x + offset_x
target_child_y = new_parent_y + offset_y
child_win.geometry(f"+{target_child_x}+{target_child_y}")
class LevelThreeBehavior:
"""
Поведение окон третьего уровня (Менеджер групп).
Свободное перемещение.
Чтобы выделить текст, просто кликай мышкой.
Чтобы двигать окно, зажми ALT + ЛКМ.
"""
def setup_level_three(self, target_window):
width, height = int(target_window.winfo_reqwidth()), int(target_window.winfo_reqheight())
target_window.minsize(width, height)
target_window.resizable(True, True)
target_window.geometry("+0+0")
target_window.protocol("WM_MINIMIZE_WINDOW", self.do_nothing)
target_window.protocol("WM_DELETE_WINDOW", target_window.destroy)
# Снимаем старые бинды
target_window.unbind("<ButtonPress-1>")
target_window.unbind("<B1-Motion>")
# 1. Сохраняем точку старта. Передаем 'target_window' вторым аргументом через lambda
target_window.bind(
"<ButtonPress-1>",
lambda e, tw=target_window: self.save_offset(e, tw),
add='+'
)
# 2. Движение работает ТОЛЬКО если зажата клавиша Alt (маска 0x0004)
target_window.bind(
"<B1-Motion>",
lambda e, tw=target_window: self.drag_free(e, tw) if e.state & 0x0004 else "",
add='+'
)
def do_nothing(self, event=None):
pass
def save_offset(self, event, window):
# Запоминаем ГДЕ НА ЭКРАНЕ была нажата мышь
self._start_screen_x = event.x_root
self._start_screen_y = event.y_root
# Запоминаем, где В ЭТОТ МОМЕНТ было окно (его угол) ПО ЭКРАНУ
self._start_win_x = window.winfo_rootx()
self._start_win_y = window.winfo_rooty()
def drag_free(self, event, window):
dx = event.x_root - self._start_screen_x
dy = event.y_root - self._start_screen_y
new_x = self._start_win_x + dx
new_y = self._start_win_y + dy
window.geometry(f"+{new_x}+{new_y}")
# Dozor\modules\manager\manager_group_inter.py
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, messagebox
from modules.manager.manager_group_work import list_groups, add_group, edit_group, delete_group
from modules.utils.window_behavior import LevelThreeBehavior
class ManagerGroupInter(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.master = parent
# Оставляем только это для иерархии окон
self.transient(parent)
# Настройки окна
self.title("Менеджер групп")
self.geometry("600x450")
# LevelThreeBehavior сам вызовет .grab_set() внутри setup_level_three
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" # <-- Даем имя, которое ищет LevelThreeBehavior
# Кнопка закрытия внутри шапки (опционально, но удобно)
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)) # Уменьшил верхний отступ, т.к. есть шапка
# Поле 1: Название
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)
# Поле 2: Код (только чтение при редактировании)
ttk.Label(top_frame, text="Код:").grid(row=1, column=0, sticky="w")
self.code_entry = ttk.Entry(top_frame, width=30)
self.code_entry.grid(row=1, column=1, padx=5, pady=5)
# === ПОЛЕ 3: УСИЛЕНИЕ (SPINBOX - ТОЛЬКО ЧИСЛА) ===
ttk.Label(top_frame, text="Усиление:").grid(row=2, column=0, sticky="w")
self.gain_var = tk.StringVar(value="1.0") # Начальное значение
self.gain_entry = ttk.Spinbox(
top_frame,
from_=0.0, to=99.9, increment=0.1,
textvariable=self.gain_var,
width=30,
format="%.1f"
)
self.gain_entry.grid(row=2, column=1, padx=5, pady=5)
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", 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"]
try:
gain_val = float(gain.replace(',', '.')) if gain else None
except ValueError:
gain_val = None
return {"code": code, "name": name, "gain_group": gain_val}
def add_group(self):
name = self.name_entry.get().strip()
code = self.code_entry.get().strip()
gain_raw = self.gain_var.get().strip() # Берем из Spinbox
try:
gain_val = float(gain_raw.replace(',', '.')) if gain_raw else None
except ValueError:
messagebox.showwarning("Ошибка", "Усиление должно быть числом.")
return
if not name or not code:
messagebox.showwarning("Ошибка", "Заполните название и код.")
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"] # Код берем старый, менять нельзя!
new_name = self.name_entry.get().strip() or data["name"]
gain_raw = self.gain_var.get().strip()
current_gain = data.get("gain_group")
try:
new_gain = float(gain_raw.replace(',', '.')) if gain_raw else current_gain
except ValueError:
messagebox.showwarning("Ошибка", "Усиление должно быть числом.")
return
from modules.utils.app_state import disable_app
disable_app(self.master)
# Окно редактора открывается ПОД курсором мыши (без прыжков)
edit_win = tk.Toplevel(self.master)
edit_win.title("Подтверждение")
edit_win.geometry("350x150")
x = self.winfo_pointerx()
y = self.winfo_pointery()
edit_win.geometry(f"+{x}+{y}")
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: self._apply_edit(old_code, new_name, new_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, code, name, gain, win):
try:
edit_group(code, name, code, gain) # ID=code, Новый Code тоже code
messagebox.showinfo("Готово", "Группа обновлена.")
self.update_tree()
from modules.utils.app_state import enable_app
enable_app(self.master)
win.destroy()
except Exception as e:
messagebox.showerror("Ошибка БД", str(e))
def delete_group(self):
data = self.get_selected()
if not data:
messagebox.showwarning("Ошибка", "Выберите группу.")
return
if messagebox.askyesno("Подтверждение", f"Удалить группу {data['name']}?"):
try:
delete_group(data["code"])
messagebox.showinfo("Успех", "Группа удалена.")
self.update_tree()
self._clear_form()
except Exception as e:
messagebox.showerror("Ошибка БД", str(e))
def _clear_form(self):
self.name_entry.delete(0, tk.END)
self.code_entry.delete(0, tk.END)
self.gain_var.set("1.0") # Очищаем спинбокс до значения по умолчанию
def on_close(self):
self.destroy()
# Dozor/modules/manager/manager_group_work.py
# -*- 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:
"""Добавление группы. Пробрасывает IntegrityError при дублях."""
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):
"""Редактирование группы по ID."""
with _get_cursor() as cursor:
cursor.execute(
"UPDATE groups SET name = ?, code = ?, gain_group = ? WHERE id = ?",
(new_name, new_code, new_gain, group_id)
)
def delete_group(group_id: int):
"""Удаление группы по ID. Индикаторы удалятся каскадно через FK в другой таблице."""
with _get_cursor() as cursor:
cursor.execute("DELETE FROM groups WHERE id = ?", (group_id,))
def delete_group(group_code: str):
"""Удаление группы из реальной БД"""
print(f"[WORK LOGIC] Вызов: delete_group | Код: {group_code}")
conn = get_db_connection()
cursor = conn.cursor()
try:
cursor.execute("DELETE FROM groups WHERE code = ?", (group_code,))
conn.commit()
print("[SUCCESS] Группа удалена из БД.")
except Exception as e:
print(f"[DB ERROR] delete_group: {e}")
conn.rollback()
raise e
finally:
conn.close()
# Dozor\modules\manager\manager_metrics_inter.py
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from docx import Document
import random
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
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
root = self.master
if hasattr(root, 'register_child_window'):
try:
root.register_child_window(self, offset_x=20, offset_y=20)
except Exception:
pass
self.use_master_db = False
class FakeSession:
def query(self, *args, **kwargs):
class QueryObj:
def filter_by(self, **kw): return self
def all(self): return []
return QueryObj()
def close(self): pass
self.session = FakeSession()
self.group_names_to_codes = {}
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}")
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()
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)
scrollbar.config(command=self.indicators_tree.yview)
self.indicators_tree.bind("<Double-Button-1>", self.edit_indicator)
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))
def paste_from_clipboard(self):
try:
clipboard_text = self.parent_main.clipboard_get()
self.name_entry.insert(tk.END, clipboard_text)
except tk.TclError:
messagebox.showwarning("Буфер обмена", "Буфер обмена пуст.")
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))
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()
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))
def print_to_docx(self):
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
filename = f"{group_name.replace(' ', '_')}_indicators.docx"
path = None
try:
path = filedialog.asksaveasfilename(initialfile=filename, defaultextension=".docx",
filetypes=[("Word files", "*.docx"), ("All files", "*.*")])
except Exception:
path = "test_export.docx"
if path:
try:
doc = Document()
doc.add_heading(f'Показатели группы: {group_name}', 0)
test_data = [
type('obj', (object,), {'name': 'Пример 1', 'code': 'T-1'})(),
type('obj', (object,), {'name': 'Пример 2', 'code': 'T-2'})()
]
for idx, ind in enumerate(test_data, start=1):
doc.add_paragraph(f"{idx}. [{getattr(ind, 'code', '--')}] {getattr(ind, 'name', '--')}")
doc.save(path)
messagebox.showinfo("Успех", f"Файл сохранен:\n{path}")
except Exception as e:
messagebox.showerror("Ошибка", f"Docx Error: {e}")
def on_close(self):
self.destroy()
# Dozor/modules/manager/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_id(indicator_id: int):
print(f"[WORK LOGIC] DELETE | ID: {indicator_id}")
with _get_cursor() as cursor:
cursor.execute("DELETE FROM indicators WHERE id = ?", (indicator_id,))
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_sequential_code(group_code: str) -> str:
"""
Генерирует код вида ГРУППА-N, где N - следующий свободный порядковый номер в этой группе.
Например: Эко-1, Эко-2.
"""
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:
print(f"[WORK LOGIC] Вызов: save_docx_template | Путь: {path} | Группа: {group_name}")
# Проверяем данные
if not indicators:
print("[WORK LOGIC ERROR] Список показателей пуст.")
return False
try:
from docx import Document
# Создаем папку, если её нет
dir_path = os.path.dirname(path)
if dir_path and not os.path.exists(dir_path):
os.makedirs(dir_path, exist_ok=True)
doc = Document()
doc.add_heading(f'Группа: {group_name}', 0)
table = doc.add_table(rows=1, cols=3)
hdr_cells = table.rows[0].cells
hdr_cells[0].text = '№ п/п'
hdr_cells[1].text = 'Код'
hdr_cells[2].text = 'Наименование'
for idx, ind in enumerate(indicators, start=1):
cells = table.add_row().cells
cells[0].text = str(idx)
cells[1].text = ind.code
cells[2].text = ind.name
doc.save(path)
return True
except ImportError:
print("[WORK LOGIC ERROR] Библиотека python-docx не установлена. Выполните: pip install python-docx")
return False
except Exception as e:
print(f"[WORK LOGIC ERROR] Не удалось сохранить файл: {e}")
return False
# Dozor\modules\manager\manager_ui.py
# -*- coding: utf-8 -*-
import tkinter as tk
class ManagerPanel(tk.Frame):
"""
Главный фасад модуля "Менеджер базы".
Полный визуальный аналог AdminPanel из administrator/admin_ui.py.
"""
def __init__(self, master, current_user):
# Используем тот же светло-серый цвет фона, что и у Админа (#F5F5F5)
super().__init__(master, bg="#F5F5F5")
self.master_root = master
self.current_user = current_user
print("[DEBUG] ManagerPanel создан.")
# Настраиваем сетку фасада (внутри него будут хедер и тело)
self.grid_rowconfigure(0, weight=1) # Хедер (фиксированный размер)
self.grid_rowconfigure(1, weight=5) # Основное тело (растягивается)
self.grid_columnconfigure(0, weight=1)
self._create_header()
self._create_body()
def _create_header(self):
"""Верхняя полоса с заголовком и кнопкой назад."""
header = tk.Frame(self, bg="#34495e", height=50) # Высота 50px как у админа
header.grid(row=0, column=0, sticky="ew")
title = tk.Label(header, text="Менеджер базы", bg="#34495e", fg="white", font=("Arial", 16))
title.pack(side="left", padx=20, pady=10)
back_btn = tk.Button(header, text="← Назад", command=self.destroy, bg="#7f8c8d", fg="white")
back_btn.pack(side="right", padx=20, pady=10)
def _create_body(self):
"""Центральная часть с двумя большими кнопками."""
body = tk.Frame(self, bg="white") # Белый фон рабочей области
body.grid(row=1, column=0, sticky="nsew", padx=30, pady=30)
# Делим тело на две равные колонки
body.grid_columnconfigure(0, weight=1)
body.grid_columnconfigure(1, weight=1)
group_btn = tk.Button(body, text="Менеджер группы", font=("Arial", 14), width=18, height=2,
command=self.open_group_module,
bg="#3498db", fg="white", relief="raised", bd=2)
group_btn.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
metrics_btn = tk.Button(body, text="Менеджер\nпоказателей", font=("Arial", 14), width=18, height=3,
command=self.open_metrics_module,
bg="#27ae60", fg="white", relief="raised", bd=2, wraplength=120)
metrics_btn.grid(row=0, column=1, padx=10, pady=10, sticky="nsew")
def open_group_module(self):
"""Открывает рабочее окно менеджера групп."""
from modules.utils.app_state import disable_app, enable_app
disable_app(self.master_root)
try:
from modules.manager.manager_group_inter import ManagerGroupInter
win = ManagerGroupInter(self.master_root)
if hasattr(self.master_root, 'register_child_window'):
self.master_root.register_child_window(win, offset_x=20, offset_y=20)
self.master_root.wait_window(win)
except Exception as e:
print(f"[FATAL] Ошибка запуска Менеджера Групп: {e}")
finally:
enable_app(self.master_root)
def open_metrics_module(self):
"""Открывает рабочий редактор индикаторов"""
print("[ACTION] Открытие Менеджера Показателей...")
from modules.utils.app_state import disable_app, enable_app
# 1. Сначала блокируем главное окно
disable_app(self.master_root)
try:
# 2. Импортируем ваш рабочий класс
from modules.manager.manager_metrics_inter import ManagerMetricsInter
# 3. Создаем экземпляр окна
metrics_win = ManagerMetricsInter(self.master_root)
# === БЕЗОПАСНАЯ РЕГИСТРАЦИЯ ОКНА (Защита №1) ===
root = self.master_root
if root and hasattr(root, 'winfo_exists') and root.winfo_exists():
register_method = getattr(root, 'register_child_window', None)
if callable(register_method):
try:
register_method(metrics_win, offset_x=20, offset_y=20)
except Exception as e:
print(f"[UI] Ошибка внутри метода register: {e}")
else:
print("[UI INFO] Метод register_child_window не найден у Root.")
else:
print("[UI WARNING] Главное окно недоступно для регистрации.")
# 4. Ждем закрытия рабочего окна
self.master_root.wait_window(metrics_win)
except Exception as e:
print(f"[FATAL] Ошибка запуска Менеджера Показателей: {e}")
from tkinter import messagebox
messagebox.showerror("Ошибка", f"Не удалось открыть модуль:\\n{e}")
finally:
# 5. Обязательно разблокируем интерфейс в любом случае
enable_app(self.master_root)