feat(gro-205): OOBE setup wizard backend + frontend

Backend:
- GET /api/setup/status — public, returns { needsSetup: boolean }
- POST /api/setup — authenticated, marks staff as super user and
  sets business name in a transaction; returns 409 if super user exists
- Setup router registered before auth middleware (GET public, POST protected)

Frontend:
- SetupWizard multi-step page (welcome, business name, super user info,
  second admin info, done)
- needsSetup check after auth: authenticated user with no super user
  redirects to /setup
- BrandingContext refresh after completing wizard

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
groombook-ci[bot]
2026-03-28 20:50:00 +00:00
committed by Flea Flicker
parent 3834e45b66
commit a547931f9b
5 changed files with 351 additions and 5 deletions
+6 -1
View File
@@ -13,7 +13,6 @@ import { Communication } from "./sections/Communication.js";
import { AccountSettings } from "./sections/AccountSettings.js";
import { ImpersonationBanner } from "./ImpersonationBanner.js";
import { AuditLogViewer } from "./AuditLogViewer.js";
import { CUSTOMER } from "./mockData.js";
import { useBranding } from "../BrandingContext.js";
import type { ImpersonationSession } from "@groombook/types";
@@ -37,6 +36,7 @@ export function CustomerPortal() {
const [rescheduleAppointment, setRescheduleAppointment] = useState<Record<string, unknown> | null>(null);
const [session, setSession] = useState<ImpersonationSession | null>(null);
const [sessionExtended, setSessionExtended] = useState(false);
const [clientName, setClientName] = useState<string>("");
const { branding } = useBranding();
const [searchParams, setSearchParams] = useSearchParams();
@@ -57,6 +57,11 @@ export function CustomerPortal() {
.then((s) => {
if (s && s.status === "active") {
setSession(s);
// Fetch client name for display
fetch(`/api/portal/me`, { headers: { "X-Impersonation-Session-Id": s.id } })
.then(r => r.ok ? r.json() : null)
.then(data => { if (data?.name) setClientName(data.name); })
.catch(() => {});
}
// Clean sessionId from URL
setSearchParams({}, { replace: true });