""" views/admin_websites_view.py — Admin panel: Website Link Management tab. Changes: added Check Type (Daily / Weekly) field and treeview column. """ import tkinter as tk from tkinter import ttk import logging from utils.ui_helpers import ( COLOURS, FONT, FONT_BOLD, FONT_HEADING, FONT_SMALL, scrolled_text, show_error, show_info, confirm_delete ) logger = logging.getLogger("admin_websites_view") CHECK_TYPE_OPTIONS = ["daily", "weekly"] CHECK_TYPE_LABELS = {"daily": "📅 Daily", "weekly": "🗓 Weekly"} class AdminWebsitesView(ttk.Frame): def __init__(self, parent, current_user: dict): super().__init__(parent) self.current_user = current_user self._build_ui() self._load_websites() # ─── Layout ─────────────────────────────────────────────────────────────── def _build_ui(self): toolbar = ttk.Frame(self) toolbar.pack(fill="x", pady=(0, 10)) ttk.Label(toolbar, text="Website Link Management", style="Heading.TLabel").pack(side="left") ttk.Button(toolbar, text="+ Add Website", command=self._open_add).pack(side="right", padx=(4, 0)) ttk.Button(toolbar, text="✎ Edit", style="Ghost.TButton", command=self._open_edit).pack(side="right", padx=(4, 0)) ttk.Button(toolbar, text="✕ Delete", style="Danger.TButton", command=self._delete_selected).pack(side="right") cols = ("ID", "Name", "Type", "URL", "Note", "Created By") widths = [40, 170, 80, 230, 150, 100] self.tree = ttk.Treeview(self, columns=cols, show="headings", selectmode="browse") for col, w in zip(cols, widths): self.tree.heading(col, text=col) self.tree.column(col, width=w, anchor="center" if w <= 80 else "w") self.tree.pack(fill="both", expand=True) self.tree.bind("", lambda _: self._open_edit()) # Tag weekly rows with a subtle colour self.tree.tag_configure("weekly", foreground=COLOURS["warning"]) 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_websites(self): from models import get_all_websites self.tree.delete(*self.tree.get_children()) try: for w in get_all_websites(): ct = w.get("check_type") or "daily" tag = "weekly" if ct == "weekly" else "" self.tree.insert( "", "end", iid=str(w["id"]), tags=(tag,), values=( w["id"], w["name"], CHECK_TYPE_LABELS.get(ct, ct), w["url"], (w["note"] or "")[:60], w["creator"] or "", ) ) except Exception as e: show_error(f"Failed to load websites:\n{e}") def _get_selected_id(self): sel = self.tree.selection() return int(sel[0]) if sel else None # ─── Actions ────────────────────────────────────────────────────────────── def _open_add(self): WebsiteDialog(self, self.current_user, website_data=None, on_save=self._load_websites) def _open_edit(self): wid = self._get_selected_id() if not wid: show_error("Please select a website to edit.") return from models import get_website_by_id, get_website_credentials, get_website_assigned_users data = get_website_by_id(wid) creds = get_website_credentials(wid) data["credentials"] = creds data["assigned_users"] = get_website_assigned_users(wid) WebsiteDialog(self, self.current_user, website_data=data, on_save=self._load_websites) def _delete_selected(self): wid = self._get_selected_id() if not wid: show_error("Please select a website to delete.") return vals = self.tree.item(wid, "values") name = vals[1] if vals else str(wid) if confirm_delete(name): try: from models import delete_website delete_website(self.current_user["id"], wid) logger.info(f"Website id={wid} deleted by admin.") show_info(f"Website '{name}' deleted.") self._load_websites() except Exception as e: show_error(f"Delete failed:\n{e}") # ─── Website Dialog (Add / Edit) ────────────────────────────────────────────── class WebsiteDialog(tk.Toplevel): def __init__(self, parent, current_user, website_data, on_save): super().__init__(parent) self.current_user = current_user self.website_data = website_data self.on_save = on_save self.is_edit = website_data is not None self.cred_rows = [] self.title("Edit Website" if self.is_edit else "Add Website") self.configure(bg=COLOURS["bg"]) self.grab_set() self._build_ui() self._centre() def _centre(self): self.update_idletasks() w, h = 600, 680 x = (self.winfo_screenwidth() - w) // 2 y = (self.winfo_screenheight() - h) // 2 self.geometry(f"{w}x{h}+{x}+{y}") def _build_ui(self): # Scrollable container canvas = tk.Canvas(self, bg=COLOURS["bg"], highlightthickness=0) vsb = ttk.Scrollbar(self, orient="vertical", command=canvas.yview) canvas.configure(yscrollcommand=vsb.set) vsb.pack(side="right", fill="y") canvas.pack(fill="both", expand=True) self.inner = ttk.Frame(canvas) self.inner.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) canvas.create_window((0, 0), window=self.inner, anchor="nw") # Scope mousewheel to canvas hover — avoids stale-widget errors on close def _enter(e): canvas.bind_all("", lambda ev: _safe_scroll(ev)) def _leave(e): try: canvas.unbind_all("") except Exception: pass def _safe_scroll(ev): try: canvas.yview_scroll(int(-1 * (ev.delta / 120)), "units") except Exception: pass canvas.bind("", _enter) canvas.bind("", _leave) self.protocol("WM_DELETE_WINDOW", lambda: (_leave(None), self.destroy())) ttk.Label(self.inner, text="Edit Website" if self.is_edit else "New Website", style="Heading.TLabel").pack(anchor="w", padx=24, pady=(20, 4)) form = ttk.Frame(self.inner) form.pack(fill="x", padx=24, pady=4) form.columnconfigure(1, weight=1) def text_row(label, r): ttk.Label(form, text=label).grid(row=r, column=0, sticky="nw", padx=(0, 10), pady=6) var = tk.StringVar() ent = ttk.Entry(form, textvariable=var) ent.grid(row=r, column=1, sticky="ew", pady=6) return var self.name_var = text_row("Name *", 0) self.url_var = text_row("URL *", 1) # ── Check Type ──────────────────────────────────────────────────────── ttk.Label(form, text="Check Type").grid( row=2, column=0, sticky="w", padx=(0, 10), pady=6) type_frame = tk.Frame(form, bg=COLOURS["bg"]) type_frame.grid(row=2, column=1, sticky="w", pady=6) self.check_type_var = tk.StringVar(value="daily") for val, lbl in [("daily", "📅 Daily"), ("weekly", "🗓 Weekly")]: rb = tk.Radiobutton( type_frame, text=lbl, variable=self.check_type_var, value=val, bg=COLOURS["bg"], fg=COLOURS["text"], activebackground=COLOURS["bg"], activeforeground=COLOURS["accent"], selectcolor=COLOURS["surface2"], font=FONT, cursor="hand2", ) rb.pack(side="left", padx=(0, 16)) # ── Note ────────────────────────────────────────────────────────────── ttk.Label(form, text="Note").grid(row=3, column=0, sticky="nw", padx=(0, 10), pady=6) note_frame, self.note_txt = scrolled_text(form, height=4) note_frame.grid(row=3, column=1, sticky="ew", pady=6) # ── Visibility ──────────────────────────────────────────────────────── ttk.Label(form, text="Visibility").grid( row=4, column=0, sticky="w", padx=(0, 10), pady=6) vis_frame = tk.Frame(form, bg=COLOURS["bg"]) vis_frame.grid(row=4, column=1, sticky="w", pady=6) self.visibility_var = tk.StringVar(value="all") for val, lbl in [("all", "👥 All Users"), ("assigned", "🔒 Assigned Only")]: rb = tk.Radiobutton( vis_frame, text=lbl, variable=self.visibility_var, value=val, bg=COLOURS["bg"], fg=COLOURS["text"], activebackground=COLOURS["bg"], activeforeground=COLOURS["accent"], selectcolor=COLOURS["surface2"], font=FONT, cursor="hand2", command=self._on_visibility_change, ) rb.pack(side="left", padx=(0, 16)) # ── Assigned users (shown only when visibility='assigned') ───────────── self._user_assign_frame = ttk.Frame(self.inner) self._user_assign_frame.pack(fill="x", padx=24, pady=(0, 4)) ttk.Label(self._user_assign_frame, text="Assigned Users", style="Heading.TLabel").pack(anchor="w", pady=(4, 6)) # Load all active regular users for the picker from models import get_all_users try: all_users = [u for u in get_all_users() if u["is_active"] and u["role"] == "user"] except Exception: all_users = [] self._user_vars = {} # user_id -> BooleanVar user_grid = tk.Frame(self._user_assign_frame, bg=COLOURS["surface"], padx=12, pady=8) user_grid.pack(fill="x") for i, u in enumerate(all_users): var = tk.BooleanVar(value=False) self._user_vars[u["id"]] = var col, row = i % 3, i // 3 tk.Checkbutton( user_grid, text=f"{u['username']} ({u['full_name'] or ''})", variable=var, bg=COLOURS["surface"], fg=COLOURS["text"], activebackground=COLOURS["surface"], activeforeground=COLOURS["accent"], selectcolor=COLOURS["surface2"], font=FONT_SMALL, cursor="hand2", anchor="w", ).grid(row=row, column=col, sticky="w", padx=8, pady=2) # Hide initially; shown when visibility='assigned' self._user_assign_frame.pack_forget() # ── Credentials section (collapsed by default) ─────────────────────── ttk.Separator(self.inner, orient="horizontal").pack( fill="x", padx=24, pady=12) # Toggle header row cred_toggle = ttk.Frame(self.inner) cred_toggle.pack(fill="x", padx=24) ttk.Label(cred_toggle, text="Login Credentials", style="Heading.TLabel").pack(side="left") self._cred_expanded = False self._cred_toggle_btn = ttk.Button( cred_toggle, text="🔑 Show Credentials", style="Ghost.TButton", command=self._toggle_credentials, ) self._cred_toggle_btn.pack(side="right") ttk.Button(cred_toggle, text="+ Add Credential", command=self._add_cred_row_visible).pack(side="right", padx=(0, 6)) # Collapsible body self._cred_body = ttk.Frame(self.inner) self.creds_container = ttk.Frame(self._cred_body) self.creds_container.pack(fill="x") # _cred_body is NOT packed initially — hidden by default # ── Buttons ─────────────────────────────────────────────────────────── ttk.Separator(self.inner, orient="horizontal").pack( fill="x", padx=24, pady=12) btn_frame = ttk.Frame(self.inner) btn_frame.pack(fill="x", padx=24, pady=(0, 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") # Pre-populate if editing if self.is_edit: d = self.website_data self.name_var.set(d.get("name") or "") self.url_var.set(d.get("url") or "") self.check_type_var.set(d.get("check_type") or "daily") self.note_txt.insert("1.0", d.get("note") or "") vis = d.get("visibility") or "all" self.visibility_var.set(vis) # Pre-tick assigned users assigned_ids = {u["id"] for u in d.get("assigned_users", [])} for uid, var in self._user_vars.items(): var.set(uid in assigned_ids) self._on_visibility_change() existing_creds = d.get("credentials", []) for cred in existing_creds: self._add_cred_row(cred) # Auto-expand credentials section if creds already exist if existing_creds: self._expand_credentials() # For new websites: credentials section stays collapsed def _on_visibility_change(self): """Show or hide the user assignment panel based on visibility selection.""" if self.visibility_var.get() == "assigned": # Use before=self._cred_body (not self.creds_container) because # pack's `before` argument requires the reference widget to share # the same parent. _user_assign_frame and _cred_body are both # children of self.inner; creds_container is a child of _cred_body, # so referencing it across that boundary raises a TclError. self._user_assign_frame.pack(fill="x", padx=24, pady=(0, 4), before=self._cred_body) else: self._user_assign_frame.pack_forget() def _expand_credentials(self): """Show the credentials body and update the toggle button label.""" self._cred_expanded = True self._cred_body.pack(fill="x", padx=24, pady=(4, 0)) self._cred_toggle_btn.config(text="🔒 Hide Credentials") def _toggle_credentials(self): """Show or hide the collapsible credentials section.""" if self._cred_expanded: self._cred_expanded = False self._cred_body.pack_forget() self._cred_toggle_btn.config(text="🔑 Show Credentials") else: self._expand_credentials() def _add_cred_row_visible(self): """Expand the section (if collapsed) then add an empty credential row.""" if not self._cred_expanded: self._expand_credentials() self._add_cred_row() def _add_cred_row(self, cred=None): frame = ttk.Frame(self.creds_container, style="Surface.TFrame") frame.pack(fill="x", pady=4, ipady=4) label_var = tk.StringVar(value=cred.get("label", "") if cred else "") user_var = tk.StringVar(value=cred.get("username", "") if cred else "") pass_var = tk.StringVar(value=cred.get("password", "") if cred else "") for col_n, (lbl, var, show) in enumerate([ ("Label", label_var, None), ("Username", user_var, None), ("Password", pass_var, "•"), ]): tk.Label(frame, text=lbl, bg=COLOURS["surface"], fg=COLOURS["text_dim"], font=FONT_SMALL).grid( row=0, column=col_n*2, sticky="w", padx=(8, 2)) ent = ttk.Entry(frame, textvariable=var, show=show or "", width=14) ent.grid(row=0, column=col_n*2+1, sticky="ew", padx=(0, 8), pady=4) ttk.Button(frame, text="✕", style="Danger.TButton", width=3, command=lambda f=frame: self._remove_cred_row(f)).grid( row=0, column=6, padx=(0, 8)) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) frame.columnconfigure(5, weight=1) self.cred_rows.append((label_var, user_var, pass_var, frame)) def _remove_cred_row(self, frame): self.cred_rows = [(l, u, p, f) for l, u, p, f in self.cred_rows if f is not frame] frame.destroy() def _save(self): name = self.name_var.get().strip() url = self.url_var.get().strip() check_type = self.check_type_var.get() note = self.note_txt.get("1.0", "end-1c").strip() visibility = self.visibility_var.get() assigned_user_ids = [uid for uid, var in self._user_vars.items() if var.get()] if not name or not url: show_error("Name and URL are required.") return if visibility == "assigned" and not assigned_user_ids: show_error("Please assign at least one user, or set visibility to All Users.") return credentials = [] for label_var, user_var, pass_var, _ in self.cred_rows: u = user_var.get().strip() if u: credentials.append({ "label": label_var.get().strip(), "username": u, "password": pass_var.get().strip(), }) try: if self.is_edit: from models import update_website update_website(self.current_user["id"], self.website_data["id"], name, url, check_type, note, credentials, visibility, assigned_user_ids) logger.info(f"Website id={self.website_data['id']} updated " f"check_type='{check_type}' visibility='{visibility}'.") show_info("Website updated successfully.") else: from models import create_website create_website(self.current_user["id"], name, url, check_type, note, credentials, visibility, assigned_user_ids) logger.info(f"New website '{name}' created " f"check_type='{check_type}' visibility='{visibility}'.") show_info("Website created successfully.") self.on_save() self.destroy() except Exception as e: show_error(f"Save failed:\n{e}")