"""Application login dialog backed by the ``Operatori`` table.""" from __future__ import annotations import tkinter as tk from tkinter import messagebox, ttk from typing import Any from audit_log import log_session_event from gestione_aree import AsyncRunner from user_session import UserSession, create_user_session SQL_LOGIN = """ SELECT TOP (1) ID, Login, Nominativo, Privilegio, CodiceUnita FROM dbo.Operatori WHERE LTRIM(RTRIM(Login)) = :login AND LTRIM(RTRIM([Password])) = :password ORDER BY ID; """ def _rows_to_dicts(res: Any) -> list[dict[str, Any]]: """Normalize DB responses into a list of row dictionaries.""" if res is None: return [] if isinstance(res, list): return [row for row in res if isinstance(row, dict)] if isinstance(res, dict): rows = res.get("rows") or res.get("data") or res.get("records") or [] if rows and isinstance(rows[0], dict): return rows cols = res.get("columns") or [] out: list[dict[str, Any]] = [] for row in rows: if isinstance(row, (list, tuple)) and cols: out.append({str(cols[i]): row[i] for i in range(min(len(cols), len(row)))}) return out return [] class LoginWindow(tk.Toplevel): """Small modal dialog used to authenticate one warehouse operator.""" def __init__(self, parent: tk.Misc, db_client): super().__init__(parent) self.db_client = db_client self.result_session: UserSession | None = None self._async = AsyncRunner(self) self._login_button: ttk.Button | None = None self._cancel_button: ttk.Button | None = None self._status_var = tk.StringVar(value="") self.title("Login Warehouse") self.geometry("420x250") self.resizable(False, False) try: if parent is not None and parent.winfo_viewable(): self.transient(parent) except Exception: pass self.protocol("WM_DELETE_WINDOW", self._on_cancel) self.login_var = tk.StringVar() self.password_var = tk.StringVar() self._build_ui() self.update_idletasks() self.grab_set() self.deiconify() self.lift() self.attributes("-topmost", True) self.after(50, self._show_ready) def _build_ui(self) -> None: """Build the compact operator login form.""" body = ttk.Frame(self, padding=12) body.pack(fill="both", expand=True) body.columnconfigure(1, weight=1) ttk.Label( body, text="Autenticazione operatore", font=("Segoe UI", 11, "bold"), ).grid(row=0, column=0, columnspan=2, sticky="w", pady=(4, 14)) ttk.Label(body, text="Login").grid(row=1, column=0, sticky="w", padx=(0, 10), pady=6) self.login_entry = ttk.Entry(body, textvariable=self.login_var, width=28) self.login_entry.grid(row=1, column=1, sticky="ew", pady=6) ttk.Label(body, text="Password").grid(row=2, column=0, sticky="w", padx=(0, 10), pady=6) self.password_entry = ttk.Entry(body, textvariable=self.password_var, width=28, show="*") self.password_entry.grid(row=2, column=1, sticky="ew", pady=6) self.info_label = ttk.Label( body, text="Per ora tutti gli operatori autenticati possono usare tutte le funzioni.", justify="left", wraplength=320, ) self.info_label.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(10, 8)) self.status_label = ttk.Label(body, textvariable=self._status_var, foreground="#555555") self.status_label.grid(row=4, column=0, columnspan=2, sticky="w", pady=(2, 2)) actions = ttk.Frame(body) actions.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(6, 0)) actions.columnconfigure(0, weight=1) self._cancel_button = ttk.Button(actions, text="Annulla", command=self._on_cancel) self._cancel_button.grid(row=0, column=1, padx=(0, 8), pady=8) self._login_button = ttk.Button(actions, text="Accedi", command=self._on_login) self._login_button.grid(row=0, column=2, pady=8) self.bind("", lambda _e: self._on_login()) self.bind("", lambda _e: self._on_cancel()) def _set_busy(self, busy: bool, message: str = "") -> None: """Enable or disable user interaction during async authentication.""" state = "disabled" if busy else "normal" try: self.login_entry.configure(state=state) self.password_entry.configure(state=state) if self._login_button is not None: self._login_button.configure(state=state) if self._cancel_button is not None: self._cancel_button.configure(state=state) self.configure(cursor="watch" if busy else "") self._status_var.set(message) self.update_idletasks() except Exception: pass def _focus_login(self) -> None: """Focus the login entry as soon as the modal becomes visible.""" try: self.login_entry.focus_force() except Exception: pass def _show_ready(self) -> None: """Make the login visible and ready even when the bootstrap root is hidden.""" try: self.attributes("-topmost", True) self.deiconify() self.lift() self.focus_force() self._focus_login() finally: try: self.after(250, lambda: self.attributes("-topmost", False)) except Exception: pass def _on_login(self) -> None: """Validate credentials against the Operatori table.""" login = str(self.login_var.get() or "").strip() password = str(self.password_var.get() or "").strip() if not login or not password: messagebox.showwarning("Login", "Inserisci login e password.", parent=self) return params = {"login": login, "password": password} self._set_busy(True, "Verifico credenziali...") def _ok(res: Any) -> None: rows = _rows_to_dicts(res) self._set_busy(False, "") if not rows: log_session_event( None, action="login.failed", outcome="denied", details={"login": login}, ) messagebox.showerror("Login", "Credenziali non valide.", parent=self) try: self.password_var.set("") self.password_entry.focus_force() except Exception: pass return row = rows[0] self.result_session = create_user_session( operator_id=int(row.get("ID") or 0), login=str(row.get("Login") or login).strip(), nominativo=str(row.get("Nominativo") or "").strip(), privilegio=int(row["Privilegio"]) if row.get("Privilegio") is not None else None, codice_unita=str(row.get("CodiceUnita") or "").strip(), ) log_session_event( self.result_session, action="login.success", outcome="ok", details={"display_name": self.result_session.display_name}, ) self._close() def _err(ex: Exception) -> None: self._set_busy(False, "") log_session_event( None, action="login.error", outcome="error", details={"login": login, "error": str(ex)}, ) messagebox.showerror("Login", f"Verifica credenziali fallita:\n{ex}", parent=self) self._async.run( self.db_client.query_json(SQL_LOGIN, params), _ok, _err, ) def _on_cancel(self) -> None: """Abort the login flow and close the modal.""" log_session_event(None, action="login.cancel", outcome="cancelled") self.result_session = None self._close() def _close(self) -> None: """Release the modal grab and destroy the login window.""" try: self.grab_release() except Exception: pass try: self.destroy() except Exception: pass def prompt_login(parent: tk.Misc, db_client) -> UserSession | None: """Open the login modal and return the authenticated user session, if any.""" dialog = LoginWindow(parent, db_client) dialog.wait_window() return dialog.result_session