mirror of
https://github.com/Tony0410/nextstep.git
synced 2026-05-25 13:51:40 +08:00
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:
55
src/app/api/workspaces/[id]/activity/route.ts
Normal file
55
src/app/api/workspaces/[id]/activity/route.ts
Normal 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 })
|
||||
}
|
||||
})
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
)
|
||||
71
src/app/api/workspaces/[id]/calendar.ics/route.ts
Normal file
71
src/app/api/workspaces/[id]/calendar.ics/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
132
src/app/api/workspaces/[id]/emergency-info/route.ts
Normal file
132
src/app/api/workspaces/[id]/emergency-info/route.ts
Normal 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 })
|
||||
}
|
||||
})
|
||||
107
src/app/api/workspaces/[id]/export/summary.pdf/route.ts
Normal file
107
src/app/api/workspaces/[id]/export/summary.pdf/route.ts
Normal 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 })
|
||||
}
|
||||
})
|
||||
@@ -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 })
|
||||
}
|
||||
})
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
85
src/app/api/workspaces/[id]/symptoms/[symptomId]/route.ts
Normal file
85
src/app/api/workspaces/[id]/symptoms/[symptomId]/route.ts
Normal 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 })
|
||||
}
|
||||
})
|
||||
110
src/app/api/workspaces/[id]/symptoms/route.ts
Normal file
110
src/app/api/workspaces/[id]/symptoms/route.ts
Normal 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 })
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user