feat(web): add "View as Customer" impersonation button on staff Clients page

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: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Groom Book CTO
2026-03-19 12:11:55 +00:00
parent f2501d9972
commit b97f52386e
2 changed files with 28 additions and 1 deletions
+6
View File
@@ -360,6 +360,12 @@ export function ClientsPage() {
)}
</div>
<div style={{ display: "flex", gap: "0.5rem", marginLeft: "auto" }}>
<a
href={`/?impersonate=true&clientName=${encodeURIComponent(selectedClient.name)}&staffName=${encodeURIComponent("Staff")}&reason=${encodeURIComponent(`Support view for ${selectedClient.name}`)}`}
style={{ ...btnStyle, backgroundColor: "#fef3c7", color: "#92400e", borderColor: "#fde68a", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "0.3rem" }}
>
👁 View as Customer
</a>
<button onClick={() => openEditClient(selectedClient)} style={btnStyle}>
Edit client
</button>
+22 -1
View File
@@ -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({