mirror of
https://github.com/Tony0410/nextstep.git
synced 2026-05-24 21:31:43 +08:00
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.
130 lines
4.6 KiB
TypeScript
130 lines
4.6 KiB
TypeScript
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'
|
|
|
|
const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
|
|
const ALLOWED_TYPES = ['application/pdf', 'image/jpeg', 'image/png']
|
|
const VALID_CATEGORIES = ['LAB_REPORT', 'SCAN', 'INSURANCE', 'ID_CARD', 'PRESCRIPTION', 'OTHER']
|
|
|
|
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 limit = Math.min(parseInt(searchParams.get('limit') || '50'), 200)
|
|
|
|
const where: Record<string, unknown> = { workspaceId, deletedAt: null }
|
|
if (category && VALID_CATEGORIES.includes(category)) where.category = category
|
|
|
|
// Return metadata only — no file data in list
|
|
const documents = await prisma.medicalDocument.findMany({
|
|
where,
|
|
orderBy: { createdAt: 'desc' },
|
|
take: limit,
|
|
select: {
|
|
id: true,
|
|
workspaceId: true,
|
|
title: true,
|
|
category: true,
|
|
fileName: true,
|
|
fileSize: true,
|
|
mimeType: true,
|
|
dateTaken: true,
|
|
expiryDate: true,
|
|
notes: true,
|
|
createdAt: true,
|
|
createdBy: { select: { id: true, name: true } },
|
|
},
|
|
})
|
|
|
|
return NextResponse.json({ documents })
|
|
} catch (error) {
|
|
console.error('List documents error:', error)
|
|
return NextResponse.json({ error: 'Failed to list documents' }, { 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 formData = await req.formData()
|
|
const file = formData.get('file') as File | null
|
|
const title = formData.get('title') as string | null
|
|
const category = formData.get('category') as string | null
|
|
const dateTaken = formData.get('dateTaken') as string | null
|
|
const expiryDate = formData.get('expiryDate') as string | null
|
|
const notes = formData.get('notes') as string | null
|
|
|
|
if (!file) return NextResponse.json({ error: 'File is required' }, { status: 400 })
|
|
if (!title?.trim()) return NextResponse.json({ error: 'Title is required' }, { status: 400 })
|
|
if (!category || !VALID_CATEGORIES.includes(category)) {
|
|
return NextResponse.json({ error: 'Valid category is required' }, { status: 400 })
|
|
}
|
|
if (file.size > MAX_FILE_SIZE) {
|
|
return NextResponse.json({ error: 'File too large (max 10MB)' }, { status: 400 })
|
|
}
|
|
if (!ALLOWED_TYPES.includes(file.type)) {
|
|
return NextResponse.json({ error: 'Only PDF, JPG, and PNG files allowed' }, { status: 400 })
|
|
}
|
|
|
|
// Read file into buffer
|
|
const arrayBuffer = await file.arrayBuffer()
|
|
const fileData = Buffer.from(arrayBuffer)
|
|
|
|
const doc = await prisma.medicalDocument.create({
|
|
data: {
|
|
workspaceId,
|
|
title: title.trim(),
|
|
category,
|
|
fileName: file.name,
|
|
fileSize: file.size,
|
|
mimeType: file.type,
|
|
fileData,
|
|
dateTaken: dateTaken ? new Date(dateTaken) : null,
|
|
expiryDate: expiryDate ? new Date(expiryDate) : null,
|
|
notes: notes?.trim() || null,
|
|
createdById: req.session.user.id,
|
|
},
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
category: true,
|
|
fileName: true,
|
|
fileSize: true,
|
|
mimeType: true,
|
|
dateTaken: true,
|
|
expiryDate: true,
|
|
notes: true,
|
|
createdAt: true,
|
|
createdBy: { select: { id: true, name: true } },
|
|
},
|
|
})
|
|
|
|
await prisma.auditLog.create({
|
|
data: {
|
|
workspaceId, userId: req.session.user.id,
|
|
action: 'CREATE', entityType: 'MEDICAL_DOCUMENT', entityId: doc.id,
|
|
details: { title: doc.title, category: doc.category, fileSize: file.size },
|
|
},
|
|
})
|
|
|
|
return NextResponse.json({ document: doc }, { status: 201 })
|
|
} catch (error) {
|
|
console.error('Upload document error:', error)
|
|
return NextResponse.json({ error: 'Failed to upload document' }, { status: 500 })
|
|
}
|
|
})
|