329 lines
9.1 KiB
TypeScript
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 }
|
|
)
|
|
}
|
|
}
|