This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
app/apps/web/src/portal/ImpersonationBanner.tsx
T
Lint Roller 355f11fdaa fix(e2e): address CTO review feedback on PR #101
- Fix route mismatch: mock /api/impersonation/sessions/session-1 (plural)
- Navigate to /?sessionId=session-1 so CustomerPortal fetches session
- Replace .bg-amber-500 with data-testid="impersonation-banner"
- Remove waitForTimeout(1100), use proper waitFor
- Fix locale-dependent time regex in "banner shows started time" test
- Fix loading state race by waiting for response before fulfilling
- Add data-testid to ImpersonationBanner component
- Add trailing newlines to both spec files

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-22 11:36:07 +00:00

82 lines
2.7 KiB
TypeScript

import { useState, useEffect } from "react";
import { Eye, Clock, LogOut, FileSearch } from "lucide-react";
import type { ImpersonationSession } from "@groombook/types";
interface Props {
session: ImpersonationSession;
isExtended: boolean;
onEnd: () => void;
onExtend: () => void;
onShowAudit: () => void;
}
export function ImpersonationBanner({ session, isExtended, onEnd, onExtend, onShowAudit }: Props) {
const [remaining, setRemaining] = useState("");
const [showWarning, setShowWarning] = useState(false);
useEffect(() => {
const tick = () => {
const now = Date.now();
const expires = new Date(session.expiresAt).getTime();
const diff = expires - now;
if (diff <= 0) {
setRemaining("Expired");
onEnd();
return;
}
const mins = Math.floor(diff / 60000);
const secs = Math.floor((diff % 60000) / 1000);
setRemaining(`${mins}:${secs.toString().padStart(2, "0")}`);
setShowWarning(mins < 5);
};
tick();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, [session.expiresAt, onEnd]);
return (
<div data-testid="impersonation-banner" 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>
{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>
<div className="flex items-center gap-2 ml-auto">
<span className={`flex items-center gap-1 text-xs ${showWarning ? "text-red-800 font-bold animate-pulse" : "text-amber-800"}`}>
<Clock size={14} />
{remaining}
</span>
{showWarning && !isExtended && (
<button
onClick={onExtend}
className="px-2 py-1 text-xs bg-amber-600 text-white rounded hover:bg-amber-700"
>
Extend
</button>
)}
<button
onClick={onShowAudit}
className="px-2 py-1 text-xs bg-amber-100 text-amber-800 rounded hover:bg-amber-200 flex items-center gap-1"
>
<FileSearch size={12} />
Audit
</button>
<button
onClick={onEnd}
className="px-2 py-1 text-xs bg-red-600 text-white rounded hover:bg-red-700 flex items-center gap-1"
>
<LogOut size={12} />
End Session
</button>
</div>
</div>
);
}