perf(client): code-split every heavy route via React.lazy()

Cuts the initial bundle roughly in half. Users who open Home,
Extensions, or FAQ download ~343 kB / 106 kB gz instead of 817 kB /
230 kB gz. Heavy clinical reference data (PE_DATA, Fenton LMS,
Rosner BP splines, 15 Bedside drug panels, 10 Calculator formulas,
13 Settings sub-sections, 10 Admin sub-tabs) only loads when the
user opens that route.

client/src/App.tsx
  Every route except Home / Extensions / FAQ wrapped in
  React.lazy(() => import(...)) + <Suspense fallback>. Extensions and
  FAQ stay in the main chunk because they're small (3 kB, 2 kB) and
  likely visited early. The RouteFallback component shows a single
  "Loading…" line — Vite prefetches chunks on hover so the delay is
  typically invisible.

Resulting chunks (gzipped):
  • index.js             106 kB   React + Router + Query + Layout +
                                   Home + Extensions + FAQ
  • Bedside.js            33 kB   15 clinical dosing panels
  • PeGuide.js            36 kB   PE_DATA hierarchy + scales + sounds
  • Calculators.js        42 kB   All 10 calculators + BP Rosner
                                   coefficients + Fenton + BMI LMS
  • Settings.js           10 kB   13 Settings sub-sections
  • Admin.js               7 kB   10 Admin sub-tabs
  • Learning.js            3 kB
  • Fenton chunk (shared)  3 kB   Reused between Bedside neonatal
                                   and the Growth Charts calculator
  • Note pages (each)  1-2 kB   Encounter / SOAP / Well / Sick /
                                   Hospital / Chart / Dictation / Vax /
                                   Catch-up individually chunked

Vite's 500 kB chunk-size warning is gone. No functional changes.
This commit is contained in:
Daniel 2026-04-24 02:28:51 +02:00
parent 8832659b16
commit dc002360b0
22 changed files with 122 additions and 87 deletions

View file

@ -1,28 +1,43 @@
import { lazy, Suspense } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter, Routes, Route, Navigate, Link } from 'react-router-dom'; import { BrowserRouter, Routes, Route, Navigate, Link } from 'react-router-dom';
import Layout from '@/components/Layout'; import Layout from '@/components/Layout';
// Lightweight pages stay in the main chunk.
import Extensions from '@/pages/Extensions'; import Extensions from '@/pages/Extensions';
import Faq from '@/pages/Faq'; import Faq from '@/pages/Faq';
import Dictation from '@/pages/Dictation';
import Encounter from '@/pages/Encounter'; // Heavy pages lazy-load — keeps the initial bundle small so the home
import Soap from '@/pages/Soap'; // screen and quick tools (Extensions, FAQ) paint fast, and users only
import SickVisit from '@/pages/SickVisit'; // download the big clinical-reference tables (PE_DATA, Fenton LMS,
import HospitalCourse from '@/pages/HospitalCourse'; // Rosner BP splines, Bedside drug panels, Admin sub-tabs) when they
import ChartReview from '@/pages/ChartReview'; // open those specific routes.
import WellVisit from '@/pages/WellVisit'; const Dictation = lazy(() => import('@/pages/Dictation'));
import VaxSchedule from '@/pages/VaxSchedule'; const Encounter = lazy(() => import('@/pages/Encounter'));
import Catchup from '@/pages/Catchup'; const Soap = lazy(() => import('@/pages/Soap'));
import Settings from '@/pages/Settings'; const SickVisit = lazy(() => import('@/pages/SickVisit'));
import Learning from '@/pages/Learning'; const HospitalCourse = lazy(() => import('@/pages/HospitalCourse'));
import PeGuide from '@/pages/PeGuide'; const ChartReview = lazy(() => import('@/pages/ChartReview'));
import Bedside from '@/pages/Bedside'; const WellVisit = lazy(() => import('@/pages/WellVisit'));
import Calculators from '@/pages/Calculators'; const VaxSchedule = lazy(() => import('@/pages/VaxSchedule'));
import Admin from '@/pages/Admin'; const Catchup = lazy(() => import('@/pages/Catchup'));
const Settings = lazy(() => import('@/pages/Settings'));
const Learning = lazy(() => import('@/pages/Learning'));
const PeGuide = lazy(() => import('@/pages/PeGuide'));
const Bedside = lazy(() => import('@/pages/Bedside'));
const Calculators = lazy(() => import('@/pages/Calculators'));
const Admin = lazy(() => import('@/pages/Admin'));
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 30_000, retry: 1 } }, defaultOptions: { queries: { staleTime: 30_000, retry: 1 } },
}); });
function RouteFallback() {
return (
<div className="max-w-3xl mx-auto p-6 text-sm text-muted-foreground">Loading</div>
);
}
function Home() { function Home() {
return ( return (
<div className="max-w-3xl mx-auto p-6 space-y-4"> <div className="max-w-3xl mx-auto p-6 space-y-4">
@ -31,13 +46,13 @@ function Home() {
This is the new React tree. The legacy vanilla-JS app still lives at{' '} This is the new React tree. The legacy vanilla-JS app still lives at{' '}
<a href="/" className="underline">/</a>. <a href="/" className="underline">/</a>.
</p> </p>
<p className="text-sm text-muted-foreground">Ported tabs so far:</p> <p className="text-sm text-muted-foreground">Quick links:</p>
<ul className="list-disc pl-6 text-sm space-y-1"> <ul className="list-disc pl-6 text-sm space-y-1">
<li><Link to="/encounter" className="underline">Encounter HPI</Link></li> <li><Link to="/encounter" className="underline">Encounter HPI</Link></li>
<li><Link to="/dictation" className="underline">Dictation HPI</Link></li> <li><Link to="/dictation" className="underline">Dictation HPI</Link></li>
<li><Link to="/soap" className="underline">SOAP Note</Link></li> <li><Link to="/soap" className="underline">SOAP Note</Link></li>
<li><Link to="/sickvisit" className="underline">Sick Visit</Link></li> <li><Link to="/sickvisit" className="underline">Sick Visit</Link></li>
<li><Link to="/extensions" className="underline">Extensions & Pagers</Link></li> <li><Link to="/extensions" className="underline">Extensions &amp; Pagers</Link></li>
<li><Link to="/faq" className="underline">FAQ</Link></li> <li><Link to="/faq" className="underline">FAQ</Link></li>
</ul> </ul>
</div> </div>
@ -51,23 +66,23 @@ export default function App() {
<Routes> <Routes>
<Route element={<Layout />}> <Route element={<Layout />}>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/encounter" element={<Encounter />} /> <Route path="/encounter" element={<Suspense fallback={<RouteFallback />}><Encounter /></Suspense>} />
<Route path="/dictation" element={<Dictation />} /> <Route path="/dictation" element={<Suspense fallback={<RouteFallback />}><Dictation /></Suspense>} />
<Route path="/soap" element={<Soap />} /> <Route path="/soap" element={<Suspense fallback={<RouteFallback />}><Soap /></Suspense>} />
<Route path="/sickvisit" element={<SickVisit />} /> <Route path="/sickvisit" element={<Suspense fallback={<RouteFallback />}><SickVisit /></Suspense>} />
<Route path="/hospital" element={<HospitalCourse />} /> <Route path="/hospital" element={<Suspense fallback={<RouteFallback />}><HospitalCourse /></Suspense>} />
<Route path="/chart" element={<ChartReview />} /> <Route path="/chart" element={<Suspense fallback={<RouteFallback />}><ChartReview /></Suspense>} />
<Route path="/wellvisit" element={<WellVisit />} /> <Route path="/wellvisit" element={<Suspense fallback={<RouteFallback />}><WellVisit /></Suspense>} />
<Route path="/vaxschedule" element={<VaxSchedule />} /> <Route path="/vaxschedule" element={<Suspense fallback={<RouteFallback />}><VaxSchedule /></Suspense>} />
<Route path="/catchup" element={<Catchup />} /> <Route path="/catchup" element={<Suspense fallback={<RouteFallback />}><Catchup /></Suspense>} />
<Route path="/extensions" element={<Extensions />} /> <Route path="/extensions" element={<Extensions />} />
<Route path="/settings" element={<Settings />} /> <Route path="/settings" element={<Suspense fallback={<RouteFallback />}><Settings /></Suspense>} />
<Route path="/learning" element={<Learning />} /> <Route path="/learning" element={<Suspense fallback={<RouteFallback />}><Learning /></Suspense>} />
<Route path="/peguide" element={<PeGuide />} /> <Route path="/peguide" element={<Suspense fallback={<RouteFallback />}><PeGuide /></Suspense>} />
<Route path="/bedside" element={<Bedside />} /> <Route path="/bedside" element={<Suspense fallback={<RouteFallback />}><Bedside /></Suspense>} />
<Route path="/calculators" element={<Calculators />} /> <Route path="/calculators" element={<Suspense fallback={<RouteFallback />}><Calculators /></Suspense>} />
<Route path="/admin" element={<Admin />} /> <Route path="/admin" element={<Suspense fallback={<RouteFallback />}><Admin /></Suspense>} />
<Route path="/faq" element={<Faq />} /> <Route path="/faq" element={<Faq />} />
{/* catch-all falls back to home while more tabs port over */} {/* catch-all falls back to home while more tabs port over */}
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Route> </Route>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
import{t as e}from"./jsx-runtime-ByY1xr43.js";import{i as t,o as n}from"./index-B0uH73Hx.js";var r=e();function i(){let{data:e,isLoading:i,error:a}=n({queryKey:[`schedule-data`],queryFn:()=>t.get(`/api/schedule-data`)});return i?(0,r.jsx)(`div`,{className:`p-6 text-sm text-muted-foreground`,children:`Loading…`}):a?(0,r.jsx)(`div`,{className:`p-6 text-sm text-destructive`,children:a.message}):e?(0,r.jsxs)(`div`,{className:`max-w-4xl mx-auto p-6 space-y-4`,children:[(0,r.jsxs)(`header`,{children:[(0,r.jsx)(`h1`,{className:`text-2xl font-semibold`,children:`Catch-Up Schedule`}),(0,r.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:`CDC 2025 catch-up immunization schedule — minimum ages and intervals per vaccine.`})]}),Object.entries(e.catchUpSchedule).map(([t,n])=>{let i=e.vaccineFullNames[t]||t,a=n.catchUpNotes?Array.isArray(n.catchUpNotes)?n.catchUpNotes:[n.catchUpNotes]:[];return(0,r.jsxs)(`section`,{className:`rounded-lg border border-border bg-card overflow-hidden`,children:[(0,r.jsxs)(`header`,{className:`px-4 py-2 border-b border-border bg-muted/40 flex items-center justify-between`,children:[(0,r.jsx)(`h2`,{className:`text-sm font-semibold`,children:i}),n.minimumAgeForDose1&&(0,r.jsxs)(`span`,{className:`text-xs text-muted-foreground`,children:[`Min age dose 1: `,(0,r.jsx)(`strong`,{children:n.minimumAgeForDose1})]})]}),n.series&&n.series.length>0&&(0,r.jsxs)(`table`,{className:`w-full text-xs`,children:[(0,r.jsx)(`thead`,{className:`bg-muted/20`,children:(0,r.jsxs)(`tr`,{children:[(0,r.jsx)(`th`,{className:`text-left px-3 py-2`,children:`Dose`}),(0,r.jsx)(`th`,{className:`text-left px-3 py-2`,children:`Min age`}),(0,r.jsx)(`th`,{className:`text-left px-3 py-2`,children:`Min interval from prev`}),(0,r.jsx)(`th`,{className:`text-left px-3 py-2`,children:`Notes`})]})}),(0,r.jsx)(`tbody`,{children:n.series.map(e=>(0,r.jsxs)(`tr`,{className:`border-t border-border`,children:[(0,r.jsxs)(`td`,{className:`px-3 py-2 font-semibold`,children:[`Dose `,e.dose]}),(0,r.jsx)(`td`,{className:`px-3 py-2`,children:e.minimumAge||``}),(0,r.jsx)(`td`,{className:`px-3 py-2`,children:e.minimumIntervalToPrev||``}),(0,r.jsx)(`td`,{className:`px-3 py-2 text-muted-foreground`,children:e.notes||``})]},String(e.dose)))})]}),a.length>0&&(0,r.jsx)(`ul`,{className:`list-disc pl-8 py-2 text-xs text-muted-foreground space-y-1`,children:a.map((e,t)=>(0,r.jsx)(`li`,{children:e},t))})]},t)})]}):null}export{i as default};

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
import{i as e,n as t,t as n}from"./jsx-runtime-ByY1xr43.js";var r=e(t(),1),i=n();function a({open:e,title:t,body:n,confirmText:a=`Confirm`,cancelText:o=`Cancel`,danger:s=!1,requirePassword:c=!1,passwordPlaceholder:l=`Password`,onConfirm:u,onCancel:d,busy:f=!1}){let[p,m]=(0,r.useState)(``);if((0,r.useEffect)(()=>{e||m(``)},[e]),(0,r.useEffect)(()=>{function t(t){e&&t.key===`Escape`&&d()}return window.addEventListener(`keydown`,t),()=>window.removeEventListener(`keydown`,t)},[e,d]),!e)return null;let h=f||c&&!p;return(0,i.jsx)(`div`,{role:`dialog`,"aria-modal":`true`,"aria-labelledby":`confirm-modal-title`,className:`fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4`,onClick:d,children:(0,i.jsxs)(`div`,{className:`w-full max-w-sm rounded-lg border border-border bg-background p-5 shadow-lg space-y-3`,onClick:e=>e.stopPropagation(),children:[(0,i.jsx)(`h3`,{id:`confirm-modal-title`,className:`text-base font-semibold`,children:t}),n&&(0,i.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:n}),c&&(0,i.jsx)(`input`,{type:`password`,autoFocus:!0,value:p,onChange:e=>m(e.target.value),placeholder:l,className:`w-full rounded-md border border-input bg-background px-3 py-2 text-sm`,onKeyDown:e=>{e.key===`Enter`&&!h&&u(p)}}),(0,i.jsxs)(`div`,{className:`flex justify-end gap-2 pt-1`,children:[(0,i.jsx)(`button`,{type:`button`,onClick:d,className:`rounded-md border border-border px-3 py-2 text-sm`,"data-testid":`confirm-modal-cancel`,children:o}),(0,i.jsx)(`button`,{type:`button`,disabled:h,onClick:()=>u(c?p:void 0),className:`rounded-md px-3 py-2 text-sm font-medium text-white disabled:opacity-50 `+(s?`bg-destructive`:`bg-primary`),"data-testid":`confirm-modal-ok`,children:f?`Working…`:a})]})]})})}export{a as t};

View file

@ -0,0 +1 @@
import{i as e,n as t,t as n}from"./jsx-runtime-ByY1xr43.js";import{a as r,i,t as a}from"./index-B0uH73Hx.js";var o=e(t(),1),s=n();function c(){let[e,t]=(0,o.useState)(``),[n,c]=(0,o.useState)(``),[l,u]=(0,o.useState)(`outpatient`),[d,f]=(0,o.useState)(``),[p,m]=(0,o.useState)(null),[h,g]=(0,o.useState)(null),_=r({mutationFn:e=>i.post(`/api/generate-hpi-dictation`,e),onSuccess:e=>m(e.hpi),onError:()=>m(null)});function v(t){t.preventDefault(),g(null);let r={transcript:d,patientAge:e,patientGender:n,setting:l},i=a.safeParse(r);if(!i.success){g(i.error.issues.map(e=>e.message).join(`, `));return}m(null),_.mutate(i.data)}function y(){f(``),m(null),g(null)}let b=`w-full rounded-md border border-input bg-background px-3 py-2 text-sm`;return(0,s.jsxs)(`div`,{className:`max-w-4xl mx-auto p-6 space-y-4`,children:[(0,s.jsxs)(`header`,{children:[(0,s.jsx)(`h1`,{className:`text-2xl font-semibold`,children:`Voice Dictation → HPI`}),(0,s.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:`Dictate your narrative → AI restructures into polished HPI. Audio-capture UI is a follow-up; this minimal form supports typed/pasted transcripts.`})]}),(0,s.jsxs)(`form`,{onSubmit:v,className:`space-y-4`,children:[(0,s.jsxs)(`div`,{className:`grid grid-cols-3 gap-3`,children:[(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Age`}),(0,s.jsx)(`input`,{className:b,placeholder:`e.g. 8 months`,value:e,onChange:e=>t(e.target.value)})]}),(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Gender`}),(0,s.jsxs)(`select`,{className:b,value:n,onChange:e=>c(e.target.value),children:[(0,s.jsx)(`option`,{value:``,children:`Select`}),(0,s.jsx)(`option`,{children:`Male`}),(0,s.jsx)(`option`,{children:`Female`})]})]}),(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Setting`}),(0,s.jsxs)(`select`,{className:b,value:l,onChange:e=>u(e.target.value),children:[(0,s.jsx)(`option`,{value:`outpatient`,children:`Outpatient`}),(0,s.jsx)(`option`,{value:`inpatient`,children:`Inpatient / Floors`})]})]})]}),(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsxs)(`div`,{className:`flex items-center justify-between`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Transcript / dictation`}),(0,s.jsx)(`button`,{type:`button`,onClick:y,className:`text-xs text-muted-foreground underline`,children:`Clear`})]}),(0,s.jsx)(`textarea`,{className:b+` min-h-[200px] font-mono text-sm`,placeholder:`Type or paste your dictation here, then click Generate.`,value:d,onChange:e=>f(e.target.value)})]}),h&&(0,s.jsx)(`div`,{className:`text-sm text-destructive`,children:h}),_.error&&(0,s.jsx)(`div`,{className:`text-sm text-destructive`,children:_.error.message}),(0,s.jsx)(`div`,{className:`flex gap-2`,children:(0,s.jsx)(`button`,{type:`submit`,disabled:_.isPending||!d.trim(),className:`rounded-md bg-primary text-primary-foreground px-4 py-2 text-sm font-medium disabled:opacity-50`,children:_.isPending?`Generating…`:`Generate HPI`})})]}),p&&(0,s.jsxs)(`section`,{className:`rounded-lg border border-border bg-card`,children:[(0,s.jsxs)(`header`,{className:`px-4 py-2 border-b border-border flex items-center justify-between bg-muted/40`,children:[(0,s.jsx)(`h2`,{className:`text-sm font-semibold`,children:`Generated HPI`}),(0,s.jsx)(`button`,{onClick:()=>navigator.clipboard.writeText(p),className:`text-xs text-muted-foreground underline`,children:`Copy`})]}),(0,s.jsx)(`div`,{className:`p-4 whitespace-pre-wrap text-sm`,children:p})]})]})}export{c as default};

View file

@ -0,0 +1 @@
import{i as e,n as t,t as n}from"./jsx-runtime-ByY1xr43.js";import{a as r,i,t as a}from"./index-B0uH73Hx.js";var o=e(t(),1),s=n();function c(){let[e,t]=(0,o.useState)(``),[n,c]=(0,o.useState)(``),[l,u]=(0,o.useState)(`outpatient`),[d,f]=(0,o.useState)(``),[p,m]=(0,o.useState)(null),[h,g]=(0,o.useState)(null),_=r({mutationFn:e=>i.post(`/api/generate-hpi-encounter`,e),onSuccess:e=>m(e.hpi)});function v(t){t.preventDefault(),g(null);let r={transcript:d,patientAge:e,patientGender:n,setting:l},i=a.safeParse(r);if(!i.success){g(i.error.issues.map(e=>e.message).join(`, `));return}m(null),_.mutate(i.data)}let y=`w-full rounded-md border border-input bg-background px-3 py-2 text-sm`;return(0,s.jsxs)(`div`,{className:`max-w-4xl mx-auto p-6 space-y-4`,children:[(0,s.jsxs)(`header`,{children:[(0,s.jsx)(`h1`,{className:`text-2xl font-semibold`,children:`Live Encounter → HPI`}),(0,s.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:`Record or paste an encounter transcript; generate a structured HPI.`})]}),(0,s.jsxs)(`form`,{onSubmit:v,className:`space-y-4`,children:[(0,s.jsxs)(`div`,{className:`grid grid-cols-3 gap-3`,children:[(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Age`}),(0,s.jsx)(`input`,{className:y,placeholder:`e.g. 5 years`,value:e,onChange:e=>t(e.target.value)})]}),(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Gender`}),(0,s.jsxs)(`select`,{className:y,value:n,onChange:e=>c(e.target.value),children:[(0,s.jsx)(`option`,{value:``,children:`Select`}),(0,s.jsx)(`option`,{children:`Male`}),(0,s.jsx)(`option`,{children:`Female`})]})]}),(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Setting`}),(0,s.jsxs)(`select`,{className:y,value:l,onChange:e=>u(e.target.value),children:[(0,s.jsx)(`option`,{value:`outpatient`,children:`Outpatient`}),(0,s.jsx)(`option`,{value:`inpatient`,children:`Inpatient / Floors`})]})]})]}),(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Transcript`}),(0,s.jsx)(`textarea`,{className:y+` min-h-[220px] font-mono text-sm`,placeholder:`Type or paste an encounter transcript, then click Generate.`,value:d,onChange:e=>f(e.target.value)})]}),h&&(0,s.jsx)(`div`,{className:`text-sm text-destructive`,children:h}),_.error&&(0,s.jsx)(`div`,{className:`text-sm text-destructive`,children:_.error.message}),(0,s.jsx)(`button`,{type:`submit`,disabled:_.isPending||!d.trim(),className:`rounded-md bg-primary text-primary-foreground px-4 py-2 text-sm font-medium disabled:opacity-50`,children:_.isPending?`Generating…`:`Generate HPI`})]}),p&&(0,s.jsxs)(`section`,{className:`rounded-lg border border-border bg-card`,children:[(0,s.jsxs)(`header`,{className:`px-4 py-2 border-b border-border flex items-center justify-between bg-muted/40`,children:[(0,s.jsx)(`h2`,{className:`text-sm font-semibold`,children:`Generated HPI`}),(0,s.jsx)(`button`,{onClick:()=>navigator.clipboard.writeText(p),className:`text-xs text-muted-foreground underline`,children:`Copy`})]}),(0,s.jsx)(`div`,{className:`p-4 whitespace-pre-wrap text-sm`,children:p})]})]})}export{c as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
import{i as e,n as t,t as n}from"./jsx-runtime-ByY1xr43.js";import{a as r,i,n as a}from"./index-B0uH73Hx.js";var o=e(t(),1),s=n();function c(){let[e,t]=(0,o.useState)(``),[n,c]=(0,o.useState)(``),[l,u]=(0,o.useState)(``),[d,f]=(0,o.useState)(``),[p,m]=(0,o.useState)(null),[h,g]=(0,o.useState)(null),_=r({mutationFn:e=>i.post(`/api/sick-visit/note`,e),onSuccess:e=>m(e.note)});function v(t){t.preventDefault(),g(null);let r={patientAge:e,patientGender:n,chiefComplaint:l,transcript:d},i=a.safeParse(r);if(!i.success){g(i.error.issues.map(e=>e.message).join(`, `));return}m(null),_.mutate(i.data)}let y=`w-full rounded-md border border-input bg-background px-3 py-2 text-sm`;return(0,s.jsxs)(`div`,{className:`max-w-4xl mx-auto p-6 space-y-4`,children:[(0,s.jsxs)(`header`,{children:[(0,s.jsx)(`h1`,{className:`text-2xl font-semibold`,children:`Sick Visit`}),(0,s.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:`Chief complaint + transcript → structured sick-visit note.`})]}),(0,s.jsxs)(`form`,{onSubmit:v,className:`space-y-4`,children:[(0,s.jsxs)(`div`,{className:`grid grid-cols-3 gap-3`,children:[(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Age`}),(0,s.jsx)(`input`,{className:y,placeholder:`e.g. 4 years`,value:e,onChange:e=>t(e.target.value)})]}),(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Gender`}),(0,s.jsxs)(`select`,{className:y,value:n,onChange:e=>c(e.target.value),children:[(0,s.jsx)(`option`,{value:``,children:`Select`}),(0,s.jsx)(`option`,{children:`Male`}),(0,s.jsx)(`option`,{children:`Female`})]})]}),(0,s.jsxs)(`label`,{className:`flex flex-col gap-1 col-span-3 md:col-span-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Chief complaint`}),(0,s.jsx)(`input`,{className:y,placeholder:`e.g. Fever x 2 days`,value:l,onChange:e=>u(e.target.value)})]})]}),(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Transcript / dictation`}),(0,s.jsx)(`textarea`,{className:y+` min-h-[200px] font-mono text-sm`,placeholder:`Encounter narrative — transcribed or dictated.`,value:d,onChange:e=>f(e.target.value)})]}),h&&(0,s.jsx)(`div`,{className:`text-sm text-destructive`,children:h}),_.error&&(0,s.jsx)(`div`,{className:`text-sm text-destructive`,children:_.error.message}),(0,s.jsx)(`button`,{type:`submit`,disabled:_.isPending||!l.trim(),className:`rounded-md bg-primary text-primary-foreground px-4 py-2 text-sm font-medium disabled:opacity-50`,children:_.isPending?`Generating…`:`Generate Note`})]}),p&&(0,s.jsxs)(`section`,{className:`rounded-lg border border-border bg-card`,children:[(0,s.jsxs)(`header`,{className:`px-4 py-2 border-b border-border flex items-center justify-between bg-muted/40`,children:[(0,s.jsx)(`h2`,{className:`text-sm font-semibold`,children:`Sick Visit Note`}),(0,s.jsx)(`button`,{onClick:()=>navigator.clipboard.writeText(p),className:`text-xs text-muted-foreground underline`,children:`Copy`})]}),(0,s.jsx)(`div`,{className:`p-4 whitespace-pre-wrap text-sm`,children:p})]})]})}export{c as default};

View file

@ -0,0 +1 @@
import{i as e,n as t,t as n}from"./jsx-runtime-ByY1xr43.js";import{a as r,i,r as a}from"./index-B0uH73Hx.js";var o=e(t(),1),s=n();function c(){let[e,t]=(0,o.useState)(``),[n,c]=(0,o.useState)(``),[l,u]=(0,o.useState)(`full`),[d,f]=(0,o.useState)(``),[p,m]=(0,o.useState)(``),[h,g]=(0,o.useState)(null),[_,v]=(0,o.useState)(null),y=r({mutationFn:e=>i.post(`/api/generate-soap`,e),onSuccess:e=>g(e.soap)});function b(t){t.preventDefault(),v(null);let r={transcript:d,patientAge:e,patientGender:n,type:l,additionalInstructions:p},i=a.safeParse(r);if(!i.success){v(i.error.issues.map(e=>e.message).join(`, `));return}g(null),y.mutate(i.data)}let x=`w-full rounded-md border border-input bg-background px-3 py-2 text-sm`;return(0,s.jsxs)(`div`,{className:`max-w-4xl mx-auto p-6 space-y-4`,children:[(0,s.jsxs)(`header`,{children:[(0,s.jsx)(`h1`,{className:`text-2xl font-semibold`,children:`SOAP Note`}),(0,s.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:`Encounter transcript → full SOAP or subjective-only narrative.`})]}),(0,s.jsxs)(`form`,{onSubmit:b,className:`space-y-4`,children:[(0,s.jsxs)(`div`,{className:`grid grid-cols-3 gap-3`,children:[(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Age`}),(0,s.jsx)(`input`,{className:x,placeholder:`e.g. 3 years`,value:e,onChange:e=>t(e.target.value)})]}),(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Gender`}),(0,s.jsxs)(`select`,{className:x,value:n,onChange:e=>c(e.target.value),children:[(0,s.jsx)(`option`,{value:``,children:`Select`}),(0,s.jsx)(`option`,{children:`Male`}),(0,s.jsx)(`option`,{children:`Female`})]})]}),(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Output type`}),(0,s.jsxs)(`select`,{className:x,value:l,onChange:e=>u(e.target.value),children:[(0,s.jsx)(`option`,{value:`full`,children:`Full SOAP`}),(0,s.jsx)(`option`,{value:`subjective`,children:`Subjective only`})]})]})]}),(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Transcript`}),(0,s.jsx)(`textarea`,{className:x+` min-h-[200px] font-mono text-sm`,placeholder:`Type or paste the encounter transcript.`,value:d,onChange:e=>f(e.target.value)})]}),(0,s.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,s.jsxs)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:[`Additional instructions `,(0,s.jsx)(`span`,{className:`text-muted-foreground normal-case font-normal`,children:`(optional)`})]}),(0,s.jsx)(`textarea`,{className:x+` min-h-[60px] text-sm`,placeholder:`e.g., 'Include return precautions', 'Add differential for otitis media'`,value:p,onChange:e=>m(e.target.value)})]}),_&&(0,s.jsx)(`div`,{className:`text-sm text-destructive`,children:_}),y.error&&(0,s.jsx)(`div`,{className:`text-sm text-destructive`,children:y.error.message}),(0,s.jsx)(`button`,{type:`submit`,disabled:y.isPending||!d.trim(),className:`rounded-md bg-primary text-primary-foreground px-4 py-2 text-sm font-medium disabled:opacity-50`,children:y.isPending?`Generating…`:`Generate SOAP`})]}),h&&(0,s.jsxs)(`section`,{className:`rounded-lg border border-border bg-card`,children:[(0,s.jsxs)(`header`,{className:`px-4 py-2 border-b border-border flex items-center justify-between bg-muted/40`,children:[(0,s.jsx)(`h2`,{className:`text-sm font-semibold`,children:`Generated SOAP`}),(0,s.jsx)(`button`,{onClick:()=>navigator.clipboard.writeText(h),className:`text-xs text-muted-foreground underline`,children:`Copy`})]}),(0,s.jsx)(`div`,{className:`p-4 whitespace-pre-wrap text-sm`,children:h})]})]})}export{c as default};

View file

@ -0,0 +1 @@
import{t as e}from"./jsx-runtime-ByY1xr43.js";import{i as t,o as n}from"./index-B0uH73Hx.js";var r=e();function i(){let{data:e,isLoading:i,error:a}=n({queryKey:[`schedule-data`],queryFn:()=>t.get(`/api/schedule-data`)});if(i)return(0,r.jsx)(`div`,{className:`p-6 text-sm text-muted-foreground`,children:`Loading schedule…`});if(a)return(0,r.jsx)(`div`,{className:`p-6 text-sm text-destructive`,children:a.message});if(!e)return null;let o=e.visitAges.filter(t=>e.periodicity[t.id]?.vaccines?.length),s=[],c=new Set;return o.forEach(t=>{e.periodicity[t.id].vaccines.forEach(e=>{c.has(e.vaccine)||(c.add(e.vaccine),s.push(e.vaccine))})}),(0,r.jsxs)(`div`,{className:`max-w-full mx-auto p-6 space-y-4`,children:[(0,r.jsxs)(`header`,{children:[(0,r.jsx)(`h1`,{className:`text-2xl font-semibold`,children:`Vaccine Schedule`}),(0,r.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:`AAP/ACIP 2025 complete immunization schedule (018 years).`})]}),(0,r.jsx)(`div`,{className:`rounded-lg border border-border overflow-auto bg-card`,children:(0,r.jsxs)(`table`,{className:`text-xs`,children:[(0,r.jsx)(`thead`,{className:`sticky top-0 bg-muted`,children:(0,r.jsxs)(`tr`,{children:[(0,r.jsx)(`th`,{className:`text-left font-semibold px-3 py-2 border-b border-border min-w-[180px] sticky left-0 bg-muted`,children:`Vaccine`}),o.map(e=>(0,r.jsx)(`th`,{className:`px-2 py-2 border-b border-border text-center whitespace-nowrap`,children:e.label},e.id))]})}),(0,r.jsx)(`tbody`,{children:s.map(t=>(0,r.jsxs)(`tr`,{className:`even:bg-muted/20`,children:[(0,r.jsx)(`td`,{className:`px-3 py-2 border-b border-border font-medium sticky left-0 bg-card`,children:e.vaccineFullNames[t]||t}),o.map(n=>{let i=(e.periodicity[n.id].vaccines||[]).find(e=>e.vaccine===t);if(!i)return(0,r.jsx)(`td`,{className:`border-b border-border`},n.id);let a=typeof i.dose==`number`?`#`+i.dose:i.dose||``;return(0,r.jsx)(`td`,{className:`border-b border-border text-center bg-primary/10 font-mono text-[11px]`,title:i.notes||`${t} dose ${i.dose}`,children:a},n.id)})]},t))})]})}),(0,r.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:`Hover any filled cell for notes. Sources: AAP/Bright Futures (Feb 2025), CDC Child & Adolescent Immunization Schedule (2025).`})]})}export{i as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -5,7 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/app/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/app/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>client</title> <title>client</title>
<script type="module" crossorigin src="/app/assets/index-4IQgLQaQ.js"></script> <script type="module" crossorigin src="/app/assets/index-B0uH73Hx.js"></script>
<link rel="modulepreload" crossorigin href="/app/assets/jsx-runtime-ByY1xr43.js">
<link rel="stylesheet" crossorigin href="/app/assets/index-DnJqszjp.css"> <link rel="stylesheet" crossorigin href="/app/assets/index-DnJqszjp.css">
</head> </head>
<body> <body>