Загрузка данных
# ====================================================
# Файл №1: Dozor/main.py
# - запускающий файл, лежит в корне приложения в папке Dozor
# ====================================================
# ---*--- Dozor/main.py ---*---
# -*- coding: utf-8 -*-
import sys
import tkinter as tk
from tkinter import ttk
from gui.main_interface import MainInterface
from modules.data.database_module import init_database
from modules.utils.app_state import disable_app, enable_app
import ctypes # Импортируем для работы с WinAPI
try:
from gui.auth_interface import AuthInterface
except ImportError:
print("[FATAL] Не найден файл gui/auth_interface.py")
sys.exit(1)
init_database()
def main():
app = MainInterface()
# Устанавливаем временный базовый заголовок
app.title("ДОЗОР -- Heri-Hodie-Cras")
disable_app(app)
screen_w = app.winfo_screenwidth()
screen_h = app.winfo_screenheight()
win_w, win_h = 458, 360
pos_x = int((screen_w / 2) - (win_w / 2))
pos_y = int((screen_h / 2) - (win_h / 2))
app.geometry(f"{win_w}x{win_h}+{pos_x}+{pos_y}")
app.deiconify()
app.lift()
app.focus_force()
current_user = {"login": None}
def do_login(username, role_from_db):
if username and role_from_db:
current_user["login"] = username
enable_app(app)
auth_win.destroy()
app.current_user = {
"login": username,
"role": role_from_db
}
# Показываем интерфейс главного меню
app._show_main_screen()
# === WINAPI ХАК: Принудительная смена заголовка ОС ===
try:
# Получаем HWND (хэндл/номер окна) нашего приложения
hwnd = app.winfo_id()
# Формируем правильную строку (важно кодировка!)
new_title = f"ДОЗОР -- {username} ({role_from_db})"
# Вызываем функцию SetWindowTextA из user32.dll
ctypes.windll.user32.SetWindowTextA(hwnd, new_title.encode('utf-8'))
except Exception as api_err:
print(f"[WARNING] Не удалось сменить заголовок через WinAPI: {api_err}")
# Если WinAPI упал, пробуем стандартный метод (на всякий случай)
app.title(new_title)
else:
from tkinter import messagebox
messagebox.showwarning("Ошибка", "Данные авторизации некорректны")
try:
auth_win = AuthInterface(app, on_login_click=do_login)
auth_win.transient(app)
auth_win.grab_set()
app.wait_window(auth_win)
except Exception as e:
print(f"[CRITICAL ERROR] Failed to initialize Auth Window: {e}")
app.destroy()
return
if not current_user["login"]:
app.destroy()
return
app.mainloop()
if __name__ == "__main__":
main()
# ====================================================
# Файл №2: Dozor/gui/main_interface_old.py
# - старый файл, который подвергается рефакторингу.
# - это файл донор для организации АДМИНИСТРАТОР
# ====================================================
# ---*--- Dozor/gui/main_interface_old.py ---*---
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageTk
import os
from gui.auth_interface import AuthInterface
from modules.administrator.admin_user_inter import AdminUserInter
from modules.utils.window_behavior import LevelOneBehavior
from modules.administrator.admin_db_inter import AdminDBInter
from modules.project.project_create_inter import ProjectCreateInterface
from modules.project.project_edit_inter import ProjectEditInterface
from modules.project.project_create_work import ProjectCreateController
from modules.project.services.project_service import ProjectService
def open_window(title):
print(f"[DEBUG] Вызван универсальный open_window для: {title}")
temp_win = tk.Toplevel()
temp_win.title(title)
temp_win.geometry("300x150")
ttk.Label(temp_win, text=f"Интерфейс '{title}'", font=("Arial", 14)).pack(pady=20)
ttk.Button(temp_win, text="Закрыть", command=temp_win.destroy).pack(pady=10)
class MainInterface(tk.Tk):
def __init__(self):
super().__init__()
# Флаг-предохранитель от ошибок winfo при смене пользователя
self._ui_blocked_by_logout = False
# Текущий пользователь (единый источник истины)
self.current_user = {"login": None}
behavior = LevelOneBehavior()
behavior.setup_level_one(self)
self.title("ДОЗОР -- Heri-Hodie-Cras")
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)
self.active_popup = None
try:
from modules.project.project_create_work import ProjectCreateController
from modules.data.database_module import get_db_connection
db_conn = get_db_connection()
self.manager = ProjectCreateController(db_conn)
except Exception as e:
print(f"[FATAL] Не удалось инициализировать менеджер проектов: {e}")
self.manager = None
self.bind("<Button-1>", self.close_popup_global)
left_texts = [
["Администратор", [("Пользователи", "users"), ("Базы данных", "db")]],
["Менеджер базы", [("Менеджер группы", "group"), ("Менеджер показателей", "metrics")]],
["Эксперты", [("Создание файлов", "create_files"), ("Агрегирование файлов", "aggregate")]],
["Проект", [("Создать", "new"), ("Редактировать", "edit")]],
["Прогнозирование", []]
]
# --- ЛЕВАЯ ПАНЕЛЬ С УТОЧНЕНИЕМ ДОСТУПА ---
for row_index, item in enumerate(left_texts):
btn_text = item[0]
def execute_button(title=btn_text, index=row_index):
if title == "Администратор":
user_role = getattr(self, 'current_user', {}).get("role")
clean_role = str(user_role).strip().lower() if user_role else ""
if clean_role != "администратор":
from tkinter import messagebox
answer = messagebox.askyesno(
"Требуются права администратора",
f"У вашей учетной записи ('{clean_role.capitalize()}') недостаточно прав.\n\nХотите сменить пользователя и войти как Администратор?"
)
if answer:
self.switch_user_radical()
return
self.show_dropdown(title, index)
btn = tk.Button(
main_frame,
text=btn_text,
font=button_font,
width=button_width,
height=button_height,
anchor="w",
command=execute_button
)
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.bind("<B1-Motion>", self._on_drag_main_window)
self._drag_start_x = None
self._drag_start_y = None
def _on_drag_main_window(self, event):
if self._drag_start_x is None:
self._drag_start_x = event.x_root
self._drag_start_y = event.y_root
return
delta_x = event.x_root - self._drag_start_x
delta_y = event.y_root - self._drag_start_y
if abs(delta_x) > 2 or abs(delta_y) > 2:
if self.active_popup and self.active_popup.winfo_exists():
geo = self.active_popup.geometry()
parts = geo.split('+')
if len(parts) == 3:
new_x = int(parts[1]) + delta_x
new_y = int(parts[2]) + delta_y
self.active_popup.geometry(f"+{new_x}+{new_y}")
self._update_child_positions(event.x_root - self.winfo_rootx(), event.y_root - self.winfo_rooty())
self._drag_start_x = event.x_root
self._drag_start_y = event.y_root
def show_dropdown(self, menu_name, base_row):
from modules.utils.app_state import disable_app, enable_app
if self.active_popup is not None:
if hasattr(self.active_popup, '_child_windows'):
for child_data in list(self.active_popup._child_windows):
win, _, _ = child_data
try:
win.destroy()
except Exception:
pass
self.active_popup.destroy()
self.active_popup = None
root_win = self.winfo_toplevel()
data = {
"Администратор": [("Пользователи", "users"), ("Базы данных", "db")],
"Менеджер базы": [("Менеджер группы", "group"), ("Менеджер показателей", "metrics")],
"Эксперты": [("Создание файлов", "create_files"), ("Агрегирование файлов", "aggregate")],
"Проект": [("Создать", "new"), ("Редактировать", "edit")],
}.get(menu_name, [])
if not data:
return
x = self.winfo_rootx() + 5
y = self.winfo_rooty() + (base_row * 35) + 50
popup = tk.Toplevel(self)
popup.overrideredirect(True)
popup.geometry(f"+{x}+{y}")
self.active_popup = popup
popup_frame = tk.Frame(popup, bg="#F9F9F9", bd=1, relief="solid")
popup_frame.pack()
def make_command(action_title, target_popup=None):
def execute():
if target_popup:
target_popup.destroy()
disable_app(root_win)
app_instance = self
def handle_logout(restart=False):
global current_user_session, current_user
current_user_session = {"login": None}
current_user = {"login": None}
if hasattr(app_instance, 'current_user'):
app_instance.current_user = {"login": None}
print("[DEBUG] handle_logout вызван")
if restart:
# === КРИТИЧЕСКИ ВАЖНО: Ставим блок ПЕРЕД уничтожением окна ===
app_instance._ui_blocked_by_logout = True
disable_app(root_win)
root_win.withdraw()
login_callback = app_instance.do_login
new_auth = AuthInterface(root_win, on_login_click=login_callback)
root_win.wait_window(new_auth)
print(f"[DEBUG] Авторизация завершена. Текущий юзер: {current_user.get('login')}")
if current_user.get("login"):
root_win.deiconify()
enable_app(root_win)
role_display = current_user.get("role", "Без роли")
root_win.title(f"ДОЗОР -- {current_user['login']} ({role_display})")
# Разблокируем только после успешного входа
app_instance._ui_blocked_by_logout = False
else:
try:
root_win.destroy()
except Exception:
pass
else:
root_win.destroy()
if action_title == "Администратор":
user_role = getattr(self, 'current_user', {}).get("role")
clean_role = str(user_role).strip().lower() if user_role else ""
if clean_role != "администратор":
enable_app(root_win)
from tkinter import messagebox
messagebox.showwarning("Доступ запрещен", f"Раздел '{action_title}' доступен только пользователю с ролью Администратор.")
return
if action_title == "Пользователи":
users_win = AdminUserInter(root_win)
root_win.register_child_window(users_win, offset_x=20, offset_y=20)
root_win.wait_window(users_win)
# === ГЛАВНАЯ ПРОВЕРКА В ОКНЕ АДМИНКИ ===
# Если флаг блокировки стоит — значит, идет смена юзера, просто выходим.
if app_instance._ui_blocked_by_logout:
return
final_login = getattr(app_instance, 'current_user', {}).get("login")
if final_login is None:
print("[INFO] Пользователь инициировал смену через закрытие окна. Перезапуск...")
disable_app(root_win)
root_win.withdraw()
login_callback = app_instance.do_login
new_auth = AuthInterface(root_win, on_login_click=login_callback)
root_win.wait_window(new_auth)
after_login = getattr(app_instance, 'current_user', {}).get("login")
if after_login:
root_win.deiconify()
enable_app(root_win)
role_display = app_instance.current_user.get("role", "Без роли")
root_win.title(f"ДОЗОР -- {app_instance.current_user['login']} ({role_display})")
app_instance._ui_blocked_by_logout = False
else:
try:
root_win.destroy()
except Exception:
pass
return
else:
enable_app(root_win)
return
elif action_title == "Базы данных":
db_win = AdminDBInter(root_win)
root_win.register_child_window(db_win, offset_x=20, offset_y=20)
root_win.wait_window(db_win)
elif action_title == "Менеджер группы":
from modules.manager.manager_group_inter import ManagerGroupInter
win = ManagerGroupInter(root_win)
root_win.register_child_window(win, offset_x=20, offset_y=20)
win.lift()
elif action_title == "Менеджер показателей":
from modules.manager.manager_metrics_inter import ManagerMetricsInter
win = ManagerMetricsInter(root_win)
root_win.register_child_window(win, offset_x=20, offset_y=20)
win.lift()
elif action_title == "Создание файлов":
from modules.excel.excel_export_inter import ExpertExportInterface
export_win = ExpertExportInterface(root_win)
root_win.register_child_window(export_win, offset_x=20, offset_y=20)
export_win.lift()
elif action_title == "Агрегирование файлов":
from modules.excel.excel_aggregate_inter import ExpertAggregateInterface
agg_win = ExpertAggregateInterface(root_win)
root_win.register_child_window(agg_win, offset_x=20, offset_y=20)
agg_win.lift()
elif action_title == "Создать":
from modules.project.project_create_inter import ProjectCreateInterface
create_win = ProjectCreateInterface(root_win, self.manager)
root_win.register_child_window(create_win, offset_x=20, offset_y=20)
create_win.lift()
elif action_title == "Редактировать":
from modules.project.project_edit_inter import ProjectEditInterface
def on_project_selected(data_dict):
from modules.project.project_event_inter import ProjectEventWindow
event_win = ProjectEventWindow(manager=self.manager, selected_data=data_dict, root_window=root_win)
root_win.register_child_window(event_win, offset_x=20, offset_y=20)
event_win.lift()
edit_win = ProjectEditInterface(root_win, manager=self.manager, on_select_callback=on_project_selected)
root_win.register_child_window(edit_win, offset_x=20, offset_y=20)
edit_win.lift()
else:
open_window(action_title)
enable_app(root_win)
return execute
# --- ЦИКЛ СОЗДАНИЯ КНОПОК В ОКНЕ ---
for sub_text, action_key in data:
btn_command = make_command(sub_text)
def wrap_execute(cmd=None, pop_ref=popup, r_win=root_win):
if pop_ref and pop_ref.winfo_exists():
if hasattr(pop_ref, '_child_windows') and pop_ref._child_windows:
pop_ref.after(10, pop_ref.destroy)
else:
pop_ref.destroy()
# === ФИНАЛЬНЫЙ БАРЬЕР ДЛЯ ЛЯМБДА-ФУНКЦИИ ===
# Проверяем флаг перед тем, как вообще пытаться запустить команду cmd()
if cmd is not None and not r_win._ui_blocked_by_logout:
cmd()
else:
enable_app(r_win)
sub_btn = tk.Button(
popup_frame,
text=sub_text,
font=("Arial", 11),
width=20,
height=1,
anchor="w",
bd=0,
relief="flat",
cursor="hand2",
command=lambda c=btn_command: wrap_execute(c)
)
sub_btn.pack(fill="x", ipadx=5, pady=3)
sep = tk.Frame(popup_frame, bg="#E0E0E0", height=1)
sep.pack(fill="x")
def check_leave(e):
if not popup.winfo_exists():
return
mouse_x = e.x_root
mouse_y = e.y_root
pop_x = popup.winfo_rootx()
pop_y = popup.winfo_rooty()
pop_w = popup.winfo_width()
pop_h = popup.winfo_height()
safe_zone_x1 = pop_x - 30
safe_zone_y1 = pop_y - 30
safe_zone_x2 = pop_x + pop_w + 30
safe_zone_y2 = pop_y + pop_h + 30
if not (safe_zone_x1 <= mouse_x <= safe_zone_x2 and safe_zone_y1 <= mouse_y <= safe_zone_y2):
popup.destroy()
popup.bind("<Motion>", check_leave)
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)
def open_admin_users(self):
from modules.utils.app_state import disable_app, enable_app
root = self.winfo_toplevel()
if not root or not hasattr(root, 'winfo_exists') or not root.winfo_exists():
return
disable_app(root)
users_win = AdminUserInter(self)
self.wait_window(users_win)
enable_app(root)
def open_admin_dbs(self):
from modules.utils.app_state import disable_app, enable_app
root = self.winfo_toplevel()
if not root or not hasattr(root, 'winfo_exists') or not root.winfo_exists():
return
disable_app(root)
db_win = AdminDBInter(root)
self.wait_window(db_win)
enable_app(root)
def close_popup_global(self, event):
if self.active_popup is not None:
widget = event.widget
while widget:
if widget == self.active_popup:
return
widget = widget.master
self.active_popup.destroy()
self.active_popup = None
def _update_child_positions(self, new_x, new_y):
if hasattr(self, '_child_windows'):
for child_data in self._child_windows:
win, offset_x, offset_y = child_data
try:
win.geometry(f"+{new_x + offset_x}+{new_y + offset_y}")
except Exception:
pass
def register_child_window(self, window, offset_x=0, offset_y=0):
if not hasattr(self, '_child_windows'):
self._child_windows = []
for item in self._child_windows:
if item[0] == window:
return
self._child_windows.append((window, offset_x, offset_y))
if __name__ == "__main__":
app = MainInterface()
app.mainloop()
# ====================================================
# Файл №3: Dozor/modules/administrator/user_logic.py
# - старый файл, который подвергается рефакторингу.
# - это файл донор для организации АДМИНИСТРАТОР
# ====================================================
# ---*--- Dozor/modules/administrator/user_logic.py ---*---
# -*- coding: utf-8 -*-
import os
import sqlite3
import hashlib
import secrets
class UserLogic:
def __init__(self, db_path="db/users.db"):
self.db_path = db_path
if not os.path.exists(os.path.dirname(db_path)):
os.makedirs(os.path.dirname(db_path))
self._ensure_schema()
def _hash_password(self, password: str, salt: str) -> str:
return hashlib.sha256((salt + password).encode()).hexdigest()
def _generate_salt(self) -> str:
return secrets.token_hex(16)
def _ensure_schema(self):
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
# Проверяем структуру. Если колонки login нет — таблица старая.
cursor.execute("PRAGMA table_info(users)")
columns = [col[1] for col in cursor.fetchall()]
if "login" not in columns or len(columns) < 4:
print("[INFO] Структура users.db некорректна. Пересоздаем...")
cursor.execute("DROP TABLE IF EXISTS users")
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
salt TEXT NOT NULL,
role TEXT NOT NULL
)
''')
# Создаем дефолтного админа ТОЛЬКО если таблицы пуста
cursor.execute("SELECT COUNT(*) FROM users")
if cursor.fetchone()[0] == 0:
salt = secrets.token_hex(16)
pwd_hash = hashlib.sha256((salt + "admin").encode()).hexdigest()
cursor.execute(
"INSERT INTO users (login, password_hash, salt, role) VALUES (?, ?, ?, ?)",
("admin", pwd_hash, salt, "Администратор")
)
print("[INFO] Создан пользователь 'admin' с паролем 'admin'")
conn.commit()
def get_all_users(self):
try:
with sqlite3.connect(self.db_path) as conn:
return conn.execute("SELECT id, login, role FROM users ORDER BY id ASC").fetchall()
except Exception:
self._ensure_schema()
return [(1, "admin", "Администратор")]
def add_user(self, login: str, role: str, password: str):
valid_roles = ["Администратор", "Эксперт", "Пользователь"]
if role not in valid_roles:
raise ValueError(f"Недопустимая роль: {role}")
salt = self._generate_salt()
pwd_hash = self._hash_password(password, salt)
with sqlite3.connect(self.db_path) as conn:
conn.execute(
"INSERT INTO users (login, password_hash, salt, role) VALUES (?, ?, ?, ?)",
(login, pwd_hash, salt, role)
)
conn.commit()
def delete_user(self, user_id: int):
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM users WHERE id=?", (user_id,))
cursor.execute("UPDATE users SET id = id - 1 WHERE id > ?", (user_id,))
max_id = cursor.execute("SELECT MAX(id) FROM users").fetchone()[0] or 0
cursor.execute("UPDATE sqlite_sequence SET seq=? WHERE name='users'", (max_id,))
conn.commit()
def validate_credentials(self, login: str, password: str) -> bool:
"""
Проверяет пару логин/пароль по базе данных.
Возвращает True при успехе, False при неудаче.
"""
try:
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
# Ищем пользователя по логину
row = cursor.execute(
"SELECT password_hash, salt FROM users WHERE login=?", (login,)
).fetchone()
if not row:
return False # Пользователь не найден
stored_hash, salt = row
# Хэшируем введенный пароль с той же солью из базы
test_hash = hashlib.sha256((salt + password).encode()).hexdigest()
# Сравниваем хэши
return stored_hash == test_hash
except Exception as e:
print(f"[DB ERROR] Ошибка проверки пароля: {e}")
return False
# ====================================================
# Файл №4: Dozor/modules/administrator/login_dialog.py
# - старый файл, который подвергается рефакторингу.
# - это файл донор для организации АДМИНИСТРАТОР
# ====================================================
# ---*--- Dozor/modules/administrator/login_dialog.py - это новый адрес
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, messagebox
class LoginDialog(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
self.successful = False # Этот флаг проверяется в admin_user_inter.py
self.title("Авторизация")
self.geometry("300x150")
self.resizable(False, False)
main_frame = ttk.Frame(self, padding="15")
main_frame.pack(expand=True, fill="both")
ttk.Label(main_frame, text="Логин:").grid(row=0, column=0, sticky="w", pady=5)
self.login_entry = ttk.Entry(main_frame)
self.login_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
ttk.Label(main_frame, text="Пароль:").grid(row=1, column=0, sticky="w", pady=5)
self.pass_entry = ttk.Entry(main_frame, show="*")
self.pass_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
btn_frame = ttk.Frame(main_frame)
btn_frame.grid(row=2, columnspan=2, pady=15)
ttk.Button(btn_frame, text="Войти", command=self.try_login).pack(side="left", padx=5)
ttk.Button(btn_frame, text="Отмена", command=self.destroy).pack(side="right", padx=5)
main_frame.columnconfigure(1, weight=1)
def try_login(self):
# TODO: Здесь позже вы добавите проверку логина и пароля по базе данных users.db
# А пока просто закрываем окно как успешное, чтобы проверить цикл импорта
self.successful = True
self.destroy()
# ====================================================
# Файл №5: Dozor/modules/administrator/auth_interface.py - это новый адрес
# - старый файл, который подвергается рефакторингу.
# - это файл донор для организации АДМИНИСТРАТОР
# ====================================================
# ---*--- Dozor/modules/administrator/auth_interface.py ---*---
# -*- coding: utf-8 -*-
"""
Авторизация vFinal. С силовым выводом окна.
"""
import tkinter as tk
from tkinter import ttk
import ctypes
import sqlite3
class AuthInterface(tk.Toplevel):
def __init__(self, master, on_login_click=None):
super().__init__(master)
self.on_login_command = on_login_click
# --- БАЗОВЫЕ ПАРАМЕТРЫ ---
self.title("Авторизация")
self.resizable(False, False)
# Принудительно задаем размер ДО любых сеток
win_w, win_h = 300, 150
self.geometry(f"{win_w}x{win_h}")
# --- ЖЕСТКОЕ ПОЗИЦИОНИРОВАНИЕ ЧЕРЕЗ WINAPI ---
user32 = ctypes.windll.user32
screen_w = user32.GetSystemMetrics(0) # SM_CXSCREEN
screen_h = user32.GetSystemMetrics(1) # SM_CYSCREEN
pos_x = int((screen_w / 2) - (win_w / 2))
pos_y = int((screen_h / 2) - (win_h / 2))
# Форсируем позицию минуя логику Tkinter
self.geometry(f"+{pos_x}+{pos_y}")
# Принудительная отрисовка всех запросов ОС прямо сейчас
self.update_idletasks()
# --- ИНТЕРФЕЙС ТОЛЬКО ПОСЛЕ ТОГО, КАК ОКНО НАРИСОВАНО ---
frame = ttk.Frame(self, padding=20)
frame.pack(fill="both", expand=True)
ttk.Label(frame, text="Логин:").grid(row=0, column=0, sticky="w")
self.login_entry = ttk.Entry(frame, width=25)
self.login_entry.grid(row=0, column=1, pady=5)
ttk.Label(frame, text="Пароль:").grid(row=1, column=0, sticky="w")
self.password_entry = ttk.Entry(frame, show="*", width=25)
self.password_entry.grid(row=1, column=1, pady=5)
btn_frame = ttk.Frame(frame)
btn_frame.grid(row=2, column=0, columnspan=2, pady=15)
login_btn = ttk.Button(btn_frame, text="Войти", command=self._try_login)
login_btn.pack(side="left", padx=5)
exit_btn = ttk.Button(btn_frame, text="Выход", command=self.destroy)
exit_btn.pack(side="left", padx=5)
self.login_entry.focus()
# --- ЖЕСТКИЙ ЗАХВАТ ФОКУСА И ПОДНЯТИЕ НАД ВСЕМ ---
self.lift() # Поднимаем окно выше других
self.focus_force() # Принудительно забираем фокус ввода
self.grab_set() # Делаем окно модальным (блокируем всё остальное)
# Еще одна принудительная проверка перед захватом фокуса
self.update_idletasks()
def _try_login(self):
username = self.login_entry.get().strip()
password = self.password_entry.get().strip()
print(f"[AUTH DEBUG] Логин='{username}', Пароль есть: {len(password) > 0}")
if not username or not password:
from tkinter import messagebox
messagebox.showwarning("Ошибка", "Поля не могут быть пустыми")
return
try:
from .user_logic import UserLogic
db_manager = UserLogic()
# Проверяем путь к файлу физически
import os
if not os.path.exists(db_manager.db_path):
raise FileNotFoundError(f"База users.db не найдена! Путь: {db_manager.db_path}")
success = db_manager.validate_credentials(username, password)
print(f"[AUTH DEBUG] validate_credentials вернула: {success}")
if success:
role = None
with sqlite3.connect(db_manager.db_path) as conn:
row = conn.execute("SELECT role FROM users WHERE login=?", (username,)).fetchone()
if row:
role = row[0]
print(f"[AUTH DEBUG] Роль извлечена: '{role}'")
if callable(self.on_login_command):
# Вызываем колбэк главного окна
self.on_login_command(username, role)
print("[AUTH DEBUG] Колбэк выполнен, закрываем окно.")
self.destroy()
else:
print("[FATAL ERROR] on_login_command is not callable!")
else:
from tkinter import messagebox
messagebox.showerror("Ошибка", "Неверный логин или пароль")
self.password_entry.delete(0, tk.END)
self.password_entry.focus()
except Exception as e:
from tkinter import messagebox
messagebox.showerror("Критическая ошибка", str(e))
print(f"[FATAL LOGIN ERROR]: {e}")
# ====================================================
# Файл №6: Dozor/modules/administrator/admin_user_inter.py
# - старый файл, который подвергается рефакторингу.
# - это файл донор для организации АДМИНИСТРАТОР
# ====================================================
# ---*--- Dozor/modules/administrator/admin_user_inter.py ---*---
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
from modules.administrator.user_logic import UserLogic
class AdminUserInter(tk.Toplevel):
def __init__(self, parent, on_logout_callback=None):
super().__init__(parent)
self.parent_main = parent
self.on_logout_callback = on_logout_callback
self.title("Управление пользователями")
self.geometry("550x450")
self.resizable(True, True)
self.transient(parent)
self.grab_set()
container = ttk.Frame(self, padding="20")
container.pack(expand=True, fill="both")
left_panel = ttk.Frame(container)
left_panel.pack(side="left", fill="y", padx=(0, 10))
right_panel = ttk.Frame(container)
right_panel.pack(side="right", fill="both", expand=True)
actions = [
("Добавить пользователя", self.action_add),
("Сменить пользователя", self.action_change),
("Удалить пользователя", self.action_remove)
]
for text, cmd in actions:
ttk.Button(left_panel, text=text, command=cmd).pack(fill="x", pady=5)
columns = ("№", "Логин", "Роль")
self.users_tree = ttk.Treeview(right_panel, columns=columns, show="headings")
self.users_tree.heading("#1", text="№")
self.users_tree.column("#1", anchor="center", width=50)
self.users_tree.heading("#2", text="Логин")
self.users_tree.column("#2", anchor="w", width=180)
self.users_tree.heading("#3", text="Роль")
self.users_tree.column("#3", anchor="center", width=240)
self.users_tree.pack(fill="both", expand=True)
# Хранилище реального ID для удаления
self._currently_selected_real_id = None
# Отслеживание выбора строки
self.users_tree.bind("<<TreeviewSelect>>", self._on_tree_select)
self.user_manager = UserLogic()
self.load_data()
self.update_idletasks()
p_x = self.parent_main.winfo_rootx()
p_y = self.parent_main.winfo_rooty()
x = p_x + 20
y = p_y + 20
self.geometry(f"+{x}+{y}")
def _on_tree_select(self, event):
selected_item = self.users_tree.selection()
if selected_item:
tags = self.users_tree.item(selected_item)['tags']
if tags:
try:
self._currently_selected_real_id = int(tags[0])
except ValueError:
self._currently_selected_real_id = None
def load_data(self):
for i in self.users_tree.get_children():
self.users_tree.delete(i)
users_from_db = self.user_manager.get_all_users()
for idx, user_row in enumerate(users_from_db, start=1):
login = user_row[1]
role = user_row[2]
real_id = user_row[0]
self.users_tree.insert("", "end", values=(idx, login, role), tags=(str(real_id),))
def action_add(self):
dialog = tk.Toplevel(self)
dialog.title("Новый пользователь")
dialog.geometry("300x220")
dialog.transient(self)
dialog.grab_set()
main_frame = ttk.Frame(dialog, padding="15")
main_frame.pack(expand=True, fill="both")
ttk.Label(main_frame, text="Логин:").grid(row=0, column=0, sticky="w", pady=5)
login_e = ttk.Entry(main_frame); login_e.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
ttk.Label(main_frame, text="Пароль:").grid(row=1, column=0, sticky="w", pady=5)
pass_e = ttk.Entry(main_frame, show="*"); pass_e.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
ttk.Label(main_frame, text="Роль:").grid(row=2, column=0, sticky="w", pady=5)
role_values = ["Администратор", "Эксперт", "Пользователь"]
role_combo = ttk.Combobox(main_frame, values=role_values, state="readonly")
role_combo.current(2)
role_combo.grid(row=2, column=1, padx=5, pady=5, sticky="ew")
btn_frame = ttk.Frame(main_frame)
btn_frame.grid(row=3, column=0, columnspan=2, pady=15)
def do_add():
try:
self.user_manager.add_user(login_e.get(), role_combo.get(), pass_e.get())
self.load_data()
dialog.destroy()
messagebox.showinfo("Успех", "Пользователь добавлен.")
except Exception as e:
messagebox.showerror("Ошибка", str(e))
ttk.Button(btn_frame, text="Создать", command=do_add).pack(side="left", padx=5)
main_frame.columnconfigure(1, weight=1)
login_e.focus()
def action_remove(self):
if self._currently_selected_real_id is None:
messagebox.showwarning("Ошибка", "Выберите пользователя для удаления!")
return
answer = messagebox.askyesno(
"Подтверждение",
f"УДАЛИТЬ пользователя с реальным ID={self._currently_selected_real_id}?\nЭто действие нельзя отменить."
)
if answer:
try:
self.user_manager.delete_user(self._currently_selected_real_id)
self._currently_selected_real_id = None
self.load_data()
messagebox.showinfo("Успех", "Пользователь удален.")
except Exception as e:
messagebox.showerror("Ошибка", str(e))
# === ИСПРАВЛЕННЫЙ МЕТОД СМЕНЫ ПОЛЬЗОВАТЕЛЯ ===
def action_change(self):
# 1. Сначала получаем ссылку на ГЛАВНОЕ ОКНО (оно еще живо)
root = self.parent_main.winfo_toplevel()
# 2. Останавливаем работу главного окна (его внутренний цикл Tcl)
try:
if hasattr(root, 'quit'):
root.quit()
except Exception:
pass
# 3. Закрываем само окно админки
self.destroy()
# 4. Теперь пытаемся уничтожить нативное окно ОС
try:
if root and root.winfo_exists():
root.destroy()
except Exception:
pass
# 5. Запускаем новый процесс
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)
# ====================================================
# Файл №7: Dozor/modules/administrator/admin_db_work.py
# - старый файл, который подвергается рефакторингу.
# - это файл донор для организации АДМИНИСТРАТОР
# ====================================================
# ---*--- Dozor/modules/administrator/admin_db_work.py ---*---
# -*- coding: utf-8 -*-
import io
import os
import shutil
import sqlite3
from datetime import datetime
from pathlib import Path
class DBFileManager:
def __init__(self):
# ЕДИНСТВЕННАЯ точка определения корня проекта
self.project_root = Path(__file__).resolve().parents[2]
self.databases = {
"database": str(self.project_root / "db" / "database.db"),
"masterdata": str(self.project_root / "db" / "masterdata.db")
}
self.active_flag_path = str(self.project_root / "db" / "active.flag")
self.backup_dir = str(self.project_root / "backups") # Теперь точно сюда
os.makedirs(self.backup_dir, exist_ok=True)
self._ensure_active_flag()
def _ensure_active_flag(self):
try:
if not os.path.exists(self.active_flag_path):
with open(self.active_flag_path, 'w') as f:
f.write("database")
except Exception as e:
print(f"[WARN] Не удалось создать флаг активной БД: {e}")
def _get_active_name(self):
try:
with open(self.active_flag_path, 'r') as f:
name = f.read().strip()
return name if name in self.databases else "database"
except FileNotFoundError:
return "database"
def _set_active_name(self, name):
with open(self.active_flag_path, 'w') as f:
f.write(name)
def get_active_path(self):
return self.databases[self._get_active_name()]
def create_backup(self):
src = self.get_active_path()
ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
filename = f"{ts}__{os.path.basename(src)}"
dst = os.path.join(self.backup_dir, filename)
shutil.copy2(src, dst)
return filename
def restore_from_file(self, backup_filename):
import io
project_root = Path(__file__).resolve().parents[2]
src_path = str(project_root / "backups" / backup_filename)
active_name_str = self._get_active_name()
dest_path = str(project_root / "db" / f"{active_name_str}.db")
print(f"[DEBUG RESTORE-DUMP] From Backup: {src_path}")
print(f"[DEBUG RESTORE-DUMP] To Active DB: {dest_path}")
try:
# 1. Читаем бэкап в строку
with sqlite3.connect(src_path) as src_con:
sql_dump_io = io.StringIO()
for line in src_con.iterdump():
sql_dump_io.write(line + '\n')
full_sql_script = sql_dump_io.getvalue()
print("[DEBUG RESTORE-DUMP] Бэкап прочитан в RAM.")
# === ФИЛЬТРАЦИЯ СКРИПТА (Убираем CREATE TABLE) ===
lines = full_sql_script.split('\n')
filtered_lines = []
skip_create_table = False
for line in lines:
if line.strip().upper().startswith('CREATE TABLE'):
skip_create_table = True
continue
if skip_create_table:
if ');' in line:
skip_create_table = False
continue
filtered_lines.append(line)
filtered_script = '\n'.join(filtered_lines)
# ==================================================================================
# 2. Выполнение БЕЗ ручного BEGIN/COMMIT
with sqlite3.connect(dest_path) as dest_con:
cursor = dest_con.cursor()
# Выключаем ключи для скорости очистки
cursor.execute("PRAGMA foreign_keys=OFF;")
# ОЧИСТКА ВСЕХ ТАБЛИЦ ОДНИМ ЗАПРОСОМ (вместо цикла FOR)
# Это быстрее и надежнее. Сбрасываем счетчики AUTOINCREMENT тоже.
cursor.execute("""
DELETE FROM sqlite_sequence;
""")
tables_query = """
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%';
"""
cursor.execute(tables_query)
tables = [row[0] for row in cursor.fetchall()]
delete_statements = "; ".join([f"DELETE FROM {t}" for t in tables]) + ";"
if delete_statements.strip() != ";":
cursor.executescript(delete_statements)
# Вставка данных. executescript сам управляет своими внутренними транзакциями.
cursor.executescript(filtered_script)
# Включаем ключи обратно
cursor.execute("PRAGMA foreign_keys=ON;")
print("[DEBUG RESTORE-DUMP] SUCCESS: Восстановление завершено.")
except Exception as e:
# Выведем ПЕРВИЧНУЮ ошибку в консоль, чтобы понять, почему скрипт упал
print(f"[CRITICAL DUMP ERROR] Первичная ошибка дампа: {str(e)}")
import traceback
traceback.print_exc()
raise RuntimeError(f"Восстановление прервано. Смотрите логи в терминале.")
def migrate_data(self, source_key: str, dest_key: str):
import io
import re
project_root = Path(__file__).resolve().parents[2]
source_path = str(project_root / "db" / f"{source_key}.db")
dest_path = str(project_root / "db" / f"{dest_key}.db")
print(f"[DEBUG MIGRATE-DUMP] From: {source_path}")
print(f"[DEBUG MIGRATE-DUMP] To: {dest_path} (через SQL Dump)")
try:
# 1. Читаем ВСЮ исходную базу в строку
with sqlite3.connect(source_path) as src_con:
sql_dump_io = io.StringIO()
for line in src_con.iterdump():
sql_dump_io.write(line + '\n')
full_sql_script = sql_dump_io.getvalue()
print("[DEBUG DUMP] Данные успешно сериализованы в SQL.")
# 2. Фильтруем скрипт: убираем CREATE TABLE, оставляем INSERT и прочее
lines = full_sql_script.split('\n')
filtered_lines = []
skip_create_table = False
for line in lines:
if line.strip().upper().startswith('CREATE TABLE'):
skip_create_table = True
continue
if skip_create_table:
if ');' in line:
skip_create_table = False
continue
filtered_lines.append(line)
filtered_script = '\n'.join(filtered_lines)
# 3. Очищаем целевую базу и вставляем данные БЕЗ ручного BEGIN/COMMIT
with sqlite3.connect(dest_path) as dest_con:
cursor = dest_con.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
tables = [row[0] for row in cursor.fetchall() if not row[0].startswith('sqlite_')]
cursor.execute("PRAGMA foreign_keys=OFF;")
for table in tables:
cursor.execute(f"DELETE FROM {table};")
cursor.executescript(filtered_script)
cursor.execute("PRAGMA foreign_keys=ON;")
print("[DEBUG MIGRATE-DUMP] SUCCESS: Данные перенесены через фильтрованный SQL-скрипт.")
except Exception as e:
# Сюда упала ошибка SyntaxError - этого блока не хватало
raise RuntimeError(f"Миграция через SQL Dump прервана: {str(e)}")
# ====================================================
# Файл №8: Dozor/modules/administrator/admin_db_inter.py
# - старый файл, который подвергается рефакторингу.
# - это файл донор для организации АДМИНИСТРАТОР
# ====================================================
# ---*--- Dozor/modules/administrator/admin_db_inter.py ---*---
# -*- coding: utf-8 -*-
import os
import shutil
import sqlite3
import tkinter as tk
from tkinter import ttk, messagebox
from datetime import datetime
from pathlib import Path
from .admin_db_work import DBFileManager
class AdminDBInter(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.parent_main = parent
self.title("Управление базами данных")
self.geometry("600x400")
self.resizable(True, True)
self.transient(parent)
self.grab_set()
# Здесь тоже используем правильный импорт из пакета administrator
self.file_manager = DBFileManager()
self.build_ui()
self.refresh_backups_list()
self.sync_active_flag()
def build_ui(self):
container = ttk.Frame(self)
container.pack(fill="both", expand=True, padx=10, pady=10)
top_panel = ttk.Frame(container)
top_panel.pack(fill="x", pady=10)
self.use_master_db = tk.BooleanVar(value=(self.file_manager._get_active_name() == "masterdata"))
style = ttk.Style()
style.configure("RedText.TCheckbutton", foreground="red")
ttk.Checkbutton(
top_panel,
text="Использовать основную базу данных (masterdata.db)",
variable=self.use_master_db,
style="RedText.TCheckbutton",
command=self.toggle_active_db
).pack(side="left", padx=10)
tools_panel = ttk.Frame(container)
tools_panel.pack(fill="x", pady=10)
ttk.Button(tools_panel, text="Создать резервную копию", command=self.do_backup).pack(side="left", padx=5)
ttk.Button(tools_panel, text="Восстановить из копии", command=self.do_restore).pack(side="left", padx=5)
ttk.Button(tools_panel, text="Мигрировать данные", command=self.do_migrate).pack(side="left", padx=5)
backup_columns = ("Дата", "Файл")
self.backups_tree = ttk.Treeview(container, columns=backup_columns, show="headings", height=10)
for col in backup_columns:
self.backups_tree.heading(col, text=col)
self.backups_tree.column(col, width=270)
self.backups_tree.pack(fill="both", expand=True, pady=10)
def refresh_backups_list(self):
self.backups_tree.delete(*self.backups_tree.get_children())
backup_dir = Path(self.file_manager.backup_dir)
if not backup_dir.exists():
return
# Сортируем по дате создания файла на диске, а не по имени
files = sorted(backup_dir.glob("*.db"), key=os.path.getctime, reverse=True)
for file_path in files:
# Выводим только имя файла без лишних преобразований
display_name = file_path.name
# Дата модификации файла в читаемом виде
mod_time = datetime.fromtimestamp(file_path.stat().st_mtime).strftime("%d.%m %H:%M")
self.backups_tree.insert("", "end", values=(mod_time, display_name))
def sync_active_flag(self):
is_master = self.file_manager._get_active_name() == "masterdata"
self.use_master_db.set(is_master)
def toggle_active_db(self):
target = "masterdata" if self.use_master_db.get() else "database"
self.file_manager._set_active_name(target)
messagebox.showinfo("Готово", f"Теперь активной будет {target}.db\nПерезапустите приложение для полной смены контекста.")
def do_backup(self):
try:
fname = self.file_manager.create_backup()
self.refresh_backups_list()
messagebox.showinfo("Бэкап", f"Копия создана:\n{fname}\nПуть: {self.file_manager.backup_dir}")
except Exception as e:
messagebox.showerror("Ошибка", str(e))
def do_restore(self):
# Игнорируем выделение в таблице. Ищем самый свежий файл сами.
backup_dir = Path(self.file_manager.backup_dir)
if not backup_dir.exists():
messagebox.showwarning("Внимание", "Папка с бэкапами пуста.")
return
files = sorted(backup_dir.glob("*.db"), key=os.path.getctime, reverse=True)
if not files:
messagebox.showwarning("Внимание", "Не найдено ни одного файла .db в папке backups.")
return
latest_backup = files[0]
try:
self.file_manager.restore_from_file(latest_backup.name)
messagebox.showinfo("Успех", f"База восстановлена из {latest_backup.name}\nПриложение нужно перезапустить.")
except Exception as e:
print(f"[CRITICAL RESTORE ERROR] {e}")
import traceback
traceback.print_exc() # Выведем полную ошибку в консоль
messagebox.showerror("Ошибка", f"Критическая ошибка при восстановлении:\n{str(e)}")
def do_migrate(self):
answer = messagebox.askyesnocancel(
"Миграция данных",
"Перенести ВСЕ данные из НЕАКТИВНОЙ базы в АКТИВНУЮ?\nАктивная база будет ПОЛНОСТЬЮ ЗАТЕРТА данными из другой.\nДействие необратимо."
)
if answer is None or not answer:
return
current = self.file_manager._get_active_name()
source = "masterdata" if current == "database" else "database"
try:
self.file_manager.migrate_data(source, current)
messagebox.showinfo("Миграция", f"Данные перенесены из {source}.db в {current}.db\nПерезапустите приложение.")
except Exception as e:
messagebox.showerror("Ошибка", str(e))
# ---*--- Конец файла ---*---
# ====================================================
# Файл №9: Dozor/gui/main_interface.py
# - НОВЫЙ ОСНОВНОЙ ФАЙЛ с которым работаем.
# - это основной файл для главного окна интерфейса
# - в этом файле организовываем пока первую кнопку АДМИНИСТРАТОР
# ====================================================
# ---*--- Dozor/gui/main_interface.py ---*---
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageTk
import os
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_rowconfigure(0, weight=1)
self.menu_container.grid_columnconfigure(0, weight=1)
def _create_menu_panel(container, title_text):
panel = tk.Frame(container, bg="#f0f0f0", bd=2, relief="sunken")
panel.grid_propagate(False)
header_frame = tk.Frame(panel, bg="#4a4a4a", height=30)
header_frame.pack(fill='x')
close_btn = tk.Button(
header_frame,
text="← Назад",
font=("Arial", 9),
fg="white",
bg="#888888",
borderwidth=0,
command=self._show_main_screen
)
close_btn.pack(side='right', padx=5, pady=2)
content_frame = tk.Frame(panel, bg="#f0f0f0")
content_frame.pack(fill='both', expand=True, padx=10, pady=10)
ttk.Label(content_frame, text=f"Здесь будет функционал:\n{title_text}").pack(expand=True)
return panel
self.admin_panel = _create_menu_panel(self.menu_container, "Подменю Администратор")
self.manager_panel = _create_menu_panel(self.menu_container, "Подменю Менеджер базы")
self.experts_panel = _create_menu_panel(self.menu_container, "Подменю Эксперты")
self.project_panel = _create_menu_panel(self.menu_container, "Подменю Проект")
self.forecast_panel = _create_menu_panel(self.menu_container, "Подменю Прогнозирование")
self.guide_panel = _create_menu_panel(self.menu_container, "Подменю Руководство")
self.about_panel = _create_menu_panel(self.menu_container, "Подменю О программе")
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):
panels = {
'admin': self.admin_panel,
'manager': self.manager_panel,
'experts': self.experts_panel,
'project': self.project_panel,
'forecast': self.forecast_panel,
'guide': self.guide_panel,
'about': self.about_panel
}
if self._current_menu_visible and self._current_menu_visible in panels:
panels[self._current_menu_visible].grid_remove()
self._current_menu_visible = None
# --- ОБРАБОТЧИКИ КНОПОК (БЕЗ .title()) ---
def _show_admin_menu(self):
self._show_main_screen()
self.menu_container.grid(row=0, column=0, rowspan=6, columnspan=2, sticky="nsew")
self.admin_panel.grid(row=0, column=0, sticky="nsew")
self._current_menu_visible = 'admin'
def _show_manager_menu(self):
self._show_main_screen()
self.menu_container.grid(row=0, column=0, rowspan=6, columnspan=2, sticky="nsew")
self.manager_panel.grid(row=0, column=0, sticky="nsew")
self._current_menu_visible = 'manager'
def _show_experts_menu(self):
self._show_main_screen()
self.menu_container.grid(row=0, column=0, rowspan=6, columnspan=2, sticky="nsew")
self.experts_panel.grid(row=0, column=0, sticky="nsew")
self._current_menu_visible = 'experts'
def _show_project_menu(self):
self._show_main_screen()
self.menu_container.grid(row=0, column=0, rowspan=6, columnspan=2, sticky="nsew")
self.project_panel.grid(row=0, column=0, sticky="nsew")
self._current_menu_visible = 'project'
def _show_forecast_menu(self):
self._show_main_screen()
self.menu_container.grid(row=0, column=0, rowspan=6, columnspan=2, sticky="nsew")
self.forecast_panel.grid(row=0, column=0, sticky="nsew")
self._current_menu_visible = 'forecast'
def _show_guide_menu(self):
self._show_main_screen()
self.menu_container.grid(row=0, column=0, rowspan=6, columnspan=2, sticky="nsew")
self.guide_panel.grid(row=0, column=0, sticky="nsew")
self._current_menu_visible = 'guide'
def _show_about_menu(self):
self._show_main_screen()
self.menu_container.grid(row=0, column=0, rowspan=6, columnspan=2, sticky="nsew")
self.about_panel.grid(row=0, column=0, sticky="nsew")
self._current_menu_visible = 'about'
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)