import { useState, useCallback, useEffect, useRef } from "react"; import { useSearchParams } from "react-router-dom"; import { Home, Calendar, PawPrint, FileText, CreditCard, MessageSquare, Settings, LogOut, Shield, } from "lucide-react"; import { Dashboard } from "./sections/Dashboard.js"; import { AppointmentsSection, RescheduleFlow } from "./sections/Appointments.js"; import { PetProfiles } from "./sections/PetProfiles.js"; import { ReportCards } from "./sections/ReportCards.js"; import { BillingPayments } from "./sections/BillingPayments.js"; import { Communication } from "./sections/Communication.js"; import { AccountSettings } from "./sections/AccountSettings.js"; import { ImpersonationBanner } from "./ImpersonationBanner.js"; import { AuditLogViewer } from "./AuditLogViewer.js"; import { useBranding } from "../BrandingContext.js"; import type { ImpersonationSession } from "@groombook/types"; type Section = "dashboard" | "appointments" | "pets" | "reports" | "billing" | "messages" | "settings"; const NAV_ITEMS: { id: Section; label: string; icon: typeof Home }[] = [ { id: "dashboard", label: "Home", icon: Home }, { id: "appointments", label: "Appointments", icon: Calendar }, { id: "pets", label: "My Pets", icon: PawPrint }, { id: "reports", label: "Report Cards", icon: FileText }, { id: "billing", label: "Billing", icon: CreditCard }, { id: "messages", label: "Messages", icon: MessageSquare }, { id: "settings", label: "Settings", icon: Settings }, ]; export function CustomerPortal() { const [activeSection, setActiveSection] = useState
("dashboard"); const [mobileNavOpen, setMobileNavOpen] = useState(false); const [showAuditLog, setShowAuditLog] = useState(false); const [showReschedule, setShowReschedule] = useState(false); const [rescheduleAppointment, setRescheduleAppointment] = useState | null>(null); const [session, setSession] = useState(null); const [sessionExtended, setSessionExtended] = useState(false); const [clientName, setClientName] = useState(""); const { branding } = useBranding(); const [searchParams, setSearchParams] = useSearchParams(); // On mount: load session from ?sessionId= URL param const initDone = useRef(false); useEffect(() => { if (initDone.current) return; initDone.current = true; const sessionId = searchParams.get("sessionId"); if (!sessionId) return; fetch(`/api/impersonation/sessions/${sessionId}`) .then((r) => { if (!r.ok) return null; return r.json() as Promise; }) .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 }); }) .catch(() => { setSearchParams({}, { replace: true }); }); }, []); const handleEnd = useCallback(async () => { if (!session) return; try { await fetch(`/api/impersonation/sessions/${session.id}/end`, { method: "POST" }); } catch { // Ignore — session ends on the client regardless } setSession(null); setSessionExtended(false); window.location.href = "/admin/clients"; }, [session]); const handleExtend = useCallback(async () => { if (!session) return; try { const r = await fetch(`/api/impersonation/sessions/${session.id}/extend`, { method: "POST" }); if (r.ok) { const updated = await r.json() as ImpersonationSession; setSession(updated); setSessionExtended(true); } } catch { // Best-effort } }, [session]); const logPageView = useCallback((page: string) => { if (!session) return; void fetch(`/api/impersonation/sessions/${session.id}/log`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "page_view", pageVisited: page }), }); }, [session]); const handleNavClick = (section: Section) => { setActiveSection(section); setMobileNavOpen(false); if (session?.status === "active") { logPageView(section); } }; const handleReschedule = useCallback((appointment: Record) => { setRescheduleAppointment(appointment); setShowReschedule(true); }, []); const isReadOnly = session?.status === "active"; const renderSection = () => { switch (activeSection) { case "dashboard": return ; case "appointments": return ; case "pets": return ; case "reports": return ; case "billing": return ; case "messages": return ; case "settings": return ; } }; return (
{session?.status === "active" && ( <> { void handleEnd(); }} onExtend={() => { void handleExtend(); }} onShowAudit={() => setShowAuditLog(true)} /> {/* Watermark */}
STAFF VIEW
)} {showAuditLog && session && ( setShowAuditLog(false)} /> )} {showReschedule && rescheduleAppointment && ( // eslint-disable-next-line @typescript-eslint/no-explicit-any { setShowReschedule(false); setRescheduleAppointment(null); }} sessionId={session?.id ?? null} /> )} {/* Mobile Header */}
{branding.businessName}
SM
{/* Sidebar Navigation */} {/* Mobile nav overlay */} {mobileNavOpen && (
setMobileNavOpen(false)} /> )} {/* Main Content */}

{NAV_ITEMS.find(n => n.id === activeSection)?.label}

Hi, {CUSTOMER.name.split(" ")[0]}
SM
{renderSection()}
); }