220 lines
9.2 KiB
Python
220 lines
9.2 KiB
Python
"""
|
||
views/admin_users_view.py — Admin panel: User Management tab.
|
||
"""
|
||
|
||
import tkinter as tk
|
||
from tkinter import ttk
|
||
import logging
|
||
from utils.ui_helpers import (
|
||
COLOURS, FONT, FONT_BOLD, FONT_HEADING,
|
||
show_error, show_info, confirm_delete
|
||
)
|
||
|
||
logger = logging.getLogger("admin_users_view")
|
||
|
||
|
||
class AdminUsersView(ttk.Frame):
|
||
def __init__(self, parent, current_user: dict):
|
||
super().__init__(parent)
|
||
self.current_user = current_user
|
||
self.configure(style="TFrame")
|
||
self._selected_user_id = None
|
||
self._build_ui()
|
||
self._load_users()
|
||
|
||
# ─── Layout ───────────────────────────────────────────────────────────────
|
||
|
||
def _build_ui(self):
|
||
# Top toolbar
|
||
toolbar = ttk.Frame(self)
|
||
toolbar.pack(fill="x", pady=(0, 10))
|
||
|
||
ttk.Label(toolbar, text="User Management", style="Heading.TLabel").pack(side="left")
|
||
|
||
ttk.Button(toolbar, text="+ Add User",
|
||
command=self._open_add_dialog).pack(side="right", padx=(4, 0))
|
||
ttk.Button(toolbar, text="✎ Edit",
|
||
style="Ghost.TButton",
|
||
command=self._open_edit_dialog).pack(side="right", padx=(4, 0))
|
||
ttk.Button(toolbar, text="✕ Delete",
|
||
style="Danger.TButton",
|
||
command=self._delete_selected).pack(side="right")
|
||
|
||
# Treeview
|
||
cols = ("ID", "Username", "Full Name", "Role", "Active", "Created")
|
||
self.tree = ttk.Treeview(self, columns=cols, show="headings", selectmode="browse")
|
||
widths = [40, 140, 180, 80, 60, 160]
|
||
for col, w in zip(cols, widths):
|
||
self.tree.heading(col, text=col)
|
||
self.tree.column(col, width=w, anchor="center" if w < 120 else "w")
|
||
self.tree.pack(fill="both", expand=True)
|
||
self.tree.bind("<Double-1>", lambda _: self._open_edit_dialog())
|
||
|
||
vsb = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview)
|
||
self.tree.configure(yscrollcommand=vsb.set)
|
||
vsb.place(relx=1, rely=0, relheight=1, anchor="ne")
|
||
|
||
# ─── Data ─────────────────────────────────────────────────────────────────
|
||
|
||
def _load_users(self):
|
||
from models import get_all_users
|
||
self.tree.delete(*self.tree.get_children())
|
||
try:
|
||
for u in get_all_users():
|
||
active = "✔" if u["is_active"] else "✘"
|
||
created = str(u["created_at"])[:16] if u["created_at"] else ""
|
||
self.tree.insert("", "end", iid=str(u["id"]),
|
||
values=(u["id"], u["username"],
|
||
u["full_name"] or "", u["role"],
|
||
active, created))
|
||
except Exception as e:
|
||
show_error(f"Failed to load users:\n{e}")
|
||
|
||
def _get_selected_id(self):
|
||
sel = self.tree.selection()
|
||
return int(sel[0]) if sel else None
|
||
|
||
# ─── Dialogs ──────────────────────────────────────────────────────────────
|
||
|
||
def _open_add_dialog(self):
|
||
UserDialog(self, self.current_user, user_data=None,
|
||
on_save=self._load_users)
|
||
|
||
def _open_edit_dialog(self):
|
||
uid = self._get_selected_id()
|
||
if not uid:
|
||
show_error("Please select a user to edit.")
|
||
return
|
||
from models import get_user_by_id
|
||
user_data = get_user_by_id(uid)
|
||
UserDialog(self, self.current_user, user_data=user_data,
|
||
on_save=self._load_users)
|
||
|
||
def _delete_selected(self):
|
||
uid = self._get_selected_id()
|
||
if not uid:
|
||
show_error("Please select a user to delete.")
|
||
return
|
||
if uid == self.current_user["id"]:
|
||
show_error("You cannot delete your own account.")
|
||
return
|
||
vals = self.tree.item(uid, "values")
|
||
username = vals[1] if vals else str(uid)
|
||
if confirm_delete(username):
|
||
try:
|
||
from models import delete_user
|
||
delete_user(self.current_user["id"], uid)
|
||
logger.info(f"Admin {self.current_user['username']} deleted user id={uid}.")
|
||
show_info(f"User '{username}' deleted successfully.")
|
||
self._load_users()
|
||
except Exception as e:
|
||
show_error(f"Delete failed:\n{e}")
|
||
|
||
|
||
# ─── User Dialog (Add / Edit) ─────────────────────────────────────────────────
|
||
|
||
class UserDialog(tk.Toplevel):
|
||
def __init__(self, parent, current_user, user_data, on_save):
|
||
super().__init__(parent)
|
||
self.current_user = current_user
|
||
self.user_data = user_data
|
||
self.on_save = on_save
|
||
self.is_edit = user_data is not None
|
||
self.title("Edit User" if self.is_edit else "Add User")
|
||
self.resizable(False, False)
|
||
self.configure(bg=COLOURS["bg"])
|
||
self.grab_set()
|
||
self._build_ui()
|
||
self._centre()
|
||
|
||
def _centre(self):
|
||
self.update_idletasks()
|
||
w, h = 420, 420
|
||
x = (self.winfo_screenwidth() - w) // 2
|
||
y = (self.winfo_screenheight() - h) // 2
|
||
self.geometry(f"{w}x{h}+{x}+{y}")
|
||
|
||
def _build_ui(self):
|
||
pad = {"padx": 24, "pady": 8}
|
||
ttk.Label(self,
|
||
text="Edit User" if self.is_edit else "New User",
|
||
style="Heading.TLabel").pack(anchor="w", padx=24, pady=(20, 4))
|
||
|
||
form = ttk.Frame(self)
|
||
form.pack(fill="x", padx=24, pady=8)
|
||
form.columnconfigure(1, weight=1)
|
||
|
||
def row(label, row_n, show=None):
|
||
ttk.Label(form, text=label).grid(row=row_n, column=0,
|
||
sticky="w", padx=(0, 10), pady=6)
|
||
var = tk.StringVar()
|
||
ent = ttk.Entry(form, textvariable=var, show=show or "")
|
||
ent.grid(row=row_n, column=1, sticky="ew", pady=6)
|
||
return var, ent
|
||
|
||
self.full_name_var, _ = row("Full Name", 0)
|
||
self.username_var, _ = row("Username", 1)
|
||
self.password_var, _ = row("Password", 2, show="•")
|
||
pw_hint = "(leave blank to keep unchanged)" if self.is_edit else ""
|
||
ttk.Label(form, text=pw_hint, style="Dim.TLabel").grid(
|
||
row=3, column=1, sticky="w")
|
||
|
||
ttk.Label(form, text="Role").grid(row=4, column=0, sticky="w",
|
||
padx=(0, 10), pady=6)
|
||
self.role_var = tk.StringVar(value="user")
|
||
role_cb = ttk.Combobox(form, textvariable=self.role_var,
|
||
values=["admin", "user"], state="readonly")
|
||
role_cb.grid(row=4, column=1, sticky="ew", pady=6)
|
||
|
||
ttk.Label(form, text="Active").grid(row=5, column=0, sticky="w",
|
||
padx=(0, 10), pady=6)
|
||
self.active_var = tk.BooleanVar(value=True)
|
||
ttk.Checkbutton(form, variable=self.active_var).grid(row=5, column=1,
|
||
sticky="w", pady=6)
|
||
|
||
if self.is_edit:
|
||
d = self.user_data
|
||
self.full_name_var.set(d.get("full_name") or "")
|
||
self.username_var.set(d.get("username") or "")
|
||
self.role_var.set(d.get("role") or "user")
|
||
self.active_var.set(bool(d.get("is_active", 1)))
|
||
|
||
btn_frame = ttk.Frame(self)
|
||
btn_frame.pack(fill="x", padx=24, pady=(16, 20))
|
||
ttk.Button(btn_frame, text="Save", command=self._save).pack(side="right", padx=(6, 0))
|
||
ttk.Button(btn_frame, text="Cancel", style="Ghost.TButton",
|
||
command=self.destroy).pack(side="right")
|
||
|
||
def _save(self):
|
||
full_name = self.full_name_var.get().strip()
|
||
username = self.username_var.get().strip()
|
||
password = self.password_var.get()
|
||
role = self.role_var.get()
|
||
is_active = int(self.active_var.get())
|
||
|
||
if not username:
|
||
show_error("Username is required.")
|
||
return
|
||
if not self.is_edit and not password:
|
||
show_error("Password is required for new users.")
|
||
return
|
||
|
||
try:
|
||
if self.is_edit:
|
||
from models import update_user
|
||
update_user(self.current_user["id"],
|
||
self.user_data["id"],
|
||
username, role, full_name, is_active,
|
||
password if password else None)
|
||
logger.info(f"User id={self.user_data['id']} updated by admin.")
|
||
show_info("User updated successfully.")
|
||
else:
|
||
from models import create_user
|
||
create_user(self.current_user["id"], username, password, role, full_name)
|
||
logger.info(f"New user '{username}' created by admin.")
|
||
show_info("User created successfully.")
|
||
self.on_save()
|
||
self.destroy()
|
||
except Exception as e:
|
||
show_error(f"Save failed:\n{e}")
|