1// phase8e-calendar-api: central client for all calendar Firestore operations now via REST API. 2import { Auth } from './auth.js'; 3// calendar-sync-v1 (2026-07-27): Jede Terminaenderung meldet sich, damit andere 4// offene Ansichten (Kalender, digitale Rezeption) nicht auf altem Stand stehen 5// bleiben. opts.source sagt, wer geschrieben hat - der Kalender aktualisiert 6// sich selbst und braucht die Meldung dann nicht ein zweites Mal. 7import { notifyCalendarChanged } from './calendarSync.js'; 8 9const cache = { 10 connections: { data: null, ts: 0 }, 11}; 12const TTL_MS = 8000; 13 14async function _fetch(path, init = {}) { 15 const token = await Auth.getIdToken(); 16 const headers = { 'Content-Type': 'application/json', ...(init.headers || {}) }; 17 if (token) headers['Authorization'] = `Bearer ${token}`; 18 const res = await fetch(path, { ...init, headers, credentials: 'include' }); 19 if (!res.ok) { 20 let body = null; 21 try { body = await res.json(); } catch (_) { /* noop */ } 22 const err = new Error(body?.error || `HTTP ${res.status}`); 23 err.status = res.status; 24 err.body = body; 25 throw err; 26 } 27 return res.json(); 28} 29 30// --- CONNECTIONS --- 31export async function listConnections({ force = false } = {}) { 32 const now = Date.now(); 33 if (!force && cache.connections.data && (now - cache.connections.ts) < TTL_MS) { 34 return cache.connections.data; 35 } 36 const r = await _fetch('/api/calendar/connections'); 37 cache.connections = { data: r.connections || {}, ts: now }; 38 return cache.connections.data; 39} 40 41export async function getConnection(provider) { 42 const r = await _fetch(`/api/calendar/connections/${encodeURIComponent(provider)}`); 43 return { exists: !!r.exists, data: r.connection || null }; 44} 45 46export async function setConnection(provider, data, { merge = false } = {}) { 47 cache.connections = { data: null, ts: 0 }; 48 return _fetch(`/api/calendar/connections/${encodeURIComponent(provider)}${merge ? '?merge=1' : ''}`, { 49 method: 'POST', 50 body: JSON.stringify(data || {}), 51 }); 52} 53 54export async function patchConnection(provider, data) { 55 cache.connections = { data: null, ts: 0 }; 56 return _fetch(`/api/calendar/connections/${encodeURIComponent(provider)}`, { 57 method: 'PATCH', 58 body: JSON.stringify(data || {}), 59 }); 60} 61 62export async function deleteConnection(provider) { 63 cache.connections = { data: null, ts: 0 }; 64 return _fetch(`/api/calendar/connections/${encodeURIComponent(provider)}`, { method: 'DELETE' }); 65} 66 67// --- INTEGRATIONS (alt location) --- 68export async function getIntegration(provider) { 69 const r = await _fetch(`/api/calendar/integrations/${encodeURIComponent(provider)}`); 70 return { exists: !!r.exists, data: r.integration || null }; 71} 72 73export async function setIntegration(provider, data, { merge = false } = {}) { 74 return _fetch(`/api/calendar/integrations/${encodeURIComponent(provider)}${merge ? '?merge=1' : ''}`, { 75 method: 'POST', 76 body: JSON.stringify(data || {}), 77 }); 78} 79 80export async function deleteIntegration(provider) { 81 return _fetch(`/api/calendar/integrations/${encodeURIComponent(provider)}`, { method: 'DELETE' }); 82} 83 84// --- CALENDAR TYPES (existing endpoints in routes/calendar.py) --- 85export async function listCalendarTypes() { 86 const r = await _fetch('/api/calendar/types'); 87 return r.types || r.calendarTypes || r.items || (Array.isArray(r) ? r : []); 88} 89 90export async function getCalendarType(id) { 91 return _fetch(`/api/calendar/types/${encodeURIComponent(id)}`); 92} 93 94export async function createCalendarType(data) { 95 return _fetch('/api/calendar/types', { method: 'POST', body: JSON.stringify(data || {}) }); 96} 97 98export async function updateCalendarType(id, data) { 99 return _fetch(`/api/calendar/types/${encodeURIComponent(id)}`, { 100 method: 'PUT', 101 body: JSON.stringify(data || {}), 102 }); 103} 104 105export async function deleteCalendarType(id) { 106 return _fetch(`/api/calendar/types/${encodeURIComponent(id)}`, { method: 'DELETE' }); 107} 108 109// --- EVENT MAPPINGS --- 110export async function listEventMappings(filters = {}) { 111 const qs = new URLSearchParams(); 112 for (const [k, v] of Object.entries(filters)) { 113 if (v != null && v !== '') qs.append(k, v); 114 } 115 const r = await _fetch(`/api/calendar/event-mappings${qs.toString() ? '?' + qs.toString() : ''}`); 116 return r.mappings || []; 117} 118 119export async function createEventMapping(data) { 120 return _fetch('/api/calendar/event-mappings', { method: 'POST', body: JSON.stringify(data || {}) }); 121} 122 123export async function createEventMappingsBatch(mappings) { 124 return _fetch('/api/calendar/event-mappings', { 125 method: 'POST', 126 body: JSON.stringify({ mappings: mappings || [] }), 127 }); 128} 129 130export async function deleteEventMapping(id) { 131 return _fetch(`/api/calendar/event-mappings/${encodeURIComponent(id)}`, { method: 'DELETE' }); 132} 133 134export async function deleteEventMappingsBatch({ ids = null, provider = null, calendar_id = null } = {}) { 135 return _fetch('/api/calendar/event-mappings', { 136 method: 'DELETE', 137 body: JSON.stringify({ ids, provider, calendar_id }), 138 }); 139} 140 141// --- EVENTS (top-level calendar collection) --- 142export async function listEvents({ from, to, calendar_id, limit } = {}) { 143 const qs = new URLSearchParams(); 144 if (from) qs.append('from', from); 145 if (to) qs.append('to', to); 146 if (calendar_id) qs.append('calendar_id', calendar_id); 147 if (limit) qs.append('limit', limit); 148 const r = await _fetch(`/api/calendar/events${qs.toString() ? '?' + qs.toString() : ''}`); 149 return r.events || r.items || (Array.isArray(r) ? r : []); 150} 151 152export async function listEventsByCalendar({ calendar_id, googleEventId, from, to } = {}) { 153 const qs = new URLSearchParams(); 154 if (calendar_id) qs.append('calendar_id', calendar_id); 155 if (googleEventId) qs.append('googleEventId', googleEventId); 156 if (from) qs.append('from', from); 157 if (to) qs.append('to', to); 158 const r = await _fetch(`/api/calendar/events/by-calendar?${qs.toString()}`); 159 return r.events || []; 160} 161 162export async function getEvent(id) { 163 return _fetch(`/api/calendar/events/${encodeURIComponent(id)}`); 164} 165 166export async function createEvent(data, opts) { 167 const _r = await _fetch('/api/calendar/events', { method: 'POST', body: JSON.stringify(data || {}) }); 168 notifyCalendarChanged({ source: (opts && opts.source) || 'kalender', action: 'create', eventId: (_r && (_r.id || _r.eventId)) || '' }); 169 return _r; 170} 171 172export async function updateEvent(id, data, opts) { 173 const _r = await _fetch(`/api/calendar/events/${encodeURIComponent(id)}`, { 174 method: 'PUT', 175 body: JSON.stringify(data || {}), 176 }); 177 notifyCalendarChanged({ source: (opts && opts.source) || 'kalender', action: 'update', eventId: id }); 178 return _r; 179} 180 181export async function deleteEvent(id, opts) { 182 const _r = await _fetch(`/api/calendar/events/${encodeURIComponent(id)}`, { method: 'DELETE' }); 183 notifyCalendarChanged({ source: (opts && opts.source) || 'kalender', action: 'delete', eventId: id }); 184 return _r; 185} 186 187export async function batchEvents(ops, opts) { 188 const _r = await _fetch('/api/calendar/events/batch', { 189 method: 'POST', 190 body: JSON.stringify({ ops: ops || [] }), 191 }); 192 notifyCalendarChanged({ source: (opts && opts.source) || 'kalender', action: 'batch', eventId: '' }); 193 return _r; 194} 195 196// --- BOOKING SETTINGS --- 197export async function getBookingSettings() { 198 const r = await _fetch('/api/calendar/booking-settings'); 199 return r.bookingSettings || {}; 200} 201 202export async function patchBookingSettings(bookingSettings) { 203 return _fetch('/api/calendar/booking-settings', { 204 method: 'PATCH', 205 body: JSON.stringify({ bookingSettings: bookingSettings || {} }), 206 }); 207} 208 209export async function getSmsUsage() { 210 return _fetch('/api/calendar/sms-usage'); 211} 212 213// --- OUTLOOK SHADOW EVENTS --- 214export async function getOutlookEvent(id) { 215 const r = await _fetch(`/api/calendar/events-outlook/${encodeURIComponent(id)}`); 216 return { exists: !!r.exists, data: r.event || null }; 217} 218 219export async function setOutlookEvent(id, data) { 220 return _fetch(`/api/calendar/events-outlook/${encodeURIComponent(id)}`, { 221 method: 'POST', 222 body: JSON.stringify(data || {}), 223 }); 224} 225 226export async function deleteOutlookEvent(id) { 227 return _fetch(`/api/calendar/events-outlook/${encodeURIComponent(id)}`, { method: 'DELETE' }); 228} 229 230// --- PROVIDER MAPPINGS (per-user subcollections) --- 231export async function listProviderMappings(provider) { 232 const r = await _fetch(`/api/calendar/${encodeURIComponent(prov
232ider)}-mappings`); 233 return r.mappings || []; 234} 235 236export async function createProviderMappings(provider, mappings) { 237 return _fetch(`/api/calendar/${encodeURIComponent(provider)}-mappings`, { 238 method: 'POST', 239 body: JSON.stringify({ mappings: Array.isArray(mappings) ? mappings : [mappings] }), 240 }); 241} 242 243export async function deleteProviderMappingsAll(provider, ids = null) { 244 return _fetch(`/api/calendar/${encodeURIComponent(provider)}-mappings`, { 245 method: 'DELETE', 246 body: JSON.stringify(ids ? { ids } : {}), 247 }); 248} 249 250const calendarApi = { 251 listConnections, getConnection, setConnection, patchConnection, deleteConnection, 252 getIntegration, setIntegration, deleteIntegration, 253 listCalendarTypes, getCalendarType, createCalendarType, updateCalendarType, deleteCalendarType, 254 listEventMappings, createEventMapping, createEventMappingsBatch, deleteEventMapping, deleteEventMappingsBatch, 255 listEvents, listEventsByCalendar, getEvent, createEvent, updateEvent, deleteEvent, batchEvents, 256 getBookingSettings, patchBookingSettings, getSmsUsage, getOutlookEvent, setOutlookEvent, deleteOutlookEvent, listProviderMappings, createProviderMappings, deleteProviderMappingsAll, 257 invalidateCache() { cache.connections = { data: null, ts: 0 }; }, 258}; 259 260export { calendarApi }; 261export default calendarApi;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.