/** * Calendar API client functions. * Consumes the calendar backend built in 05-03. * All calls use credentials: 'include' for cookie-based auth. * * Anti-pattern (RESEARCH): The browser NEVER fetches external calendars directly. * All event data flows through /api/calendar/events which proxies via the backend. */ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; // --- Types mirroring the 05-03 backend contract --- /** * Calendar source as returned by GET /calendar/sources. * Note: encryptedPassword is NEVER returned (T-05-09). */ export interface CalendarSource { id: string; userId: string; tenantId: string; name: string; type: 'caldav' | 'ics' | 'exchange'; exchangeMode?: string | null; domain?: string | null; url: string; username?: string | null; color?: string | null; isVisible: boolean; syncIntervalMin: number; lastSyncAt?: string | null; lastSyncError?: string | null; createdAt: string; updatedAt: string; } /** * Normalized calendar event from GET /calendar/events. * Mirrors CalendarEvent from calendar.service.ts. */ export interface CalendarEvent { id: string; sourceId: string; title: string; start: string; end: string; allDay: boolean; location?: string; description?: string; color?: string; } /** * Payload for creating a new calendar source. */ export interface CreateSourcePayload { name: string; type: 'caldav' | 'ics' | 'exchange'; url: string; username?: string; password?: string; exchangeMode?: string; domain?: string; color?: string; } /** * Payload for updating an existing calendar source. * All fields are optional. */ export interface UpdateSourcePayload { name?: string; type?: 'caldav' | 'ics' | 'exchange'; url?: string; username?: string; password?: string; exchangeMode?: string; domain?: string; color?: string; isVisible?: boolean; } // --- API functions --- /** * Fetch all calendar sources for the current user. * GET /calendar/sources */ export async function fetchSources(): Promise { const res = await fetch(`${API_URL}/calendar/sources`, { credentials: 'include', }); if (!res.ok) throw new Error('Failed to fetch calendar sources'); return res.json(); } /** * Add a new calendar source. * POST /calendar/sources */ export async function addSource( payload: CreateSourcePayload, ): Promise { const res = await fetch(`${API_URL}/calendar/sources`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(payload), }); if (!res.ok) throw new Error('Failed to add calendar source'); return res.json(); } /** * Update an existing calendar source (including isVisible toggle for CAL-02). * PATCH /calendar/sources/:id */ export async function updateSource( id: string, payload: UpdateSourcePayload, ): Promise { const res = await fetch(`${API_URL}/calendar/sources/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(payload), }); if (!res.ok) throw new Error('Failed to update calendar source'); return res.json(); } /** * Delete a calendar source. * DELETE /calendar/sources/:id */ export async function deleteSource(id: string): Promise { const res = await fetch(`${API_URL}/calendar/sources/${id}`, { method: 'DELETE', credentials: 'include', }); if (!res.ok) throw new Error('Failed to delete calendar source'); } /** * Test a calendar configuration without saving it. * POST /calendar/sources/test-config */ export async function testSourceConfig(config: { type: string; url: string; username?: string; password?: string; exchangeMode?: string; domain?: string; }): Promise<{ success: boolean; error?: string }> { const res = await fetch(`${API_URL}/calendar/sources/test-config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(config), }); if (!res.ok) throw new Error('Test request failed'); return res.json(); } /** * Test connection to a calendar source. * POST /calendar/sources/:id/test * Returns { success: boolean } from the backend. */ export async function testSource( id: string, ): Promise<{ success: boolean; error?: string }> { const res = await fetch(`${API_URL}/calendar/sources/${id}/test`, { method: 'POST', credentials: 'include', }); if (!res.ok) throw new Error('Failed to test calendar source'); return res.json(); } /** * Fetch aggregated events from all visible sources. * GET /calendar/events?from=...&to=... * Default window: now to now + 30 days (backend default). */ export async function fetchEvents( from?: string, to?: string, ): Promise { const params = new URLSearchParams(); if (from) params.set('from', from); if (to) params.set('to', to); const queryStr = params.toString(); const url = `${API_URL}/calendar/events${queryStr ? `?${queryStr}` : ''}`; const res = await fetch(url, { credentials: 'include', }); if (!res.ok) throw new Error('Failed to fetch calendar events'); return res.json(); }