import { useState, useEffect } from "react"; import { Navigate } from "react-router-dom"; import { Calendar, Clock, PawPrint, CreditCard, Star, ChevronRight, AlertTriangle } from "lucide-react"; import { getDevUser } from "../../pages/DevLoginSelector"; interface DashboardProps { sessionId: string | null; clientName: string; onNavigate: (section: "appointments" | "pets" | "billing" | "reports") => void; readOnly: boolean; onReschedule: (appointmentId: string) => void; /** True when a sessionId param was in the URL and the session is still loading */ isImpersonating?: boolean; } interface Appointment { id: string; date: string; time: string; petName: string; serviceName: string; status: string; staffName?: string; services?: string[]; addOns?: string[]; groomerName?: string; } interface Pet { id: string; name: string; species: string; breed?: string; dateOfBirth?: string; weight?: number; healthAlerts: string[]; photo?: string; vaccinations?: { name: string; status: string }[]; } interface Invoice { id: string; invoiceNumber: string; date: string; amount: number; status: string; dueDate?: string; items: { description: string; price: number }[]; } interface Branding { clinicName: string; logoUrl?: string; primaryColor: string; } function daysUntil(dateStr: string): number { const now = new Date(); now.setHours(0, 0, 0, 0); const target = new Date(dateStr); target.setHours(0, 0, 0, 0); return Math.ceil((target.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); } function formatDate(dateStr: string): string { return new Date(dateStr).toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", }); } export function Dashboard({ sessionId, clientName, onNavigate, readOnly, onReschedule, isImpersonating, }: DashboardProps) { const [appointments, setAppointments] = useState([]); const [pets, setPets] = useState([]); const [pendingInvoices, setPendingInvoices] = useState([]); const [branding, setBranding] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { if (!sessionId) { setLoading(false); return; } setLoading(true); setError(null); try { const headers = { "X-Impersonation-Session-Id": sessionId, }; const [appointmentsRes, petsRes, invoicesRes, brandingRes] = await Promise.all([ fetch("/api/portal/appointments", { headers }), fetch("/api/portal/pets", { headers }), fetch("/api/portal/invoices", { headers }), fetch("/api/branding", { headers }), ]); if (!appointmentsRes.ok || !petsRes.ok || !invoicesRes.ok || !brandingRes.ok) { throw new Error("Failed to fetch dashboard data"); } const appointmentsData = await appointmentsRes.json(); const petsData = await petsRes.json(); const invoicesData = await invoicesRes.json(); const brandingData = await brandingRes.json(); setAppointments(appointmentsData.appointments || []); setPets(petsData.pets || []); // Filter for pending invoices only (not "outstanding") const pending = (invoicesData.invoices || []).filter( (invoice: Invoice) => invoice.status === "pending" ); setPendingInvoices(pending); setBranding(brandingData); } catch (err) { setError(err instanceof Error ? err.message : "An error occurred"); } finally { setLoading(false); } }; fetchData(); }, [sessionId]); const getUpcomingAppointments = (): Appointment[] => { const now = new Date(); return appointments .filter((apt) => new Date(`${apt.date}T${apt.time}`) >= now) .sort( (a, b) => new Date(`${a.date}T${a.time}`).getTime() - new Date(`${b.date}T${b.time}`).getTime() ) .slice(0, 5); }; const getPetHealthAlerts = (): { petName: string; alert: string }[] => { return pets .filter((pet) => pet.healthAlerts && pet.healthAlerts.length > 0) .flatMap((pet) => pet.healthAlerts.map((alert) => ({ petName: pet.name, alert })) ); }; const formatCurrency = (amount: number): string => { return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }).format(amount); }; const getPendingBalance = (): number => { return pendingInvoices.reduce((sum, invoice) => sum + invoice.amount, 0); }; if (loading) { return (
); } if (error) { return (

Error: {error}

); } // Don't redirect to /login if we have a dev user — dev sessions may not have // sessionId set immediately after creation (session?.id may be null due to // timing or API response issues). Dev users are stored in localStorage and // verified via the dev-session flow, so they should see the portal. if (!sessionId && !isImpersonating && !getDevUser()) { return ; } const upcomingAppointments = getUpcomingAppointments(); const healthAlerts = getPetHealthAlerts(); const pendingBalance = getPendingBalance(); const nextAppt = upcomingAppointments[0]; return (
{/* Welcome */}

Welcome back, {clientName}

Here's what's happening at {branding?.clinicName || "your clinic"}

{/* Next Appointment */} {nextAppt && (
Next Appointment
{nextAppt.status}

{nextAppt.petName} {nextAppt.groomerName && ` with ${nextAppt.groomerName}`} {nextAppt.staffName && ` with ${nextAppt.staffName}`}

{nextAppt.services?.join(", ") || nextAppt.serviceName || "Appointment"} {nextAppt.addOns && nextAppt.addOns.length > 0 && ` + ${nextAppt.addOns.join(", ")}`}

{formatDate(nextAppt.date)} {nextAppt.time}
{daysUntil(nextAppt.date)}
days away
{!readOnly && (
)}
)} {/* Pet Cards & Loyalty */}
{/* Pet Cards */} {pets.map((pet) => { const petAlerts = pet.healthAlerts || []; return ( ); })} {/* Loyalty Card Placeholder */}
Loyalty Rewards

Coming Soon

Earn points with every visit and redeem for exclusive rewards

{/* Pending Balance & Recent Activity */}
{/* Pending Invoices */} {pendingInvoices.length > 0 && (
Pending Invoices

{formatCurrency(pendingBalance)}

{!readOnly && ( )}
{pendingInvoices.slice(0, 3).map((invoice) => (
{invoice.invoiceNumber} - {formatCurrency(invoice.amount)} Due {invoice.dueDate ? formatDate(invoice.dueDate) : formatDate(invoice.date)}
))}
)} {/* Health Alerts */} {healthAlerts.length > 0 && (
Health Alerts
{healthAlerts.slice(0, 5).map((item, index) => (
{item.petName}:{" "} {item.alert}
))}
)}
); }