Загрузка данных
'''
День добрый!
Необходимо решить 2 задачи после анализа высланных файлов.
1. Техническая часть.
Необходимо все файлы как бы объеденить в решение одной задачи -
Вход в приложение через авторизацию и далее решение задач по всему что выполняется под кнопкой АДМИНИСТРАТОР. Нужно не указывая названия имеющихся классов описать что для этого нужно сделать.
Например, для выполнения входа авторизации необходимо разработать 3 класса: 1 класс для...., 2 класс для и так далее. Название не указывать, так как это задача программисту и он сам вправе называть,как ему удобнее. И так описать полностью. Описание должно быть похоже на пояснительную записку, но более обще.
2. Тоже самое, но уже человеческим языком описать как будто это инструкция или руководство пользователя.
Здесь должно быть подробно остановлено на каждом окне, для чего оно,что делать, каждый элнемент управления должен быть объяснен.
В итоге я должен получить от тебя два материала.
'''
# modules/admin/admin_accordion_window.py
import customtkinter as ctk
from tkinter import ttk, messagebox
from .users_tab import AdminUsersPanel
from .db_manager_panel import DBManagerPanel
from .migration_panel import MigrationPanel
class AdminAccordionWindow(ctk.CTkToplevel):
def __init__(self, master):
super().__init__(master)
self.parent_main = master
self.title("Администрирование")
# --- 1. БАЗОВЫЕ ГАБАРИТЫ ОКНА ---
window_width = 650
initial_height = 380
screen_w = self.winfo_screenwidth()
screen_h = self.winfo_screenheight()
x_cord = int((screen_w - window_width) / 2)
y_cord = int((screen_h - initial_height) / 2)
self.geometry(f"{window_width}x{initial_height}+{x_cord}+{y_cord}")
# === 2. ЖЕСТКИЕ ОГРАНИЧЕНИЯ РАЗМЕРА ===
self.minsize(width=window_width, height=250)
self.maxsize(width=900, height=screen_h - 50)
self.resizable(True, False)
self.grab_set()
self.transient(master)
# --- КОНТЕЙНЕРЫ ---
main_container = ctk.CTkFrame(self, corner_radius=10)
main_container.pack(padx=10, pady=10, fill="both", expand=True)
title_lbl = ctk.CTkLabel(main_container, text="Панель администратора", font=ctk.CTkFont(size=20, weight="bold"))
title_lbl.pack(pady=(0, 15))
accordion_frame = ctk.CTkFrame(main_container)
accordion_frame.pack(fill="both", expand=True, pady=(10, 0))
self.accordion_data = {}
self.currently_open_button = None
# === ЗАГОЛОВКИ АККОРДЕОНА ===
users_item = self._create_accordion_header(accordion_frame, "Управление пользователями")
security_item = self._create_accordion_header(accordion_frame, "Безопасность")
migration_item = self._create_accordion_header(accordion_frame, "Миграция")
# Наполняем секции контентом
self._fill_users_section(users_item["content"])
self._fill_security_section(security_item["content"])
self._fill_migration_section(migration_item["content"])
# Привязка команд переключения
for item in [users_item, security_item, migration_item]:
btn = item["button"]
content = item["content"]
self.accordion_data[btn] = {"content": content}
btn.configure(command=lambda b=btn, c=content: self.toggle_section(b, c))
# Открываем первую секцию
self.toggle_section(users_item["button"], users_item["content"])
# Кнопки внизу
button_frame = ctk.CTkFrame(main_container)
button_frame.pack(fill="x", pady=(10, 0))
close_btn = ctk.CTkButton(button_frame, text="Закрыть", width=120, command=self.destroy)
close_btn.pack(side="right", padx=10, pady=10)
self.protocol("WM_DELETE_WINDOW", self.on_closing)
# === 3. ЦЕНТРИРОВАНИЕ ПОСЛЕ ПОЛНОЙ ОТРИСТОВКИ ===
self.after(100, self._center_over_parent)
def _center_over_parent(self):
if not self.parent_main or not self.parent_main.winfo_exists():
return
self.update_idletasks()
w = self.winfo_width()
h = self.winfo_height()
root_win = self.parent_main
p_x = root_win.winfo_rootx()
p_y = root_win.winfo_rooty()
final_x = int(p_x + (root_win.winfo_width() - w) / 2)
final_y = int(p_y + (root_win.winfo_height() - h) / 2)
self.geometry(f"+{final_x}+{final_y}")
# === МЕТОД toggle_section (твой оригинальный) ===
def toggle_section(self, button, content):
is_currently_open = (self.currently_open_button == button)
if self.currently_open_button and not is_currently_open:
prev_content = self.accordion_data[self.currently_open_button]["content"]
prev_content.pack_forget()
self.currently_open_button.configure(fg_color="#34495e")
if is_currently_open:
content.pack_forget()
button.configure(fg_color="#34495e")
self.currently_open_button = None
new_geo = self._calculate_window_geometry(collapsed=True)
else:
content.pack(fill="x", padx=5, pady=(0, 5), anchor="center")
button.configure(fg_color="#2c3e50")
self.currently_open_button = button
new_geo = self._calculate_window_geometry(collapsed=False)
self.geometry(new_geo)
# === РАСЧЕТ ГЕОМЕТРИИ (Исправлено!) ===
def _calculate_window_geometry(self, collapsed=False):
current_width = self.winfo_width()
# Если ширина еще не посчиталась (например, при первом запуске)
if current_width == 1:
current_width = 650
header_height = 50 * 3 + 30
# === ФИКС ДЛЯ КНОПОК: ДОБАВИЛИ ОТСТУП ПОД НИЖНЮЮ ПАНЕЛЬ ===
button_panel_height = 70 # Высота рамки с кнопкой "Закрыть" + её отступы
if collapsed or not self.currently_open_button:
target_height = header_height + 80 + button_panel_height
else:
data = self.accordion_data[self.currently_open_button]
content_widget = data["content"]
# Обязательно дожидаемся перерисовки виджета перед замером высоты!
self.update_idletasks()
open_content_height = content_widget.winfo_reqheight()
# Считаем общую высоту: шапка + контент + нижняя панель
target_height = header_height + open_content_height + button_panel_height
max_h = self.winfo_screenheight() - 50
min_h = 250
final_height = min(target_height, max_h) # Не выше экрана
final_height = max(final_height, min_h) # Не ниже порога для кнопок
return f"{current_width}x{final_height}"
# === СОЗДАНИЕ ХЕДЕРА ===
def _create_accordion_header(self, parent, title):
item_container = ctk.CTkFrame(parent, fg_color="#f0f2f5", border_color="#d1d8e0", border_width=1, corner_radius=8)
item_container.pack(fill="x", pady=5)
header_btn = ctk.CTkButton(
item_container,
text=title,
height=45,
font=ctk.CTkFont(weight="bold", size=16),
anchor="w",
fg_color="#34495e",
hover_color="#3c5a72",
)
header_btn.pack(fill="x")
content_frame = ctk.CTkFrame(item_container, fg_color="transparent", corner_radius=0)
return {"button": header_btn, "content": content_frame}
# === НАПОЛНЕНИЕ СЕКЦИЙ ===
def _fill_users_section(self, frame):
panel = AdminUsersPanel(frame)
panel.pack(fill="both", expand=True, padx=10, pady=10)
def _fill_security_section(self, frame):
panel = DBManagerPanel(frame)
panel.pack(fill="both", expand=True, padx=10, pady=10)
def _fill_migration_section(self, frame):
panel = MigrationPanel(frame)
panel.pack(fill="both", expand=True, padx=10, pady=10)
def on_closing(self):
try:
self.after_cancel("all")
except Exception:
pass
try:
self.destroy()
except Exception:
pass
# modules/admin/db_manager_panel.py
import os
import shutil
import sqlite3
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from datetime import datetime
from pathlib import Path
class DBManagerPanel(tk.Frame):
def __init__(self, master):
super().__init__(master)
self.project_root = Path(__file__).resolve().parents[2]
self.db_dir = str(self.project_root / "db")
self.backup_dir = str(self.project_root / "backups")
# Создаем папки, если их нет
os.makedirs(self.db_dir, exist_ok=True)
os.makedirs(self.backup_dir, exist_ok=True)
container = ttk.Frame(self)
container.pack(fill="both", expand=True, padx=10, pady=10)
# === ГРУППА ВЫБОРА БАЗЫ ДАННЫХ (Радиокнопки) ===
selection_frame = ttk.LabelFrame(container, text="Выбор активной базы данных:")
selection_frame.pack(fill="x", pady=(0, 15))
self.active_db_var = tk.StringVar(value="database") # По умолчанию user-база
# Радиокнопка 1: Пользовательская (database.db)
ttk.Radiobutton(
selection_frame,
text="Пользовательская база (database.db)",
variable=self.active_db_var,
value="database"
).pack(anchor="w", padx=10, pady=5)
# Радиокнопка 2: Основная (masterdata.db)
ttk.Radiobutton(
selection_frame,
text="Основная база (masterdata.db)",
variable=self.active_db_var,
value="masterdata"
).pack(anchor="w", padx=10, pady=5)
# Кнопка подтверждения смены контекста
apply_btn = ttk.Button(selection_frame, text="Применить выбор (требует перезапуска)", command=self.apply_selection)
apply_btn.pack(pady=10)
# === ГРУППА РЕЗЕРВНОГО КОПИРОВАНИЯ ===
backup_frame = ttk.LabelFrame(container, text="Резервное копирование (.db -> .bak)")
backup_frame.pack(fill="x", pady=10)
info_text = f"Текущая рабочая директория:\n{self.db_dir}"
ttk.Label(backup_frame, text=info_text, font=("Arial", 9)).pack(anchor="w", padx=10, pady=5)
btn_backup = ttk.Button(backup_frame, text="Создать резервную копию АКТИВНОЙ базы", command=self.do_backup)
btn_backup.pack(side="left", padx=10, pady=10)
# === ГРУППА ВОССТАНОВЛЕНИЯ ===
restore_frame = ttk.LabelFrame(container, text="Восстановление из копии (.bak -> .db)")
restore_frame.pack(fill="x", pady=10)
self.restore_path_var = tk.StringVar()
entry_restore = ttk.Entry(restore_frame, textvariable=self.restore_path_var, width=60, state='readonly')
entry_restore.pack(side="left", padx=10, pady=5, fill="x", expand=True)
browse_btn = ttk.Button(restore_frame, text="Выбрать файл...", command=self.browse_file)
browse_btn.pack(side="left", padx=5, pady=5)
btn_restore = ttk.Button(restore_frame, text="Восстановить базу", command=self.do_restore)
btn_restore.pack(side="left", padx=10, pady=10)
def get_active_db_path(self):
db_name = self.active_db_var.get()
return os.path.join(self.db_dir, f"{db_name}.db")
def apply_selection(self):
chosen = self.active_db_var.get()
flag_path = os.path.join(self.db_dir, "active.flag")
try:
with open(flag_path, 'w') as f:
f.write(chosen)
messagebox.showinfo("Готово", f"Выбрана база {chosen}.db\nПерезапустите приложение.")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось записать флаг:\n{e}")
def do_backup(self):
src = self.get_active_db_path()
if not os.path.exists(src):
messagebox.showwarning("Файл не найден", f"База {src} еще не создана.\nДобавьте пользователя, чтобы создать файл.")
return
ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
base_name = self.active_db_var.get()
filename = f"{ts}__{base_name}.bak"
dst = os.path.join(self.backup_dir, filename)
try:
shutil.copy2(src, dst)
messagebox.showinfo("Успех", f"Копия создана:\n{filename}\nПуть: {self.backup_dir}")
except Exception as e:
messagebox.showerror("Ошибка", str(e))
def browse_file(self):
filetypes = [("Backup files", "*.bak"), ("All files", "*.*")]
initialdir = self.backup_dir
filepath = filedialog.askopenfilename(title="Выберите файл бэкапа", filetypes=filetypes, initialdir=initialdir)
if filepath:
self.restore_path_var.set(filepath)
def do_restore(self):
src = self.restore_path_var.get()
dest = self.get_active_db_path()
if not src or not os.path.exists(src):
messagebox.showwarning("Ошибка", "Сначала выберите файл бэкапа кнопкой справа.")
return
answer = messagebox.askyesno("Подтверждение", f"Затереть текущую базу {os.path.basename(dest)}\nданными из {os.path.basename(src)}?")
if not answer:
return
try:
shutil.copy2(src, dest)
messagebox.showinfo("Успех", f"База восстановлена из {os.path.basename(src)}.\nПриложение нужно перезапустить.")
except Exception as e:
messagebox.showerror("Ошибка", str(e))
# modules/admin/migration_panel.py
import os
import sqlite3
import tkinter as tk
from tkinter import ttk, messagebox
from pathlib import Path
class MigrationPanel(tk.Frame):
def __init__(self, master):
super().__init__(master)
self.project_root = Path(__file__).resolve().parents[2]
self.db_dir = str(self.project_root / "db")
os.makedirs(self.db_dir, exist_ok=True)
container = ttk.Frame(self)
container.pack(fill="both", expand=True, padx=10, pady=10)
warning_lbl = ttk.Label(container, text="Переносит ТОЛЬКО ДАННЫЕ (INSERT).\nСтруктура таблиц должна совпадать.", foreground="red", wraplength=400)
warning_lbl.pack(pady=10)
direction_label = ttk.Label(container, text="Направление миграции:", anchor="w")
direction_label.pack(fill="x", pady=(10, 5))
self.migrate_direction_var = tk.StringVar(value="to_master")
rb_to_main = ttk.Radiobutton(
container,
text="Из Пользовательской (database) -> В Основную (masterdata)",
variable=self.migrate_direction_var,
value="to_master"
)
rb_to_main.pack(anchor="w", padx=20, pady=2)
rb_to_user = ttk.Radiobutton(
container,
text="Из Основной (masterdata) -> В Пользовательскую (database)",
variable=self.migrate_direction_var,
value="to_database"
)
rb_to_user.pack(anchor="w", padx=20, pady=2)
btn_migrate = ttk.Button(container, text="Выполнить миграцию (затереть приемник!)", command=self.do_migrate)
btn_migrate.pack(pady=20)
def _get_paths(self):
source_key = "database" if self.migrate_direction_var.get() == "to_master" else "masterdata"
dest_key = "masterdata" if self.migrate_direction_var.get() == "to_master" else "database"
src = os.path.join(self.db_dir, f"{source_key}.db")
dest = os.path.join(self.db_dir, f"{dest_key}.db")
return src, dest, source_key, dest_key
def do_migrate(self):
src, dest, source_key, dest_key = self._get_paths()
if not os.path.exists(src):
messagebox.showwarning("Ошибка", f"Исходная база '{source_key}.db' не найдена.")
return
warning_text = (
f"ВНИМАНИЕ!\nДанные в базе '{dest_key}.db' будут ПОЛНОСТЬЮ ЗАТЕРТЫ данными из '{source_key}.db'.\n"
f"Это необратимо."
)
answer = messagebox.askokcancel("Критическое действие", warning_text)
if not answer:
return
temp_backup = dest + ".tmp.bak"
success = False
try:
# Делаем страховочный бэкап цели
if os.path.exists(dest):
import shutil
shutil.copy2(dest, temp_backup)
con_src = sqlite3.connect(src)
sql_dump = ""
for line in con_src.iterdump():
# Оставляем только вставку данных, структуру целевой базы НЕ трогаем
if not line.strip().upper().startswith('CREATE TABLE'):
sql_dump += line + "\n"
con_src.close()
# Очищаем целевую базу от старых данных
con_dest = sqlite3.connect(dest)
cur = con_dest.cursor()
cur.execute("PRAGMA foreign_keys=OFF;")
# Удаляем данные во всех таблицах
tables_query = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';"
cur.execute(tables_query)
tables = [row[0] for row in cur.fetchall()]
for table in tables:
cur.execute(f"DELETE FROM {table};")
# Заливаем новые данные
cur.executescript(sql_dump)
cur.execute("PRAGMA foreign_keys=ON;")
con_dest.commit()
con_dest.close()
success = True
messagebox.showinfo("Успех", f"Миграция завершена:\n{source_key}.db -> {dest_key}.db")
except Exception as e:
messagebox.showerror("Ошибка миграции", str(e))
finally:
# Если всё прошло хорошо или плохо, но был сделан бэкап - удалим его мусорный файл
if os.path.exists(temp_backup):
if success:
os.remove(temp_backup)
else:
# Попытка отката при ошибке
try:
import shutil
shutil.copy2(temp_backup, dest)
os.remove(temp_backup)
messagebox.showinfo("Откат", "Целевая база возвращена к состоянию до миграции.")
except Exception:
pass
# D:\dukora_V1_31_07_2026\auth_window.py
import customtkinter as ctk
from tkinter import messagebox
import sqlite3
from pathlib import Path
from hashlib import sha256
class AuthWindow(ctk.CTkToplevel):
def __init__(self, master):
super().__init__(master)
self.title("Авторизация")
self.geometry("350x280")
self.resizable(False, False)
# Привязываем к главному окну
self.transient(master)
self.grab_set()
frame = ctk.CTkFrame(self)
frame.pack(expand=True, fill="both", padx=20, pady=(20, 10))
container = ctk.CTkFrame(frame)
container.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
for i in range(5):
container.grid_rowconfigure(i, weight=0)
container.grid_columnconfigure(0, weight=1)
# Поле логина
ctk.CTkLabel(container, text="Логин:", font=("Arial", 14)).grid(row=0, column=0, pady=(0, 5), sticky="w")
self.entry_login = ctk.CTkEntry(container, width=300)
self.entry_login.grid(row=1, column=0, pady=(0, 15), sticky="ew")
# Поле пароля
ctk.CTkLabel(container, text="Введите пароль:", font=("Arial", 14)).grid(row=2, column=0, pady=(0, 5), sticky="w")
self.entry_password = ctk.CTkEntry(container, show="*", width=300)
self.entry_password.grid(row=3, column=0, pady=(0, 20), sticky="ew")
# Кнопка входа
btn_login = ctk.CTkButton(
container,
text="Войти",
command=self.attempt_login,
width=300, height=45
)
btn_login.grid(row=4, column=0, sticky="ew")
self.entry_login.focus()
def attempt_login(self):
login = self.entry_login.get().strip()
password = self.entry_password.get()
if not login or not password:
messagebox.showwarning("Ошибка", "Заполните оба поля")
return
# === ИСПРАВЛЕННЫЙ ПУТЬ ===
# Используем жесткий путь к диску D:, чтобы избежать проблем с рабочей директорией
db_path = Path(r"D:\\dukora_V1_31_07_2026\\db") / "users.db"
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Проверяем наличие таблицы перед запросом
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='users'")
if not cursor.fetchone():
raise Exception(f"Таблица 'users' не найдена в базе {db_path}")
cursor.execute('SELECT password_hash FROM users WHERE login = ?', (login,))
result = cursor.fetchone()
if result is None:
messagebox.showerror("Ошибка", f"Пользователь '{login}' не найден.")
return
stored_hash = result[0]
input_hash = sha256(password.encode()).hexdigest()
if input_hash == stored_hash:
print(f"[INFO] Пользователь '{login}' успешно авторизован.")
self.destroy() # Закрываем ТОЛЬКО при успехе
else:
messagebox.showerror("Ошибка", "Неверный пароль.")
except Exception as e:
# КРИТИЧЕСКАЯ ОШИБКА: выводим её и НЕ даем войти
messagebox.showerror("Критическая ошибка БД",
f"Не удалось подключиться к базе данных.\n\n{e}\n\nПроверьте путь к файлу 'db/users.db'.")
# ВАЖНО: НЕ вызываем self.destroy(), оставляем окно открытым для исправления
finally:
if 'conn' in locals():
conn.close()
# gui\main_interface.py
import os
import customtkinter as ctk
from PIL import Image
import sys
from modules.manager.manager_window import ManagerWindow
from modules.experts.experts_window import ExpertsWindow
from modules.project.project_window import ProjectWindow
from modules.forecasting.forecast_window import ForecastWindow
class MainApp(ctk.CTk):
# 1. Исправлено имя конструктора (было def init)
def __init__(self):
super().__init__()
self.title("Dozor System")
try:
if sys.platform == "win32":
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except Exception:
pass
target_w, target_h = 422, 335
self.geometry(f"{target_w}x{target_h}")
self.resizable(False, False)
bg_color = "#F0F0F0"
self.configure(fg_color=bg_color)
main_bg = ctk.CTkFrame(self, fg_color=bg_color)
main_bg.pack(fill="both", expand=True, padx=1, pady=1)
main_bg.grid_columnconfigure(0, minsize=210, weight=0)
main_bg.grid_columnconfigure(1, weight=1)
for i in range(6):
main_bg.grid_rowconfigure(i, weight=0)
main_bg.grid_rowconfigure(6, weight=1)
button_font = ("Arial", 13, "bold")
btn_height = 45
# === ЛЕВАЯ ПАНЕЛЬ МЕНЮ ===
sidebar = ctk.CTkFrame(main_bg, fg_color="#EEEEEE", width=210)
sidebar.grid(row=0, column=0, rowspan=7, sticky="nsew", padx=(5, 5), pady=5)
sidebar.grid_columnconfigure(0, weight=1)
actions = [
("Администратор", self.open_admin_module),
("Менеджер базы", self.open_manager_module),
("Эксперты", self.open_experts_module),
("Проект", self.open_project_module),
("Прогнозирование", self.open_forecasting_module)
]
# 2. Убран лишний блок if-elif. Команда задается сразу из словаря.
for i, (text, command_func) in enumerate(actions):
btn = ctk.CTkButton(
master=sidebar,
text=text,
font=button_font,
height=btn_height,
command=command_func
)
btn.grid(row=i, column=0, padx=10, pady=5, sticky="ew")
# === ПРАВАЯ ЧАСТЬ ВИТРИНЫ ===
content_area = ctk.CTkFrame(main_bg, fg_color=bg_color)
content_area.grid(row=0, column=1, rowspan=7, sticky="nsew", padx=(0, 10), pady=10)
content_area.grid_columnconfigure(0, weight=1)
content_area.grid_rowconfigure(0, weight=0)
content_area.grid_rowconfigure(1, minsize=2, weight=0)
content_area.grid_rowconfigure(2, minsize=27, weight=0)
content_area.grid_rowconfigure(3, weight=0)
pic_container = ctk.CTkFrame(content_area, fg_color="#d9ead3", width=220, height=130)
pic_container.grid(row=0, column=0, pady=(5, 0))
pic_container.grid_propagate(False)
img_loaded = False
try:
img_path = os.path.join(os.getcwd(), 'images', 'Сова1.png')
pil_img = Image.open(img_path)
target_w_i, target_h_i = 210, 110
ratio = pil_img.width / pil_img.height
box_ratio = target_w_i / target_h_i
if ratio > box_ratio:
new_w, new_h = target_w_i, int(target_w_i / ratio)
else:
new_w, new_h = int(target_h_i * ratio), target_h_i
resized_img = pil_img.resize((new_w, new_h), resample=Image.LANCZOS)
tk_image = ctk.CTkImage(light_image=resized_img, size=(new_w, new_h))
lbl_pic = ctk.CTkLabel(pic_container, image=tk_image, text="", fg_color="#d9ead3")
lbl_pic.place(relx=0.5, rely=0.5, anchor='center', x=0, y=-3)
img_loaded = True
except Exception as e:
fail_lbl = ctk.CTkLabel(pic_container, text="Совa\nне найдена", font=("Arial", 14), fg_color="#d9ead3")
fail_lbl.place(relx=0.5, rely=0.5, anchor="center")
bottom_btns_frame = ctk.CTkFrame(content_area, fg_color=bg_color)
bottom_btns_frame.grid(row=3, column=0, sticky="ew", padx=0)
bottom_btns_frame.grid_columnconfigure(0, minsize=195, weight=0)
ctk.CTkButton(
bottom_btns_frame,
text="Руководство",
font=button_font,
height=btn_height,
command=self.open_guide
).grid(row=0, column=0, sticky="ew", pady=(0, 8))
ctk.CTkButton(
bottom_btns_frame,
text="О программе",
font=button_font,
height=btn_height,
command=self.show_about
).grid(row=1, column=0, sticky="ew")
footer_frame = ctk.CTkFrame(main_bg, fg_color="#0078D7", height=30)
footer_frame.grid(row=7, column=0, columnspan=2, sticky="ew", pady=(10, 0))
footer_label = ctk.CTkLabel(footer_frame,
text="Copyright © Минск, НИИ, 2027 год",
text_color="#FF8C00",
fg_color="#0078D7",
font=("Arial", 13, "bold"))
footer_label.pack(expand=True, ipady=4)
self.update_idletasks()
self.tk.call('wm', 'geometry', self._w, f"{target_w}x{target_h}")
screen_w = self.winfo_screenwidth()
screen_h = self.winfo_screenheight()
pos_x = (screen_w - target_w) // 2
pos_y = (screen_h - target_h) // 2
self.geometry(f"+{pos_x}+{pos_y}")
# 3. Оставлен только ОДИН рабочий метод open_admin_module
def open_admin_module(self):
if hasattr(self, '_admin_win') and self._admin_win.winfo_exists():
self._admin_win.lift()
return
# Открываем наше новое красивое окно-аккордеон
from modules.admin.admin_accordion_window import AdminAccordionWindow
self._admin_win = AdminAccordionWindow(self)
self.wait_window(self._admin_win)
def open_manager_module(self):
if hasattr(self, '_manager_win') and self._manager_win.winfo_exists():
self._manager_win.lift()
return
self._manager_win = ManagerWindow(master=self)
self.wait_window(self._manager_win)
def open_experts_module(self):
if hasattr(self, '_experts_win') and self._experts_win.winfo_exists():
self._experts_win.lift()
return
self._experts_win = ExpertsWindow(master=self)
self.wait_window(self._experts_win)
def open_project_module(self):
if hasattr(self, '_project_win') and self._project_win.winfo_exists():
self._project_win.lift()
return
self._project_win = ProjectWindow(master=self)
self.wait_window(self._project_win)
def open_forecasting_module(self):
if hasattr(self, '_forecast_win') and self._forecast_win.winfo_exists():
self._forecast_win.lift()
return
self._forecast_win = ForecastWindow(master=self)
self.wait_window(self._forecast_win)
def open_guide(self):
file_name = "manual.chm"
full_path = os.path.join(os.getcwd(), file_name)
print(f"Пытаюсь открыть: {full_path}")
if os.path.exists(full_path):
try:
os.startfile(full_path)
except Exception as e:
print(f"Система отказалась открывать файл: {e}")
else:
print("Ошибка: ФАЙЛА НЕТ в этой папке!")
def show_about(self):
from tkinter import messagebox
info_text = (
"Система Dozor V1.0\n"
"Разработано: Минск, НИИ\n"
"Версия сборки: 31.07.2026\n\n"
"(c) Все права защищены."
)
messagebox.showinfo(title="О программе", message=info_text)
# 4. Исправлено имя переменной (__name__)
if __name__ == "__main__":
app = MainApp()
app.mainloop()
# D:\\dukora_V1_31_07_2026\\main.py
import customtkinter as ctk
from gui.main_interface import MainApp
from auth_window import AuthWindow # Теперь Питон легко найдет этот файл в той же папке
def start_application():
app = MainApp()
# Создаем модальное окно поверх замороженного главного
auth_win = AuthWindow(app)
app.wait_window(auth_win)
# Запускаем основной интерфейс только после успешной авторизации
app.mainloop()
if __name__ == "__main__":
try:
ctk.set_appearance_mode("System")
ctk.set_default_color_theme("blue")
except Exception:
pass
start_application()
# modules/admin/users_tab.py
import customtkinter as ctk
import tkinter as tk
from tkinter import ttk, messagebox
import sqlite3
from hashlib import sha256
from pathlib import Path
class AdminUsersPanel(ctk.CTkFrame):
def __init__(self, master):
# Исправлено: правильное имя конструктора
super().__init__(master)
# Определяем корень проекта один раз. Теперь база ищется по пути /db/users.db от этого файла
self.project_root = Path(__file__).resolve().parents[2]
self.db_path = self.project_root / "db" / "users.db"
# --- Твой оригинальный UI ---
container = ctk.CTkFrame(self)
container.pack(fill="both", expand=True, padx=10, pady=10)
style = ttk.Style()
style.theme_use("default")
style.configure("mystyle.Treeview", background="#2b2b2b", fieldbackground="#2b2b2b", foreground="white", borderwidth=0, font=("Arial", 10))
style.map('mystyle.Treeview', background=[('selected', '#4a4a4a')])
table_frame = ctk.CTkFrame(container)
table_frame.pack(fill="both", expand=True, side="top")
# УБРАЛИ "Пароль" из списка колонок, чтобы не путать пользователя
self.tree = ttk.Treeview(table_frame, columns=("ID", "Логин", "ФИО", "Роль"), show="headings", style="mystyle.Treeview")
self.tree.heading("ID", text="ID")
self.tree.heading("Логин", text="Логин")
self.tree.heading("ФИО", text="ФИО")
self.tree.heading("Роль", text="Роль")
self.tree.column("ID", width=40)
self.tree.column("Логин", width=100)
self.tree.column("ФИО", width=180)
self.tree.column("Роль", width=80)
self.tree.pack(side="left", fill="both", expand=True)
scrollbar = ctk.CTkScrollbar(table_frame, command=self.tree.yview)
scrollbar.pack(side="right", fill="y")
self.tree.configure(yscrollcommand=scrollbar.set)
form_frame = ctk.CTkFrame(container)
form_frame.pack(fill="x", pady=(10, 0))
# === ФОРМА ВВОДА (исправлены индексы grid) ===
self.login_entry = ctk.CTkEntry(form_frame, placeholder_text="Логин", width=100)
self.login_entry.grid(row=0, column=0, padx=5, pady=5)
self.fio_entry = ctk.CTkEntry(form_frame, placeholder_text="ФИО", width=180)
self.fio_entry.grid(row=0, column=1, padx=5, pady=5)
# Пароль ставим после ФИО (столбец 2)
self.pass_entry = ctk.CTkEntry(form_frame, placeholder_text="Пароль", width=120, show="*")
self.pass_entry.grid(row=0, column=2, padx=5, pady=5)
# Роль сдвигаем вправо (столбец 3), чтобы форма была ровной
self.role_cb = ctk.CTkComboBox(form_frame, values=["Админ", "Эксперт", "Пользователь"], width=100)
self.role_cb.grid(row=0, column=3, padx=5, pady=5)
self.role_cb.set("Эксперт")
btn_frame = ctk.CTkFrame(container)
btn_frame.pack(pady=10)
add_btn = ctk.CTkButton(btn_frame, text="Добавить", command=self.add_user)
add_btn.pack(side="left", padx=5)
edit_btn = ctk.CTkButton(btn_frame, text="Изменить", command=self.edit_user)
edit_btn.pack(side="left", padx=5)
del_btn = ctk.CTkButton(btn_frame, text="Удалить", command=self.delete_user, fg_color="#d9534f", hover_color="#c9302c")
del_btn.pack(side="left", padx=5)
# Загрузка данных при инициализации
self.load_users()
def load_users(self):
self.tree.delete(*self.tree.get_children())
# === ИСПРАВЛЕННЫЙ ПУТЬ ===
db_path = Path(r"D:\\dukora_V1_31_07_2026\\db") / "users.db"
try:
conn = sqlite3.connect(db_path)
# ... остальной код ...
cursor = conn.cursor()
# Создаем базу, если её нет
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
fio TEXT NOT NULL,
role TEXT NOT NULL
)
''')
conn.commit()
# Забираем данные, сортируя их по ID (чтобы порядок был стабильным)
cursor.execute('SELECT id, login, fio, role FROM users ORDER BY id')
rows = cursor.fetchall()
for index, row in enumerate(rows, start=1):
# Вместо технического ID из БД (row[0]) вставляем визуальный номер (index)
# Технический ID сохраняем внутри самого элемента Treeview через tags
self.tree.insert("", "end", values=(index, row[1], row[2], row[3]), tags=(str(row[0]),))
except Exception as e:
messagebox.showerror("Ошибка БД", f"Не удалось загрузить пользователей:\\n{e}")
finally:
if 'conn' in locals():
conn.close()
def get_form_data(self):
return {
"login": self.login_entry.get(),
"fio": self.fio_entry.get(),
"password": self.pass_entry.get(),
"role": self.role_cb.get()
}
def clear_form(self):
self.login_entry.delete(0, "end")
self.fio_entry.delete(0, "end")
self.pass_entry.delete(0, "end")
self.role_cb.set("Эксперт")
def add_user(self):
data = self.get_form_data()
if not all([data["login"], data["fio"], data["password"]]):
messagebox.showwarning("Ошибка", "Заполните Логин, ФИО и Пароль")
return
password_hash = sha256(data["password"].encode()).hexdigest()
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO users (login, password_hash, fio, role)
VALUES (?, ?, ?, ?)
''', (data["login"], password_hash, data["fio"], data["role"]))
conn.commit()
messagebox.showinfo("Успех", f"Пользователь {data['login']} добавлен")
self.clear_form()
self.load_users()
except sqlite3.IntegrityError:
messagebox.showwarning("Ошибка", "Пользователь с таким логином уже существует.")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось сохранить:\n{e}")
finally:
if 'conn' in locals():
conn.close()
def _get_selected_db_id(self):
"""Возвращает настоящий ID пользователя из базы для выбранной строки"""
selected = self.tree.selection()
if not selected:
return None
# Достаём технический ID, который мы сохранили в теги при создании строки
item = self.tree.item(selected)
db_id_str = item['tags'][0]
return int(db_id_str)
def delete_user(self):
db_id = self._get_selected_db_id()
if not db_id:
messagebox.showwarning("Ошибка", "Выберите строку")
return
# Для сообщения пользователю берем логин из видимых ячеек
login = self.tree.item(self.tree.selection())['values'][1]
db_path = Path(__file__).resolve().parents[2] / "db" / "users.db"
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute("DELETE FROM users WHERE id=?", (db_id,))
conn.commit()
messagebox.showinfo("Успех", f"Пользователь '{login}' удален")
self.clear_form()
self.load_users() # <--- После этой строки вызовется наш новый enumeratе и всё красиво пересчитается
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось удалить:\n{e}")
finally:
if 'conn' in locals():
conn.close()
def edit_user(self):
db_id = self._get_selected_db_id()
if not db_id:
messagebox.showwarning("Ошибка", "Выберите строку")
return
# Получаем текущие данные из скрытого ID
db_path = Path(__file__).resolve().parents[2] / "db" / "users.db"
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute('SELECT login, fio, role FROM users WHERE id=?', (db_id,))
user_data = cursor.fetchone()
if user_data:
# Заполняем форму реальными данными для редактирования
self.login_entry.delete(0, "end")
self.login_entry.insert(0, user_data[0])
self.fio_entry.delete(0, "end")
self.fio_entry.insert(0, user_data[1])
self.role_cb.set(user_data[2])
# Поле пароля оставляем пустым при редактировании (если пароль менять не надо)
self.pass_entry.delete(0, "end")
# Можно заблокировать поле логина, чтобы его нельзя было сменить mid-edit
self.login_entry.configure(state="disabled")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось прочитать данные:\n{e}")
finally:
if 'conn' in locals():
conn.close()