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.
76 lines
1.7 KiB
TypeScript
76 lines
1.7 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react'
|
|
|
|
interface AvailabilityResult {
|
|
available: boolean
|
|
reason?: string
|
|
checking: boolean
|
|
error?: string
|
|
}
|
|
|
|
interface UseAvailabilityParams {
|
|
artistId: string | null
|
|
startTime: string | null
|
|
endTime: string | null
|
|
enabled?: boolean
|
|
}
|
|
|
|
export function useAvailability({
|
|
artistId,
|
|
startTime,
|
|
endTime,
|
|
enabled = true,
|
|
}: UseAvailabilityParams): AvailabilityResult {
|
|
const [result, setResult] = useState<AvailabilityResult>({
|
|
available: false,
|
|
checking: false,
|
|
})
|
|
|
|
const checkAvailability = useCallback(async () => {
|
|
if (!enabled || !artistId || !startTime || !endTime) {
|
|
setResult({ available: false, checking: false })
|
|
return
|
|
}
|
|
|
|
setResult(prev => ({ ...prev, checking: true, error: undefined }))
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
artistId,
|
|
startTime,
|
|
endTime,
|
|
})
|
|
|
|
const response = await fetch(`/api/caldav/availability?${params}`)
|
|
const data = await response.json()
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Failed to check availability')
|
|
}
|
|
|
|
setResult({
|
|
available: data.available,
|
|
reason: data.reason,
|
|
checking: false,
|
|
})
|
|
} catch (error) {
|
|
setResult({
|
|
available: false,
|
|
checking: false,
|
|
error: error instanceof Error ? error.message : 'Failed to check availability',
|
|
})
|
|
}
|
|
}, [artistId, startTime, endTime, enabled])
|
|
|
|
useEffect(() => {
|
|
// Debounce the availability check
|
|
const timer = setTimeout(() => {
|
|
checkAvailability()
|
|
}, 300)
|
|
|
|
return () => clearTimeout(timer)
|
|
}, [checkAvailability])
|
|
|
|
return result
|
|
}
|
|
|