pediatric-ai-scribe-v3/client/src/components/Layout.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

115 lines
3.6 KiB
TypeScript

// ============================================================
// LAYOUT — sidebar + main content shell shared across every page.
// Structure mirrors the vanilla app so a user moving between the two
// trees during migration sees consistent navigation.
// ============================================================
import { NavLink, Outlet } from 'react-router-dom';
import type { ReactNode } from 'react';
interface NavItem {
to: string;
label: string;
available?: boolean; // false = rendered as "coming soon" stub
}
interface NavGroup {
label: string;
items: NavItem[];
}
const NAV: NavGroup[] = [
{
label: 'Encounters',
items: [
{ to: '/encounter', label: 'Encounter HPI', available: true },
{ to: '/dictation', label: 'Dictation HPI', available: true },
],
},
{
label: 'Notes',
items: [
{ to: '/hospital', label: 'Hospital Course', available: true },
{ to: '/chart', label: 'Chart Review', available: true },
{ to: '/soap', label: 'SOAP Note', available: true },
{ to: '/wellvisit', label: 'Well Visit', available: true },
{ to: '/sickvisit', label: 'Sick Visit', available: true },
],
},
{
label: 'Clinical Tools',
items: [
{ to: '/vaxschedule', label: 'Vaccine Schedule', available: true },
{ to: '/catchup', label: 'Catch-Up Schedule', available: true },
{ to: '/peguide', label: 'Physical Exam Guide', available: false },
{ to: '/bedside', label: 'Bedside', available: false },
{ to: '/calculators', label: 'Calculators', available: false },
{ to: '/extensions', label: 'Pagers & Extensions', available: true },
{ to: '/learning', label: 'Learning Hub', available: false },
],
},
{
label: 'Account',
items: [
{ to: '/settings', label: 'Settings', available: false },
{ to: '/faq', label: 'FAQ', available: true },
],
},
];
function SidebarLink({ item }: { item: NavItem }) {
if (!item.available) {
return (
<div
className="px-3 py-2 text-sm rounded-md text-muted-foreground italic cursor-not-allowed opacity-60"
title="Not yet ported to React — still available in the vanilla app at /"
>
{item.label} <span className="text-[10px]">· pending</span>
</div>
);
}
return (
<NavLink
to={item.to}
className={({ isActive }) =>
'block px-3 py-2 text-sm rounded-md transition-colors ' +
(isActive
? 'bg-primary text-primary-foreground'
: 'hover:bg-muted text-foreground')
}
>
{item.label}
</NavLink>
);
}
export default function Layout({ children }: { children?: ReactNode }) {
return (
<div className="min-h-screen bg-background text-foreground flex">
{/* Sidebar */}
<aside className="w-64 border-r border-border bg-muted/30 flex-shrink-0 p-3 space-y-4 sticky top-0 h-screen overflow-y-auto">
<div className="px-2 py-1 border-b border-border pb-3">
<div className="font-semibold">Pediatric AI Scribe</div>
<a href="/" className="text-[11px] text-muted-foreground underline">
back to legacy app
</a>
</div>
{NAV.map((group) => (
<div key={group.label} className="space-y-1">
<div className="px-3 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
{group.label}
</div>
{group.items.map((item) => (
<SidebarLink key={item.to} item={item} />
))}
</div>
))}
</aside>
{/* Main */}
<main className="flex-1 min-w-0">
{children ?? <Outlet />}
</main>
</div>
);
}