pediatric-ai-scribe-v3/client/src/pages/VaxSchedule.tsx
Daniel efd3e32574 feat: port Vaccine Schedule + Catch-Up Schedule (real tables, not stubs)
Instead of shipping placeholder pages, added a new backend endpoint
so the React client can render the real AAP/CDC tables without
duplicating the 2000-line pediatricScheduleData module in the client
bundle:

  GET /api/schedule-data  (authed)
    Returns { visitAges, periodicity, catchUpSchedule, vaccineFullNames }
    — everything the vaccine table and catch-up views need from the
    server-side pediatricScheduleData require(). VACCINE_FULL_NAMES
    (which was inlined in public/js/wellVisit.js) now lives in the
    route file so it's single-sourced.

client/src/pages/VaxSchedule.tsx
  useQuery → /api/schedule-data. Renders the full vaccine × visit-age
  grid with sticky header + sticky first column for long scrolling.
  Each filled cell shows the dose number or bullet, with the original
  note text on hover.

client/src/pages/Catchup.tsx
  Card-per-vaccine layout with min-age / min-interval tables and
  catch-up notes. Matches the vanilla layout.

Layout: vaccine + catch-up links now available in the sidebar.

Typecheck green both sides. Vite build 382 kB / 112 kB gzipped.
2026-04-23 22:25:41 +02:00

91 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ============================================================
// VACCINE SCHEDULE — full AAP/ACIP table, sourced live from
// GET /api/schedule-data.
// ============================================================
import { useQuery } from '@tanstack/react-query';
import { api } from '@/lib/api';
interface VisitAge { id: string; label: string; era: string }
interface VaccineDose { vaccine: string; dose?: number | string; notes?: string }
interface ScheduleData {
visitAges: VisitAge[];
periodicity: Record<string, { vaccines?: VaccineDose[] }>;
vaccineFullNames: Record<string, string>;
}
export default function VaxSchedule() {
const { data, isLoading, error } = useQuery<ScheduleData>({
queryKey: ['schedule-data'],
queryFn: () => api.get<ScheduleData>('/api/schedule-data'),
});
if (isLoading) return <div className="p-6 text-sm text-muted-foreground">Loading schedule</div>;
if (error) return <div className="p-6 text-sm text-destructive">{(error as Error).message}</div>;
if (!data) return null;
const visitsWithVax = data.visitAges.filter((v) => data.periodicity[v.id]?.vaccines?.length);
const vaxKeys: string[] = [];
const seen = new Set<string>();
visitsWithVax.forEach((v) => {
data.periodicity[v.id].vaccines!.forEach((dose) => {
if (!seen.has(dose.vaccine)) { seen.add(dose.vaccine); vaxKeys.push(dose.vaccine); }
});
});
return (
<div className="max-w-full mx-auto p-6 space-y-4">
<header>
<h1 className="text-2xl font-semibold">Vaccine Schedule</h1>
<p className="text-sm text-muted-foreground">
AAP/ACIP 2025 complete immunization schedule (018 years).
</p>
</header>
<div className="rounded-lg border border-border overflow-auto bg-card">
<table className="text-xs">
<thead className="sticky top-0 bg-muted">
<tr>
<th className="text-left font-semibold px-3 py-2 border-b border-border min-w-[180px] sticky left-0 bg-muted">
Vaccine
</th>
{visitsWithVax.map((v) => (
<th key={v.id} className="px-2 py-2 border-b border-border text-center whitespace-nowrap">
{v.label}
</th>
))}
</tr>
</thead>
<tbody>
{vaxKeys.map((key) => (
<tr key={key} className="even:bg-muted/20">
<td className="px-3 py-2 border-b border-border font-medium sticky left-0 bg-card">
{data.vaccineFullNames[key] || key}
</td>
{visitsWithVax.map((v) => {
const vaxList = data.periodicity[v.id].vaccines || [];
const match = vaxList.find((d) => d.vaccine === key);
if (!match) return <td key={v.id} className="border-b border-border" />;
const label = typeof match.dose === 'number' ? '#' + match.dose : (match.dose || '•');
return (
<td
key={v.id}
className="border-b border-border text-center bg-primary/10 font-mono text-[11px]"
title={match.notes || `${key} dose ${match.dose}`}
>
{label}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
<p className="text-xs text-muted-foreground">
Hover any filled cell for notes. Sources: AAP/Bright Futures (Feb 2025), CDC Child &amp; Adolescent Immunization Schedule (2025).
</p>
</div>
);
}