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:
73
src/app/api/workspaces/[id]/contacts/[contactId]/route.ts
Normal file
73
src/app/api/workspaces/[id]/contacts/[contactId]/route.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
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 { contactSchema } from '@/lib/validation'
|
||||
|
||||
export const PATCH = withAuth(async (
|
||||
req: AuthenticatedRequest,
|
||||
{ params }: { params: Promise<Record<string, string>> }
|
||||
) => {
|
||||
try {
|
||||
const { id: workspaceId, contactId } = 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.contact.findFirst({ where: { id: contactId, workspaceId, deletedAt: null } })
|
||||
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
|
||||
const body = await req.json()
|
||||
const result = contactSchema.partial().safeParse(body)
|
||||
if (!result.success) return NextResponse.json({ error: 'Invalid input', details: result.error.flatten() }, { status: 400 })
|
||||
|
||||
const contact = await prisma.contact.update({
|
||||
where: { id: contactId },
|
||||
data: { ...result.data, updatedById: req.session.user.id },
|
||||
include: {
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
updatedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId, userId: req.session.user.id,
|
||||
action: 'UPDATE', entityType: 'CONTACT', entityId: contactId,
|
||||
details: result.data,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ contact })
|
||||
} catch (error) {
|
||||
console.error('Update contact error:', error)
|
||||
return NextResponse.json({ error: 'Failed to update contact' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
|
||||
export const DELETE = withAuth(async (
|
||||
req: AuthenticatedRequest,
|
||||
{ params }: { params: Promise<Record<string, string>> }
|
||||
) => {
|
||||
try {
|
||||
const { id: workspaceId, contactId } = 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.contact.findFirst({ where: { id: contactId, workspaceId, deletedAt: null } })
|
||||
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
|
||||
await prisma.contact.update({ where: { id: contactId }, data: { deletedAt: new Date() } })
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId, userId: req.session.user.id,
|
||||
action: 'DELETE', entityType: 'CONTACT', entityId: contactId,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Delete contact error:', error)
|
||||
return NextResponse.json({ error: 'Failed to delete contact' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
87
src/app/api/workspaces/[id]/contacts/route.ts
Normal file
87
src/app/api/workspaces/[id]/contacts/route.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
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 { contactSchema } from '@/lib/validation'
|
||||
|
||||
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 category = searchParams.get('category')
|
||||
|
||||
const where: Record<string, unknown> = { workspaceId, deletedAt: null }
|
||||
if (category) where.category = category
|
||||
|
||||
const contacts = await prisma.contact.findMany({
|
||||
where,
|
||||
orderBy: [{ isEmergency: 'desc' }, { sortOrder: 'asc' }, { name: 'asc' }],
|
||||
include: {
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
updatedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ contacts })
|
||||
} catch (error) {
|
||||
console.error('List contacts error:', error)
|
||||
return NextResponse.json({ error: 'Failed to list contacts' }, { 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 = contactSchema.safeParse(body)
|
||||
if (!result.success) return NextResponse.json({ error: 'Invalid input', details: result.error.flatten() }, { status: 400 })
|
||||
|
||||
const contact = await prisma.contact.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
name: result.data.name,
|
||||
role: result.data.role,
|
||||
category: result.data.category,
|
||||
phone: result.data.phone,
|
||||
phone2: result.data.phone2 || null,
|
||||
email: result.data.email || null,
|
||||
address: result.data.address || null,
|
||||
hours: result.data.hours || null,
|
||||
notes: result.data.notes || null,
|
||||
isEmergency: result.data.isEmergency,
|
||||
sortOrder: result.data.sortOrder,
|
||||
createdById: req.session.user.id,
|
||||
updatedById: req.session.user.id,
|
||||
},
|
||||
include: {
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
updatedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId, userId: req.session.user.id,
|
||||
action: 'CREATE', entityType: 'CONTACT', entityId: contact.id,
|
||||
details: { name: contact.name, category: contact.category },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ contact }, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Create contact error:', error)
|
||||
return NextResponse.json({ error: 'Failed to create contact' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user