"""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 locale_text import load_locale_catalog, text as loc_text from ui_theme import theme_section, theme_value 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, *, compact: bool = False): super().__init__(parent) self.db_client = db_client self.compact = bool(compact) self.result_session: UserSession | None = None self._async = AsyncRunner(self) self._theme = theme_section("login_window", {}) self._locale_catalog = load_locale_catalog() self._login_button: ttk.Button | None = None self._cancel_button: ttk.Button | None = None self._status_var = tk.StringVar(value="") self._show_ready_after_id: str | None = None self._clear_topmost_after_id: str | None = None self.title(loc_text("login.msg.title", catalog=self._locale_catalog, default="Login")) self.geometry("170x145+0+0" if self.compact else str(theme_value(self._theme, "window_geometry", "165x155+0+0"))) 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._show_ready_after_id = self.after(50, self._show_ready) def _build_ui(self) -> None: """Build the compact operator login form.""" body = ttk.Frame(self, padding=8 if self.compact else 8) body.pack(fill="both", expand=True) body.columnconfigure(1, weight=0) row_offset = 0 ttk.Label(body, text="User:").grid(row=row_offset, column=0, sticky="w", padx=(0, 4), pady=4) self.login_entry = ttk.Entry(body, textvariable=self.login_var, width=10, font=("Segoe UI", 10 if self.compact else 9)) self.login_entry.grid(row=row_offset, column=1, sticky="w", pady=4) ttk.Label(body, text="Pwd:").grid(row=row_offset + 1, column=0, sticky="w", padx=(0, 4), pady=4) self.password_entry = ttk.Entry(body, textvariable=self.password_var, width=10, show="*", font=("Segoe UI", 10 if self.compact else 9)) self.password_entry.grid(row=row_offset + 1, column=1, sticky="w", pady=4) if self.compact: actions = ttk.Frame(body) actions.grid(row=row_offset + 2, column=0, columnspan=2, sticky="ew", pady=(6, 0)) self._cancel_button = ttk.Button( actions, text="Annulla", command=self._on_cancel, ) self._cancel_button.grid(row=1, column=0, sticky="ew", pady=(4, 0)) self._login_button = ttk.Button( actions, text="OK", command=self._on_login, ) self._login_button.grid(row=0, column=0, sticky="ew") else: self.status_label = ttk.Label(body, textvariable=self._status_var, foreground="#555555") self.status_label.grid(row=2, column=0, columnspan=2, sticky="w", pady=(2, 2)) actions = ttk.Frame(body) actions.grid(row=3, column=0, columnspan=2, sticky="w", pady=(6, 0)) self._cancel_button = ttk.Button( actions, text=loc_text("login.button.cancel", catalog=self._locale_catalog, default="Annulla"), command=self._on_cancel, ) self._cancel_button.grid(row=1, column=0, sticky="ew", pady=(4, 0)) self._login_button = ttk.Button( actions, text=loc_text("login.button.submit", catalog=self._locale_catalog, default="OK"), command=self._on_login, ) self._login_button.grid(row=0, column=0, sticky="ew") self.bind("", lambda _e: self._on_login()) self.bind("", lambda _e: self._on_cancel()) self.login_var.trace_add("write", lambda *_: self._limit_var(self.login_var, 10)) self.password_var.trace_add("write", lambda *_: self._limit_var(self.password_var, 10)) def _limit_var(self, variable: tk.StringVar, max_len: int) -> None: value = str(variable.get() or "") if len(value) > max_len: variable.set(value[:max_len]) 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._clear_topmost_after_id = 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( loc_text("login.msg.title", catalog=self._locale_catalog, default="Login"), loc_text("login.msg.missing", catalog=self._locale_catalog, default="Inserisci login e password."), parent=self, ) return params = {"login": login, "password": password} self._set_busy(True, loc_text("login.status.checking", catalog=self._locale_catalog, default="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( loc_text("login.msg.title", catalog=self._locale_catalog, default="Login"), loc_text("login.msg.invalid", catalog=self._locale_catalog, default="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( loc_text("login.msg.title", catalog=self._locale_catalog, default="Login"), loc_text("login.msg.error", catalog=self._locale_catalog, default="Verifica credenziali fallita:\n{error}").format(error=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.""" if self._show_ready_after_id is not None: try: self.after_cancel(self._show_ready_after_id) except Exception: pass self._show_ready_after_id = None if self._clear_topmost_after_id is not None: try: self.after_cancel(self._clear_topmost_after_id) except Exception: pass self._clear_topmost_after_id = None 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 def prompt_login_compact(parent: tk.Misc, db_client) -> UserSession | None: """Open the barcode-oriented compact login modal.""" dialog = LoginWindow(parent, db_client, compact=True) dialog.wait_window() return dialog.result_session