From 12ad7c66a09470a0029b742f641ff378dd72654f Mon Sep 17 00:00:00 2001 From: "groombook-paperclip[bot]" <268890960+groombook-paperclip[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:47:26 +0000 Subject: [PATCH] feat: add View as Customer impersonation button on Clients page (#64) Staff can now click "View as Customer" on any client profile in the admin panel. This navigates to the customer portal with impersonation auto-activated, showing the portal exactly as that customer would see it (read-only, with full audit trail). The portal reads impersonate/clientName/reason/staffName from URL search params on mount, auto-starts the impersonation session, then cleans up the URL. Co-authored-by: Groom Book CTO Co-authored-by: Paperclip --- apps/web/src/pages/Clients.tsx | 6 ++++++ apps/web/src/portal/CustomerPortal.tsx | 23 ++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/apps/web/src/pages/Clients.tsx b/apps/web/src/pages/Clients.tsx index 50df051..242d33a 100644 --- a/apps/web/src/pages/Clients.tsx +++ b/apps/web/src/pages/Clients.tsx @@ -360,6 +360,12 @@ export function ClientsPage() { )}
+ + 👁 View as Customer + diff --git a/apps/web/src/portal/CustomerPortal.tsx b/apps/web/src/portal/CustomerPortal.tsx index e70543a..bb77072 100644 --- a/apps/web/src/portal/CustomerPortal.tsx +++ b/apps/web/src/portal/CustomerPortal.tsx @@ -1,4 +1,4 @@ -import { useState, useReducer, useCallback } from "react"; +import { useState, useReducer, useCallback, useEffect } from "react"; import { Home, Calendar, PawPrint, FileText, CreditCard, MessageSquare, Settings, Eye, LogOut, Clock, Shield, @@ -101,6 +101,27 @@ export function CustomerPortal() { const [impersonation, dispatchImpersonation] = useReducer(impersonationReducer, null); const { branding } = useBranding(); + // Auto-start impersonation from URL params (staff flow from admin panel). + // Runs once on mount only — impersonation state is managed by the reducer after init. + const [impersonationInitDone, setImpersonationInitDone] = useState(false); + useEffect(() => { + if (impersonationInitDone) return; + const params = new URLSearchParams(window.location.search); + if (params.get("impersonate") === "true") { + const clientName = params.get("clientName") || "Unknown Customer"; + const reason = params.get("reason") || `Viewing portal as ${clientName}`; + const staffName = params.get("staffName") || "Staff"; + dispatchImpersonation({ + type: "START", + staffName, + staffRole: "Admin", + reason, + }); + window.history.replaceState({}, "", window.location.pathname); + } + setImpersonationInitDone(true); + }, [impersonationInitDone]); + const logPageView = useCallback((page: string) => { if (impersonation?.active) { dispatchImpersonation({