chore(verify): drop vanilla-era test + linter; rebuild React bundle

The verify pipeline carried two scripts whose targets no longer
exist after the vanilla deletion at commit 25a9c4c:

  test/calc-math.test.js  — required public/js/calc-math.js (gone);
    every assertion is now covered by shared/clinical/
    calculators.test.ts (vitest).
  scripts/lint-references.js — scans public/js/*.js for DOM id
    references and checks each target exists in public/components/
    *.html. Those trees are gone; TypeScript + knip + vite build
    cover the equivalent dead-reference class for the React tree.

package.json:
  • `verify` drops `test:node` and `lint:refs`; now runs
    typecheck + vitest + knip + client vite build.
  • `test:node` script removed (test/ directory empty).

public/app/assets/ — fresh vite build so the bundle hashes match
the current source (new chunks for ByVisitAge, Cms, DxPicker,
EditableResult, LabsList, Milestones, OutputActions, Recorder,
Shadess, SlideEditor, VisitNote — all features landed in the
preceding commits).

Verified locally on node 20: tsc + 9 test files / 177 tests pass,
knip finds nothing, vite builds 35 chunks, client bundle is 343 kB
initial / 106 kB gzipped (Cms is the biggest lazy chunk at 402 kB
due to Tiptap/ProseMirror).
This commit is contained in:
Daniel 2026-04-24 05:46:45 +02:00
parent f26ad9f33e
commit ae4c36ff8e
45 changed files with 239 additions and 437 deletions

View file

@ -11,14 +11,12 @@
"prebuild": "rm -rf dist",
"build": "tsc",
"typecheck": "tsc --noEmit",
"verify": "npm run typecheck && npm test && npm run test:node && npm run lint:refs && npm run lint:dead && npm --prefix client run build",
"verify": "npm run typecheck && npm test && npm run lint:dead && npm --prefix client run build",
"verify:full": "npm run verify && npm run e2e",
"dev": "ts-node-dev --respawn --transpile-only server.ts",
"test": "vitest run",
"test:node": "node --test test/",
"test:coverage": "vitest run --coverage",
"e2e": "./scripts/e2e.sh",
"lint:refs": "node scripts/lint-references.js",
"lint:dead": "knip --no-config-hints",
"maint:check": "node scripts/maintenance.js check",
"maint:reindex": "node scripts/maintenance.js reindex",

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

@ -1 +1 @@
import{t as e}from"./jsx-runtime-ByY1xr43.js";import{i as t,l as n}from"./index-BuU36CvS.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};
import{t as e}from"./jsx-runtime-ByY1xr43.js";import{i as t,l as n}from"./index-C9wKWOAN.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

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{c as r,i,t as a}from"./index-C9wKWOAN.js";import{n as o,t as s}from"./EditableResult-DaHQp1Pv.js";import{t as c}from"./Recorder-DdXTjnSC.js";var l=e(t(),1),u=n(),d=`dictation`;function f(){let[e,t]=(0,l.useState)(``),[n,f]=(0,l.useState)(``),[p,m]=(0,l.useState)(``),[h,g]=(0,l.useState)(`outpatient`),[_,v]=(0,l.useState)(``),[y,b]=(0,l.useState)(``),[x,S]=(0,l.useState)(null),[C,w]=(0,l.useState)(null),[T,E]=(0,l.useState)(null),D=r({mutationFn:e=>i.post(`/api/generate-hpi-dictation`,e),onSuccess:e=>S(e.hpi),onError:()=>S(null)});function O(e){e.preventDefault(),w(null);let t={transcript:(y||_).trim(),patientAge:n,patientGender:p,setting:h},r=a.safeParse(t);if(!r.success){w(r.error.issues.map(e=>e.message).join(`, `));return}S(null),D.mutate(r.data)}function k(){v(``),b(``),S(null),w(null)}let A=`w-full rounded-md border border-input bg-background px-3 py-2 text-sm`,j=y||_;return(0,u.jsxs)(`div`,{className:`max-w-4xl mx-auto p-6 space-y-4`,children:[(0,u.jsxs)(`header`,{children:[(0,u.jsx)(`h1`,{className:`text-2xl font-semibold`,children:`Voice Dictation → HPI`}),(0,u.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:`Dictate your narrative → AI restructures into polished HPI.`})]}),(0,u.jsx)(o,{type:d,label:e,setLabel:t,transcript:_,generatedNote:x||``,partialData:{age:n,gender:p,setting:h},onLoad:e=>{v(e.transcript||``),b(``),S(e.generated_note||null);try{let t=e.partial_data?JSON.parse(e.partial_data):null;t?.age&&f(t.age),t?.gender&&m(t.gender),t?.setting&&g(t.setting)}catch{}t(e.label||``)},onClear:()=>{k(),f(``),m(``),g(`outpatient`)}}),(0,u.jsxs)(`form`,{onSubmit:O,className:`space-y-4`,children:[(0,u.jsxs)(`div`,{className:`grid grid-cols-3 gap-3`,children:[(0,u.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,u.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Age`}),(0,u.jsx)(`input`,{className:A,placeholder:`e.g. 8 months`,value:n,onChange:e=>f(e.target.value)})]}),(0,u.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,u.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Gender`}),(0,u.jsxs)(`select`,{className:A,value:p,onChange:e=>m(e.target.value),children:[(0,u.jsx)(`option`,{value:``,children:`Select`}),(0,u.jsx)(`option`,{children:`Male`}),(0,u.jsx)(`option`,{children:`Female`})]})]}),(0,u.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,u.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Setting`}),(0,u.jsxs)(`select`,{className:A,value:h,onChange:e=>g(e.target.value),children:[(0,u.jsx)(`option`,{value:`outpatient`,children:`Outpatient`}),(0,u.jsx)(`option`,{value:`inpatient`,children:`Inpatient / Floors`})]})]})]}),(0,u.jsx)(c,{module:`dictation`,onTranscript:(e,t)=>{v(n=>t.appended&&n?n+` `+e:e),b(``),E(null)},onInterim:e=>b(e?_?_+` `+e:e:``),onError:e=>E(e)}),T&&(0,u.jsx)(`div`,{className:`text-sm text-destructive`,children:T}),(0,u.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,u.jsxs)(`div`,{className:`flex items-center justify-between`,children:[(0,u.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Transcript / dictation`}),(0,u.jsx)(`button`,{type:`button`,onClick:k,className:`text-xs text-muted-foreground underline`,children:`Clear`})]}),(0,u.jsx)(`textarea`,{className:A+` min-h-[200px] font-mono text-sm`,placeholder:`Click Start recording, or type / paste your dictation here.`,value:j,onChange:e=>{v(e.target.value),b(``)}})]}),C&&(0,u.jsx)(`div`,{className:`text-sm text-destructive`,children:C}),D.error&&(0,u.jsx)(`div`,{className:`text-sm text-destructive`,children:D.error.message}),(0,u.jsx)(`div`,{className:`flex gap-2`,children:(0,u.jsx)(`button`,{type:`submit`,disabled:D.isPending||!j.trim(),className:`rounded-md bg-primary text-primary-foreground px-4 py-2 text-sm font-medium disabled:opacity-50`,children:D.isPending?`Generating…`:`Generate HPI`})})]}),x!==null&&(0,u.jsx)(s,{text:x,onChange:S,section:`hpi`,title:`Generated HPI`,exportLabel:`hpi-dictation`,exportType:`hpi`,sourceContext:j})]})}export{f as default};

View file

@ -1 +0,0 @@
import{i as e,n as t,t as n}from"./jsx-runtime-ByY1xr43.js";import{c as r,i,t as a}from"./index-BuU36CvS.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};

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{c as r,i,t as a}from"./index-C9wKWOAN.js";import{n as o,t as s}from"./EditableResult-DaHQp1Pv.js";import{t as c}from"./Recorder-DdXTjnSC.js";var l=e(t(),1),u=n(),d=`encounter`;function f(){let[e,t]=(0,l.useState)(``),[n,f]=(0,l.useState)(``),[p,m]=(0,l.useState)(``),[h,g]=(0,l.useState)(`outpatient`),[_,v]=(0,l.useState)(``),[y,b]=(0,l.useState)(``),[x,S]=(0,l.useState)(null),[C,w]=(0,l.useState)(null),[T,E]=(0,l.useState)(null),D=r({mutationFn:e=>i.post(`/api/generate-hpi-encounter`,e),onSuccess:e=>S(e.hpi)});function O(e){e.preventDefault(),w(null);let t={transcript:(y||_).trim(),patientAge:n,patientGender:p,setting:h},r=a.safeParse(t);if(!r.success){w(r.error.issues.map(e=>e.message).join(`, `));return}S(null),D.mutate(r.data)}let k=`w-full rounded-md border border-input bg-background px-3 py-2 text-sm`,A=y||_;return(0,u.jsxs)(`div`,{className:`max-w-4xl mx-auto p-6 space-y-4`,children:[(0,u.jsxs)(`header`,{children:[(0,u.jsx)(`h1`,{className:`text-2xl font-semibold`,children:`Live Encounter → HPI`}),(0,u.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:`Record or paste an encounter transcript; generate a structured HPI.`})]}),(0,u.jsx)(o,{type:d,label:e,setLabel:t,transcript:_,generatedNote:x||``,partialData:{age:n,gender:p,setting:h},onLoad:e=>{v(e.transcript||``),b(``),S(e.generated_note||null);try{let t=e.partial_data?JSON.parse(e.partial_data):null;t?.age&&f(t.age),t?.gender&&m(t.gender),t?.setting&&g(t.setting)}catch{}t(e.label||``)},onClear:()=>{v(``),b(``),S(null),f(``),m(``),g(`outpatient`)}}),(0,u.jsxs)(`form`,{onSubmit:O,className:`space-y-4`,children:[(0,u.jsxs)(`div`,{className:`grid grid-cols-3 gap-3`,children:[(0,u.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,u.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Age`}),(0,u.jsx)(`input`,{className:k,placeholder:`e.g. 5 years`,value:n,onChange:e=>f(e.target.value)})]}),(0,u.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,u.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Gender`}),(0,u.jsxs)(`select`,{className:k,value:p,onChange:e=>m(e.target.value),children:[(0,u.jsx)(`option`,{value:``,children:`Select`}),(0,u.jsx)(`option`,{children:`Male`}),(0,u.jsx)(`option`,{children:`Female`})]})]}),(0,u.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,u.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Setting`}),(0,u.jsxs)(`select`,{className:k,value:h,onChange:e=>g(e.target.value),children:[(0,u.jsx)(`option`,{value:`outpatient`,children:`Outpatient`}),(0,u.jsx)(`option`,{value:`inpatient`,children:`Inpatient / Floors`})]})]})]}),(0,u.jsx)(c,{module:`encounter`,onTranscript:(e,t)=>{v(n=>t.appended&&n?n+` `+e:e),b(``),E(null)},onInterim:e=>b(e?_?_+` `+e:e:``),onError:e=>E(e)}),T&&(0,u.jsx)(`div`,{className:`text-sm text-destructive`,children:T}),(0,u.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,u.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Transcript`}),(0,u.jsx)(`textarea`,{className:k+` min-h-[220px] font-mono text-sm`,placeholder:`Click Start recording, or type / paste an encounter transcript here.`,value:A,onChange:e=>{v(e.target.value),b(``)}})]}),C&&(0,u.jsx)(`div`,{className:`text-sm text-destructive`,children:C}),D.error&&(0,u.jsx)(`div`,{className:`text-sm text-destructive`,children:D.error.message}),(0,u.jsx)(`button`,{type:`submit`,disabled:D.isPending||!A.trim(),className:`rounded-md bg-primary text-primary-foreground px-4 py-2 text-sm font-medium disabled:opacity-50`,children:D.isPending?`Generating…`:`Generate HPI`})]}),x!==null&&(0,u.jsx)(s,{text:x,onChange:S,section:`encounter`,title:`Generated HPI`,exportLabel:`hpi-encounter`,exportType:`hpi`,sourceContext:A})]})}export{f as default};

View file

@ -1 +0,0 @@
import{i as e,n as t,t as n}from"./jsx-runtime-ByY1xr43.js";import{c as r,i,t as a}from"./index-BuU36CvS.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

View file

@ -0,0 +1 @@
import{t as e}from"./jsx-runtime-ByY1xr43.js";var t=e(),n=`rounded-md border border-input bg-background px-2 py-1 text-sm`,r=`inline-flex items-center gap-1 rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium hover:bg-muted`;function i(){return{date:``,values:``}}function a({value:e,onChange:a,placeholder:o,testIdPrefix:s=`labs`}){function c(t,n){a(e.map((e,r)=>r===t?{...e,...n}:e))}function l(){a([...e,i()])}function u(t){a(e.filter((e,n)=>n!==t))}return(0,t.jsxs)(`div`,{className:`space-y-2`,"data-testid":s,children:[e.length===0&&(0,t.jsx)(`div`,{className:`text-xs text-muted-foreground italic`,children:`No labs added.`}),e.map((e,r)=>(0,t.jsxs)(`div`,{className:`flex flex-col sm:flex-row items-start gap-2`,"data-testid":s+`-row-`+r,children:[(0,t.jsx)(`input`,{type:`date`,value:e.date,onChange:e=>c(r,{date:e.target.value}),className:n+` w-40 shrink-0`}),(0,t.jsx)(`textarea`,{value:e.values,onChange:e=>c(r,{values:e.target.value}),placeholder:o||`e.g. WBC 12.5, H/H 10.2/31, BMP Na 138, K 3.5`,className:n+` flex-1 min-h-[60px] font-mono text-xs`}),(0,t.jsx)(`button`,{type:`button`,onClick:()=>u(r),className:`text-xs text-destructive hover:text-red-700`,title:`Remove`,children:`🗑`})]},r)),(0,t.jsx)(`button`,{type:`button`,onClick:l,className:r,children:`+ Add lab`})]})}export{i as n,a as t};

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{i as r}from"./index-C9wKWOAN.js";var i=e(t(),1),a=n();function o({text:e,onUpdate:t,sourceContext:n,exportLabel:o,exportType:s}){let[c,l]=(0,i.useState)(``),[u,d]=(0,i.useState)(null),[f,p]=(0,i.useState)(null),m=(0,i.useRef)(null),h=(0,i.useRef)(null);async function g(){try{await navigator.clipboard.writeText(e),p({kind:`ok`,text:`Copied`})}catch{p({kind:`err`,text:`Copy failed`})}}async function _(){if(!e.trim()){p({kind:`err`,text:`Nothing to read`});return}if(m.current&&!m.current.paused){m.current.pause(),m.current=null,h.current&&=(URL.revokeObjectURL(h.current),null);return}d(`tts`),p(null);try{let t=await fetch(`/api/text-to-speech`,{method:`POST`,credentials:`include`,headers:{"Content-Type":`application/json`},body:JSON.stringify({text:e})});if(!t.ok){let e=(t.headers.get(`content-type`)||``).includes(`json`)&&(await t.json()).error||`TTS failed`;throw Error(e)}let n=await t.blob(),r=URL.createObjectURL(n);h.current=r;let i=new Audio(r);m.current=i,i.onended=()=>{URL.revokeObjectURL(r),h.current===r&&(h.current=null),m.current===i&&(m.current=null)},await i.play()}catch(e){p({kind:`err`,text:e.message})}finally{d(null)}}async function v(){if(!e.trim()){p({kind:`err`,text:`Nothing to export`});return}d(`export`),p(null);try{let t=o+`-`+Date.now();p({kind:`ok`,text:(await r.post(`/api/nextcloud/export`,{content:e,filename:t,type:s})).message||`Exported`})}catch(e){p({kind:`err`,text:e.message||`Nextcloud not connected`})}finally{d(null)}}async function y(){if(!e.trim()){p({kind:`err`,text:`No document to refine`});return}if(!c.trim()){p({kind:`err`,text:`Enter instructions`});return}d(`refine`),p(null);try{t((await r.post(`/api/refine`,{currentDocument:e,instructions:c.trim(),sourceContext:n||void 0})).refined),l(``),p({kind:`ok`,text:`Refined`})}catch(e){p({kind:`err`,text:e.message||`Refine failed`})}finally{d(null)}}async function b(){if(!e.trim()){p({kind:`err`,text:`Nothing to shorten`});return}d(`shorten`),p(null);try{t((await r.post(`/api/shorten`,{document:e})).shortened),p({kind:`ok`,text:`Shortened`})}catch(e){p({kind:`err`,text:e.message||`Shorten failed`})}finally{d(null)}}let x=`inline-flex items-center gap-1 rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium hover:bg-muted disabled:opacity-50`,S=`inline-flex items-center gap-1 rounded-md bg-primary text-primary-foreground px-3 py-1.5 text-xs font-medium disabled:opacity-50`;return(0,a.jsxs)(`div`,{className:`space-y-2`,"data-testid":`output-actions-`+s,children:[(0,a.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[(0,a.jsx)(`button`,{type:`button`,onClick:g,className:S,"data-testid":`out-copy`,children:`📋 Copy`}),(0,a.jsx)(`button`,{type:`button`,onClick:_,disabled:u===`tts`,className:x,"data-testid":`out-read`,children:u===`tts`?`⌛ Loading…`:m.current&&!m.current.paused?`⏹ Stop`:`🔊 Read`}),(0,a.jsx)(`button`,{type:`button`,onClick:v,disabled:u===`export`,className:x,"data-testid":`out-export`,children:u===`export`?`⌛ Exporting…`:`☁️ Export to Nextcloud`})]}),(0,a.jsxs)(`div`,{className:`flex flex-col sm:flex-row gap-2`,children:[(0,a.jsx)(`textarea`,{className:`flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[60px]`,placeholder:`Tell AI how to modify (e.g., 'make it shorter', 'add that patient has asthma history')`,value:c,onChange:e=>l(e.target.value),"data-testid":`out-refine-input`}),(0,a.jsxs)(`div`,{className:`flex sm:flex-col gap-2`,children:[(0,a.jsx)(`button`,{type:`button`,onClick:y,disabled:u===`refine`||!c.trim(),className:S,"data-testid":`out-refine`,children:u===`refine`?`⌛ Refining…`:`✏️ Refine`}),(0,a.jsx)(`button`,{type:`button`,onClick:b,disabled:u===`shorten`,className:x,"data-testid":`out-shorten`,children:u===`shorten`?`⌛ Shortening…`:`📏 Shorter`})]})]}),f&&(0,a.jsx)(`div`,{className:`text-xs `+(f.kind===`ok`?`text-green-600`:`text-destructive`),children:f.text})]})}export{o as t};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
import{i as e,n as t,t as n}from"./jsx-runtime-ByY1xr43.js";import{i as r,o as i,s as a}from"./index-BuU36CvS.js";var o=e(t(),1),s=n(),c=`rounded-2xl border border-border bg-card p-6 shadow-lg space-y-4 w-full max-w-md`,l=`w-full rounded-md bg-primary text-primary-foreground px-4 py-3 text-sm font-semibold disabled:opacity-60`,u=`w-full rounded-md border border-input bg-background px-3 py-2.5 text-sm outline-none focus:ring-2 focus:ring-ring`,d=`block text-xs font-semibold text-muted-foreground mb-1`,f=`rounded-md bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-900 p-3 text-sm text-green-800 dark:text-green-100`,p=`rounded-md bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900 p-3 text-sm text-red-800 dark:text-red-100`,m=`rounded-md bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-900 p-3 text-sm text-amber-900 dark:text-amber-100`;function h(){let[e]=a(),t=i(),n=e.get(`token`)||``,[h,g]=(0,o.useState)(``),[_,v]=(0,o.useState)(``),[y,b]=(0,o.useState)(``),[x,S]=(0,o.useState)(``),[C,w]=(0,o.useState)(``),[T,E]=(0,o.useState)(!1);async function D(e){if(e.preventDefault(),b(``),S(``),w(``),!n){b(`Missing reset token. Open the reset link from your email.`);return}if(h.length<8){b(`Password must be 8+ characters`);return}if(h!==_){b(`Passwords do not match`);return}E(!0);try{let e=await r.post(`/api/auth/reset-password`,{token:n,newPassword:h});S(`Password reset. You can now sign in.`),e.passwordWarning&&w(e.passwordWarning),setTimeout(()=>t(`/auth`,{replace:!0}),2500)}catch(e){b(e.message||`Reset failed. The link may have expired.`)}finally{E(!1)}}return(0,s.jsx)(`div`,{className:`min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50 dark:from-slate-900 dark:via-slate-950 dark:to-slate-900 flex items-center justify-center p-4`,children:(0,s.jsxs)(`div`,{className:c,children:[(0,s.jsxs)(`header`,{className:`text-center space-y-1`,children:[(0,s.jsx)(`div`,{className:`text-4xl`,children:`🔑`}),(0,s.jsx)(`h1`,{className:`text-xl font-bold`,children:`Set a new password`}),(0,s.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:`Choose a password at least 8 characters long.`})]}),y&&(0,s.jsx)(`div`,{className:p,children:y}),x&&(0,s.jsx)(`div`,{className:f,children:x}),C&&(0,s.jsx)(`div`,{className:m,children:C}),(0,s.jsxs)(`form`,{onSubmit:D,className:`space-y-3`,"data-testid":`reset-password-form`,children:[(0,s.jsxs)(`div`,{children:[(0,s.jsx)(`label`,{className:d,children:`New password`}),(0,s.jsx)(`input`,{type:`password`,required:!0,minLength:8,className:u,value:h,onChange:e=>g(e.target.value),autoFocus:!0,"data-testid":`reset-new`})]}),(0,s.jsxs)(`div`,{children:[(0,s.jsx)(`label`,{className:d,children:`Confirm new password`}),(0,s.jsx)(`input`,{type:`password`,required:!0,minLength:8,className:u,value:_,onChange:e=>v(e.target.value),"data-testid":`reset-confirm`})]}),(0,s.jsx)(`button`,{type:`submit`,className:l,disabled:T||!n,"data-testid":`reset-submit`,children:T?`Resetting…`:`Reset password`})]})]})})}export{h as default};
import{i as e,n as t,t as n}from"./jsx-runtime-ByY1xr43.js";import{i as r,o as i,s as a}from"./index-C9wKWOAN.js";var o=e(t(),1),s=n(),c=`rounded-2xl border border-border bg-card p-6 shadow-lg space-y-4 w-full max-w-md`,l=`w-full rounded-md bg-primary text-primary-foreground px-4 py-3 text-sm font-semibold disabled:opacity-60`,u=`w-full rounded-md border border-input bg-background px-3 py-2.5 text-sm outline-none focus:ring-2 focus:ring-ring`,d=`block text-xs font-semibold text-muted-foreground mb-1`,f=`rounded-md bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-900 p-3 text-sm text-green-800 dark:text-green-100`,p=`rounded-md bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900 p-3 text-sm text-red-800 dark:text-red-100`,m=`rounded-md bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-900 p-3 text-sm text-amber-900 dark:text-amber-100`;function h(){let[e]=a(),t=i(),n=e.get(`token`)||``,[h,g]=(0,o.useState)(``),[_,v]=(0,o.useState)(``),[y,b]=(0,o.useState)(``),[x,S]=(0,o.useState)(``),[C,w]=(0,o.useState)(``),[T,E]=(0,o.useState)(!1);async function D(e){if(e.preventDefault(),b(``),S(``),w(``),!n){b(`Missing reset token. Open the reset link from your email.`);return}if(h.length<8){b(`Password must be 8+ characters`);return}if(h!==_){b(`Passwords do not match`);return}E(!0);try{let e=await r.post(`/api/auth/reset-password`,{token:n,newPassword:h});S(`Password reset. You can now sign in.`),e.passwordWarning&&w(e.passwordWarning),setTimeout(()=>t(`/auth`,{replace:!0}),2500)}catch(e){b(e.message||`Reset failed. The link may have expired.`)}finally{E(!1)}}return(0,s.jsx)(`div`,{className:`min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50 dark:from-slate-900 dark:via-slate-950 dark:to-slate-900 flex items-center justify-center p-4`,children:(0,s.jsxs)(`div`,{className:c,children:[(0,s.jsxs)(`header`,{className:`text-center space-y-1`,children:[(0,s.jsx)(`div`,{className:`text-4xl`,children:`🔑`}),(0,s.jsx)(`h1`,{className:`text-xl font-bold`,children:`Set a new password`}),(0,s.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:`Choose a password at least 8 characters long.`})]}),y&&(0,s.jsx)(`div`,{className:p,children:y}),x&&(0,s.jsx)(`div`,{className:f,children:x}),C&&(0,s.jsx)(`div`,{className:m,children:C}),(0,s.jsxs)(`form`,{onSubmit:D,className:`space-y-3`,"data-testid":`reset-password-form`,children:[(0,s.jsxs)(`div`,{children:[(0,s.jsx)(`label`,{className:d,children:`New password`}),(0,s.jsx)(`input`,{type:`password`,required:!0,minLength:8,className:u,value:h,onChange:e=>g(e.target.value),autoFocus:!0,"data-testid":`reset-new`})]}),(0,s.jsxs)(`div`,{children:[(0,s.jsx)(`label`,{className:d,children:`Confirm new password`}),(0,s.jsx)(`input`,{type:`password`,required:!0,minLength:8,className:u,value:_,onChange:e=>v(e.target.value),"data-testid":`reset-confirm`})]}),(0,s.jsx)(`button`,{type:`submit`,className:l,disabled:T||!n,"data-testid":`reset-submit`,children:T?`Resetting…`:`Reset password`})]})]})})}export{h 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

View file

@ -1 +0,0 @@
import{i as e,n as t,t as n}from"./jsx-runtime-ByY1xr43.js";import{c as r,i,n as a}from"./index-BuU36CvS.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{c as r,i,r as a}from"./index-C9wKWOAN.js";import{n as o,t as s}from"./EditableResult-DaHQp1Pv.js";import{t as c}from"./Recorder-DdXTjnSC.js";var l=e(t(),1),u=n(),d=`soap`;function f(){let[e,t]=(0,l.useState)(``),[n,f]=(0,l.useState)(``),[p,m]=(0,l.useState)(``),[h,g]=(0,l.useState)(`full`),[_,v]=(0,l.useState)(``),[y,b]=(0,l.useState)(``),[x,S]=(0,l.useState)(``),[C,w]=(0,l.useState)(null),[T,E]=(0,l.useState)(null),[D,O]=(0,l.useState)(null),k=r({mutationFn:e=>i.post(`/api/generate-soap`,e),onSuccess:e=>w(e.soap)});function A(e){e.preventDefault(),E(null);let t={transcript:(y||_).trim(),patientAge:n,patientGender:p,type:h,additionalInstructions:x},r=a.safeParse(t);if(!r.success){E(r.error.issues.map(e=>e.message).join(`, `));return}w(null),k.mutate(r.data)}let j=`w-full rounded-md border border-input bg-background px-3 py-2 text-sm`,M=y||_;return(0,u.jsxs)(`div`,{className:`max-w-4xl mx-auto p-6 space-y-4`,children:[(0,u.jsxs)(`header`,{children:[(0,u.jsx)(`h1`,{className:`text-2xl font-semibold`,children:`SOAP Note`}),(0,u.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:`Encounter transcript → full SOAP or subjective-only narrative.`})]}),(0,u.jsx)(o,{type:d,label:e,setLabel:t,transcript:_,generatedNote:C||``,partialData:{age:n,gender:p,type:h,additionalInstructions:x},onLoad:e=>{v(e.transcript||``),b(``),w(e.generated_note||null);try{let t=e.partial_data?JSON.parse(e.partial_data):null;t?.age&&f(t.age),t?.gender&&m(t.gender),t?.type&&g(t.type),t?.additionalInstructions&&S(t.additionalInstructions)}catch{}t(e.label||``)},onClear:()=>{v(``),b(``),w(null),E(null),f(``),m(``),g(`full`),S(``)}}),(0,u.jsxs)(`form`,{onSubmit:A,className:`space-y-4`,children:[(0,u.jsxs)(`div`,{className:`grid grid-cols-3 gap-3`,children:[(0,u.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,u.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Age`}),(0,u.jsx)(`input`,{className:j,placeholder:`e.g. 3 years`,value:n,onChange:e=>f(e.target.value)})]}),(0,u.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,u.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Gender`}),(0,u.jsxs)(`select`,{className:j,value:p,onChange:e=>m(e.target.value),children:[(0,u.jsx)(`option`,{value:``,children:`Select`}),(0,u.jsx)(`option`,{children:`Male`}),(0,u.jsx)(`option`,{children:`Female`})]})]}),(0,u.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,u.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Output type`}),(0,u.jsxs)(`select`,{className:j,value:h,onChange:e=>g(e.target.value),children:[(0,u.jsx)(`option`,{value:`full`,children:`Full SOAP`}),(0,u.jsx)(`option`,{value:`subjective`,children:`Subjective only`})]})]})]}),(0,u.jsx)(c,{module:`soap`,onTranscript:(e,t)=>{v(n=>t.appended&&n?n+` `+e:e),b(``),O(null)},onInterim:e=>b(e?_?_+` `+e:e:``),onError:e=>O(e)}),D&&(0,u.jsx)(`div`,{className:`text-sm text-destructive`,children:D}),(0,u.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,u.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Transcript`}),(0,u.jsx)(`textarea`,{className:j+` min-h-[200px] font-mono text-sm`,placeholder:`Click Start recording, or type / paste the encounter transcript.`,value:M,onChange:e=>{v(e.target.value),b(``)}})]}),(0,u.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,u.jsxs)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:[`Additional instructions `,(0,u.jsx)(`span`,{className:`text-muted-foreground normal-case font-normal`,children:`(optional)`})]}),(0,u.jsx)(`textarea`,{className:j+` min-h-[60px] text-sm`,placeholder:`e.g., 'Include return precautions', 'Add differential for otitis media'`,value:x,onChange:e=>S(e.target.value)})]}),T&&(0,u.jsx)(`div`,{className:`text-sm text-destructive`,children:T}),k.error&&(0,u.jsx)(`div`,{className:`text-sm text-destructive`,children:k.error.message}),(0,u.jsx)(`button`,{type:`submit`,disabled:k.isPending||!M.trim(),className:`rounded-md bg-primary text-primary-foreground px-4 py-2 text-sm font-medium disabled:opacity-50`,children:k.isPending?`Generating…`:`Generate SOAP`})]}),C!==null&&(0,u.jsx)(s,{text:C,onChange:w,section:`soap`,title:`Generated SOAP`,exportLabel:`soap-note`,exportType:`soap`,sourceContext:M})]})}export{f as default};

View file

@ -1 +0,0 @@
import{i as e,n as t,t as n}from"./jsx-runtime-ByY1xr43.js";import{c as r,i,r as a}from"./index-BuU36CvS.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

@ -1 +1 @@
import{t as e}from"./jsx-runtime-ByY1xr43.js";import{i as t,l as n}from"./index-BuU36CvS.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};
import{t as e}from"./jsx-runtime-ByY1xr43.js";import{i as t,l as n}from"./index-C9wKWOAN.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};

View file

@ -0,0 +1,5 @@
import{i as e,n as t,t as n}from"./jsx-runtime-ByY1xr43.js";import{c as r,i}from"./index-C9wKWOAN.js";import{n as ee,t as te}from"./EditableResult-DaHQp1Pv.js";import{t as ne}from"./Recorder-DdXTjnSC.js";import{a,c as o,i as re,n as s,o as c,r as l,s as u,t as ie}from"./DxPicker-CCeyfxop.js";var d=e(t(),1),f=n(),ae=`wellvisit`,p=`w-full rounded-md border border-input bg-background px-3 py-2 text-sm`;function m(){let[e,t]=(0,d.useState)(``),[n,m]=(0,d.useState)(``),[h,g]=(0,d.useState)(``),[_,v]=(0,d.useState)(``),[y,b]=(0,d.useState)(``),[x,S]=(0,d.useState)(``),[C,w]=(0,d.useState)(``),[T,E]=(0,d.useState)(``),[D,O]=(0,d.useState)(``),[k,A]=(0,d.useState)(null),[j,M]=(0,d.useState)(``),[N,P]=(0,d.useState)(``),[F,I]=(0,d.useState)(``),[L,R]=(0,d.useState)(``),[z,B]=(0,d.useState)(``),[V,H]=(0,d.useState)({}),[U,W]=(0,d.useState)({}),[G,K]=(0,d.useState)([]),[q,J]=(0,d.useState)(``),[Y,X]=(0,d.useState)(`full`),[Z,Q]=(0,d.useState)(null);(0,d.useEffect)(()=>{try{let e=sessionStorage.getItem(`wv-shadess-assessment`);e&&M(e);let t=sessionStorage.getItem(`wv-milestones-narrative`);t&&P(t);let n=sessionStorage.getItem(`wv-byvisit-statuses`);n&&B(n);let r=sessionStorage.getItem(`ped_visit_age`);r&&v(e=>e||r)}catch{}},[]);let $=r({mutationFn:e=>i.post(`/api/well-visit/note`,e),onSuccess:e=>Q(e.note)});function oe(e){e.preventDefault(),Q(null);let t=a(l,V,`ROS`),r=a(s,U,`PHYSICAL EXAM`),i=re(G,q);$.mutate({patientAge:n,patientGender:h,visitAge:_,vitals:y,measurements:x,parentConcerns:C,transcript:(D||T).trim(),shadessAssessment:j||void 0,screenings:[F,z].filter(Boolean).join(`
`),vaccines:L,ros:t||void 0,physicalExam:r||void 0,diagnoses:i||void 0,physicianMemories:N?`[DEVELOPMENTAL ASSESSMENT]
`+N:void 0,noteStyle:Y})}let se=D||T;return(0,f.jsxs)(`div`,{className:`space-y-4`,children:[(0,f.jsx)(ee,{type:ae,label:e,setLabel:t,transcript:T,generatedNote:Z||``,partialData:{age:n,gender:h,visitAge:_,vitals:y,measurements:x,parentConcerns:C,shadess:j,milestones:N,byvisit:z,screenings:F,vaccines:L,rosData:V,peData:U,diagnoses:G,dxFreetext:q,noteStyle:Y},onLoad:e=>{E(e.transcript||``),O(``),Q(e.generated_note||null);try{let t=e.partial_data?JSON.parse(e.partial_data):null;t?.age&&m(t.age),t?.gender&&g(t.gender),t?.visitAge&&v(t.visitAge),t?.vitals&&b(t.vitals),t?.measurements&&S(t.measurements),t?.parentConcerns&&w(t.parentConcerns),t?.shadess&&M(t.shadess),t?.milestones&&P(t.milestones),t?.byvisit&&B(t.byvisit),t?.screenings&&I(t.screenings),t?.vaccines&&R(t.vaccines),t?.rosData&&H(t.rosData),t?.peData&&W(t.peData),t?.diagnoses&&K(t.diagnoses),t?.dxFreetext&&J(t.dxFreetext),t?.noteStyle&&X(t.noteStyle)}catch{}t(e.label||``)},onClear:()=>{E(``),O(``),Q(null),m(``),g(``),v(``),b(``),S(``),w(``),M(``),P(``),B(``),I(``),R(``),H({}),W({}),K([]),J(``),X(`full`)}}),(0,f.jsxs)(`form`,{onSubmit:oe,className:`space-y-4`,children:[(0,f.jsxs)(`div`,{className:`grid grid-cols-3 gap-3`,children:[(0,f.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,f.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Age`}),(0,f.jsx)(`input`,{className:p,value:n,onChange:e=>m(e.target.value)})]}),(0,f.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,f.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Gender`}),(0,f.jsxs)(`select`,{className:p,value:h,onChange:e=>g(e.target.value),children:[(0,f.jsx)(`option`,{value:``,children:`Select`}),(0,f.jsx)(`option`,{children:`Male`}),(0,f.jsx)(`option`,{children:`Female`})]})]}),(0,f.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,f.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Visit age`}),(0,f.jsx)(`input`,{className:p,placeholder:`e.g. 6 months`,value:_,onChange:e=>v(e.target.value)})]})]}),(0,f.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,f.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,f.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Vital signs`}),(0,f.jsx)(`textarea`,{className:p+` min-h-[60px] font-mono text-xs`,value:y,onChange:e=>b(e.target.value)})]}),(0,f.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,f.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Measurements / growth`}),(0,f.jsx)(`textarea`,{className:p+` min-h-[60px] font-mono text-xs`,value:x,onChange:e=>S(e.target.value)})]})]}),(0,f.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,f.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Parent / patient concerns`}),(0,f.jsx)(`textarea`,{className:p+` min-h-[60px] text-sm`,value:C,onChange:e=>w(e.target.value)})]}),(0,f.jsx)(ne,{module:`wellvisit`,onTranscript:(e,t)=>{E(n=>t.appended&&n?n+` `+e:e),O(``),A(null)},onInterim:e=>O(e?T?T+` `+e:e:``),onError:e=>A(e)}),k&&(0,f.jsx)(`div`,{className:`text-sm text-destructive`,children:k}),(0,f.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,f.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Transcript / dictation`}),(0,f.jsx)(`textarea`,{className:p+` min-h-[160px] font-mono text-sm`,value:se,onChange:e=>{E(e.target.value),O(``)},placeholder:`Click Start recording, or type / paste.`})]}),(j||N||z)&&(0,f.jsxs)(`div`,{className:`rounded-lg border border-amber-200 bg-amber-50 dark:bg-amber-950/30 p-3 space-y-2`,children:[(0,f.jsx)(`div`,{className:`text-xs font-semibold text-amber-800 dark:text-amber-200`,children:`Carry-overs from other tabs (used in note generation)`}),N&&(0,f.jsxs)(`details`,{className:`text-xs`,children:[(0,f.jsxs)(`summary`,{className:`cursor-pointer text-muted-foreground`,children:[`Developmental assessment (`,N.length,` chars)`]}),(0,f.jsx)(`textarea`,{className:p+` mt-1 min-h-[80px] font-mono text-xs`,value:N,onChange:e=>P(e.target.value)})]}),j&&(0,f.jsxs)(`details`,{className:`text-xs`,children:[(0,f.jsxs)(`summary`,{className:`cursor-pointer text-muted-foreground`,children:[`SSHADESS assessment (`,j.length,` chars)`]}),(0,f.jsx)(`textarea`,{className:p+` mt-1 min-h-[80px] font-mono text-xs`,value:j,onChange:e=>M(e.target.value)})]}),z&&(0,f.jsxs)(`details`,{className:`text-xs`,children:[(0,f.jsxs)(`summary`,{className:`cursor-pointer text-muted-foreground`,children:[`By-visit-age statuses (`,z.split(`
`).length,` lines)`]}),(0,f.jsx)(`textarea`,{className:p+` mt-1 min-h-[80px] font-mono text-xs`,value:z,onChange:e=>B(e.target.value)})]})]}),(0,f.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,f.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,f.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Screenings completed`}),(0,f.jsx)(`textarea`,{className:p+` min-h-[60px] text-xs`,value:F,onChange:e=>I(e.target.value)})]}),(0,f.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,f.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Immunizations today`}),(0,f.jsx)(`textarea`,{className:p+` min-h-[60px] text-xs`,value:L,onChange:e=>R(e.target.value)})]})]}),(0,f.jsxs)(`div`,{className:`rounded-lg border border-border bg-card`,children:[(0,f.jsxs)(`div`,{className:`px-3 py-2 flex items-center justify-between bg-muted/40 border-b border-border`,children:[(0,f.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider`,children:`Review of Systems`}),(0,f.jsxs)(`div`,{className:`flex gap-1`,children:[(0,f.jsx)(`button`,{type:`button`,onClick:()=>H(u(l,V)),className:`text-xs rounded border border-border px-2 py-1 hover:bg-muted`,children:` All WNL`}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>H(o(V,l)),className:`text-xs rounded border border-border px-2 py-1 hover:bg-muted`,children:`Clear`})]})]}),(0,f.jsx)(c,{systems:l,data:V,onChange:H,btnLabels:{wnl:`WNL`,abnormal:`Abnormal`,notrev:`Not reviewed`},testIdPrefix:`wv-ros`})]}),(0,f.jsxs)(`div`,{className:`rounded-lg border border-border bg-card`,children:[(0,f.jsxs)(`div`,{className:`px-3 py-2 flex items-center justify-between bg-muted/40 border-b border-border`,children:[(0,f.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider`,children:`Physical Examination`}),(0,f.jsxs)(`div`,{className:`flex gap-1`,children:[(0,f.jsx)(`button`,{type:`button`,onClick:()=>W(u(s,U)),className:`text-xs rounded border border-border px-2 py-1 hover:bg-muted`,children:` All Normal`}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>W(o(U,s)),className:`text-xs rounded border border-border px-2 py-1 hover:bg-muted`,children:`Clear`})]})]}),(0,f.jsx)(c,{systems:s,data:U,onChange:W,btnLabels:{wnl:`Normal`,abnormal:`Abnormal`,notrev:`Not examined`},testIdPrefix:`wv-pe`})]}),(0,f.jsxs)(`div`,{className:`rounded-lg border border-border bg-card p-3 space-y-2`,children:[(0,f.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider`,children:`Diagnoses (ICD-10)`}),(0,f.jsx)(ie,{value:G,onChange:K,testIdPrefix:`wv-dx`}),(0,f.jsxs)(`label`,{className:`block`,children:[(0,f.jsx)(`span`,{className:`text-[11px] text-muted-foreground`,children:`Additional free-text diagnosis / note (optional)`}),(0,f.jsx)(`input`,{type:`text`,value:q,onChange:e=>J(e.target.value),className:p+` text-sm`,placeholder:`e.g. Rule out iron-deficiency anaemia pending labs`})]})]}),(0,f.jsxs)(`label`,{className:`flex flex-col gap-1`,children:[(0,f.jsx)(`span`,{className:`text-xs font-semibold uppercase tracking-wider text-muted-foreground`,children:`Note style`}),(0,f.jsxs)(`select`,{className:p,value:Y,onChange:e=>X(e.target.value),children:[(0,f.jsx)(`option`,{value:`full`,children:`Full encounter note`}),(0,f.jsx)(`option`,{value:`short`,children:`Brief SOAP`})]})]}),$.error&&(0,f.jsx)(`div`,{className:`text-sm text-destructive`,children:$.error.message}),(0,f.jsx)(`button`,{type:`submit`,disabled:$.isPending||!n.trim()&&!_.trim(),className:`rounded-md bg-primary text-primary-foreground px-4 py-2 text-sm font-medium disabled:opacity-50`,children:$.isPending?`Generating`:`Generate Well Visit Note`})]}),Z!==null&&(0,f.jsx)(te,{text:Z,onChange:Q,section:`wellvisit`,title:`Well Visit Note`,exportLabel:`well-visit-note`,exportType:`well-visit`,sourceContext:se})]})}export{m as default};

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/ByVisitAge-jMcNy23H.js","assets/jsx-runtime-ByY1xr43.js","assets/index-C9wKWOAN.js","assets/preload-helper-BmrihbwG.js","assets/index-D2yFXEFs.css","assets/Milestones-BIwzyW98.js","assets/OutputActions-6XZlmtIw.js","assets/Shadess-DU9cvMlg.js","assets/Recorder-DdXTjnSC.js","assets/VisitNote-B6TwVraG.js","assets/DxPicker-CCeyfxop.js","assets/EditableResult-DaHQp1Pv.js"])))=>i.map(i=>d[i]);
import{i as e,n as t,t as n}from"./jsx-runtime-ByY1xr43.js";import{t as r}from"./preload-helper-BmrihbwG.js";var i=e(t(),1),a=n(),o=(0,i.lazy)(()=>r(()=>import(`./ByVisitAge-jMcNy23H.js`),__vite__mapDeps([0,1,2,3,4]))),s=(0,i.lazy)(()=>r(()=>import(`./Milestones-BIwzyW98.js`),__vite__mapDeps([5,1,2,3,4,6]))),c=(0,i.lazy)(()=>r(()=>import(`./Shadess-DU9cvMlg.js`),__vite__mapDeps([7,1,2,3,4,6,8]))),l=(0,i.lazy)(()=>r(()=>import(`./VisitNote-B6TwVraG.js`),__vite__mapDeps([9,1,2,3,4,10,11,6,8]))),u=[{id:`byvisit`,icon:`👶`,label:`By Visit Age`},{id:`milestones`,icon:`🍼`,label:`Milestones`},{id:`shadess`,icon:`🧠`,label:`SSHADESS (12+)`},{id:`note`,icon:`📄`,label:`Visit Note`}],d=`ped_wellvisit_subtab`;function f(){try{let e=localStorage.getItem(d);if(e===`byvisit`||e===`milestones`||e===`shadess`||e===`note`)return e}catch{}return`byvisit`}function p(){let[e,t]=(0,i.useState)(f);function n(e){t(e);try{localStorage.setItem(d,e)}catch{}}return(0,a.jsxs)(`div`,{className:`max-w-5xl mx-auto p-6 space-y-4`,children:[(0,a.jsxs)(`header`,{children:[(0,a.jsx)(`h1`,{className:`text-2xl font-semibold`,children:`Well Visit / Preventive Care`}),(0,a.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:`AAP 2025 Bright Futures periodicity — vaccines, screenings, billing codes, milestones, SSHADESS, and the encounter note.`})]}),(0,a.jsx)(`div`,{className:`flex flex-wrap gap-2`,"data-testid":`wellvisit-subnav`,children:u.map(t=>(0,a.jsxs)(`button`,{type:`button`,onClick:()=>n(t.id),className:`px-3 py-1.5 rounded-full text-xs font-medium border transition-colors `+(e===t.id?`bg-primary text-primary-foreground border-primary`:`bg-muted hover:bg-muted/80 border-border`),"data-testid":`wellvisit-pill-`+t.id,children:[(0,a.jsx)(`span`,{className:`mr-1`,"aria-hidden":!0,children:t.icon}),t.label]},t.id))}),(0,a.jsxs)(i.Suspense,{fallback:(0,a.jsx)(`div`,{className:`text-sm text-muted-foreground`,children:`Loading…`}),children:[e===`byvisit`&&(0,a.jsx)(o,{}),e===`milestones`&&(0,a.jsx)(s,{}),e===`shadess`&&(0,a.jsx)(c,{}),e===`note`&&(0,a.jsx)(l,{})]})]})}export{p 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 @@
var e=`modulepreload`,t=function(e){return`/app/`+e},n={},r=function(r,i,a){let o=Promise.resolve();if(i&&i.length>0){let r=document.getElementsByTagName(`link`),s=document.querySelector(`meta[property=csp-nonce]`),c=s?.nonce||s?.getAttribute(`nonce`);function l(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}o=l(i.map(i=>{if(i=t(i,a),i in n)return;n[i]=!0;let o=i.endsWith(`.css`),s=o?`[rel="stylesheet"]`:``;if(a)for(let e=r.length-1;e>=0;e--){let t=r[e];if(t.href===i&&(!o||t.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${i}"]${s}`))return;let l=document.createElement(`link`);if(l.rel=o?`stylesheet`:e,o||(l.as=`script`),l.crossOrigin=``,l.href=i,c&&l.setAttribute(`nonce`,c),document.head.appendChild(l),o)return new Promise((e,t)=>{l.addEventListener(`load`,e),l.addEventListener(`error`,()=>t(Error(`Unable to preload CSS for ${i}`)))})}))}function s(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return o.then(e=>{for(let t of e||[])t.status===`rejected`&&s(t.reason);return r().catch(s)})};export{r as t};

View file

@ -5,9 +5,10 @@
<link rel="icon" type="image/svg+xml" href="/app/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>client</title>
<script type="module" crossorigin src="/app/assets/index-BuU36CvS.js"></script>
<script type="module" crossorigin src="/app/assets/index-C9wKWOAN.js"></script>
<link rel="modulepreload" crossorigin href="/app/assets/jsx-runtime-ByY1xr43.js">
<link rel="stylesheet" crossorigin href="/app/assets/index-BEGt1l85.css">
<link rel="modulepreload" crossorigin href="/app/assets/preload-helper-BmrihbwG.js">
<link rel="stylesheet" crossorigin href="/app/assets/index-D2yFXEFs.css">
</head>
<body>
<div id="root"></div>

View file

@ -1,179 +0,0 @@
#!/usr/bin/env node
// ============================================================
// STATIC REFERENCE LINTER
// ============================================================
// Scans public/js/ for DOM id references and verifies each target
// exists in some public/*.html or public/components/*.html. Catches
// the class of bug where a component is moved/renamed/deleted and a
// JS file keeps calling getElementById('orphan') — the handler then
// silently no-ops at runtime. The lightbox that broke for Bedside
// pathway images was exactly this: the markup lived in
// calculators.html, but after the reorg the bedside tab never loaded
// it, so getElementById('img-lightbox') returned null.
//
// Also checks:
// - <button data-img-src="/path"> resolves to a file in public/
// - <audio|img src="/path"> resolves to a file in public/
//
// Prints a list of unresolved references and exits non-zero. An
// optional allowlist below handles IDs that are legitimately created
// at runtime (e.g. dynamically injected components).
// ============================================================
'use strict';
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
const PUB = path.join(ROOT, 'public');
// ── Files to scan for id references ──────────────────────────
function walk(dir, ext, acc = []) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, entry.name);
if (entry.isDirectory()) walk(p, ext, acc);
else if (ext.test(entry.name)) acc.push(p);
}
return acc;
}
const jsFiles = walk(path.join(PUB, 'js'), /\.(js|mjs)$/);
const htmlFiles = walk(PUB, /\.html$/);
// ── Collect every id defined anywhere ────────────────────────
// Both static HTML attributes and dynamic JS (innerHTML strings,
// createElement().id = 'X'). If an id is "defined" by any file in
// the repo, a reference to it is considered resolved — whether it
// appears in the DOM at any given moment depends on control flow,
// which this linter doesn't model. We only catch the dead-reference
// class of bug: a JS module reaches for an id that literally no
// source in the tree ever produces.
const definedIds = new Set();
function collect(src) {
for (const m of src.matchAll(/\bid\s*=\s*["']([a-zA-Z_][\w-]*)["']/g)) definedIds.add(m[1]);
for (const m of src.matchAll(/\.id\s*=\s*["']([a-zA-Z_][\w-]*)["']/g)) definedIds.add(m[1]);
}
for (const f of htmlFiles) collect(fs.readFileSync(f, 'utf8'));
for (const f of jsFiles) collect(fs.readFileSync(f, 'utf8'));
// Dynamic-id allowlist: created by JS via innerHTML or createElement.
// Keep this list short and specific; expand only when sure the id is
// genuinely runtime-constructed. Use prefixes ending in '-' to match
// an entire family.
const DYNAMIC_ID_PREFIXES = [
'em-', // bedside section containers built by sub-nav.js
'shadess-', // wellvisit SSHADESS domain injection
'wv-panel-', // wellvisit subpanel ids (these exist in HTML but also built in JS)
'milestone-', // milestones checklist
'quiz-', // learning hub quiz questions
'lh-quiz-', // learning hub quiz answers
'pe-', // PE guide steps are rendered dynamically
'ros-', // ROS dynamic rows
'nrp-', // NRP pathway dynamic widgets
'seizure-', // seizure pathway dynamic widgets
'sepsis-', // sepsis pathway dynamic widgets
'airway-', // airway pathway dynamic widgets
'cardiac-', // cardiac-arrest pathway dynamic widgets
'anaph-', // anaphylaxis
'burn-', // burns
'vent-', // ventilation
'tox-', // toxicology
'trauma-', // trauma
'neo-', // neonatal
'resp-', // respiratory
'sed-', // sedation
'agit-', // agitation
'antiemetic-', // antiemetics
'antibio-', // antimicrobials
'bili-', // bilirubin dynamic columns
'bmi-', // BMI chart elements
'bp-', // BP percentile chart elements
'growth-', // growth chart elements
'dose-', // dosing widgets
'bsa-', // BSA calc
'equip-', // equipment sizing
'resus-', // resus meds
'gcs-', // GCS components
'vitals-', // vitals reference
'catchup-', // catch-up schedule
'totp-', // 2FA elements
'sessions-', // active sessions list
'nc-', // nextcloud settings
'mem-', // memories
'doc-', // documents
'corrections-', // corrections list
'audio-', // audio backups
'saved-', // saved encounters
'ext-', // extensions CRUD
'cms-', // content manager
'fpa-', // forgot-password accept screen
'rsp-', // reset password
'browser-whisper-', // whisper settings
'admin-', // admin panel
'adminms-', // admin milestones
'2fa-', // 2FA UI
'calc-', // calculator panels (some may be missing from HTML)
];
function isDynamicId(id) {
return DYNAMIC_ID_PREFIXES.some(p => id.startsWith(p));
}
// ── Scan JS for getElementById / querySelector('#id') / '#id' ─
const unresolved = [];
const idRef = /getElementById\s*\(\s*['"]([^'"]+)['"]\s*\)|querySelector(?:All)?\s*\(\s*['"]#([a-zA-Z_][\w-]*)\s*['"]|querySelector(?:All)?\s*\(\s*['"][^'"]*?#([a-zA-Z_][\w-]+)/g;
for (const f of jsFiles) {
const src = fs.readFileSync(f, 'utf8');
const seen = new Set();
for (const m of src.matchAll(idRef)) {
const id = m[1] || m[2] || m[3];
if (!id || seen.has(id)) continue;
seen.add(id);
if (definedIds.has(id) || isDynamicId(id)) continue;
unresolved.push({ file: path.relative(ROOT, f), id });
}
}
// ── Scan HTML for asset references that must resolve on disk ──
const brokenAssets = [];
function checkAsset(file, p) {
if (!p || !p.startsWith('/')) return; // only root-absolute
if (p.startsWith('//') || p.startsWith('/api/') || p.startsWith('/components/')) return;
if (p.startsWith('/#')) return; // hash-only
const onDisk = path.join(PUB, p.split('?')[0].split('#')[0]);
if (!fs.existsSync(onDisk)) brokenAssets.push({ file: path.relative(ROOT, file), path: p });
}
const ASSET_PATTERNS = [
/data-img-src\s*=\s*["']([^"']+)["']/g,
/<(?:img|audio|video|source|link)[^>]*\bsrc\s*=\s*["']([^"']+)["']/g,
/<(?:link)[^>]*\bhref\s*=\s*["']([^"']+)["']/g,
];
for (const f of htmlFiles) {
const html = fs.readFileSync(f, 'utf8');
for (const pat of ASSET_PATTERNS) {
for (const m of html.matchAll(pat)) checkAsset(f, m[1]);
}
}
// ── Report ────────────────────────────────────────────────────
let failed = false;
if (unresolved.length) {
console.error('\n❌ Unresolved DOM id references:');
for (const { file, id } of unresolved) console.error(` ${file} -> #${id}`);
failed = true;
} else {
console.log('✓ All JS id references resolve to an HTML id.');
}
if (brokenAssets.length) {
console.error('\n❌ Broken asset paths:');
for (const { file, path: p } of brokenAssets) console.error(` ${file} -> ${p}`);
failed = true;
} else {
console.log('✓ All asset paths resolve to a file on disk.');
}
process.exit(failed ? 1 : 0);

View file

@ -1,184 +0,0 @@
// ============================================================
// Unit tests for public/js/calc-math.js (pediatric dose math).
// Run with: npm test (alias for: node --test test/)
// ============================================================
const { test } = require('node:test');
const assert = require('node:assert/strict');
const M = require('../public/js/calc-math.js');
// ── estimateWeightFromAgeMonths ────────────────────────────────
test('APLS 0-12 mo: 6 mo -> 7 kg', () => {
const r = M.estimateWeightFromAgeMonths(6);
assert.equal(r.weight, 7); // 0.5*6 + 4 = 7
assert.match(r.formulaLabel, /0-12 mo/);
});
test('APLS 1-5 yr: 3 yr -> 14 kg', () => {
const r = M.estimateWeightFromAgeMonths(36);
assert.equal(r.weight, 14); // 2*3 + 8 = 14
assert.match(r.formulaLabel, /1-5 yr/);
});
test('APLS 6-12 yr: 8 yr -> 31 kg', () => {
const r = M.estimateWeightFromAgeMonths(96);
assert.equal(r.weight, 31); // 3*8 + 7 = 31
assert.match(r.formulaLabel, /6-12 yr/);
});
test('Best Guess 14 yr -> 56 kg (switches away from APLS at >=13 yr)', () => {
const r = M.estimateWeightFromAgeMonths(168);
assert.equal(r.weight, 56); // 4 * 14 = 56
assert.match(r.formulaLabel, /Best Guess/);
});
test('APLS and Best Guess both reported', () => {
const r = M.estimateWeightFromAgeMonths(48);
assert.equal(r.all.apls, 16); // 2*4 + 8
assert.equal(r.all.bestGuess, 18); // 2*(4+5)
});
test('Null/negative age returns null', () => {
assert.equal(M.estimateWeightFromAgeMonths(null), null);
assert.equal(M.estimateWeightFromAgeMonths(-5), null);
});
// ── Parkland ────────────────────────────────────────────────────
test('Parkland 20kg, 25% TBSA -> 2000 mL total, 1000 first 8h', () => {
const p = M.parkland(20, 25);
assert.equal(p.totalMl, 2000); // 4 * 20 * 25
assert.equal(p.first8Ml, 1000);
assert.equal(p.next16Ml, 1000);
assert.equal(p.rateFirst8, 125); // 1000 / 8
assert.equal(p.rateNext16, 63); // 1000 / 16 = 62.5 -> 63
});
test('Parkland invalid inputs return null', () => {
assert.equal(M.parkland(0, 25), null);
assert.equal(M.parkland(20, 0), null);
assert.equal(M.parkland(null, 25), null);
});
// ── Maintenance 4-2-1 ──────────────────────────────────────────
test('Maintenance: 8 kg -> 32 mL/hr', () => { assert.equal(M.maintenance421(8), 32); });
test('Maintenance: 15 kg -> 50 mL/hr', () => { assert.equal(M.maintenance421(15), 50); });
test('Maintenance: 25 kg -> 65 mL/hr', () => { assert.equal(M.maintenance421(25), 65); });
test('Maintenance: 70 kg -> 110 mL/hr', () => { assert.equal(M.maintenance421(70), 110); });
// ── PRAM ────────────────────────────────────────────────────────
test('PRAM all zero -> Mild (0)', () => {
const r = M.pramScore(0, 0, 0, 0, 0);
assert.equal(r.total, 0); assert.equal(r.severity, 'Mild');
});
test('PRAM total 5 -> Moderate', () => {
const r = M.pramScore(1, 2, 0, 1, 1);
assert.equal(r.total, 5); assert.equal(r.severity, 'Moderate');
});
test('PRAM total 10 -> Severe', () => {
const r = M.pramScore(2, 2, 2, 2, 2);
assert.equal(r.total, 10); assert.equal(r.severity, 'Severe');
});
// ── Westley ─────────────────────────────────────────────────────
test('Westley 2 -> Mild', () => { assert.equal(M.westleyScore(0, 0, 1, 0, 1).severity, 'Mild'); });
test('Westley 4 -> Moderate', () => { assert.equal(M.westleyScore(0, 0, 2, 1, 1).severity, 'Moderate'); });
test('Westley 8 -> Severe', () => { assert.equal(M.westleyScore(0, 4, 2, 1, 1).severity, 'Severe'); });
test('Westley 12 -> Impending respiratory failure', () => { assert.equal(M.westleyScore(5, 4, 0, 1, 2).severity, 'Impending respiratory failure'); });
// ── Epinephrine (anaphylaxis vs arrest) ─────────────────────────
test('Epi anaphylaxis IM: 20 kg -> 0.2 mg (1:1000)', () => {
const r = M.epiAnaphylaxisIM(20);
assert.equal(r.doseMg, 0.2);
assert.equal(r.volumeMl_1in1000, 0.2); // 1 mg/mL concentration
});
test('Epi anaphylaxis IM: max 0.5 mg for >=50 kg', () => {
assert.equal(M.epiAnaphylaxisIM(80).doseMg, 0.5);
});
test('Epi arrest IV: 10 kg -> 0.1 mg, 1 mL of 1:10,000', () => {
const r = M.epiArrestIV(10);
assert.equal(r.doseMg, 0.1);
assert.equal(r.volumeMl_1in10000, 1); // 0.1 mg/mL concentration
});
test('Epi arrest IV: max 1 mg for >=100 kg', () => {
assert.equal(M.epiArrestIV(120).doseMg, 1);
});
// ── NRP epi ──────────────────────────────────────────────────────
test('NRP epi: 3 kg newborn -> 0.03-0.09 mg IV (0.3-0.9 mL of 1:10,000)', () => {
const r = M.nrpEpiIV(3);
assert.equal(r.lowMg, 0.03);
assert.equal(r.highMg, 0.09);
assert.equal(r.lowMl, 0.3);
assert.equal(r.highMl, 0.9);
});
// ── RSI drugs ────────────────────────────────────────────────────
test('Ketamine RSI 20 kg -> 30-40 mg', () => {
const r = M.rsiKetamine(20);
assert.equal(r.lowMg, 30);
assert.equal(r.highMg, 40);
});
test('Rocuronium 25 kg -> 30 mg (1.2 mg/kg)', () => {
assert.equal(M.rsiRocuronium(25).mg, 30);
});
test('Succinylcholine dose/kg: <10 kg = 2 mg/kg; >=10 kg = 1.5 mg/kg', () => {
assert.equal(M.rsiSuccinylcholine(8).perKg, 2);
assert.equal(M.rsiSuccinylcholine(20).perKg, 1.5);
assert.equal(M.rsiSuccinylcholine(20).mg, 30); // 20*1.5
});
// ── Min systolic BP ─────────────────────────────────────────────
test('Min SBP: 5 yr -> 80', () => { assert.equal(M.minSBP(5), 80); });
test('Min SBP: infant (<1 yr) -> 70', () => { assert.equal(M.minSBP(0.5), 70); });
test('Min SBP: 12 yr -> 90', () => { assert.equal(M.minSBP(12), 90); });
// ── ETT sizing ──────────────────────────────────────────────────
test('ETT 4 yr: uncuffed 5.0, cuffed 4.5, depth 15', () => {
const r = M.ettSize(4);
assert.equal(r.uncuffedMm, 5);
assert.equal(r.cuffedMm, 4.5);
assert.equal(r.depthCm, 15);
});
// ── Lund-Browder TBSA ───────────────────────────────────────────
test('Lund-Browder: full head infant = 18%', () => {
assert.equal(M.lundBrowderTBSA('infant', { head: 100 }), 18);
});
test('Lund-Browder: full head adult = 7%', () => {
assert.equal(M.lundBrowderTBSA('adult', { head: 100 }), 7);
});
test('Lund-Browder: full body adds to ~100% (infant) — tolerance for published rounding', () => {
const all = {};
Object.keys(M.LB).forEach((k) => { all[k] = 100; });
const t = M.lundBrowderTBSA('infant', all);
// Published charts typically sum to 99-100% depending on head % (18 vs 19 at 0 yr vs 1 yr).
assert.ok(t >= 98 && t <= 100.5, 'expected 98-100.5, got ' + t);
});
test('Lund-Browder: 50% of both thighs young child = 8%', () => {
// young (1-5) thigh = 8% each, so 50% of each = 4% + 4% = 8%
assert.equal(M.lundBrowderTBSA('young', { r_thigh: 50, l_thigh: 50 }), 8);
});
test('Lund-Browder: invalid bracket returns null', () => {
assert.equal(M.lundBrowderTBSA('bogus', { head: 50 }), null);
});
// ── drugs.json schema ───────────────────────────────────────────
// Asserts the JSON is loadable and that each of the 5 extracted
// bedside sections exists with a non-empty drugs array.
test('drugs.json: schema loads with all 5 extracted sections + non-empty drugs', () => {
const fs = require('node:fs');
const path = require('node:path');
const raw = fs.readFileSync(path.join(__dirname, '..', 'public', 'data', 'drugs.json'), 'utf8');
const j = JSON.parse(raw);
assert.equal(j.version, '1.0');
assert.ok(j.sections, 'top-level sections object present');
['anaphylaxis', 'sedation', 'agitation', 'antiemetics', 'seizure'].forEach((key) => {
const sec = j.sections[key];
assert.ok(sec, `section ${key} present`);
assert.ok(Array.isArray(sec.drugs) && sec.drugs.length > 0, `${key}.drugs non-empty`);
});
});