feat(calendar): Phase 3 – Weboberfläche mit Monats-, Wochen- und Listenansicht
Neue Seite CalendarPage (Menüpunkt 'Calendar' in der Seitenleiste,
zwischen Home und Papierkorb) mit Kalenderliste zum Anlegen/Umbenennen/
Löschen/Ein- und Ausblenden, Export/Import als .ics über die
Phase-2-API, und Termine anlegen/ändern/löschen über einen neuen
JSON-Endpunkt.
Backend: neuer events_api.go-Handler unter /api/v1/calendars/{id}/events
(Session-Auth) neben dem bestehenden CalDAV-Protokoll – ein Termin, den
die Web-Oberfläche anlegt, ist ein ganz normales CalDAV-Objekt und
synchronisiert genauso zu DAVx5 wie umgekehrt. Ein per CalDAV mit RRULE
angelegter, wiederkehrender Termin zeigt sich hier nur (recurring:true),
lässt sich aber bewusst nicht über dieses vereinfachte Formular
bearbeiten (409) – das würde die Wiederholungsregel eher beschädigen
als korrekt anpassen.
Gegen den laufenden Container getestet: Kalender/Termin anlegen, per
Web-API ändern und löschen, ein per CalDAV mit RRULE angelegter
Wiederkehrer taucht als recurring:true auf und blockt PATCH mit 409.
TypeScript-Build (tsc + vite build) sauber.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4d635fff20
commit
27a026421f
10 files changed
+1163
-6
No files matched your search
@@ -884,4 +884,78 @@ export const adminApi = {
|
||||
request<AppSettings>('PATCH', '/admin/settings', data),
|
||||
}
|
||||
|
||||
// ── Calendar ─────────────────────────────────────────────────────────────
|
||||
// Termine selbst laufen für CalDAV-Clients (DAVx5 & Co.) über /caldav/ mit
|
||||
// App-Passwörtern – ein eigenes, Basic-Auth-geschütztes Protokoll. Diese
|
||||
// Web-Oberfläche spricht stattdessen /api/v1/calendars mit der normalen
|
||||
// Session, wie jeder andere Bereich.
|
||||
|
||||
export interface CalendarItem {
|
||||
id: string
|
||||
owner_id: string
|
||||
name: string
|
||||
color: string
|
||||
description: string
|
||||
supports_todo: boolean
|
||||
timezone: string
|
||||
ctag: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface CalendarEvent {
|
||||
uid: string
|
||||
summary: string
|
||||
description: string
|
||||
location: string
|
||||
/** ISO 8601, immer UTC ("...Z") – der Server rechnet nicht um, das
|
||||
* übernimmt der Browser beim Anzeigen (new Date(...) kennt die lokale
|
||||
* Zeitzone bereits). */
|
||||
start: string
|
||||
end?: string
|
||||
all_day: boolean
|
||||
/** Über einen echten CalDAV-Client mit Wiederholung angelegt – hier nur
|
||||
* sichtbar, nicht bearbeitbar (siehe events_api.go, UpdateEvent). */
|
||||
recurring: boolean
|
||||
etag: string
|
||||
}
|
||||
|
||||
export interface EventInput {
|
||||
summary: string
|
||||
description?: string
|
||||
location?: string
|
||||
start: string
|
||||
end?: string
|
||||
all_day: boolean
|
||||
}
|
||||
|
||||
export const calendarApi = {
|
||||
list: () => request<CalendarItem[]>('GET', '/calendars'),
|
||||
create: (name: string, color: string, description = '') =>
|
||||
request<CalendarItem>('POST', '/calendars', { name, color, description }),
|
||||
update: (id: string, patch: Partial<Pick<CalendarItem, 'name' | 'color' | 'description' | 'timezone'>>) =>
|
||||
request<CalendarItem>('PATCH', `/calendars/${id}`, patch),
|
||||
remove: (id: string) => request<void>('DELETE', `/calendars/${id}`),
|
||||
|
||||
exportUrl: (id: string) => `/api/v1/calendars/${id}/export`,
|
||||
|
||||
import: async (id: string, file: File): Promise<{ imported: number }> => {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'text/calendar' }
|
||||
if (csrfToken) headers['X-CSRF-Token'] = csrfToken
|
||||
const res = await fetch(`/api/v1/calendars/${id}/import`, {
|
||||
method: 'POST', headers, credentials: 'include', body: file,
|
||||
})
|
||||
const json = await res.json().catch(() => ({ error: res.statusText }))
|
||||
if (!res.ok) throw new APIError(res.status, json.error ?? 'Import failed')
|
||||
return json.data
|
||||
},
|
||||
|
||||
listEvents: (id: string) => request<CalendarEvent[]>('GET', `/calendars/${id}/events`),
|
||||
createEvent: (id: string, input: EventInput) =>
|
||||
request<CalendarEvent>('POST', `/calendars/${id}/events`, input),
|
||||
updateEvent: (id: string, uid: string, input: EventInput) =>
|
||||
request<CalendarEvent>('PATCH', `/calendars/${id}/events/${encodeURIComponent(uid)}`, input),
|
||||
deleteEvent: (id: string, uid: string) =>
|
||||
request<void>('DELETE', `/calendars/${id}/events/${encodeURIComponent(uid)}`),
|
||||
}
|
||||
|
||||
export { APIError }
|
||||
Reference in new issue
Block a user