Files
nextstep/src/app/(app)/weight/page.tsx
Tony0410 f0f674945c 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.
2026-03-02 11:17:38 +00:00

149 lines
4.7 KiB
TypeScript

'use client'
import { useState, useEffect, useCallback, useMemo } from 'react'
import { useRouter } from 'next/navigation'
import { History, Scale } from 'lucide-react'
import { useLiveQuery } from 'dexie-react-hooks'
import { db } from '@/lib/sync'
import { Card, LoadingState } from '@/components/ui'
import { Header, PageContainer } from '@/components/layout/header'
import { WeightQuickLog } from '@/components/weight/WeightQuickLog'
import { WeightCard } from '@/components/weight/WeightCard'
import { WeightChart } from '@/components/weight/WeightChart'
import { WeightAlert } from '@/components/weight/WeightAlert'
import { useApp } from '../provider'
export default function WeightPage() {
const router = useRouter()
const { currentWorkspace, refreshData } = useApp()
const [serverData, setServerData] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const localData = useLiveQuery(
() =>
db.weightLogs
.where('workspaceId')
.equals(currentWorkspace.id)
.and((w) => !w.deletedAt)
.reverse()
.limit(100)
.toArray(),
[currentWorkspace.id]
)
const fetchData = useCallback(async () => {
try {
const response = await fetch(`/api/workspaces/${currentWorkspace.id}/weight?limit=100`)
if (response.ok) {
const data = await response.json()
setServerData(data.weightLogs)
}
} catch (err) {
console.error('Failed to fetch weight logs:', err)
} finally {
setLoading(false)
}
}, [currentWorkspace.id])
useEffect(() => {
fetchData()
}, [fetchData])
const handleLogged = () => {
fetchData()
refreshData()
}
const readings = useMemo(
() => (serverData.length > 0 ? serverData : localData || []),
[serverData, localData]
)
// Check for rapid weight change
const rapidChange = useMemo(() => {
if (readings.length < 2) return null
const latest = readings[0]
const previous = readings[1]
const hoursDiff = (new Date(latest.recordedAt).getTime() - new Date(previous.recordedAt).getTime()) / (1000 * 60 * 60)
if (hoursDiff <= 48 && Math.abs(latest.weightKg - previous.weightKg) >= 2) {
return { currentKg: latest.weightKg, previousKg: previous.weightKg, timeframeHours: Math.round(hoursDiff) }
}
return null
}, [readings])
if (loading && !localData) {
return (
<>
<Header title="Weight" />
<PageContainer><LoadingState message="Loading weight logs..." /></PageContainer>
</>
)
}
return (
<>
<Header
title="Weight"
rightAction={{
icon: <History className="w-6 h-6 text-secondary-700" />,
label: 'History',
onClick: () => router.push('/weight/history'),
}}
/>
<PageContainer className="pt-4 space-y-6">
{/* Rapid Change Alert */}
{rapidChange && (
<WeightAlert
currentKg={rapidChange.currentKg}
previousKg={rapidChange.previousKg}
timeframeHours={rapidChange.timeframeHours}
/>
)}
{/* Quick Log */}
<section>
<h2 className="text-lg font-semibold text-secondary-900 mb-3">Log Weight</h2>
<Card>
<WeightQuickLog workspaceId={currentWorkspace.id} onLogged={handleLogged} />
</Card>
</section>
{/* 30-Day Trend */}
{readings.length >= 2 && (
<section>
<h2 className="text-lg font-semibold text-secondary-900 mb-3">30-Day Trend</h2>
<Card>
<WeightChart readings={readings.map((r: any) => ({ weightKg: r.weightKg, recordedAt: r.recordedAt }))} />
</Card>
</section>
)}
{/* Recent Readings */}
<section>
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold text-secondary-900">Recent</h2>
</div>
{readings.length === 0 ? (
<Card variant="outline" className="text-center py-8">
<Scale className="w-10 h-10 text-secondary-300 mx-auto mb-3" />
<p className="text-secondary-500">No weight readings yet</p>
<p className="text-sm text-secondary-400 mt-1">Use the form above to track your weight</p>
</Card>
) : (
<div className="space-y-3">
{readings.slice(0, 5).map((reading: any, i: number) => (
<WeightCard
key={reading.id}
reading={reading}
previousKg={i < readings.length - 1 ? readings[i + 1]?.weightKg : null}
/>
))}
</div>
)}
</section>
</PageContainer>
</>
)
}