Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ Each open patient has eight focused tabs:

| Tab | Purpose |
|---|---|
| **Profile** | Demographics plus case-review notes (clinical summary, chief complaint, HPI, PMH, PE), diagnosis, and clinical details |
| **FRICHMOND** | Daily progress notes (Fluid, Respiratory, Infectious, Cardiovascular, Hema, Metabolic, Output, Neuro, Drugs) with a Copy latest entry action to carry forward all daily fields |
| **Profile** | Demographics plus case-review notes (clinical summary, chief complaint, HPI, PMH, PE), diagnosis, and clerk notes |
| **FRICHMOND** | Daily progress notes (Fluid, Respiratory, Infectious, Cardiovascular, Hema, Metabolic, Output, Neuro, Drugs), assessment, plan, and checklist with Copy latest entry carrying forward pending checklist items only |
| **Vitals** | Temp, BP, HR, RR, O₂ saturation with history |
| **Labs** | CBC, urinalysis, Blood Chemistry, ABG (with auto-calculated pO2/FiO2 and conditional Desired FiO2 when FiO2 > 21% or pO2 < 60; target PaO2 = 60), and Others (custom label + freeform result); trend comparison applies to structured templates, while Others stays plain |
| **Medications** | Active medication list with status tracking |
Expand All @@ -64,7 +64,7 @@ Each open patient has eight focused tabs:
### Reporting & Export

- **Profile summary** follows room/name header, main/referral service split, `Dx`, and optional `Notes` blocks.
- **FRICHMOND summary** uses `ROOM - LASTNAME, First — MM-DD-YYYY`, removes orders, and includes daily vitals min–max ranges.
- **FRICHMOND summary** uses `ROOM - LASTNAME, First — MM-DD-YYYY`, removes orders, includes daily vitals min–max ranges, and outputs checklist pending/completed lines when present.
- **Vitals summary** supports multi-patient selection and date/time window filtering.
- **Labs summary** supports arbitrary instance selection per patient; comparison mode runs only when exactly 2 instances of the same non-Others lab template are selected.
- **Orders summary** supports date/time filtering using order date/time fields and preserves order text exactly as entered.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "app",
"private": true,
"version": "1.3.8",
"version": "1.3.9",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
158 changes: 127 additions & 31 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { useLiveQuery } from 'dexie-react-hooks'
import { db } from './db'
import type {
DailyChecklistItem,
DailyUpdate,
LabEntry,
MedicationEntry,
Expand Down Expand Up @@ -230,8 +231,22 @@ const initialDailyUpdateForm: DailyUpdateFormState = {
other: '',
assessment: '',
plans: '',
checklist: [],
}

const normalizeDailyChecklist = (checklist: DailyChecklistItem[] | undefined): DailyChecklistItem[] => {
if (!Array.isArray(checklist)) return []
return checklist
.map((item) => ({
text: typeof item?.text === 'string' ? item.text.trim() : '',
completed: item?.completed === true,
}))
.filter((item) => item.text.length > 0)
}

const toPendingDailyChecklist = (checklist: DailyChecklistItem[] | undefined): DailyChecklistItem[] =>
normalizeDailyChecklist(checklist).filter((item) => !item.completed)

const getNormalAaDo2 = (age: number): number => {
const decadesAboveThirty = age > 30 ? Math.floor((age - 30) / 10) : 0
return 15 + decadesAboveThirty * 3
Expand Down Expand Up @@ -326,6 +341,7 @@ function App() {
const [profileForm, setProfileForm] = useState<ProfileFormState>(initialProfileForm)
const [dailyDate, setDailyDate] = useState(() => toLocalISODate())
const [dailyUpdateForm, setDailyUpdateForm] = useState<DailyUpdateFormState>(initialDailyUpdateForm)
const [dailyChecklistDraft, setDailyChecklistDraft] = useState('')
const [dailyUpdateId, setDailyUpdateId] = useState<number | undefined>(undefined)
const [vitalForm, setVitalForm] = useState<VitalFormState>(() => initialVitalForm())
const [editingVitalId, setEditingVitalId] = useState<number | null>(null)
Expand Down Expand Up @@ -1118,8 +1134,26 @@ function App() {
const loadDailyUpdate = async (patientId: number, date: string) => {
const update = await db.dailyUpdates.where('[patientId+date]').equals([patientId, date]).first()
if (!update) {
const updates = await db.dailyUpdates.where('patientId').equals(patientId).toArray()
const latestPriorUpdate = updates
.filter((candidate) => candidate.date < date)
.reduce<DailyUpdate | null>((latest, candidate) => {
if (!latest) return candidate
if (candidate.date !== latest.date) return candidate.date > latest.date ? candidate : latest
const latestTimestamp = Date.parse(latest.lastUpdated)
const candidateTimestamp = Date.parse(candidate.lastUpdated)
if (Number.isFinite(candidateTimestamp) && Number.isFinite(latestTimestamp)) {
return candidateTimestamp >= latestTimestamp ? candidate : latest
}
return candidate
}, null)

setDailyUpdateId(undefined)
setDailyUpdateForm(initialDailyUpdateForm)
setDailyUpdateForm({
...initialDailyUpdateForm,
checklist: toPendingDailyChecklist(latestPriorUpdate?.checklist),
})
setDailyChecklistDraft('')
setDailyDirty(false)
return
}
Expand All @@ -1138,7 +1172,9 @@ function App() {
other: update.other,
assessment: update.assessment,
plans: update.plans,
checklist: normalizeDailyChecklist(update.checklist),
})
setDailyChecklistDraft('')
setDailyDirty(false)
}

Expand All @@ -1156,6 +1192,7 @@ function App() {
other: update.other,
assessment: update.assessment,
plans: update.plans,
checklist: toPendingDailyChecklist(update.checklist),
})
setDailyDirty(true)
}, [])
Expand Down Expand Up @@ -2506,6 +2543,10 @@ function App() {
other: 'Ambulates with minimal assistance; tolerates soft diet.',
assessment: 'CAP, clinically improving with stable cardiorespiratory parameters.',
plans: 'Continue current antibiotics today then reassess de-escalation.\nRepeat CBC/electrolytes tomorrow.\nCoordinate discharge planning once clinically stable.',
checklist: [
{ text: 'For CTT insertion, 02-25', completed: false },
{ text: 'Chest CT with contrast', completed: true },
],
lastUpdated: now,
})

Expand Down Expand Up @@ -3138,32 +3179,6 @@ function App() {
onOpenPhotoById={openPhotoById}
/>
</div>
<div className='space-y-1'>
<Label htmlFor='profile-plans'>Plans</Label>
<PhotoMentionField
ariaLabel='Plans'
placeholder='Plans'
className='min-h-24'
value={profileForm.plans}
onChange={(nextValue) => updateProfileField('plans', nextValue)}
attachments={mentionableAttachments}
attachmentByTitle={mentionableAttachmentByTitle}
onOpenPhotoById={openPhotoById}
/>
</div>
<div className='space-y-1'>
<Label htmlFor='profile-pendings'>Pendings</Label>
<PhotoMentionField
ariaLabel='Pendings'
placeholder='Pendings'
className='min-h-24'
value={profileForm.pendings}
onChange={(nextValue) => updateProfileField('pendings', nextValue)}
attachments={mentionableAttachments}
attachmentByTitle={mentionableAttachmentByTitle}
onOpenPhotoById={openPhotoById}
/>
</div>
<div className='space-y-1'>
<Label htmlFor='profile-clerknotes'>Clerk notes</Label>
<PhotoMentionField
Expand Down Expand Up @@ -3245,7 +3260,7 @@ function App() {
<p className='text-xs text-clay'>No saved daily entries yet.</p>
)}
</div>
<p className='text-xs text-clay'>Copies all daily fields (FRICHMOND, assessment, plan) from the latest saved date.</p>
<p className='text-xs text-clay'>Copies FRICHMOND fields, assessment, plan, and only pending checklist items from the latest saved date.</p>
<div className='space-y-1'>
<Label>Fluid</Label>
<PhotoMentionField
Expand Down Expand Up @@ -3426,6 +3441,86 @@ function App() {
onOpenPhotoById={openPhotoById}
/>
</div>
<div className='space-y-2'>
<Label>Checklist</Label>
<div className='flex gap-2'>
<Input
aria-label='Checklist item'
placeholder='Add checklist item'
value={dailyChecklistDraft}
onChange={(event) => setDailyChecklistDraft(event.target.value)}
onKeyDown={(event) => {
if (event.key !== 'Enter') return
const nextItem = dailyChecklistDraft.trim()
if (!nextItem) return
event.preventDefault()
setDailyUpdateForm({
...dailyUpdateForm,
checklist: [...dailyUpdateForm.checklist, { text: nextItem, completed: false }],
})
setDailyChecklistDraft('')
setDailyDirty(true)
}}
/>
<Button
type='button'
variant='secondary'
onClick={() => {
const nextItem = dailyChecklistDraft.trim()
if (!nextItem) return
setDailyUpdateForm({
...dailyUpdateForm,
checklist: [...dailyUpdateForm.checklist, { text: nextItem, completed: false }],
})
setDailyChecklistDraft('')
setDailyDirty(true)
}}
>
Add
</Button>
</div>
{dailyUpdateForm.checklist.length > 0 ? (
<div className='space-y-1'>
{dailyUpdateForm.checklist.map((item, index) => (
<div key={index} className='flex items-start gap-2 rounded-md border border-clay/20 bg-warm-ivory px-2 py-1.5'>
<input
type='checkbox'
aria-label={`Mark checklist item ${item.text} as ${item.completed ? 'pending' : 'completed'}`}
checked={item.completed}
onChange={(event) => {
setDailyUpdateForm({
...dailyUpdateForm,
checklist: dailyUpdateForm.checklist.map((entry, entryIndex) =>
entryIndex === index ? { ...entry, completed: event.target.checked } : entry,
),
})
setDailyDirty(true)
}}
/>
<p className={cn('flex-1 text-sm text-espresso whitespace-pre-wrap break-words', item.completed && 'line-through text-clay')}>
{item.text}
</p>
<Button
type='button'
variant='ghost'
className='h-6 px-2 text-xs'
onClick={() => {
setDailyUpdateForm({
...dailyUpdateForm,
checklist: dailyUpdateForm.checklist.filter((_, entryIndex) => entryIndex !== index),
})
setDailyDirty(true)
}}
>
Remove
</Button>
</div>
))}
</div>
) : (
<p className='text-xs text-clay'>No checklist items yet.</p>
)}
</div>
</div>
</TabsContent>
<TabsContent value='vitals'>
Expand Down Expand Up @@ -4531,7 +4626,7 @@ function App() {
['Open a patient', 'Tap Open on any patient card to enter the patient view with all clinical tabs.'],
['Navigate on mobile', 'The bottom bar shows all 8 patient sections in a 2-row grid — tap any to switch. Use ← Back to return to the patient list.'],
['Switch patients', 'Tap the patient name at the top of any tab to jump to a different patient while staying on the same section.'],
['Write daily notes', 'Open FRICH, pick today\'s date, fill F-R-I-C-H-M-O-N-D fields and plan. Tap Copy latest entry to carry forward yesterday\'s note.'],
['Write daily notes', 'Open FRICH, pick today\'s date, fill F-R-I-C-H-M-O-N-D fields, plan, and checklist. Copy latest carries only pending checklist items.'],
['Generate reports', 'Open Report, configure filters, tap any export button to preview, then Copy full text to paste into a handoff or chart.'],
['Back up your data', 'Go to Settings → Export backup regularly, especially before switching devices or browsers.'],
] as [string, string][]).map(([title, detail], i) => (
Expand All @@ -4548,8 +4643,8 @@ function App() {
<p className='text-[10px] font-extrabold uppercase tracking-widest text-clay/55'>Patient tabs</p>
<div className='grid grid-cols-2 gap-1.5'>
{([
['Profile', 'Demographics, diagnosis, clinical summary, HPI, PMH, PE, plans, pendings'],
['FRICH', 'Date-based F-R-I-C-H-M-O-N-D daily notes, assessment & plan'],
['Profile', 'Demographics, diagnosis, clinical summary, HPI, PMH, PE, and clerk notes'],
['FRICH', 'Date-based F-R-I-C-H-M-O-N-D daily notes, assessment, plan, and checklist'],
['Vitals', 'Structured BP/HR/RR/Temp/SpO2 log with date & time entries'],
['Labs', 'CBC, UA, Blood Chem, ABG templates + free-text with date/time'],
['Meds', 'Structured medication list: drug, dose, route, frequency, status'],
Expand Down Expand Up @@ -4864,6 +4959,7 @@ function App() {
<strong className='block text-center'>{dailyDate}</strong>
and replace it with a duplicate of
<strong className='block text-center'>{pendingLatestDailyUpdate?.date ?? '-'}?</strong>
<span className='mt-1 block text-xs text-clay'>Only pending checklist items are carried over.</span>
</p>
<div className='flex gap-2 flex-wrap justify-center'>
<Button variant='destructive' onClick={confirmCopyLatestDailyUpdate}>Yes, replace entry</Button>
Expand Down
16 changes: 15 additions & 1 deletion src/features/reporting/reportBuilders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ type DailySummaryInput = {
other: string
assessment: string
plans: string
checklist: Array<{
text: string
completed: boolean
}>
}

const labTemplatesById = new Map(LAB_TEMPLATES.map((template) => [template.id, template] as const))
Expand Down Expand Up @@ -410,6 +414,14 @@ export const toDailySummary = (
dailyDate: string,
) => {
const vitalsLine = buildDailyVitalsRangeLine(vitalsEntries, dailyDate)
const pendingChecklist = (update.checklist ?? [])
.filter((item) => !item.completed)
.map((item) => item.text.trim())
.filter(Boolean)
const completedChecklist = (update.checklist ?? [])
.filter((item) => item.completed)
.map((item) => item.text.trim())
.filter(Boolean)

const lines = [
`${formatPatientHeader(patient)} — ${formatDateMMDDYYYY(dailyDate)}`,
Expand All @@ -426,6 +438,8 @@ export const toDailySummary = (
update.other ? `Other: ${update.other}` : '',
update.assessment ? `Assessment: ${update.assessment}` : '',
update.plans ? `Plan: ${update.plans}` : '',
pendingChecklist.length > 0 ? `Checklist pending: ${pendingChecklist.join('; ')}` : '',
completedChecklist.length > 0 ? `Checklist completed: ${completedChecklist.join('; ')}` : '',
]

return lines.filter(Boolean).join('\n')
Expand Down Expand Up @@ -541,4 +555,4 @@ export const toLabsSummary = (patient: Patient, labEntries: LabEntry[], selected
return `${formatPatientHeader(patient)}\nNo selected labs.`
}
return [formatPatientHeader(patient), ...blocks].join('\n\n')
}
}
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,15 @@ export interface DailyUpdate {
other: string
assessment: string
plans: string
checklist: DailyChecklistItem[]
lastUpdated: string
}

export interface DailyChecklistItem {
text: string
completed: boolean
}

export interface VitalEntry {
id?: number
patientId: number
Expand Down