mirror of
https://github.com/Tony0410/nextstep.git
synced 2026-05-25 22:01:39 +08:00
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:
45
src/app/api/workspaces/[id]/tasks/[taskId]/complete/route.ts
Normal file
45
src/app/api/workspaces/[id]/tasks/[taskId]/complete/route.ts
Normal 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 })
|
||||
}
|
||||
})
|
||||
93
src/app/api/workspaces/[id]/tasks/[taskId]/route.ts
Normal file
93
src/app/api/workspaces/[id]/tasks/[taskId]/route.ts
Normal 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 })
|
||||
}
|
||||
})
|
||||
101
src/app/api/workspaces/[id]/tasks/route.ts
Normal file
101
src/app/api/workspaces/[id]/tasks/route.ts
Normal 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 })
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user