feat: wire customer portal impersonation to real backend API
Replaces the local impersonationReducer (mock-based) with real API calls to the /api/impersonation/sessions endpoints added in PR #75. Changes: - CustomerPortal: reads ?sessionId= param via useSearchParams, fetches real session on mount, calls /extend and /end on user action, logs page views to /sessions/:id/log. Removes demo sidebar button. - ImpersonationBanner: updated to use ImpersonationSession from @groombook/types instead of the old mockData shape. Accepts isExtended prop to control Extend button visibility. - AuditLogViewer: now fetches from /api/impersonation/sessions/:id/audit-log instead of receiving auditLog[] as a prop. Handles loading/error states. - Clients.tsx: "View as Customer" button now POSTs to /api/impersonation/sessions first, then navigates to /?sessionId=<id>. Handles 409 (existing active session) by reusing it. - mockData.ts: removed ImpersonationSession and AuditEntry interfaces (now live in @groombook/types). - test/setup.ts: set NODE_ENV=test for React 19 + testing-library compat. - portal.test.tsx: 13 new tests covering banner, audit log viewer, and portal session loading behavior (20 total pass). Closes #76 Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Eye, Clock, LogOut, FileSearch } from "lucide-react";
|
||||
import type { ImpersonationSession } from "./mockData.js";
|
||||
import type { ImpersonationSession } from "@groombook/types";
|
||||
|
||||
interface Props {
|
||||
session: ImpersonationSession;
|
||||
isExtended: boolean;
|
||||
onEnd: () => void;
|
||||
onExtend: () => void;
|
||||
onShowAudit: () => void;
|
||||
}
|
||||
|
||||
export function ImpersonationBanner({ session, onEnd, onExtend, onShowAudit }: Props) {
|
||||
export function ImpersonationBanner({ session, isExtended, onEnd, onExtend, onShowAudit }: Props) {
|
||||
const [remaining, setRemaining] = useState("");
|
||||
const [showWarning, setShowWarning] = useState(false);
|
||||
|
||||
@@ -33,20 +34,17 @@ export function ImpersonationBanner({ session, onEnd, onExtend, onShowAudit }: P
|
||||
return () => clearInterval(id);
|
||||
}, [session.expiresAt, onEnd]);
|
||||
|
||||
if (!session.active) return null;
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-40 bg-amber-500 text-amber-950 px-4 py-2.5 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm font-medium shadow-md">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Eye size={16} />
|
||||
STAFF VIEW
|
||||
</span>
|
||||
<span className="hidden sm:inline">
|
||||
Viewing as <strong>{session.customerName}</strong>
|
||||
</span>
|
||||
<span className="hidden md:inline text-amber-800 text-xs">
|
||||
Reason: {session.reason}
|
||||
</span>
|
||||
{session.reason && (
|
||||
<span className="hidden md:inline text-amber-800 text-xs">
|
||||
Reason: {session.reason}
|
||||
</span>
|
||||
)}
|
||||
<span className="hidden sm:inline text-amber-800 text-xs">
|
||||
Started {new Date(session.startedAt).toLocaleTimeString()}
|
||||
</span>
|
||||
@@ -55,7 +53,7 @@ export function ImpersonationBanner({ session, onEnd, onExtend, onShowAudit }: P
|
||||
<Clock size={14} />
|
||||
{remaining}
|
||||
</span>
|
||||
{showWarning && !session.extended && (
|
||||
{showWarning && !isExtended && (
|
||||
<button
|
||||
onClick={onExtend}
|
||||
className="px-2 py-1 text-xs bg-amber-600 text-white rounded hover:bg-amber-700"
|
||||
|
||||
Reference in New Issue
Block a user