united-tattoo/hooks/use-availability.ts
Nicholai a77f62f949 feat: implement CalDAV Nextcloud bidirectional calendar integration
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.
2025-10-08 20:44:17 -06:00

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
}