329 lines
9.1 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { getDB } from '@/lib/db'
import { z } from 'zod'
const createAppointmentSchema = z.object({
artistId: z.string().min(1),
clientId: z.string().min(1),
title: z.string().min(1),
description: z.string().optional(),
startTime: z.string().datetime(),
endTime: z.string().datetime(),
depositAmount: z.number().optional(),
totalAmount: z.number().optional(),
notes: z.string().optional(),
})
const updateAppointmentSchema = createAppointmentSchema.partial().extend({
id: z.string().min(1),
status: z.enum(['PENDING', 'CONFIRMED', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED']).optional(),
})
export async function GET(request: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const start = searchParams.get('start')
const end = searchParams.get('end')
const artistId = searchParams.get('artistId')
const status = searchParams.get('status')
const db = getDB()
let query = `
SELECT
a.*,
ar.name as artist_name,
u.name as client_name,
u.email as client_email
FROM appointments a
JOIN artists ar ON a.artist_id = ar.id
JOIN users u ON a.client_id = u.id
WHERE 1=1
`
const params: any[] = []
if (start) {
query += ` AND a.start_time >= ?`
params.push(start)
}
if (end) {
query += ` AND a.end_time <= ?`
params.push(end)
}
if (artistId) {
query += ` AND a.artist_id = ?`
params.push(artistId)
}
if (status) {
query += ` AND a.status = ?`
params.push(status)
}
query += ` ORDER BY a.start_time ASC`
const stmt = db.prepare(query)
const result = await stmt.bind(...params).all()
return NextResponse.json({ appointments: result.results })
} catch (error) {
console.error('Error fetching appointments:', error)
return NextResponse.json(
{ error: 'Failed to fetch appointments' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const validatedData = createAppointmentSchema.parse(body)
// Check for scheduling conflicts
const db = getDB()
const conflictCheck = db.prepare(`
SELECT id FROM appointments
WHERE artist_id = ?
AND status NOT IN ('CANCELLED', 'COMPLETED')
AND (
(start_time <= ? AND end_time > ?) OR
(start_time < ? AND end_time >= ?) OR
(start_time >= ? AND end_time <= ?)
)
`)
const conflictResult = await conflictCheck.bind(
validatedData.artistId,
validatedData.startTime, validatedData.startTime,
validatedData.endTime, validatedData.endTime,
validatedData.startTime, validatedData.endTime
).all()
if (conflictResult.results.length > 0) {
return NextResponse.json(
{ error: 'Time slot conflicts with existing appointment' },
{ status: 409 }
)
}
const appointmentId = crypto.randomUUID()
const insertStmt = db.prepare(`
INSERT INTO appointments (
id, artist_id, client_id, title, description, start_time, end_time,
status, deposit_amount, total_amount, notes, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, 'PENDING', ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`)
await insertStmt.bind(
appointmentId,
validatedData.artistId,
validatedData.clientId,
validatedData.title,
validatedData.description || null,
validatedData.startTime,
validatedData.endTime,
validatedData.depositAmount || null,
validatedData.totalAmount || null,
validatedData.notes || null
).run()
// Fetch the created appointment with related data
const selectStmt = db.prepare(`
SELECT
a.*,
ar.name as artist_name,
u.name as client_name,
u.email as client_email
FROM appointments a
JOIN artists ar ON a.artist_id = ar.id
JOIN users u ON a.client_id = u.id
WHERE a.id = ?
`)
const appointment = await selectStmt.bind(appointmentId).first()
return NextResponse.json({ appointment }, { status: 201 })
} catch (error) {
console.error('Error creating appointment:', error)
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid appointment data', details: error.errors },
{ status: 400 }
)
}
return NextResponse.json(
{ error: 'Failed to create appointment' },
{ status: 500 }
)
}
}
export async function PUT(request: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const validatedData = updateAppointmentSchema.parse(body)
const db = getDB()
// Check if appointment exists
const existingStmt = db.prepare('SELECT * FROM appointments WHERE id = ?')
const existing = await existingStmt.bind(validatedData.id).first()
if (!existing) {
return NextResponse.json(
{ error: 'Appointment not found' },
{ status: 404 }
)
}
// Check for conflicts if time is being changed
if (validatedData.startTime || validatedData.endTime) {
const startTime = validatedData.startTime || existing.start_time
const endTime = validatedData.endTime || existing.end_time
const artistId = validatedData.artistId || existing.artist_id
const conflictCheck = db.prepare(`
SELECT id FROM appointments
WHERE artist_id = ?
AND id != ?
AND status NOT IN ('CANCELLED', 'COMPLETED')
AND (
(start_time <= ? AND end_time > ?) OR
(start_time < ? AND end_time >= ?) OR
(start_time >= ? AND end_time <= ?)
)
`)
const conflictResult = await conflictCheck.bind(
artistId, validatedData.id,
startTime, startTime,
endTime, endTime,
startTime, endTime
).all()
if (conflictResult.results.length > 0) {
return NextResponse.json(
{ error: 'Time slot conflicts with existing appointment' },
{ status: 409 }
)
}
}
// Build update query dynamically
const updateFields = []
const updateValues = []
Object.entries(validatedData).forEach(([key, value]) => {
if (key !== 'id' && value !== undefined) {
const dbKey = key.replace(/([A-Z])/g, '_$1').toLowerCase()
updateFields.push(`${dbKey} = ?`)
updateValues.push(value)
}
})
if (updateFields.length === 0) {
return NextResponse.json(
{ error: 'No fields to update' },
{ status: 400 }
)
}
updateFields.push('updated_at = CURRENT_TIMESTAMP')
updateValues.push(validatedData.id)
const updateStmt = db.prepare(`
UPDATE appointments
SET ${updateFields.join(', ')}
WHERE id = ?
`)
await updateStmt.bind(...updateValues).run()
// Fetch updated appointment
const selectStmt = db.prepare(`
SELECT
a.*,
ar.name as artist_name,
u.name as client_name,
u.email as client_email
FROM appointments a
JOIN artists ar ON a.artist_id = ar.id
JOIN users u ON a.client_id = u.id
WHERE a.id = ?
`)
const appointment = await selectStmt.bind(validatedData.id).first()
return NextResponse.json({ appointment })
} catch (error) {
console.error('Error updating appointment:', error)
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid appointment data', details: error.errors },
{ status: 400 }
)
}
return NextResponse.json(
{ error: 'Failed to update appointment' },
{ status: 500 }
)
}
}
export async function DELETE(request: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
if (!id) {
return NextResponse.json(
{ error: 'Appointment ID is required' },
{ status: 400 }
)
}
const db = getDB()
const deleteStmt = db.prepare('DELETE FROM appointments WHERE id = ?')
const result = await deleteStmt.bind(id).run()
if (result.changes === 0) {
return NextResponse.json(
{ error: 'Appointment not found' },
{ status: 404 }
)
}
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error deleting appointment:', error)
return NextResponse.json(
{ error: 'Failed to delete appointment' },
{ status: 500 }
)
}
}