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,45 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db/prisma'
import { checkWorkspaceAccess, canEdit } from '@/lib/db/workspace-access'
import { withAuth, type AuthenticatedRequest } from '@/lib/auth'
export const POST = withAuth(async (
req: AuthenticatedRequest,
{ params }: { params: Promise<Record<string, string>> }
) => {
try {
const { id: workspaceId, taskId } = await params
const access = await checkWorkspaceAccess(workspaceId, req.session.user.id)
if (!access || !canEdit(access.role)) return NextResponse.json({ error: 'Access denied' }, { status: 403 })
const existing = await prisma.caregiverTask.findFirst({ where: { id: taskId, workspaceId, deletedAt: null } })
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 })
const task = await prisma.caregiverTask.update({
where: { id: taskId },
data: {
status: 'DONE',
completedAt: new Date(),
completedById: req.session.user.id,
updatedById: req.session.user.id,
},
include: {
assignedTo: { select: { id: true, name: true } },
createdBy: { select: { id: true, name: true } },
},
})
await prisma.auditLog.create({
data: {
workspaceId, userId: req.session.user.id,
action: 'UPDATE', entityType: 'CAREGIVER_TASK', entityId: taskId,
details: { status: 'DONE' },
},
})
return NextResponse.json({ task })
} catch (error) {
console.error('Complete caregiver task error:', error)
return NextResponse.json({ error: 'Failed to complete task' }, { status: 500 })
}
})

View File

@@ -0,0 +1,93 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db/prisma'
import { checkWorkspaceAccess, canEdit } from '@/lib/db/workspace-access'
import { withAuth, type AuthenticatedRequest } from '@/lib/auth'
import { caregiverTaskSchema } from '@/lib/validation'
export const PATCH = withAuth(async (
req: AuthenticatedRequest,
{ params }: { params: Promise<Record<string, string>> }
) => {
try {
const { id: workspaceId, taskId } = await params
const access = await checkWorkspaceAccess(workspaceId, req.session.user.id)
if (!access || !canEdit(access.role)) return NextResponse.json({ error: 'Access denied' }, { status: 403 })
const existing = await prisma.caregiverTask.findFirst({ where: { id: taskId, workspaceId, deletedAt: null } })
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 })
const body = await req.json()
const result = caregiverTaskSchema.partial().safeParse(body)
if (!result.success) return NextResponse.json({ error: 'Invalid input', details: result.error.flatten() }, { status: 400 })
// Build update data
const updateData: Record<string, unknown> = {
...result.data,
updatedById: req.session.user.id,
}
// Convert dueDate string to Date if provided
if (result.data.dueDate !== undefined) {
updateData.dueDate = result.data.dueDate ? new Date(result.data.dueDate) : null
}
// Handle completedAt based on status changes
if (result.data.status === 'DONE' && existing.status !== 'DONE' && !existing.completedAt) {
updateData.completedAt = new Date()
updateData.completedById = req.session.user.id
} else if (result.data.status && result.data.status !== 'DONE' && existing.status === 'DONE') {
updateData.completedAt = null
updateData.completedById = null
}
const task = await prisma.caregiverTask.update({
where: { id: taskId },
data: updateData,
include: {
assignedTo: { select: { id: true, name: true } },
createdBy: { select: { id: true, name: true } },
},
})
await prisma.auditLog.create({
data: {
workspaceId, userId: req.session.user.id,
action: 'UPDATE', entityType: 'CAREGIVER_TASK', entityId: taskId,
details: result.data,
},
})
return NextResponse.json({ task })
} catch (error) {
console.error('Update caregiver task error:', error)
return NextResponse.json({ error: 'Failed to update task' }, { status: 500 })
}
})
export const DELETE = withAuth(async (
req: AuthenticatedRequest,
{ params }: { params: Promise<Record<string, string>> }
) => {
try {
const { id: workspaceId, taskId } = await params
const access = await checkWorkspaceAccess(workspaceId, req.session.user.id)
if (!access || !canEdit(access.role)) return NextResponse.json({ error: 'Access denied' }, { status: 403 })
const existing = await prisma.caregiverTask.findFirst({ where: { id: taskId, workspaceId, deletedAt: null } })
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 })
await prisma.caregiverTask.update({ where: { id: taskId }, data: { deletedAt: new Date() } })
await prisma.auditLog.create({
data: {
workspaceId, userId: req.session.user.id,
action: 'DELETE', entityType: 'CAREGIVER_TASK', entityId: taskId,
},
})
return NextResponse.json({ success: true })
} catch (error) {
console.error('Delete caregiver task error:', error)
return NextResponse.json({ error: 'Failed to delete task' }, { status: 500 })
}
})

View File

@@ -0,0 +1,101 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db/prisma'
import { checkWorkspaceAccess, canEdit } from '@/lib/db/workspace-access'
import { withAuth, type AuthenticatedRequest } from '@/lib/auth'
import { caregiverTaskSchema } from '@/lib/validation'
const PRIORITY_ORDER: Record<string, number> = {
URGENT: 0,
HIGH: 1,
NORMAL: 2,
LOW: 3,
}
export const GET = withAuth(async (
req: AuthenticatedRequest,
{ params }: { params: Promise<Record<string, string>> }
) => {
try {
const { id: workspaceId } = await params
const access = await checkWorkspaceAccess(workspaceId, req.session.user.id)
if (!access) return NextResponse.json({ error: 'Access denied' }, { status: 403 })
const { searchParams } = new URL(req.url)
const status = searchParams.get('status')
const assignedTo = searchParams.get('assignedTo')
const limit = Math.min(parseInt(searchParams.get('limit') || '100'), 500)
const where: Record<string, unknown> = { workspaceId, deletedAt: null }
if (status) where.status = status
if (assignedTo) where.assignedToId = assignedTo
const tasks = await prisma.caregiverTask.findMany({
where,
orderBy: { createdAt: 'desc' },
take: limit,
include: {
assignedTo: { select: { id: true, name: true } },
createdBy: { select: { id: true, name: true } },
},
})
// Sort by priority order (URGENT first), then by createdAt desc
tasks.sort((a: { priority: string; createdAt: Date }, b: { priority: string; createdAt: Date }) => {
const priorityDiff = (PRIORITY_ORDER[a.priority] ?? 99) - (PRIORITY_ORDER[b.priority] ?? 99)
if (priorityDiff !== 0) return priorityDiff
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
})
return NextResponse.json({ tasks })
} catch (error) {
console.error('List caregiver tasks error:', error)
return NextResponse.json({ error: 'Failed to list tasks' }, { status: 500 })
}
})
export const POST = withAuth(async (
req: AuthenticatedRequest,
{ params }: { params: Promise<Record<string, string>> }
) => {
try {
const { id: workspaceId } = await params
const access = await checkWorkspaceAccess(workspaceId, req.session.user.id)
if (!access || !canEdit(access.role)) return NextResponse.json({ error: 'Access denied' }, { status: 403 })
const body = await req.json()
const result = caregiverTaskSchema.safeParse(body)
if (!result.success) return NextResponse.json({ error: 'Invalid input', details: result.error.flatten() }, { status: 400 })
const task = await prisma.caregiverTask.create({
data: {
workspaceId,
title: result.data.title,
description: result.data.description || null,
category: result.data.category,
priority: result.data.priority || 'NORMAL',
status: result.data.status || 'TODO',
assignedToId: result.data.assignedToId || null,
dueDate: result.data.dueDate ? new Date(result.data.dueDate) : null,
createdById: req.session.user.id,
updatedById: req.session.user.id,
},
include: {
assignedTo: { select: { id: true, name: true } },
createdBy: { select: { id: true, name: true } },
},
})
await prisma.auditLog.create({
data: {
workspaceId, userId: req.session.user.id,
action: 'CREATE', entityType: 'CAREGIVER_TASK', entityId: task.id,
details: { title: task.title, category: task.category, priority: task.priority },
},
})
return NextResponse.json({ task }, { status: 201 })
} catch (error) {
console.error('Create caregiver task error:', error)
return NextResponse.json({ error: 'Failed to create task' }, { status: 500 })
}
})