mirror of
https://github.com/Tony0410/nextstep.git
synced 2026-05-25 13:51:40 +08:00
Initial commit: Next Step health management app
A calm, reliable app to help manage appointments, medications, and notes for chemo patients and their families. Features: - Today dashboard with next appointment and medications due - Medication tracking with multiple schedule types (fixed times, interval, weekdays, PRN) - One-tap dose logging with 5-minute undo window - Questions for doctor tracking - Family sharing with workspace model and invite links - Offline-first with IndexedDB and sync - Docker Compose deployment with Tailscale Funnel support Tech stack: - Next.js 14 (App Router) + TypeScript + Tailwind CSS - PostgreSQL + Prisma - Argon2 password hashing + session cookies - Dexie.js for IndexedDB Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
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 { appointmentSchema } from '@/lib/validation'
|
||||
|
||||
// GET /api/workspaces/[id]/appointments/[appointmentId]
|
||||
export const GET = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
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 }
|
||||
)
|
||||
}
|
||||
|
||||
const appointment = await prisma.appointment.findFirst({
|
||||
where: { id: appointmentId, workspaceId },
|
||||
include: {
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
updatedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
if (!appointment) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Appointment not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ appointment })
|
||||
} catch (error) {
|
||||
console.error('Get appointment error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to get appointment' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// PATCH /api/workspaces/[id]/appointments/[appointmentId]
|
||||
export const PATCH = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
try {
|
||||
const { id: workspaceId, appointmentId } = 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.appointment.findFirst({
|
||||
where: { id: appointmentId, workspaceId, deletedAt: null },
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Appointment not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const result = appointmentSchema.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,
|
||||
version: { increment: 1 },
|
||||
syncedAt: new Date(),
|
||||
}
|
||||
|
||||
if (result.data.datetime) {
|
||||
updateData.datetime = new Date(result.data.datetime)
|
||||
}
|
||||
|
||||
const appointment = await prisma.appointment.update({
|
||||
where: { id: appointmentId },
|
||||
data: updateData,
|
||||
include: {
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
updatedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
// Audit log
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
userId: req.session.user.id,
|
||||
action: 'UPDATE',
|
||||
entityType: 'APPOINTMENT',
|
||||
entityId: appointmentId,
|
||||
details: result.data,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ appointment })
|
||||
} catch (error) {
|
||||
console.error('Update appointment error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update appointment' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// DELETE /api/workspaces/[id]/appointments/[appointmentId] (soft delete)
|
||||
export const DELETE = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
try {
|
||||
const { id: workspaceId, appointmentId } = 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.appointment.findFirst({
|
||||
where: { id: appointmentId, workspaceId, deletedAt: null },
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Appointment not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
await prisma.appointment.update({
|
||||
where: { id: appointmentId },
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
updatedById: req.session.user.id,
|
||||
version: { increment: 1 },
|
||||
syncedAt: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
// Audit log
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
userId: req.session.user.id,
|
||||
action: 'DELETE',
|
||||
entityType: 'APPOINTMENT',
|
||||
entityId: appointmentId,
|
||||
details: { title: existing.title },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ message: 'Appointment deleted' })
|
||||
} catch (error) {
|
||||
console.error('Delete appointment error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete appointment' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
116
src/app/api/workspaces/[id]/appointments/route.ts
Normal file
116
src/app/api/workspaces/[id]/appointments/route.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
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 { appointmentSchema } from '@/lib/validation'
|
||||
|
||||
// GET /api/workspaces/[id]/appointments - List appointments
|
||||
export const GET = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
try {
|
||||
const { id: workspaceId } = await params
|
||||
|
||||
const access = await checkWorkspaceAccess(workspaceId, req.session.user.id)
|
||||
if (!access) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Workspace not found or access denied' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(req.url)
|
||||
const includeDeleted = searchParams.get('includeDeleted') === 'true'
|
||||
const fromDate = searchParams.get('from')
|
||||
const toDate = searchParams.get('to')
|
||||
|
||||
const where: Record<string, unknown> = {
|
||||
workspaceId,
|
||||
...(includeDeleted ? {} : { deletedAt: null }),
|
||||
}
|
||||
|
||||
if (fromDate) {
|
||||
where.datetime = { ...(where.datetime as object || {}), gte: new Date(fromDate) }
|
||||
}
|
||||
if (toDate) {
|
||||
where.datetime = { ...(where.datetime as object || {}), lte: new Date(toDate) }
|
||||
}
|
||||
|
||||
const appointments = await prisma.appointment.findMany({
|
||||
where,
|
||||
orderBy: { datetime: 'asc' },
|
||||
include: {
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
updatedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ appointments })
|
||||
} catch (error) {
|
||||
console.error('List appointments error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to list appointments' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// POST /api/workspaces/[id]/appointments - Create appointment
|
||||
export const POST = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
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 = appointmentSchema.safeParse(body)
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid input', details: result.error.flatten() },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const appointment = await prisma.appointment.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
title: result.data.title,
|
||||
datetime: new Date(result.data.datetime),
|
||||
location: result.data.location || null,
|
||||
mapUrl: result.data.mapUrl || null,
|
||||
notes: result.data.notes || null,
|
||||
createdById: req.session.user.id,
|
||||
updatedById: req.session.user.id,
|
||||
},
|
||||
include: {
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
updatedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
// Audit log
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
userId: req.session.user.id,
|
||||
action: 'CREATE',
|
||||
entityType: 'APPOINTMENT',
|
||||
entityId: appointment.id,
|
||||
details: { title: appointment.title },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ appointment }, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Create appointment error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create appointment' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
221
src/app/api/workspaces/[id]/doses/route.ts
Normal file
221
src/app/api/workspaces/[id]/doses/route.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
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 { doseLogSchema, undoDoseSchema } from '@/lib/validation'
|
||||
|
||||
// GET /api/workspaces/[id]/doses - List dose logs
|
||||
export const GET = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
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 medicationId = searchParams.get('medicationId')
|
||||
const fromDate = searchParams.get('from')
|
||||
const toDate = searchParams.get('to')
|
||||
const includeUndone = searchParams.get('includeUndone') === 'true'
|
||||
const limit = Math.min(parseInt(searchParams.get('limit') || '100'), 500)
|
||||
|
||||
const where: Record<string, unknown> = {
|
||||
workspaceId,
|
||||
...(medicationId ? { medicationId } : {}),
|
||||
...(includeUndone ? {} : { undoneAt: null }),
|
||||
}
|
||||
|
||||
if (fromDate || toDate) {
|
||||
where.takenAt = {}
|
||||
if (fromDate) (where.takenAt as Record<string, unknown>).gte = new Date(fromDate)
|
||||
if (toDate) (where.takenAt as Record<string, unknown>).lte = new Date(toDate)
|
||||
}
|
||||
|
||||
const doseLogs = await prisma.doseLog.findMany({
|
||||
where,
|
||||
orderBy: { takenAt: 'desc' },
|
||||
take: limit,
|
||||
include: {
|
||||
medication: {
|
||||
select: { id: true, name: true },
|
||||
},
|
||||
loggedBy: { select: { id: true, name: true } },
|
||||
undoneBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ doseLogs })
|
||||
} catch (error) {
|
||||
console.error('List dose logs error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to list dose logs' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// POST /api/workspaces/[id]/doses - Log a dose (Take medication)
|
||||
export const POST = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
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 = doseLogSchema.safeParse(body)
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid input', details: result.error.flatten() },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Verify medication exists and belongs to workspace
|
||||
const medication = await prisma.medication.findFirst({
|
||||
where: {
|
||||
id: result.data.medicationId,
|
||||
workspaceId,
|
||||
deletedAt: null,
|
||||
},
|
||||
})
|
||||
|
||||
if (!medication) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Medication not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const takenAt = result.data.takenAt ? new Date(result.data.takenAt) : new Date()
|
||||
|
||||
const doseLog = await prisma.doseLog.create({
|
||||
data: {
|
||||
medicationId: result.data.medicationId,
|
||||
workspaceId,
|
||||
takenAt,
|
||||
loggedById: req.session.user.id,
|
||||
},
|
||||
include: {
|
||||
medication: { select: { id: true, name: true } },
|
||||
loggedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
// Audit log
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
userId: req.session.user.id,
|
||||
action: 'TAKE_DOSE',
|
||||
entityType: 'DOSE_LOG',
|
||||
entityId: doseLog.id,
|
||||
details: { medicationName: medication.name, takenAt: takenAt.toISOString() },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ doseLog }, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Log dose error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to log dose' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// PATCH /api/workspaces/[id]/doses - Undo a dose
|
||||
export const PATCH = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
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 = undoDoseSchema.safeParse(body)
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid input', details: result.error.flatten() },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const doseLog = await prisma.doseLog.findFirst({
|
||||
where: {
|
||||
id: result.data.doseLogId,
|
||||
workspaceId,
|
||||
undoneAt: null,
|
||||
},
|
||||
include: {
|
||||
medication: { select: { name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
if (!doseLog) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Dose log not found or already undone' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if within undo window (5 minutes)
|
||||
const minutesSinceDose = (Date.now() - doseLog.takenAt.getTime()) / 1000 / 60
|
||||
if (minutesSinceDose > 5) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Undo window has expired (5 minutes)' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const updated = await prisma.doseLog.update({
|
||||
where: { id: doseLog.id },
|
||||
data: {
|
||||
undoneAt: new Date(),
|
||||
undoneById: req.session.user.id,
|
||||
},
|
||||
include: {
|
||||
medication: { select: { id: true, name: true } },
|
||||
loggedBy: { select: { id: true, name: true } },
|
||||
undoneBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
// Audit log
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
userId: req.session.user.id,
|
||||
action: 'UNDO_DOSE',
|
||||
entityType: 'DOSE_LOG',
|
||||
entityId: doseLog.id,
|
||||
details: { medicationName: doseLog.medication.name },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ doseLog: updated })
|
||||
} catch (error) {
|
||||
console.error('Undo dose error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to undo dose' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
126
src/app/api/workspaces/[id]/invite/route.ts
Normal file
126
src/app/api/workspaces/[id]/invite/route.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db/prisma'
|
||||
import { withAuth, type AuthenticatedRequest } from '@/lib/auth'
|
||||
import { inviteSchema } from '@/lib/validation'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
// Helper to check workspace access
|
||||
async function checkWorkspaceAccess(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
requiredRoles: string[] = ['OWNER']
|
||||
) {
|
||||
const member = await prisma.workspaceMember.findUnique({
|
||||
where: {
|
||||
workspaceId_userId: { workspaceId, userId },
|
||||
},
|
||||
})
|
||||
|
||||
if (!member || !requiredRoles.includes(member.role)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return member
|
||||
}
|
||||
|
||||
// POST /api/workspaces/[id]/invite - Create invite token
|
||||
export const POST = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
// Only owners can create invites
|
||||
const member = await checkWorkspaceAccess(id, req.session.user.id, ['OWNER'])
|
||||
if (!member) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Only workspace owners can create invites' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const result = inviteSchema.safeParse(body)
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid input', details: result.error.flatten() },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { role, expiresInDays } = result.data
|
||||
|
||||
const token = nanoid(32)
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setDate(expiresAt.getDate() + expiresInDays)
|
||||
|
||||
const invite = await prisma.inviteToken.create({
|
||||
data: {
|
||||
workspaceId: id,
|
||||
token,
|
||||
role,
|
||||
expiresAt,
|
||||
},
|
||||
})
|
||||
|
||||
// Build invite URL
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
||||
const inviteUrl = `${baseUrl}/invite/${token}`
|
||||
|
||||
return NextResponse.json({
|
||||
invite: {
|
||||
token: invite.token,
|
||||
role: invite.role,
|
||||
expiresAt: invite.expiresAt,
|
||||
url: inviteUrl,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Create invite error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create invite' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// GET /api/workspaces/[id]/invite - List active invites
|
||||
export const GET = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
const member = await checkWorkspaceAccess(id, req.session.user.id, ['OWNER'])
|
||||
if (!member) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Only workspace owners can view invites' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
const invites = await prisma.inviteToken.findMany({
|
||||
where: {
|
||||
workspaceId: id,
|
||||
usedAt: null,
|
||||
expiresAt: { gt: new Date() },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
||||
|
||||
return NextResponse.json({
|
||||
invites: invites.map((i) => ({
|
||||
id: i.id,
|
||||
token: i.token,
|
||||
role: i.role,
|
||||
expiresAt: i.expiresAt,
|
||||
url: `${baseUrl}/invite/${i.token}`,
|
||||
})),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('List invites error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to list invites' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
187
src/app/api/workspaces/[id]/medications/[medicationId]/route.ts
Normal file
187
src/app/api/workspaces/[id]/medications/[medicationId]/route.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
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'
|
||||
|
||||
// GET /api/workspaces/[id]/medications/[medicationId]
|
||||
export const GET = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
try {
|
||||
const { id: workspaceId, medicationId } = await params
|
||||
|
||||
const access = await checkWorkspaceAccess(workspaceId, req.session.user.id)
|
||||
if (!access) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Access denied' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
const medication = await prisma.medication.findFirst({
|
||||
where: { id: medicationId, workspaceId },
|
||||
include: {
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
updatedBy: { select: { id: true, name: true } },
|
||||
doseLogs: {
|
||||
where: { undoneAt: null },
|
||||
orderBy: { takenAt: 'desc' },
|
||||
take: 10,
|
||||
include: {
|
||||
loggedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!medication) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Medication not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ medication })
|
||||
} catch (error) {
|
||||
console.error('Get medication error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to get medication' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// PATCH /api/workspaces/[id]/medications/[medicationId]
|
||||
export const PATCH = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
try {
|
||||
const { id: workspaceId, medicationId } = 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.medication.findFirst({
|
||||
where: { id: medicationId, workspaceId, deletedAt: null },
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Medication not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const result = medicationSchema.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,
|
||||
version: { increment: 1 },
|
||||
syncedAt: new Date(),
|
||||
}
|
||||
|
||||
if (result.data.startDate !== undefined) {
|
||||
updateData.startDate = result.data.startDate ? new Date(result.data.startDate) : null
|
||||
}
|
||||
if (result.data.endDate !== undefined) {
|
||||
updateData.endDate = result.data.endDate ? new Date(result.data.endDate) : null
|
||||
}
|
||||
|
||||
const medication = await prisma.medication.update({
|
||||
where: { id: medicationId },
|
||||
data: updateData,
|
||||
include: {
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
updatedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
// Audit log
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
userId: req.session.user.id,
|
||||
action: 'UPDATE',
|
||||
entityType: 'MEDICATION',
|
||||
entityId: medicationId,
|
||||
details: result.data,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ medication })
|
||||
} catch (error) {
|
||||
console.error('Update medication error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update medication' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// DELETE /api/workspaces/[id]/medications/[medicationId] (soft delete)
|
||||
export const DELETE = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
try {
|
||||
const { id: workspaceId, medicationId } = 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.medication.findFirst({
|
||||
where: { id: medicationId, workspaceId, deletedAt: null },
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Medication not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
await prisma.medication.update({
|
||||
where: { id: medicationId },
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
active: false,
|
||||
updatedById: req.session.user.id,
|
||||
version: { increment: 1 },
|
||||
syncedAt: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
// Audit log
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
userId: req.session.user.id,
|
||||
action: 'DELETE',
|
||||
entityType: 'MEDICATION',
|
||||
entityId: medicationId,
|
||||
details: { name: existing.name },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ message: 'Medication deleted' })
|
||||
} catch (error) {
|
||||
console.error('Delete medication error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete medication' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
111
src/app/api/workspaces/[id]/medications/route.ts
Normal file
111
src/app/api/workspaces/[id]/medications/route.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
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'
|
||||
|
||||
// GET /api/workspaces/[id]/medications - List medications
|
||||
export const GET = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
try {
|
||||
const { id: workspaceId } = await params
|
||||
|
||||
const access = await checkWorkspaceAccess(workspaceId, req.session.user.id)
|
||||
if (!access) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Workspace not found or access denied' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(req.url)
|
||||
const activeOnly = searchParams.get('activeOnly') !== 'false'
|
||||
const includeDeleted = searchParams.get('includeDeleted') === 'true'
|
||||
|
||||
const where: Record<string, unknown> = {
|
||||
workspaceId,
|
||||
...(includeDeleted ? {} : { deletedAt: null }),
|
||||
...(activeOnly ? { active: true } : {}),
|
||||
}
|
||||
|
||||
const medications = await prisma.medication.findMany({
|
||||
where,
|
||||
orderBy: { name: 'asc' },
|
||||
include: {
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
updatedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ medications })
|
||||
} catch (error) {
|
||||
console.error('List medications error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to list medications' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// POST /api/workspaces/[id]/medications - Create medication
|
||||
export const POST = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
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 = medicationSchema.safeParse(body)
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid input', details: result.error.flatten() },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const medication = await prisma.medication.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
name: result.data.name,
|
||||
instructions: result.data.instructions || null,
|
||||
scheduleType: result.data.scheduleType,
|
||||
scheduleData: result.data.scheduleData,
|
||||
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,
|
||||
createdById: req.session.user.id,
|
||||
updatedById: req.session.user.id,
|
||||
},
|
||||
include: {
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
updatedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
// Audit log
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
userId: req.session.user.id,
|
||||
action: 'CREATE',
|
||||
entityType: 'MEDICATION',
|
||||
entityId: medication.id,
|
||||
details: { name: medication.name },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ medication }, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Create medication error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create medication' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
196
src/app/api/workspaces/[id]/notes/[noteId]/route.ts
Normal file
196
src/app/api/workspaces/[id]/notes/[noteId]/route.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
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 { noteSchema } from '@/lib/validation'
|
||||
|
||||
// GET /api/workspaces/[id]/notes/[noteId]
|
||||
export const GET = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
try {
|
||||
const { id: workspaceId, noteId } = await params
|
||||
|
||||
const access = await checkWorkspaceAccess(workspaceId, req.session.user.id)
|
||||
if (!access) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Access denied' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
const note = await prisma.note.findFirst({
|
||||
where: { id: noteId, workspaceId },
|
||||
include: {
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
updatedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
if (!note) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Note not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ note })
|
||||
} catch (error) {
|
||||
console.error('Get note error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to get note' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// PATCH /api/workspaces/[id]/notes/[noteId]
|
||||
export const PATCH = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
try {
|
||||
const { id: workspaceId, noteId } = 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.note.findFirst({
|
||||
where: { id: noteId, workspaceId, deletedAt: null },
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Note not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
|
||||
// Handle marking question as asked
|
||||
if (body.markAsked === true && existing.type === 'QUESTION') {
|
||||
const note = await prisma.note.update({
|
||||
where: { id: noteId },
|
||||
data: {
|
||||
askedAt: new Date(),
|
||||
updatedById: req.session.user.id,
|
||||
version: { increment: 1 },
|
||||
syncedAt: new Date(),
|
||||
},
|
||||
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: 'MARK_ASKED',
|
||||
entityType: 'NOTE',
|
||||
entityId: noteId,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ note })
|
||||
}
|
||||
|
||||
const result = noteSchema.partial().safeParse(body)
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid input', details: result.error.flatten() },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const note = await prisma.note.update({
|
||||
where: { id: noteId },
|
||||
data: {
|
||||
...result.data,
|
||||
updatedById: req.session.user.id,
|
||||
version: { increment: 1 },
|
||||
syncedAt: new Date(),
|
||||
},
|
||||
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: 'NOTE',
|
||||
entityId: noteId,
|
||||
details: result.data,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ note })
|
||||
} catch (error) {
|
||||
console.error('Update note error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update note' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// DELETE /api/workspaces/[id]/notes/[noteId] (soft delete)
|
||||
export const DELETE = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
try {
|
||||
const { id: workspaceId, noteId } = 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.note.findFirst({
|
||||
where: { id: noteId, workspaceId, deletedAt: null },
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Note not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
await prisma.note.update({
|
||||
where: { id: noteId },
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
updatedById: req.session.user.id,
|
||||
version: { increment: 1 },
|
||||
syncedAt: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
userId: req.session.user.id,
|
||||
action: 'DELETE',
|
||||
entityType: 'NOTE',
|
||||
entityId: noteId,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ message: 'Note deleted' })
|
||||
} catch (error) {
|
||||
console.error('Delete note error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete note' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
106
src/app/api/workspaces/[id]/notes/route.ts
Normal file
106
src/app/api/workspaces/[id]/notes/route.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
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 { noteSchema } from '@/lib/validation'
|
||||
|
||||
// GET /api/workspaces/[id]/notes - List notes
|
||||
export const GET = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
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 type = searchParams.get('type') as 'QUESTION' | 'GENERAL' | null
|
||||
const includeDeleted = searchParams.get('includeDeleted') === 'true'
|
||||
|
||||
const where: Record<string, unknown> = {
|
||||
workspaceId,
|
||||
...(type ? { type } : {}),
|
||||
...(includeDeleted ? {} : { deletedAt: null }),
|
||||
}
|
||||
|
||||
const notes = await prisma.note.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
updatedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ notes })
|
||||
} catch (error) {
|
||||
console.error('List notes error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to list notes' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// POST /api/workspaces/[id]/notes - Create note
|
||||
export const POST = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
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 = noteSchema.safeParse(body)
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid input', details: result.error.flatten() },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const note = await prisma.note.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
type: result.data.type,
|
||||
content: result.data.content,
|
||||
createdById: req.session.user.id,
|
||||
updatedById: req.session.user.id,
|
||||
},
|
||||
include: {
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
updatedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
// Audit log
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
userId: req.session.user.id,
|
||||
action: 'CREATE',
|
||||
entityType: 'NOTE',
|
||||
entityId: note.id,
|
||||
details: { type: note.type },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ note }, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Create note error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create note' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
145
src/app/api/workspaces/[id]/route.ts
Normal file
145
src/app/api/workspaces/[id]/route.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db/prisma'
|
||||
import { withAuth, type AuthenticatedRequest } from '@/lib/auth'
|
||||
import { updateWorkspaceSchema } from '@/lib/validation'
|
||||
|
||||
// Helper to check workspace access
|
||||
async function checkWorkspaceAccess(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
requiredRoles: string[] = ['OWNER', 'EDITOR', 'VIEWER']
|
||||
) {
|
||||
const member = await prisma.workspaceMember.findUnique({
|
||||
where: {
|
||||
workspaceId_userId: { workspaceId, userId },
|
||||
},
|
||||
})
|
||||
|
||||
if (!member || !requiredRoles.includes(member.role)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return member
|
||||
}
|
||||
|
||||
// GET /api/workspaces/[id] - Get workspace details
|
||||
export const GET = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
const member = await checkWorkspaceAccess(id, req.session.user.id)
|
||||
if (!member) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Workspace not found or access denied' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const workspace = await prisma.workspace.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
members: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!workspace) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Workspace not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
workspace: {
|
||||
id: workspace.id,
|
||||
name: workspace.name,
|
||||
clinicPhone: workspace.clinicPhone,
|
||||
emergencyPhone: workspace.emergencyPhone,
|
||||
quietHoursStart: workspace.quietHoursStart,
|
||||
quietHoursEnd: workspace.quietHoursEnd,
|
||||
largeTextMode: workspace.largeTextMode,
|
||||
role: member.role,
|
||||
members: workspace.members.map((m) => ({
|
||||
id: m.id,
|
||||
role: m.role,
|
||||
user: m.user,
|
||||
})),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Get workspace error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to get workspace' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// PATCH /api/workspaces/[id] - Update workspace settings
|
||||
export const PATCH = withAuth(async (req: AuthenticatedRequest, { params }) => {
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
const member = await checkWorkspaceAccess(id, req.session.user.id, ['OWNER', 'EDITOR'])
|
||||
if (!member) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Workspace not found or access denied' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const result = updateWorkspaceSchema.safeParse(body)
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid input', details: result.error.flatten() },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const workspace = await prisma.workspace.update({
|
||||
where: { id },
|
||||
data: result.data,
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
clinicPhone: true,
|
||||
emergencyPhone: true,
|
||||
quietHoursStart: true,
|
||||
quietHoursEnd: true,
|
||||
largeTextMode: true,
|
||||
},
|
||||
})
|
||||
|
||||
// Audit log
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
workspaceId: id,
|
||||
userId: req.session.user.id,
|
||||
action: 'UPDATE',
|
||||
entityType: 'WORKSPACE',
|
||||
entityId: id,
|
||||
details: result.data,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ workspace })
|
||||
} catch (error) {
|
||||
console.error('Update workspace error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update workspace' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user