From e965b8bb7de2c94029342f3814ed18f604d3e38f Mon Sep 17 00:00:00 2001 From: "groombook-ci[bot]" Date: Sun, 29 Mar 2026 15:15:14 +0000 Subject: [PATCH] fix(portal): add Super User grant/revoke toggle to Staff page (GRO-206) - Add isSuperUser boolean to Staff interface in types - Fetch current user via /api/staff/me to determine if super user - Show "Super User" column and Grant/Revoke buttons only for super users - Disable revoke button when target is the last active super user - Show API error messages when last-super-user guardrail triggers - Prevent self-revocation (no Grant/Revoke button on own row) Co-Authored-By: Paperclip --- apps/web/src/pages/Staff.tsx | 56 ++++++++++++++++++++++++++++++++++-- packages/types/src/index.ts | 1 + 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/apps/web/src/pages/Staff.tsx b/apps/web/src/pages/Staff.tsx index aac23a7..143311e 100644 --- a/apps/web/src/pages/Staff.tsx +++ b/apps/web/src/pages/Staff.tsx @@ -11,6 +11,7 @@ const EMPTY_FORM: StaffForm = { name: "", email: "", role: "groomer" }; export function StaffPage() { const [staff, setStaff] = useState([]); + const [currentUser, setCurrentUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [editing, setEditing] = useState(null); @@ -18,6 +19,7 @@ export function StaffPage() { const [form, setForm] = useState(EMPTY_FORM); const [formError, setFormError] = useState(null); const [saving, setSaving] = useState(false); + const [toggleError, setToggleError] = useState(null); async function load() { const r = await fetch("/api/staff?includeInactive=true"); @@ -25,8 +27,15 @@ export function StaffPage() { setStaff((await r.json()) as Staff[]); } + async function loadCurrentUser() { + const r = await fetch("/api/staff/me"); + if (r.ok) { + setCurrentUser((await r.json()) as Staff); + } + } + useEffect(() => { - load() + Promise.all([load(), loadCurrentUser()]) .catch((e: unknown) => setError(e instanceof Error ? e.message : "Unknown error")) .finally(() => setLoading(false)); }, []); @@ -71,6 +80,26 @@ export function StaffPage() { await load(); } + async function toggleSuperUser(s: Staff) { + setToggleError(null); + const res = await fetch(`/api/staff/${s.id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ isSuperUser: !s.isSuperUser }), + }); + if (!res.ok) { + const err = (await res.json()) as { error?: string }; + setToggleError(err.error ?? `HTTP ${res.status}`); + return; + } + await load(); + await loadCurrentUser(); + } + + const isCurrentSuperUser = currentUser?.isSuperUser ?? false; + const superUserCount = staff.filter((s) => s.isSuperUser && s.active).length; + const isLastSuperUser = (s: Staff) => s.isSuperUser && superUserCount === 1 && s.active; + if (loading) return

Loading…

; if (error) return

Error: {error}

; @@ -90,7 +119,7 @@ export function StaffPage() { - {["Name", "Email", "Role", "Status", ""].map((h) => ( + {["Name", "Email", "Role", ...(isCurrentSuperUser ? ["Super User"] : []), "Status", ""].map((h) => ( ))} @@ -101,12 +130,31 @@ export function StaffPage() { + {isCurrentSuperUser && ( + + )} @@ -117,6 +165,10 @@ export function StaffPage() { )} + {toggleError && ( +

{toggleError}

+ )} + {showForm && (
{h}
{s.name} {s.email} {s.role} + {s.isSuperUser ? ( + Super User + ) : ( + — + )} + {s.active ? "Active" : "Inactive"} + {isCurrentSuperUser && s.id !== currentUser?.id && ( + + )}