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 })
|
||||
}
|
||||
})
|
||||
68
src/app/api/workspaces/[id]/documents/[docId]/route.ts
Normal file
68
src/app/api/workspaces/[id]/documents/[docId]/route.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
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 GET = withAuth(async (
|
||||
req: AuthenticatedRequest,
|
||||
{ params }: { params: Promise<Record<string, string>> }
|
||||
) => {
|
||||
try {
|
||||
const { id: workspaceId, docId } = await params
|
||||
const access = await checkWorkspaceAccess(workspaceId, req.session.user.id)
|
||||
if (!access) return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
||||
|
||||
const doc = await prisma.medicalDocument.findFirst({
|
||||
where: { id: docId, workspaceId, deletedAt: null },
|
||||
})
|
||||
if (!doc) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
|
||||
// Return the file data as a downloadable response
|
||||
const uint8 = new Uint8Array(doc.fileData)
|
||||
return new NextResponse(uint8, {
|
||||
headers: {
|
||||
'Content-Type': doc.mimeType,
|
||||
'Content-Disposition': `inline; filename="${doc.fileName}"`,
|
||||
'Content-Length': String(doc.fileSize),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Download document error:', error)
|
||||
return NextResponse.json({ error: 'Failed to download document' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
|
||||
export const DELETE = withAuth(async (
|
||||
req: AuthenticatedRequest,
|
||||
{ params }: { params: Promise<Record<string, string>> }
|
||||
) => {
|
||||
try {
|
||||
const { id: workspaceId, docId } = 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.medicalDocument.findFirst({
|
||||
where: { id: docId, workspaceId, deletedAt: null },
|
||||
select: { id: true, title: true, category: true },
|
||||
})
|
||||
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
|
||||
await prisma.medicalDocument.update({
|
||||
where: { id: docId },
|
||||
data: { deletedAt: new Date() },
|
||||
})
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId, userId: req.session.user.id,
|
||||
action: 'DELETE', entityType: 'MEDICAL_DOCUMENT', entityId: docId,
|
||||
details: { title: existing.title },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Delete document error:', error)
|
||||
return NextResponse.json({ error: 'Failed to delete document' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
129
src/app/api/workspaces/[id]/documents/route.ts
Normal file
129
src/app/api/workspaces/[id]/documents/route.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
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 })
|
||||
}
|
||||
})
|
||||
83
src/app/api/workspaces/[id]/lab-results/[labId]/route.ts
Normal file
83
src/app/api/workspaces/[id]/lab-results/[labId]/route.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
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 { labResultSchema } from '@/lib/validation'
|
||||
|
||||
export const PATCH = withAuth(async (
|
||||
req: AuthenticatedRequest,
|
||||
{ params }: { params: Promise<Record<string, string>> }
|
||||
) => {
|
||||
try {
|
||||
const { id: workspaceId, labId } = 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.labResult.findFirst({ where: { id: labId, workspaceId, deletedAt: null } })
|
||||
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
|
||||
const body = await req.json()
|
||||
const result = labResultSchema.partial().safeParse(body)
|
||||
if (!result.success) return NextResponse.json({ error: 'Invalid input', details: result.error.flatten() }, { status: 400 })
|
||||
|
||||
const updateData: Record<string, unknown> = { updatedById: req.session.user.id }
|
||||
if (result.data.testDate) updateData.testDate = new Date(result.data.testDate)
|
||||
if (result.data.panelName !== undefined) updateData.panelName = result.data.panelName
|
||||
if (result.data.labName !== undefined) updateData.labName = result.data.labName || null
|
||||
if (result.data.results !== undefined) updateData.results = result.data.results as any
|
||||
if (result.data.notes !== undefined) updateData.notes = result.data.notes || null
|
||||
|
||||
const labResult = await prisma.labResult.update({
|
||||
where: { id: labId },
|
||||
data: updateData,
|
||||
include: {
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId, userId: req.session.user.id,
|
||||
action: 'UPDATE', entityType: 'LAB_RESULT', entityId: labId,
|
||||
details: { panelName: labResult.panelName },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ labResult })
|
||||
} catch (error) {
|
||||
console.error('Update lab result error:', error)
|
||||
return NextResponse.json({ error: 'Failed to update lab result' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
|
||||
export const DELETE = withAuth(async (
|
||||
req: AuthenticatedRequest,
|
||||
{ params }: { params: Promise<Record<string, string>> }
|
||||
) => {
|
||||
try {
|
||||
const { id: workspaceId, labId } = 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.labResult.findFirst({ where: { id: labId, workspaceId, deletedAt: null } })
|
||||
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
|
||||
await prisma.labResult.update({
|
||||
where: { id: labId },
|
||||
data: { deletedAt: new Date(), updatedById: req.session.user.id },
|
||||
})
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId, userId: req.session.user.id,
|
||||
action: 'DELETE', entityType: 'LAB_RESULT', entityId: labId,
|
||||
details: { panelName: existing.panelName },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Delete lab result error:', error)
|
||||
return NextResponse.json({ error: 'Failed to delete lab result' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
87
src/app/api/workspaces/[id]/lab-results/route.ts
Normal file
87
src/app/api/workspaces/[id]/lab-results/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 { labResultSchema } 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 from = searchParams.get('from')
|
||||
const to = searchParams.get('to')
|
||||
const limit = Math.min(parseInt(searchParams.get('limit') || '50'), 200)
|
||||
|
||||
const where: Record<string, unknown> = { workspaceId, deletedAt: null }
|
||||
if (from || to) {
|
||||
const dateFilter: Record<string, Date> = {}
|
||||
if (from) dateFilter.gte = new Date(from)
|
||||
if (to) dateFilter.lte = new Date(to)
|
||||
where.testDate = dateFilter
|
||||
}
|
||||
|
||||
const labResults = await prisma.labResult.findMany({
|
||||
where,
|
||||
orderBy: { testDate: 'desc' },
|
||||
take: limit,
|
||||
include: {
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ labResults })
|
||||
} catch (error) {
|
||||
console.error('List lab results error:', error)
|
||||
return NextResponse.json({ error: 'Failed to list lab results' }, { 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 = labResultSchema.safeParse(body)
|
||||
if (!result.success) return NextResponse.json({ error: 'Invalid input', details: result.error.flatten() }, { status: 400 })
|
||||
|
||||
const labResult = await prisma.labResult.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
testDate: new Date(result.data.testDate),
|
||||
panelName: result.data.panelName,
|
||||
labName: result.data.labName || null,
|
||||
results: result.data.results as any,
|
||||
notes: result.data.notes || null,
|
||||
createdById: req.session.user.id,
|
||||
updatedById: req.session.user.id,
|
||||
},
|
||||
include: {
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId, userId: req.session.user.id,
|
||||
action: 'CREATE', entityType: 'LAB_RESULT', entityId: labResult.id,
|
||||
details: { panelName: labResult.panelName, markerCount: result.data.results.length },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ labResult }, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Create lab result error:', error)
|
||||
return NextResponse.json({ error: 'Failed to create lab result' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
66
src/app/api/workspaces/[id]/lab-results/trends/route.ts
Normal file
66
src/app/api/workspaces/[id]/lab-results/trends/route.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
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'
|
||||
|
||||
interface StoredMarker {
|
||||
marker: string
|
||||
value: number
|
||||
unit: string
|
||||
refMin: number | null
|
||||
refMax: number | null
|
||||
flag: string | null
|
||||
}
|
||||
|
||||
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 markerName = searchParams.get('marker')
|
||||
if (!markerName) return NextResponse.json({ error: 'marker query param required' }, { status: 400 })
|
||||
|
||||
// Fetch all lab results with this marker
|
||||
const labResults = await prisma.labResult.findMany({
|
||||
where: { workspaceId, deletedAt: null },
|
||||
orderBy: { testDate: 'asc' },
|
||||
select: { testDate: true, results: true },
|
||||
})
|
||||
|
||||
// Extract the specific marker from each result
|
||||
const trendData: Array<{
|
||||
date: string
|
||||
value: number
|
||||
unit: string
|
||||
refMin: number | null
|
||||
refMax: number | null
|
||||
}> = []
|
||||
|
||||
for (const lr of labResults) {
|
||||
const markers = lr.results as unknown as StoredMarker[]
|
||||
if (!Array.isArray(markers)) continue
|
||||
const found = markers.find(
|
||||
(m) => m.marker.toLowerCase() === markerName.toLowerCase()
|
||||
)
|
||||
if (found) {
|
||||
trendData.push({
|
||||
date: lr.testDate.toISOString(),
|
||||
value: found.value,
|
||||
unit: found.unit,
|
||||
refMin: found.refMin ?? null,
|
||||
refMax: found.refMax ?? null,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ marker: markerName, trendData })
|
||||
} catch (error) {
|
||||
console.error('Lab result trends error:', error)
|
||||
return NextResponse.json({ error: 'Failed to fetch trends' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
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 { checkInteractions } from '@/lib/interactions/checker'
|
||||
|
||||
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) return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
||||
|
||||
// Get all active medications for this workspace
|
||||
const medications = await prisma.medication.findMany({
|
||||
where: { workspaceId, active: true, deletedAt: null },
|
||||
select: { id: true, name: true },
|
||||
})
|
||||
|
||||
if (medications.length < 2) {
|
||||
return NextResponse.json({
|
||||
interactions: [],
|
||||
message: 'Need at least 2 active medications to check for interactions.',
|
||||
medicationCount: medications.length,
|
||||
})
|
||||
}
|
||||
|
||||
const medNames = medications.map((m) => m.name)
|
||||
const interactions = checkInteractions(medNames)
|
||||
|
||||
// Cache results in DB for quick retrieval
|
||||
// Clear old interactions for this workspace first
|
||||
await prisma.drugInteraction.deleteMany({ where: { workspaceId } })
|
||||
|
||||
// Save new interactions
|
||||
if (interactions.length > 0) {
|
||||
// Map drug names back to medication IDs
|
||||
const nameToId = new Map(medications.map((m) => [m.name.toLowerCase(), m.id]))
|
||||
|
||||
for (const interaction of interactions) {
|
||||
const med1Id = nameToId.get(interaction.drug1Name.toLowerCase())
|
||||
const med2Id = nameToId.get(interaction.drug2Name.toLowerCase())
|
||||
if (med1Id && med2Id) {
|
||||
await prisma.drugInteraction.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
medication1Id: med1Id,
|
||||
medication2Id: med2Id,
|
||||
severity: interaction.severity,
|
||||
description: interaction.description,
|
||||
},
|
||||
}).catch(() => {
|
||||
// Ignore duplicate key errors
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId, userId: req.session.user.id,
|
||||
action: 'CREATE', entityType: 'DRUG_INTERACTION', entityId: workspaceId,
|
||||
details: { medicationCount: medications.length, interactionsFound: interactions.length },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
interactions,
|
||||
medicationCount: medications.length,
|
||||
checkedAt: new Date().toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Check interactions error:', error)
|
||||
return NextResponse.json({ error: 'Failed to check interactions' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
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 { milestoneSchema } from '@/lib/validation'
|
||||
|
||||
export const PATCH = withAuth(async (
|
||||
req: AuthenticatedRequest,
|
||||
{ params }: { params: Promise<Record<string, string>> }
|
||||
) => {
|
||||
try {
|
||||
const { id: workspaceId, milestoneId } = 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.treatmentMilestone.findFirst({ where: { id: milestoneId, workspaceId, deletedAt: null } })
|
||||
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
|
||||
const body = await req.json()
|
||||
const result = milestoneSchema.partial().safeParse(body)
|
||||
if (!result.success) return NextResponse.json({ error: 'Invalid input', details: result.error.flatten() }, { status: 400 })
|
||||
|
||||
const updateData: Record<string, unknown> = {
|
||||
...result.data,
|
||||
updatedById: req.session.user.id,
|
||||
}
|
||||
|
||||
// Convert date strings to Date objects
|
||||
if (result.data.plannedDate) {
|
||||
updateData.plannedDate = new Date(result.data.plannedDate)
|
||||
}
|
||||
if (result.data.actualDate !== undefined) {
|
||||
updateData.actualDate = result.data.actualDate ? new Date(result.data.actualDate) : null
|
||||
}
|
||||
|
||||
// Auto-set actualDate when completing
|
||||
if (result.data.status === 'COMPLETED' && !existing.actualDate && !result.data.actualDate) {
|
||||
updateData.actualDate = new Date()
|
||||
}
|
||||
|
||||
const milestone = await prisma.treatmentMilestone.update({
|
||||
where: { id: milestoneId },
|
||||
data: updateData,
|
||||
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: 'MILESTONE', entityId: milestoneId,
|
||||
details: result.data,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ milestone })
|
||||
} catch (error) {
|
||||
console.error('Update milestone error:', error)
|
||||
return NextResponse.json({ error: 'Failed to update milestone' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
|
||||
export const DELETE = withAuth(async (
|
||||
req: AuthenticatedRequest,
|
||||
{ params }: { params: Promise<Record<string, string>> }
|
||||
) => {
|
||||
try {
|
||||
const { id: workspaceId, milestoneId } = 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.treatmentMilestone.findFirst({ where: { id: milestoneId, workspaceId, deletedAt: null } })
|
||||
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
|
||||
await prisma.treatmentMilestone.update({ where: { id: milestoneId }, data: { deletedAt: new Date() } })
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId, userId: req.session.user.id,
|
||||
action: 'DELETE', entityType: 'MILESTONE', entityId: milestoneId,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Delete milestone error:', error)
|
||||
return NextResponse.json({ error: 'Failed to delete milestone' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
84
src/app/api/workspaces/[id]/milestones/route.ts
Normal file
84
src/app/api/workspaces/[id]/milestones/route.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
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 { milestoneSchema } 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 status = searchParams.get('status')
|
||||
const limit = Math.min(parseInt(searchParams.get('limit') || '100'), 500)
|
||||
|
||||
const where: Record<string, unknown> = { workspaceId, deletedAt: null }
|
||||
if (status) {
|
||||
where.status = status
|
||||
}
|
||||
|
||||
const milestones = await prisma.treatmentMilestone.findMany({
|
||||
where, orderBy: { plannedDate: 'asc' }, take: limit,
|
||||
include: { createdBy: { select: { id: true, name: true } } },
|
||||
})
|
||||
|
||||
return NextResponse.json({ milestones })
|
||||
} catch (error) {
|
||||
console.error('List milestones error:', error)
|
||||
return NextResponse.json({ error: 'Failed to list milestones' }, { 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 = milestoneSchema.safeParse(body)
|
||||
if (!result.success) return NextResponse.json({ error: 'Invalid input', details: result.error.flatten() }, { status: 400 })
|
||||
|
||||
const existingCount = await prisma.treatmentMilestone.count({
|
||||
where: { workspaceId, deletedAt: null },
|
||||
})
|
||||
|
||||
const milestone = await prisma.treatmentMilestone.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
type: result.data.type,
|
||||
title: result.data.title,
|
||||
description: result.data.description || null,
|
||||
plannedDate: new Date(result.data.plannedDate),
|
||||
actualDate: result.data.actualDate ? new Date(result.data.actualDate) : null,
|
||||
status: result.data.status || 'SCHEDULED',
|
||||
notes: result.data.notes || null,
|
||||
sortOrder: existingCount,
|
||||
createdById: req.session.user.id,
|
||||
updatedById: req.session.user.id,
|
||||
},
|
||||
include: { createdBy: { select: { id: true, name: true } } },
|
||||
})
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId, userId: req.session.user.id,
|
||||
action: 'CREATE', entityType: 'MILESTONE', entityId: milestone.id,
|
||||
details: { type: milestone.type, title: milestone.title },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ milestone }, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Create milestone error:', error)
|
||||
return NextResponse.json({ error: 'Failed to create milestone' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
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 })
|
||||
}
|
||||
})
|
||||
32
src/app/api/workspaces/[id]/temperature/[tempId]/route.ts
Normal file
32
src/app/api/workspaces/[id]/temperature/[tempId]/route.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
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 DELETE = withAuth(async (
|
||||
req: AuthenticatedRequest,
|
||||
{ params }: { params: Promise<Record<string, string>> }
|
||||
) => {
|
||||
try {
|
||||
const { id: workspaceId, tempId } = 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.temperatureLog.findFirst({ where: { id: tempId, workspaceId, deletedAt: null } })
|
||||
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
|
||||
await prisma.temperatureLog.update({ where: { id: tempId }, data: { deletedAt: new Date() } })
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId, userId: req.session.user.id,
|
||||
action: 'DELETE', entityType: 'TEMPERATURE_LOG', entityId: tempId,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Delete temperature log error:', error)
|
||||
return NextResponse.json({ error: 'Failed to delete temperature log' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
78
src/app/api/workspaces/[id]/temperature/route.ts
Normal file
78
src/app/api/workspaces/[id]/temperature/route.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
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 { temperatureLogSchema } 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 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, deletedAt: null }
|
||||
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 temperatureLogs = await prisma.temperatureLog.findMany({
|
||||
where, orderBy: { recordedAt: 'desc' }, take: limit,
|
||||
include: { createdBy: { select: { id: true, name: true } } },
|
||||
})
|
||||
|
||||
return NextResponse.json({ temperatureLogs })
|
||||
} catch (error) {
|
||||
console.error('List temperature logs error:', error)
|
||||
return NextResponse.json({ error: 'Failed to list temperature logs' }, { 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 = temperatureLogSchema.safeParse(body)
|
||||
if (!result.success) return NextResponse.json({ error: 'Invalid input', details: result.error.flatten() }, { status: 400 })
|
||||
|
||||
const temperatureLog = await prisma.temperatureLog.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
tempCelsius: result.data.tempCelsius,
|
||||
method: result.data.method || null,
|
||||
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 } } },
|
||||
})
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId, userId: req.session.user.id,
|
||||
action: 'CREATE', entityType: 'TEMPERATURE_LOG', entityId: temperatureLog.id,
|
||||
details: { tempCelsius: temperatureLog.tempCelsius, method: temperatureLog.method },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ temperatureLog }, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Create temperature log error:', error)
|
||||
return NextResponse.json({ error: 'Failed to create temperature log' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
32
src/app/api/workspaces/[id]/weight/[weightId]/route.ts
Normal file
32
src/app/api/workspaces/[id]/weight/[weightId]/route.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
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 DELETE = withAuth(async (
|
||||
req: AuthenticatedRequest,
|
||||
{ params }: { params: Promise<Record<string, string>> }
|
||||
) => {
|
||||
try {
|
||||
const { id: workspaceId, weightId } = 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.weightLog.findFirst({ where: { id: weightId, workspaceId, deletedAt: null } })
|
||||
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
|
||||
await prisma.weightLog.update({ where: { id: weightId }, data: { deletedAt: new Date() } })
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId, userId: req.session.user.id,
|
||||
action: 'DELETE', entityType: 'WEIGHT_LOG', entityId: weightId,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Delete weight log error:', error)
|
||||
return NextResponse.json({ error: 'Failed to delete weight log' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
77
src/app/api/workspaces/[id]/weight/route.ts
Normal file
77
src/app/api/workspaces/[id]/weight/route.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
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 { weightLogSchema } 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 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, deletedAt: null }
|
||||
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 weightLogs = await prisma.weightLog.findMany({
|
||||
where, orderBy: { recordedAt: 'desc' }, take: limit,
|
||||
include: { createdBy: { select: { id: true, name: true } } },
|
||||
})
|
||||
|
||||
return NextResponse.json({ weightLogs })
|
||||
} catch (error) {
|
||||
console.error('List weight logs error:', error)
|
||||
return NextResponse.json({ error: 'Failed to list weight logs' }, { 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 = weightLogSchema.safeParse(body)
|
||||
if (!result.success) return NextResponse.json({ error: 'Invalid input', details: result.error.flatten() }, { status: 400 })
|
||||
|
||||
const weightLog = await prisma.weightLog.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
weightKg: result.data.weightKg,
|
||||
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 } } },
|
||||
})
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId, userId: req.session.user.id,
|
||||
action: 'CREATE', entityType: 'WEIGHT_LOG', entityId: weightLog.id,
|
||||
details: { weightKg: weightLog.weightKg },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ weightLog }, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Create weight log error:', error)
|
||||
return NextResponse.json({ error: 'Failed to create weight log' }, { status: 500 })
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user