import { z } from 'zod' import type { ZeigtAls } from '@/generated/prisma/enums' import { AntwortUnbrauchbar, type AbgleichSeite, type RohTermin, } from '@/lib/kalender/typen' /** * Lesen der Antworten der Google-Kalender-API (`events.list`). * * Reine Logik: nimmt geparsten JSON entgegen, prüft ihn mit zod und gibt die * gemeinsame Form zurück. */ /** * Google liefert entweder `date` (ganztägig, reines Datum) oder `dateTime` * (mit Versatz). Genau eines von beidem. */ const googleZeit = z.looseObject({ date: z.string().optional(), dateTime: z.string().optional(), timeZone: z.string().optional(), }) const googleTermin = z.looseObject({ id: z.string().min(1), etag: z.string().optional(), status: z.string().optional(), summary: z.string().nullable().optional(), start: googleZeit.optional(), end: googleZeit.optional(), transparency: z.string().optional(), }) const googleAntwort = z.looseObject({ items: z.array(googleTermin).default([]), nextPageToken: z.string().optional(), nextSyncToken: z.string().optional(), }) /** * Google kennt nur zwei Zustände: `transparency: "transparent"` heißt frei, * alles andere (auch das Fehlen des Feldes) heißt gebucht. */ export function zeigtAlsAusGoogle(transparency: string | undefined): ZeigtAls { return transparency === 'transparent' ? 'FREI' : 'GEBUCHT' } export type GoogleZeit = { date?: string | undefined; dateTime?: string | undefined } /** * Wandelt Googles Zeitangabe in einen Zeitpunkt und sagt, ob sie ganztägig * war. Ein reines Datum wird als Mitternacht UTC gelesen — ganztägige Termine * zählen ohnehin nicht zur Belegung, es geht nur um die Einordnung im Tag. */ export function googleZeitpunkt(zeit: GoogleZeit): { zeitpunkt: Date ganztaegig: boolean } { if (zeit.date) { const zeitpunkt = new Date(`${zeit.date}T00:00:00Z`) if (Number.isNaN(zeitpunkt.getTime())) { throw new AntwortUnbrauchbar('Google', 'Datumsangabe nicht lesbar') } return { zeitpunkt, ganztaegig: true } } if (zeit.dateTime) { const zeitpunkt = new Date(zeit.dateTime) if (Number.isNaN(zeitpunkt.getTime())) { throw new AntwortUnbrauchbar('Google', 'Zeitangabe nicht lesbar') } return { zeitpunkt, ganztaegig: false } } throw new AntwortUnbrauchbar('Google', 'Zeitangabe fehlt') } /** Liest eine Antwortseite von `events.list`. */ export function googleSeiteLesen(inhalt: unknown): AbgleichSeite { const gelesen = googleAntwort.safeParse(inhalt) if (!gelesen.success) { throw new AntwortUnbrauchbar('Google', 'unerwarteter Aufbau') } const geaendert: RohTermin[] = [] const entfernt: string[] = [] for (const eintrag of gelesen.data.items) { if (eintrag.status === 'cancelled') { entfernt.push(eintrag.id) continue } if (!eintrag.start || !eintrag.end) continue const beginn = googleZeitpunkt(eintrag.start) const ende = googleZeitpunkt(eintrag.end) geaendert.push({ extId: eintrag.id, etag: eintrag.etag ?? null, betreff: eintrag.summary ?? null, beginn: beginn.zeitpunkt, ende: ende.zeitpunkt, ganztaegig: beginn.ganztaegig, zeigtAls: zeigtAlsAusGoogle(eintrag.transparency), }) } return { geaendert, entfernt, weiter: gelesen.data.nextPageToken ?? null, deltaVerweis: gelesen.data.nextSyncToken ?? null, } }