Files
planer/src/server/kalender/rueckweg.ts
T
Florian WildeandClaude Opus 5 9c1c916b58 feat(zeitbloecke): Schreibweg in den Kalender und Rückweg zurück
Anlegen, verschieben und lösen schreiben in den Kalender des Anbieters.
Jeder Zugriff auf einen bestehenden Termin geht mit If-Match und dem
zuletzt gelesenen Etag heraus; antwortet der Anbieter mit 412, gewinnt der
Kalender: der Planer schreibt nicht, verwirft seinen Fingerabdruck und
überlässt dem nächsten Abgleich, den fremden Stand zu holen.

Der Rückweg läuft nach jedem Abgleich und vergleicht die eigenen
Zeitblöcke mit dem aufgefrischten Spiegel. Wurde im Kalender verschoben,
zieht der Planer nach. Wurde dort gelöscht, verliert der Zeitblock seine
Verankerung und die Aufgabe fällt zurück auf offen, wenn nichts anderes
mehr für sie geplant ist.

Zwei Entscheidungen zum Fehlerfall:
- Scheitert das Schreiben beim Anlegen, bleibt der Zeitblock trotzdem
  bestehen, mit vermerktem Fehler. Ihn zu verwerfen hieße, die Planung
  wegen eines Netzproblems zu vergessen; das Verschieben trägt ihn nach.
- Scheitert das Entfernen, verschwindet der Zeitblock dennoch. Bliebe er
  stehen, weil der Kalender gerade nicht erreichbar ist, wäre die Planung
  blockiert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 20:49:52 +02:00

94 lines
2.9 KiB
TypeScript

import 'server-only'
import { abgleichEntscheiden, zeitraumHash } from '@/lib/zeitblock'
import { db } from '@/server/db'
/**
* Der Rückweg: Änderungen an Zeitblöcken aus dem Kalender übernehmen.
*
* Läuft nach jedem Abgleich eines Kontos. Für jeden Zeitblock wird verglichen,
* was im Kalender steht — der örtliche Spiegel (Tabelle `termin`) ist dafür
* bereits auf dem neuesten Stand.
*
* **Der Kalender gewinnt.** Wer dort etwas verschiebt, meint es so; der Planer
* zieht nach, statt zurückzuschreiben. Sonst zöge die App dem Benutzer den
* Termin unter den Händen weg.
*/
export type RueckwegBericht = {
uebernommen: number
geloest: number
}
export async function rueckwegAnwenden(
kalenderkontoId: string,
): Promise<RueckwegBericht> {
const zeitbloecke = await db.zeitblock.findMany({
where: { kalenderkontoId, extId: { not: null } },
select: {
id: true,
extId: true,
beginn: true,
ende: true,
hash: true,
aufgabeId: true,
},
})
if (zeitbloecke.length === 0) return { uebernommen: 0, geloest: 0 }
const extIds = zeitbloecke.map((z) => z.extId).filter((id): id is string => id !== null)
const termine = await db.termin.findMany({
where: { kalenderkontoId, extId: { in: extIds } },
select: { extId: true, beginn: true, ende: true, etag: true },
})
const nachExtId = new Map(termine.map((t) => [t.extId, t]))
let uebernommen = 0
let geloest = 0
for (const zeitblock of zeitbloecke) {
const imKalender = nachExtId.get(zeitblock.extId as string) ?? null
const entscheidung = abgleichEntscheiden(
{ hash: zeitblock.hash, beginn: zeitblock.beginn, ende: zeitblock.ende },
imKalender ? { beginn: imKalender.beginn, ende: imKalender.ende } : null,
)
if (entscheidung.tun === 'nichts') continue
if (entscheidung.tun === 'loesen') {
// Im Kalender gelöscht: der Zeitblock verliert seine Verankerung. Die
// Aufgabe bleibt bestehen und fällt zurück auf offen, wenn nichts
// anderes mehr für sie geplant ist.
await db.zeitblock.delete({ where: { id: zeitblock.id } })
geloest++
const nochGeplant = await db.zeitblock.count({
where: { aufgabeId: zeitblock.aufgabeId },
})
if (nochGeplant === 0) {
await db.aufgabe.updateMany({
where: { id: zeitblock.aufgabeId, status: 'GEPLANT' },
data: { status: 'OFFEN' },
})
}
continue
}
await db.zeitblock.update({
where: { id: zeitblock.id },
data: {
beginn: entscheidung.beginn,
ende: entscheidung.ende,
// Den Fingerabdruck auf den übernommenen Stand setzen, damit die
// nächste Runde ihn wiedererkennt und nichts mehr tut.
hash: zeitraumHash(entscheidung.beginn, entscheidung.ende),
etag: imKalender?.etag ?? null,
letzterFehler: null,
},
})
uebernommen++
}
return { uebernommen, geloest }
}