Add 11 major features for caregiver health management

Features added:
- Emergency Info Card: Full-screen emergency view with patient info
- Refill Tracker: Track pill counts with auto-decrement on dose
- Activity Feed: View caregiver activity with filtering
- Symptom Tracker: Log symptoms with severity and offline sync
- Print Views: Daily meds, appointments, doctor visit summaries
- iCal Export: Calendar subscription for appointments
- PDF Export: Medical summary for doctor visits
- Calendar View: Monthly calendar for appointments
- Appointment Preparation: Checklist for upcoming appointments
- Medication Reminders: PWA push notifications with quiet hours

Bug fixes:
- Fix invite workflow: Register/login now properly redirect back
- Add undo for doctor questions (can unmark "asked" questions)
- Fix API route type annotations for Next.js 14 compatibility
- Add Suspense boundary for useSearchParams in login/register

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Gemini Agent
2026-01-23 09:42:46 +00:00
parent 515376e126
commit dd4ef2c4cd
70 changed files with 7322 additions and 79 deletions

View File

@@ -0,0 +1,55 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db/prisma'
import { checkWorkspaceAccess } from '@/lib/db/workspace-access'
import { withAuth, type AuthenticatedRequest } from '@/lib/auth'
// GET /api/workspaces/[id]/activity
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 limit = Math.min(parseInt(searchParams.get('limit') || '50'), 100)
const offset = parseInt(searchParams.get('offset') || '0')
const entityType = searchParams.get('entityType')
const action = searchParams.get('action')
const where: Record<string, unknown> = {
workspaceId,
...(entityType ? { entityType } : {}),
...(action ? { action } : {}),
}
const [activities, total] = await Promise.all([
prisma.auditLog.findMany({
where,
orderBy: { createdAt: 'desc' },
take: limit,
skip: offset,
include: {
user: {
select: { id: true, name: true },
},
},
}),
prisma.auditLog.count({ where }),
])
return NextResponse.json({
activities,
total,
hasMore: offset + activities.length < total,
})
} catch (error) {
console.error('Get activity error:', error)
return NextResponse.json({ error: 'Failed to get activity' }, { status: 500 })
}
})

View File

@@ -0,0 +1,107 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db/prisma'
import { checkWorkspaceAccess } from '@/lib/db/workspace-access'
import { withAuth, type AuthenticatedRequest } from '@/lib/auth'
// GET /api/workspaces/[id]/appointments/[appointmentId]/checklist
export const GET = withAuth(
async (
req: AuthenticatedRequest,
{ params }: { params: Promise<Record<string, string>> }
) => {
try {
const { id: workspaceId, appointmentId } = await params
const access = await checkWorkspaceAccess(workspaceId, req.session.user.id)
if (!access) {
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}
// Verify appointment exists
const appointment = await prisma.appointment.findFirst({
where: { id: appointmentId, workspaceId, deletedAt: null },
})
if (!appointment) {
return NextResponse.json({ error: 'Appointment not found' }, { status: 404 })
}
// Fetch checklist items
const checklists = await prisma.appointmentChecklist.findMany({
where: { workspaceId, appointmentId },
})
// Convert to a map of { itemId: isReady }
const checkedItems: Record<string, boolean> = {}
for (const item of checklists) {
checkedItems[item.item] = item.isReady
}
return NextResponse.json({
checkedItems,
customItems: [], // Future: support custom items
})
} catch (error) {
console.error('Checklist get error:', error)
return NextResponse.json({ error: 'Failed to get checklist' }, { status: 500 })
}
}
)
// POST /api/workspaces/[id]/appointments/[appointmentId]/checklist
export const POST = withAuth(
async (
req: AuthenticatedRequest,
{ params }: { params: Promise<Record<string, string>> }
) => {
try {
const { id: workspaceId, appointmentId } = await params
const body = await req.json()
const { checkedItems } = body as { checkedItems: Record<string, boolean> }
const access = await checkWorkspaceAccess(workspaceId, req.session.user.id, [
'OWNER',
'EDITOR',
])
if (!access) {
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}
// Verify appointment exists
const appointment = await prisma.appointment.findFirst({
where: { id: appointmentId, workspaceId, deletedAt: null },
})
if (!appointment) {
return NextResponse.json({ error: 'Appointment not found' }, { status: 404 })
}
// Upsert each checklist item
for (const [itemId, isReady] of Object.entries(checkedItems)) {
await prisma.appointmentChecklist.upsert({
where: {
workspaceId_appointmentId_item: {
workspaceId,
appointmentId,
item: itemId,
},
},
create: {
workspaceId,
appointmentId,
item: itemId,
isReady: isReady as boolean,
},
update: {
isReady: isReady as boolean,
},
})
}
return NextResponse.json({ success: true })
} catch (error) {
console.error('Checklist save error:', error)
return NextResponse.json({ error: 'Failed to save checklist' }, { status: 500 })
}
}
)

View File

@@ -0,0 +1,71 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db/prisma'
import { generateICalendar } from '@/lib/calendar/ical-generator'
// GET /api/workspaces/[id]/calendar.ics - Get iCal feed
// This endpoint uses a token-based auth for calendar subscription
export async function GET(
req: Request,
{ params }: { params: Promise<Record<string, string>> }
) {
try {
const { id: workspaceId } = await params
const { searchParams } = new URL(req.url)
const token = searchParams.get('token')
if (!token) {
return new NextResponse('Unauthorized - token required', { status: 401 })
}
// Verify the token is a valid workspace membership
// Token format: userId (simplified for now - could be JWT in production)
const membership = await prisma.workspaceMember.findFirst({
where: {
workspaceId,
userId: token,
},
include: {
workspace: true,
},
})
if (!membership) {
return new NextResponse('Unauthorized - invalid token', { status: 401 })
}
// Fetch appointments (non-deleted, future and recent)
const appointments = await prisma.appointment.findMany({
where: {
workspaceId,
deletedAt: null,
},
orderBy: {
datetime: 'asc',
},
})
// Generate iCal
const icalContent = generateICalendar(
appointments.map((a) => ({
id: a.id,
title: a.title,
datetime: a.datetime,
location: a.location,
notes: a.notes,
})),
membership.workspace.name
)
return new NextResponse(icalContent, {
status: 200,
headers: {
'Content-Type': 'text/calendar; charset=utf-8',
'Content-Disposition': `attachment; filename="${membership.workspace.name}-appointments.ics"`,
'Cache-Control': 'no-cache, no-store, must-revalidate',
},
})
} catch (error) {
console.error('iCal generation error:', error)
return new NextResponse('Internal Server Error', { status: 500 })
}
}

View File

@@ -113,6 +113,20 @@ export const POST = withAuth(async (req: AuthenticatedRequest, { params }) => {
},
})
// Decrement pill count if tracking is enabled
if (medication.pillCount !== null && medication.pillsPerDose !== null) {
const newCount = Math.max(0, medication.pillCount - (medication.pillsPerDose || 1))
await prisma.medication.update({
where: { id: medication.id },
data: {
pillCount: newCount,
version: { increment: 1 },
syncedAt: new Date(),
updatedById: req.session.user.id,
},
})
}
// Audit log
await prisma.auditLog.create({
data: {
@@ -165,7 +179,7 @@ export const PATCH = withAuth(async (req: AuthenticatedRequest, { params }) => {
undoneAt: null,
},
include: {
medication: { select: { name: true } },
medication: { select: { id: true, name: true, pillCount: true, pillsPerDose: true } },
},
})
@@ -198,6 +212,20 @@ export const PATCH = withAuth(async (req: AuthenticatedRequest, { params }) => {
},
})
// Restore pill count if tracking is enabled
if (doseLog.medication.pillCount !== null && doseLog.medication.pillsPerDose !== null) {
const newCount = doseLog.medication.pillCount + (doseLog.medication.pillsPerDose || 1)
await prisma.medication.update({
where: { id: doseLog.medication.id },
data: {
pillCount: newCount,
version: { increment: 1 },
syncedAt: new Date(),
updatedById: req.session.user.id,
},
})
}
// Audit log
await prisma.auditLog.create({
data: {

View File

@@ -0,0 +1,132 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db/prisma'
import { checkWorkspaceAccess } from '@/lib/db/workspace-access'
import { withAuth, type AuthenticatedRequest } from '@/lib/auth'
import { emergencyInfoSchema } from '@/lib/validation'
// GET /api/workspaces/[id]/emergency-info
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 workspace = await prisma.workspace.findUnique({
where: { id: workspaceId },
select: {
id: true,
patientName: true,
patientDOB: true,
bloodType: true,
allergies: true,
medicalConditions: true,
primaryPhysician: true,
physicianPhone: true,
clinicPhone: true,
emergencyPhone: true,
},
})
if (!workspace) {
return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
}
// Also fetch active medications for the emergency view
const medications = await prisma.medication.findMany({
where: {
workspaceId,
active: true,
deletedAt: null,
},
select: {
id: true,
name: true,
instructions: true,
},
orderBy: { name: 'asc' },
})
return NextResponse.json({
...workspace,
medications,
})
} catch (error) {
console.error('Get emergency info error:', error)
return NextResponse.json({ error: 'Failed to get emergency info' }, { status: 500 })
}
})
// PATCH /api/workspaces/[id]/emergency-info
export const PATCH = withAuth(async (
req: AuthenticatedRequest,
{ params }: { params: Promise<Record<string, string>> }
) => {
try {
const { id: workspaceId } = await params
const body = await req.json()
const access = await checkWorkspaceAccess(workspaceId, req.session.user.id, ['OWNER', 'EDITOR'])
if (!access) {
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}
const result = emergencyInfoSchema.safeParse(body)
if (!result.success) {
return NextResponse.json(
{ error: 'Invalid input', details: result.error.flatten() },
{ status: 400 }
)
}
const data = result.data
const workspace = await prisma.workspace.update({
where: { id: workspaceId },
data: {
patientName: data.patientName,
patientDOB: data.patientDOB ? new Date(data.patientDOB) : null,
bloodType: data.bloodType,
allergies: data.allergies,
medicalConditions: data.medicalConditions,
primaryPhysician: data.primaryPhysician,
physicianPhone: data.physicianPhone,
clinicPhone: data.clinicPhone,
emergencyPhone: data.emergencyPhone,
},
select: {
id: true,
patientName: true,
patientDOB: true,
bloodType: true,
allergies: true,
medicalConditions: true,
primaryPhysician: true,
physicianPhone: true,
clinicPhone: true,
emergencyPhone: true,
},
})
// Create audit log
await prisma.auditLog.create({
data: {
workspaceId,
userId: req.session.user.id,
action: 'UPDATE',
entityType: 'WORKSPACE',
entityId: workspaceId,
details: { updated: 'emergency_info' },
},
})
return NextResponse.json(workspace)
} catch (error) {
console.error('Update emergency info error:', error)
return NextResponse.json({ error: 'Failed to update emergency info' }, { status: 500 })
}
})

View File

@@ -0,0 +1,107 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db/prisma'
import { checkWorkspaceAccess } from '@/lib/db/workspace-access'
import { withAuth, type AuthenticatedRequest } from '@/lib/auth'
import { generateMedicalSummaryPDF } from '@/lib/export/pdf-generator'
// GET /api/workspaces/[id]/export/summary.pdf - Generate PDF summary
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 })
}
// Fetch all data
const [workspace, medications, appointments, symptoms] = await Promise.all([
prisma.workspace.findUnique({
where: { id: workspaceId },
select: {
name: true,
patientName: true,
patientDOB: true,
bloodType: true,
allergies: true,
medicalConditions: true,
primaryPhysician: true,
physicianPhone: true,
clinicPhone: true,
emergencyPhone: true,
},
}),
prisma.medication.findMany({
where: { workspaceId, deletedAt: null },
orderBy: { name: 'asc' },
}),
prisma.appointment.findMany({
where: { workspaceId, deletedAt: null },
orderBy: { datetime: 'asc' },
}),
prisma.symptom.findMany({
where: { workspaceId, deletedAt: null },
orderBy: { recordedAt: 'desc' },
take: 100, // Last 100 symptoms
}),
])
if (!workspace) {
return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
}
// Generate PDF
const doc = generateMedicalSummaryPDF({
patient: workspace,
medications: medications.map((m) => ({
id: m.id,
name: m.name,
instructions: m.instructions,
scheduleType: m.scheduleType,
active: m.active,
})),
appointments: appointments.map((a) => ({
id: a.id,
title: a.title,
datetime: a.datetime,
location: a.location,
notes: a.notes,
})),
symptoms: symptoms.map((s) => ({
id: s.id,
type: s.type,
customName: s.customName,
severity: s.severity,
notes: s.notes,
recordedAt: s.recordedAt,
})),
generatedAt: new Date(),
})
// Convert PDF to buffer
const chunks: Buffer[] = []
doc.on('data', (chunk) => chunks.push(chunk))
await new Promise<void>((resolve, reject) => {
doc.on('end', () => resolve())
doc.on('error', (err) => reject(err))
doc.end()
})
const pdfBuffer = Buffer.concat(chunks)
const filename = `${workspace.patientName || workspace.name || 'medical'}-summary-${new Date().toISOString().split('T')[0]}.pdf`
return new NextResponse(pdfBuffer, {
status: 200,
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${filename}"`,
'Content-Length': String(pdfBuffer.length),
},
})
} catch (error) {
console.error('PDF generation error:', error)
return NextResponse.json({ error: 'Failed to generate PDF' }, { status: 500 })
}
})

View File

@@ -0,0 +1,86 @@
import { NextResponse } from 'next/server'
import { z } from 'zod'
import { prisma } from '@/lib/db/prisma'
import { checkWorkspaceAccess } from '@/lib/db/workspace-access'
import { withAuth, type AuthenticatedRequest } from '@/lib/auth'
const refillAmountSchema = z.object({
amount: z.number().min(1, 'Amount must be at least 1'),
})
// POST /api/workspaces/[id]/medications/[medicationId]/refill
export const POST = withAuth(async (
req: AuthenticatedRequest,
{ params }: { params: Promise<Record<string, string>> }
) => {
try {
const { id: workspaceId, medicationId } = await params
const body = await req.json()
const access = await checkWorkspaceAccess(workspaceId, req.session.user.id, ['OWNER', 'EDITOR'])
if (!access) {
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}
const result = refillAmountSchema.safeParse(body)
if (!result.success) {
return NextResponse.json(
{ error: 'Invalid input', details: result.error.flatten() },
{ status: 400 }
)
}
const { amount } = result.data
// Get current medication
const medication = await prisma.medication.findFirst({
where: {
id: medicationId,
workspaceId,
deletedAt: null,
},
})
if (!medication) {
return NextResponse.json({ error: 'Medication not found' }, { status: 404 })
}
// Update pill count
const newPillCount = (medication.pillCount ?? 0) + amount
const updated = await prisma.medication.update({
where: { id: medicationId },
data: {
pillCount: newPillCount,
lastRefillDate: new Date(),
version: { increment: 1 },
syncedAt: new Date(),
updatedById: req.session.user.id,
},
})
// Create audit log
await prisma.auditLog.create({
data: {
workspaceId,
userId: req.session.user.id,
action: 'REFILL',
entityType: 'MEDICATION',
entityId: medicationId,
details: {
amount,
previousCount: medication.pillCount,
newCount: newPillCount,
},
},
})
return NextResponse.json({
id: updated.id,
pillCount: updated.pillCount,
lastRefillDate: updated.lastRefillDate,
})
} catch (error) {
console.error('Refill error:', error)
return NextResponse.json({ error: 'Failed to record refill' }, { status: 500 })
}
})

View File

@@ -2,7 +2,7 @@ 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 { medicationSchema } from '@/lib/validation'
import { medicationWithRefillSchema } from '@/lib/validation'
// GET /api/workspaces/[id]/medications/[medicationId]
export const GET = withAuth(async (req: AuthenticatedRequest, { params }) => {
@@ -75,7 +75,7 @@ export const PATCH = withAuth(async (req: AuthenticatedRequest, { params }) => {
}
const body = await req.json()
const result = medicationSchema.partial().safeParse(body)
const result = medicationWithRefillSchema.partial().safeParse(body)
if (!result.success) {
return NextResponse.json(
@@ -97,6 +97,9 @@ export const PATCH = withAuth(async (req: AuthenticatedRequest, { params }) => {
if (result.data.endDate !== undefined) {
updateData.endDate = result.data.endDate ? new Date(result.data.endDate) : null
}
if (result.data.lastRefillDate !== undefined) {
updateData.lastRefillDate = result.data.lastRefillDate ? new Date(result.data.lastRefillDate) : null
}
const medication = await prisma.medication.update({
where: { id: medicationId },

View File

@@ -2,7 +2,7 @@ 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 { medicationSchema } from '@/lib/validation'
import { medicationWithRefillSchema } from '@/lib/validation'
// GET /api/workspaces/[id]/medications - List medications
export const GET = withAuth(async (req: AuthenticatedRequest, { params }) => {
@@ -60,7 +60,7 @@ export const POST = withAuth(async (req: AuthenticatedRequest, { params }) => {
}
const body = await req.json()
const result = medicationSchema.safeParse(body)
const result = medicationWithRefillSchema.safeParse(body)
if (!result.success) {
return NextResponse.json(
@@ -79,6 +79,11 @@ export const POST = withAuth(async (req: AuthenticatedRequest, { params }) => {
startDate: result.data.startDate ? new Date(result.data.startDate) : null,
endDate: result.data.endDate ? new Date(result.data.endDate) : null,
active: result.data.active ?? true,
// Refill tracking fields
pillCount: result.data.pillCount ?? null,
pillsPerDose: result.data.pillsPerDose ?? 1,
refillThreshold: result.data.refillThreshold ?? 7,
lastRefillDate: result.data.lastRefillDate ? new Date(result.data.lastRefillDate) : null,
createdById: req.session.user.id,
updatedById: req.session.user.id,
},

View File

@@ -0,0 +1,85 @@
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 { symptomSchema } from '@/lib/validation'
// GET /api/workspaces/[id]/symptoms/[symptomId]
export const GET = withAuth(async (
req: AuthenticatedRequest,
{ params }: { params: Promise<Record<string, string>> }
) => {
try {
const { id: workspaceId, symptomId } = await params
const access = await checkWorkspaceAccess(workspaceId, req.session.user.id)
if (!access) {
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}
const symptom = await prisma.symptom.findFirst({
where: { id: symptomId, workspaceId },
include: {
createdBy: { select: { id: true, name: true } },
},
})
if (!symptom) {
return NextResponse.json({ error: 'Symptom not found' }, { status: 404 })
}
return NextResponse.json({ symptom })
} catch (error) {
console.error('Get symptom error:', error)
return NextResponse.json({ error: 'Failed to get symptom' }, { status: 500 })
}
})
// DELETE /api/workspaces/[id]/symptoms/[symptomId] (soft delete)
export const DELETE = withAuth(async (
req: AuthenticatedRequest,
{ params }: { params: Promise<Record<string, string>> }
) => {
try {
const { id: workspaceId, symptomId } = 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.symptom.findFirst({
where: { id: symptomId, workspaceId, deletedAt: null },
})
if (!existing) {
return NextResponse.json({ error: 'Symptom not found' }, { status: 404 })
}
await prisma.symptom.update({
where: { id: symptomId },
data: {
deletedAt: new Date(),
version: { increment: 1 },
syncedAt: new Date(),
},
})
// Audit log
await prisma.auditLog.create({
data: {
workspaceId,
userId: req.session.user.id,
action: 'DELETE',
entityType: 'SYMPTOM',
entityId: symptomId,
details: { type: existing.type },
},
})
return NextResponse.json({ message: 'Symptom deleted' })
} catch (error) {
console.error('Delete symptom error:', error)
return NextResponse.json({ error: 'Failed to delete symptom' }, { status: 500 })
}
})

View File

@@ -0,0 +1,110 @@
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 { symptomSchema } from '@/lib/validation'
// GET /api/workspaces/[id]/symptoms
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 includeDeleted = searchParams.get('includeDeleted') === 'true'
const type = searchParams.get('type')
const from = searchParams.get('from')
const to = searchParams.get('to')
const limit = Math.min(parseInt(searchParams.get('limit') || '100'), 500)
const where: Record<string, unknown> = {
workspaceId,
...(includeDeleted ? {} : { deletedAt: null }),
...(type ? { type } : {}),
}
if (from || to) {
where.recordedAt = {}
if (from) (where.recordedAt as Record<string, unknown>).gte = new Date(from)
if (to) (where.recordedAt as Record<string, unknown>).lte = new Date(to)
}
const symptoms = await prisma.symptom.findMany({
where,
orderBy: { recordedAt: 'desc' },
take: limit,
include: {
createdBy: { select: { id: true, name: true } },
},
})
return NextResponse.json({ symptoms })
} catch (error) {
console.error('List symptoms error:', error)
return NextResponse.json({ error: 'Failed to list symptoms' }, { status: 500 })
}
})
// POST /api/workspaces/[id]/symptoms
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 = symptomSchema.safeParse(body)
if (!result.success) {
return NextResponse.json(
{ error: 'Invalid input', details: result.error.flatten() },
{ status: 400 }
)
}
const symptom = await prisma.symptom.create({
data: {
workspaceId,
type: result.data.type,
customName: result.data.customName || null,
severity: result.data.severity,
notes: result.data.notes || null,
recordedAt: result.data.recordedAt ? new Date(result.data.recordedAt) : new Date(),
createdById: req.session.user.id,
},
include: {
createdBy: { select: { id: true, name: true } },
},
})
// Audit log
await prisma.auditLog.create({
data: {
workspaceId,
userId: req.session.user.id,
action: 'CREATE',
entityType: 'SYMPTOM',
entityId: symptom.id,
details: { type: symptom.type, severity: symptom.severity },
},
})
return NextResponse.json({ symptom }, { status: 201 })
} catch (error) {
console.error('Create symptom error:', error)
return NextResponse.json({ error: 'Failed to create symptom' }, { status: 500 })
}
})