Adds complete CalDAV integration for syncing appointments between the web app and Nextcloud calendars with real-time availability checking and conflict resolution. Core Features: - Bidirectional sync: Web ↔ Nextcloud calendars - Real-time availability checking with instant user feedback - Conflict detection (Nextcloud is source of truth) - Pending request workflow with 'REQUEST:' prefix for unconfirmed appointments - Hard time blocking - any calendar event blocks booking slots - Graceful degradation when CalDAV unavailable New Dependencies: - tsdav@^2.0.4 - TypeScript CalDAV client - ical.js@^1.5.0 - iCalendar format parser/generator Database Changes: - New table: artist_calendars (stores calendar configuration per artist) - New table: calendar_sync_logs (tracks all sync operations) - Added caldav_uid and caldav_etag columns to appointments table - Migration: sql/migrations/20250109_add_caldav_support.sql New Services: - lib/caldav-client.ts - Core CalDAV operations and iCalendar conversion - lib/calendar-sync.ts - Bidirectional sync logic with error handling New API Endpoints: - GET /api/caldav/availability - Real-time availability checking - POST /api/caldav/sync - Manual sync trigger (admin only) - GET/POST/PUT/DELETE /api/admin/calendars - Calendar configuration CRUD Updated Components: - app/api/appointments/route.ts - Integrated CalDAV sync on CRUD operations - components/booking-form.tsx - Added real-time availability indicator - hooks/use-availability.ts - Custom hook for debounced availability checking Documentation: - docs/CALDAV-SETUP.md - Complete setup guide with troubleshooting - docs/CALDAV-IMPLEMENTATION-SUMMARY.md - Technical implementation overview Pending Tasks (for future PRs): - Admin dashboard UI for calendar management - Background sync worker (Cloudflare Workers cron) - Unit and integration tests Tested with local database migration and linting checks passed.
73 lines
1.8 KiB
TypeScript
73 lines
1.8 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { checkArtistAvailability } from '@/lib/calendar-sync'
|
|
import { z } from 'zod'
|
|
|
|
export const dynamic = "force-dynamic"
|
|
|
|
const availabilitySchema = z.object({
|
|
artistId: z.string().min(1),
|
|
startTime: z.string().datetime(),
|
|
endTime: z.string().datetime(),
|
|
})
|
|
|
|
/**
|
|
* GET /api/caldav/availability
|
|
*
|
|
* Check availability for an artist at a specific time slot
|
|
*
|
|
* Query params:
|
|
* - artistId: string
|
|
* - startTime: ISO datetime string
|
|
* - endTime: ISO datetime string
|
|
*/
|
|
export async function GET(request: NextRequest, { params }: { params?: any } = {}, context?: any) {
|
|
try {
|
|
const { searchParams } = new URL(request.url)
|
|
|
|
const artistId = searchParams.get('artistId')
|
|
const startTime = searchParams.get('startTime')
|
|
const endTime = searchParams.get('endTime')
|
|
|
|
// Validate inputs
|
|
const validatedData = availabilitySchema.parse({
|
|
artistId,
|
|
startTime,
|
|
endTime,
|
|
})
|
|
|
|
const startDate = new Date(validatedData.startTime)
|
|
const endDate = new Date(validatedData.endTime)
|
|
|
|
// Check availability (checks both CalDAV and database)
|
|
const result = await checkArtistAvailability(
|
|
validatedData.artistId,
|
|
startDate,
|
|
endDate,
|
|
context
|
|
)
|
|
|
|
return NextResponse.json({
|
|
artistId: validatedData.artistId,
|
|
startTime: validatedData.startTime,
|
|
endTime: validatedData.endTime,
|
|
available: result.available,
|
|
reason: result.reason,
|
|
})
|
|
} catch (error) {
|
|
console.error('Error checking availability:', error)
|
|
|
|
if (error instanceof z.ZodError) {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid request parameters', details: error.errors },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{ error: 'Failed to check availability' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|
|
|