Files
tessera-ctl/apps/web/src/lib/calendar-api.ts
T
schalli e49d4c7c9d fix(quick-261005-d5d): Kalender-Test bricht bei unerreichbarem Exchange nach 15 s ab
EWS-Aufrufe ueber httpntlm bekommen eine eigene 15-s-Zeitgrenze;
httpreqs timeout greift waehrend des Verbindungsaufbaus nicht
(gemessen 134 s). Netzfehler liefern im Test den Schluessel
'unreachable', das Formular meldet 'nicht erreichbar' statt
'Zugangsdaten pruefen'.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 09:29:25 +02:00

204 lines
5.2 KiB
TypeScript

/**
* 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<CalendarSource[]> {
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<CalendarSource> {
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<CalendarSource> {
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<void> {
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<CalendarEvent[]> {
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();
}