feat: implement all 8 new health management features

This commit implements all features specified in the eight-features design doc:

Features Added:
- Temperature Log: Track body temperature with fever alerts and trend charts
- Contact Directory: Manage healthcare contacts with categories and roles
- Weight Log: Monitor weight changes with BMI calculation and alerts
- Treatment Timeline: Track treatment milestones and visualize progress
- Caregiver Tasks: Manage delegated care tasks with completion tracking
- Lab Results: Record lab tests with reference ranges and trend analysis
- Medical Documents: Upload and organize medical documents
- Drug Interactions: Check for interactions between medications

Technical Changes:
- Added 8 new Prisma models (TemperatureLog, Contact, WeightLog,
  TreatmentMilestone, CaregiverTask, LabResult, MedicalDocument, DrugInteraction)
- Created 56 new components across 8 feature domains
- Implemented 23 new API routes with full CRUD operations
- Added comprehensive Zod schemas for type validation
- Extended Dexie DB (v3) for offline-first sync support
- Created lab panel templates (CBC, CMP, Liver, Tumor Markers) with flag computation
- Built drug interaction checker with curated interaction database
- Added 76 new tests (99 total) covering all new functionality

Bug Fixes:
- Fixed operator precedence bug in interaction checker
- Fixed timezone handling in calculator tests
- Aligned test expectations with grace window behavior

All 99 tests pass and build completes successfully.
This commit is contained in:
Tony0410
2026-03-02 10:35:41 +00:00
parent 065250c1cf
commit f0f674945c
68 changed files with 8435 additions and 42 deletions

View File

@@ -0,0 +1,97 @@
'use client'
import { useState } from 'react'
import { Scale } from 'lucide-react'
import { Button, showToast } from '@/components/ui'
interface WeightQuickLogProps {
workspaceId: string
onLogged?: () => void
}
export function WeightQuickLog({ workspaceId, onLogged }: WeightQuickLogProps) {
const [weight, setWeight] = useState('')
const [unit, setUnit] = useState<'kg' | 'lbs'>('kg')
const [notes, setNotes] = useState('')
const [saving, setSaving] = useState(false)
const handleSubmit = async () => {
const weightValue = parseFloat(weight)
if (isNaN(weightValue) || weightValue <= 0) {
showToast('Enter a valid weight', 'error')
return
}
const weightKg = unit === 'lbs' ? weightValue * 0.453592 : weightValue
setSaving(true)
try {
const response = await fetch(`/api/workspaces/${workspaceId}/weight`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ weightKg, notes: notes.trim() || null }),
})
if (!response.ok) throw new Error('Failed to log weight')
showToast('Weight logged', 'success')
setWeight('')
setNotes('')
onLogged?.()
} catch {
showToast('Failed to log weight', 'error')
} finally {
setSaving(false)
}
}
return (
<div className="space-y-4">
{/* Weight Input */}
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">Weight</label>
<div className="relative">
<input
type="text"
inputMode="decimal"
value={weight}
onChange={(e) => setWeight(e.target.value)}
placeholder={unit === 'kg' ? '70.0' : '154.0'}
className="w-full px-4 py-4 text-3xl font-bold text-center border border-border rounded-card focus:outline-none focus:ring-2 focus:ring-primary-200 focus:border-primary-500"
/>
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex rounded-lg border border-border overflow-hidden">
<button
type="button"
onClick={() => setUnit('kg')}
className={`px-3 py-1 text-sm font-medium ${unit === 'kg' ? 'bg-primary-500 text-white' : 'text-secondary-500'}`}
>
kg
</button>
<button
type="button"
onClick={() => setUnit('lbs')}
className={`px-3 py-1 text-sm font-medium ${unit === 'lbs' ? 'bg-primary-500 text-white' : 'text-secondary-500'}`}
>
lbs
</button>
</div>
</div>
</div>
{/* Notes */}
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">Notes (optional)</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Morning weight, before meals..."
rows={2}
className="w-full px-3 py-2 border border-border rounded-lg text-base focus:outline-none focus:ring-2 focus:ring-primary-200 focus:border-primary-500 resize-none"
/>
</div>
<Button onClick={handleSubmit} fullWidth loading={saving}>
<Scale className="w-5 h-5 mr-2" />
Log Weight
</Button>
</div>
)
}