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.
91 lines
3.6 KiB
TypeScript
91 lines
3.6 KiB
TypeScript
// ============================================================
|
||
// 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 (0–18 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 & Adolescent Immunization Schedule (2025).
|
||
</p>
|
||
</div>
|
||
);
|
||
}
|