diff --git a/.env.example b/.env.example index cf43fca..8dda2c7 100644 --- a/.env.example +++ b/.env.example @@ -59,6 +59,21 @@ WEBAUTHN_RP_NAME=Planer # Muss exakt der Herkunft im Browser entsprechen, inklusive Port. WEBAUTHN_ORIGIN=http://localhost:3000 +# --- Kalenderanbieter ------------------------------------------------------ +# Graph gibt es in beiden Instanzen, Google nur daheim. Ein Anbieter erscheint +# erst, wenn Kennung UND Geheimnis gesetzt sind. +# Einrichtung siehe README, Abschnitt "Kalenderanbieter einrichten". +# Rueckleit-URIs: +# /api/kalender/rueckleitung/graph +# /api/kalender/rueckleitung/google +GRAPH_CLIENT_ID= +GRAPH_CLIENT_SECRET= +# Tenant-ID, oder "organizations" fuer jeden Arbeitsplatz-Tenant. +GRAPH_TENANT_ID=organizations + +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + # --- Sonstiges ------------------------------------------------------------- # Zeitzone fuer Anzeigen und Tagesgrenzen. TZ=Europe/Berlin diff --git a/CHANGELOG.md b/CHANGELOG.md index 34d2f87..1c7ed44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,34 @@ die Versionierung [Semantic Versioning](https://semver.org/lang/de/). ## [Unveröffentlicht] +### Hinzugefügt + +- Kalenderkonten für Microsoft 365 (Graph) und Google verbinden, umbenennen, + von Hand abgleichen und trennen. Google gibt es nur in der Instanz + `daheim`; in `firma` liefert die Route 404. +- Termine werden örtlich gespiegelt: 7 Tage zurück bis 90 Tage voraus. + Gelesen werden nur Beginn, Ende, Betreff, ganztägig und "zeigt an als" — + keine Teilnehmer, Beschreibungen, Anhänge oder Orte. +- Die Startseite zeigt je Bereich, wie voll der heutige Tag ist: Termine aus + allen verbundenen Kalendern gegen die Tageskapazität. +- Hintergrundjobs: alle 15 Minuten abgleichen, nachts alte Termine und + abgelaufene WebAuthn-Herausforderungen entfernen. +- Handbuchseite zu den Kalenderkonten, Einrichtungsanleitung für Entra ID + und die Google Cloud Console in der README. + +### Sicherheit + +- Zugangs- und Aktualisierungstoken liegen verschlüsselt in der Datenbank + (AES-256-GCM, Schlüssel aus AUTH_SECRET abgeleitet). Wer eine Sicherung in + die Hände bekommt, kann damit ohne AUTH_SECRET nichts anfangen. +- Der Zustandswert im OAuth-Ablauf ist signiert und trägt die + Benutzerkennung; ohne das ließe sich ein fremdes Kalenderkonto an ein + beliebiges Konto hängen. Er verfällt nach 30 Minuten. +- Fehlermeldungen nennen nie den Antwortkörper des Token-Endpunkts — der + enthält das Client-Secret oft im Echo. +- Protokolliert werden beim Abgleich nur Zahlen, nie Kontonamen oder + Termininhalte. + ## [0.2.0] – 2026-09-10 ### Hinzugefügt diff --git a/README.md b/README.md index d4345ea..a77dcea 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ lesen und Aufgaben per Ziehen und Ablegen als Zeitblöcke direkt in diese Kalender schreiben — inklusive Rückweg, wenn ein Block im Kalender verschoben wird. -> **Stand:** Meilenstein 2 von 8. Erfassen und Wiederfinden steht: Aufgaben, -> Inbox, Ideen und Projekte, dazu Anmeldung, Rollen, Protokoll und der Betrieb. -> Das Lesen der Kalender kommt mit M3, die Wochenplanung per Ziehen und Ablegen -> mit M4. Der [Fahrplan](ROADMAP.md) sagt, was wann folgt. +> **Stand:** Meilenstein 3 von 8. Aufgaben, Projekte und das Lesen der +> Kalender stehen: der Planer weiß jetzt, wann überhaupt Platz ist. Das +> Schreiben der Zeitblöcke per Ziehen und Ablegen kommt mit M4. Der +> [Fahrplan](ROADMAP.md) sagt, was wann folgt. - **[Schnellstart](docs/SCHNELLSTART.md)** – in zehn Minuten von `git clone` zur ersten Anmeldung @@ -28,6 +28,8 @@ wird. nach Dringlichkeit und filterbar über die Adresszeile - **Ideen** getrennt von Aufgaben — ohne Termin, per Klick übernehmbar - **Projekte** mit Arbeitspaketen, Meilensteinen und Fortschritt +- **Kalender lesen** aus Microsoft 365 und Google; die Startseite zeigt je + Bereich, wie voll der heutige Tag schon ist - Anmeldung über einen OIDC-Anbieter, über Passkeys oder mit E-Mail und Passwort — je Instanz einzeln zuschaltbar - Rollen `Benutzer` und `Verwaltung`; jede schreibende Aktion wird @@ -38,23 +40,24 @@ wird. ### Geplant -Kalender lesen und schreiben, Wochenplanung per Ziehen und Ablegen, +Zeitblöcke in den Kalender schreiben, Wochenplanung per Ziehen und Ablegen, Benachrichtigungen, Integrationen, Schnellerfassung vom Handy und ein MCP-Server. Siehe [ROADMAP.md](ROADMAP.md). ## Bildergalerie -| | Hell | Dunkel | -| ------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| Anmelden | ![Anmeldeseite, hell](docs/bilder/anmelden-hell-schreibtisch.png) | ![Anmeldeseite, dunkel](docs/bilder/anmelden-dunkel-schreibtisch.png) | -| Start | ![Startseite, hell](docs/bilder/start-hell-schreibtisch.png) | ![Startseite, dunkel](docs/bilder/start-dunkel-schreibtisch.png) | -| Inbox | ![Inbox, hell](docs/bilder/inbox-hell-schreibtisch.png) | ![Inbox, dunkel](docs/bilder/inbox-dunkel-schreibtisch.png) | -| Aufgaben | ![Aufgaben, hell](docs/bilder/aufgaben-hell-schreibtisch.png) | ![Aufgaben, dunkel](docs/bilder/aufgaben-dunkel-schreibtisch.png) | -| Projekte | ![Projekte, hell](docs/bilder/projekte-hell-schreibtisch.png) | ![Projekte, dunkel](docs/bilder/projekte-dunkel-schreibtisch.png) | -| Ideen | ![Ideen, hell](docs/bilder/ideen-hell-schreibtisch.png) | ![Ideen, dunkel](docs/bilder/ideen-dunkel-schreibtisch.png) | -| Bereiche | ![Bereiche, hell](docs/bilder/bereiche-hell-schreibtisch.png) | ![Bereiche, dunkel](docs/bilder/bereiche-dunkel-schreibtisch.png) | -| Einstellungen | ![Einstellungen, hell](docs/bilder/einstellungen-hell-schreibtisch.png) | ![Einstellungen, dunkel](docs/bilder/einstellungen-dunkel-schreibtisch.png) | -| Protokoll | ![Protokoll, hell](docs/bilder/protokoll-hell-schreibtisch.png) | ![Protokoll, dunkel](docs/bilder/protokoll-dunkel-schreibtisch.png) | +| | Hell | Dunkel | +| ------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| Anmelden | ![Anmeldeseite, hell](docs/bilder/anmelden-hell-schreibtisch.png) | ![Anmeldeseite, dunkel](docs/bilder/anmelden-dunkel-schreibtisch.png) | +| Start | ![Startseite, hell](docs/bilder/start-hell-schreibtisch.png) | ![Startseite, dunkel](docs/bilder/start-dunkel-schreibtisch.png) | +| Inbox | ![Inbox, hell](docs/bilder/inbox-hell-schreibtisch.png) | ![Inbox, dunkel](docs/bilder/inbox-dunkel-schreibtisch.png) | +| Aufgaben | ![Aufgaben, hell](docs/bilder/aufgaben-hell-schreibtisch.png) | ![Aufgaben, dunkel](docs/bilder/aufgaben-dunkel-schreibtisch.png) | +| Projekte | ![Projekte, hell](docs/bilder/projekte-hell-schreibtisch.png) | ![Projekte, dunkel](docs/bilder/projekte-dunkel-schreibtisch.png) | +| Ideen | ![Ideen, hell](docs/bilder/ideen-hell-schreibtisch.png) | ![Ideen, dunkel](docs/bilder/ideen-dunkel-schreibtisch.png) | +| Kalender | ![Kalenderkonten, hell](docs/bilder/kalenderkonten-hell-schreibtisch.png) | ![Kalenderkonten, dunkel](docs/bilder/kalenderkonten-dunkel-schreibtisch.png) | +| Bereiche | ![Bereiche, hell](docs/bilder/bereiche-hell-schreibtisch.png) | ![Bereiche, dunkel](docs/bilder/bereiche-dunkel-schreibtisch.png) | +| Einstellungen | ![Einstellungen, hell](docs/bilder/einstellungen-hell-schreibtisch.png) | ![Einstellungen, dunkel](docs/bilder/einstellungen-dunkel-schreibtisch.png) | +| Protokoll | ![Protokoll, hell](docs/bilder/protokoll-hell-schreibtisch.png) | ![Protokoll, dunkel](docs/bilder/protokoll-dunkel-schreibtisch.png) | Alle Bilder entstehen mit `npm run screenshots` in einer Wegwerf-Instanz mit ausschließlich erfundenen Daten — niemals aus einer Instanz mit echten Daten. @@ -117,6 +120,19 @@ Alle Werte stehen in `.env`. Vorlage: [`.env.example`](.env.example). OIDC ist aktiv, sobald Issuer, Client-ID und Secret gesetzt sind. Der Rückleit-URI beim Anbieter lautet `/api/auth/callback/oidc`. +### Kalenderanbieter + +| Variable | Standard | Bedeutung | +| ---------------------- | --------------- | ------------------------------------------------------------- | +| `GRAPH_CLIENT_ID` | _(leer)_ | Anwendungs-ID der App-Registrierung in Entra ID | +| `GRAPH_CLIENT_SECRET` | _(leer)_ | Geheimnis dieser Registrierung | +| `GRAPH_TENANT_ID` | `organizations` | Tenant-ID, oder `organizations` für jeden Arbeitsplatz-Tenant | +| `GOOGLE_CLIENT_ID` | _(leer)_ | OAuth-Client-ID aus der Google Cloud Console | +| `GOOGLE_CLIENT_SECRET` | _(leer)_ | Geheimnis dieses Clients | + +Ein Anbieter erscheint erst, wenn Kennung **und** Geheimnis gesetzt sind — +und nur, wenn die Instanz ihn erlaubt (Google nur `daheim`). + ### Einrichtung und Sonstiges | Variable | Standard | Bedeutung | @@ -126,6 +142,59 @@ Rückleit-URI beim Anbieter lautet `/api/auth/callback/oidc`. | `TZ` | `Europe/Berlin` | Zeitzone für Anzeigen und Tagesgrenzen | | `DEMO_MODUS` | `false` | Nur für die Screenshot-Instanz. Im Betrieb niemals `true` | +## Kalenderanbieter einrichten + +Beides ist einmalige Handarbeit beim Anbieter. Der Planer fragt Lese- **und** +Schreibrechte an: ab M4 schreibt er Zeitblöcke zurück, und eine zweite +Zustimmungsrunde für jedes Konto wäre lästiger als eine etwas breitere +Berechtigung von Anfang an. + +### Microsoft 365 (Entra ID) + +1. → **Identität → Anwendungen → + App-Registrierungen → Neue Registrierung**. +2. Name frei wählen, etwa „Planer". +3. Unterstützte Kontotypen: **Nur Konten in diesem Organisationsverzeichnis** + (dann `GRAPH_TENANT_ID` auf die Tenant-ID setzen) oder + **Konten in einem beliebigen Organisationsverzeichnis** (dann + `organizations` belassen). +4. Umleitungs-URI, Plattform **Web**: + `/api/kalender/rueckleitung/graph` +5. Nach dem Anlegen: **Anwendungs-ID (Client)** nach `GRAPH_CLIENT_ID`, + **Verzeichnis-ID (Mandant)** nach `GRAPH_TENANT_ID`. +6. **Zertifikate & Geheimnisse → Neuer geheimer Clientschlüssel**. Den Wert + sofort nach `GRAPH_CLIENT_SECRET` kopieren — er wird nur einmal angezeigt. +7. **API-Berechtigungen → Berechtigung hinzufügen → Microsoft Graph → + Delegierte Berechtigungen**: `Calendars.ReadWrite`, `offline_access`, + `openid`, `email`. + +> Ist im Tenant die Zustimmung durch Endbenutzer abgeschaltet, muss ein +> Administrator einmalig **Administratorzustimmung erteilen** klicken. + +### Google Kalender + +1. → Projekt anlegen oder wählen. +2. **APIs & Dienste → Bibliothek** → _Google Calendar API_ aktivieren. +3. **OAuth-Zustimmungsbildschirm** einrichten. Für den Eigengebrauch genügt + _Extern_ mit Veröffentlichungsstatus _Test_ und der eigenen Adresse als + Testbenutzer. +4. Bereiche (Scopes) hinzufügen: + `.../auth/calendar.events` und `.../auth/calendar.readonly`. +5. **Anmeldedaten → Anmeldedaten erstellen → OAuth-Client-ID**, + Anwendungstyp **Webanwendung**. +6. Autorisierter Weiterleitungs-URI: + `/api/kalender/rueckleitung/google` +7. Client-ID und Clientschlüssel nach `GOOGLE_CLIENT_ID` und + `GOOGLE_CLIENT_SECRET`. + +> Bleibt das Projekt im Status _Test_, laufen die Aktualisierungstoken nach +> sieben Tagen ab und die Konten müssen neu verbunden werden. Für den +> Dauerbetrieb den Zustimmungsbildschirm **veröffentlichen**. + +Nach dem Eintragen: `docker compose up -d` und im Planer unter **Kalender** +verbinden. Die Anleitung für den Alltag steht im +[Handbuch](docs/handbuch/kalenderkonten.md). + ## Betrieb ### Starten und stoppen diff --git a/ROADMAP.md b/ROADMAP.md index b434203..61427dc 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7,7 +7,7 @@ grünen Prüfungen, aktueller Dokumentation und einem Eintrag im DEVLOG. | --------------------------------------- | ---------------------------- | -------- | | [M1](#m1--fundament) | Fundament | erledigt | | [M2](#m2--aufgaben-und-projekte) | Aufgaben und Projekte | erledigt | -| [M3](#m3--kalender-lesen) | Kalender lesen | geplant | +| [M3](#m3--kalender-lesen) | Kalender lesen | erledigt | | [M4](#m4--wochenplanung-und-zeitblöcke) | Wochenplanung und Zeitblöcke | geplant | | [M5](#m5--benachrichtigungen) | Benachrichtigungen | geplant | | [M6](#m6--integrationen) | Integrationen | geplant | @@ -63,14 +63,16 @@ später" und wartet darauf, ob es im Alltag wirklich fehlt. ## M3 – Kalender lesen +**Erledigt am 10.09.2026 mit Version 0.3.0.** + **Ziel:** Termine aus Microsoft 365 und Google sehen. -- [ ] `Kalenderkonto` mit Anbieter, Tokens und Delta-Verweis -- [ ] OAuth-Verbindung je Instanz: Graph immer, Google nur daheim -- [ ] Graph-Delta-Sync und Google-Sync über `fetch`, Antworten mit zod geprüft -- [ ] Ganztägige Termine zählen nicht zur Belegung -- [ ] Anonymisierte Beispielantworten in `test/fixtures/` -- [ ] Hintergrundjob mit node-cron +- [x] `Kalenderkonto` mit Anbieter, Tokens und Delta-Verweis +- [x] OAuth-Verbindung je Instanz: Graph immer, Google nur daheim +- [x] Graph-Delta-Sync und Google-Sync über `fetch`, Antworten mit zod geprüft +- [x] Ganztägige Termine zählen nicht zur Belegung +- [x] Anonymisierte Beispielantworten in `test/fixtures/` +- [x] Hintergrundjob mit node-cron ## M4 – Wochenplanung und Zeitblöcke diff --git a/docs/handbuch/README.md b/docs/handbuch/README.md index fdbb49e..09cc555 100644 --- a/docs/handbuch/README.md +++ b/docs/handbuch/README.md @@ -16,10 +16,12 @@ Anleitung für den Alltag mit dem Planer. Eine Seite je Thema. 4. **[Ideen](ideen.md)** – festhalten, was noch keine Aufgabe ist 5. **[Projekte](projekte.md)** – mehrere Aufgaben unter einem Ziel, mit Fortschritt +6. **[Kalenderkonten](kalenderkonten.md)** – Microsoft 365 und Google + verbinden, Belegung verstehen ## Verwaltung -6. **[Protokoll](protokoll.md)** – nachsehen, wer wann was geändert hat +7. **[Protokoll](protokoll.md)** – nachsehen, wer wann was geändert hat ## Was noch kommt @@ -27,7 +29,6 @@ Diese Seiten entstehen mit den nächsten Meilensteinen: | Thema | Meilenstein | | ------------------------------- | ----------- | -| Kalenderkonten verbinden | M3 | | Wochenplanung und Zeitblöcke | M4 | | Benachrichtigungen | M5 | | Integrationen | M6 | diff --git a/docs/handbuch/kalenderkonten.md b/docs/handbuch/kalenderkonten.md new file mode 100644 index 0000000..3902d56 --- /dev/null +++ b/docs/handbuch/kalenderkonten.md @@ -0,0 +1,116 @@ +# Kalenderkonten verbinden + +Der Planer liest deine Termine, um zu wissen, **wann überhaupt Platz ist**. +Ab M4 schreibt er Zeitblöcke in denselben Kalender zurück. + +![Die Seite Kalenderkonten](../bilder/kalenderkonten-hell-schreibtisch.png) + +## Was der Planer liest — und was nicht + +| Liest er | Nutzt er wofür | +| --------------------------------- | ------------------------------------------------ | +| Beginn und Ende | Belegung gegen die Tageskapazität | +| Betreff | Anzeige in der Wochenansicht | +| Ganztägig ja/nein | Ganztägige Termine zählen **nicht** zur Belegung | +| „Zeigt an als" (frei, gebucht, …) | Als frei markierte Termine zählen nicht | + +**Nicht gelesen** werden Teilnehmer, Beschreibungstexte, Anhänge und Orte. +Betreffs stehen in der Datenbank, damit du sie in der Ansicht siehst — sie +landen aber **niemals in Protokollen**. + +Gelesen wird ein Fenster von **7 Tagen zurück bis 90 Tage voraus**. Was aus +diesem Fenster herauswandert, wird nachts wieder entfernt. + +## Einen Kalender verbinden + +1. Im Menü auf **Kalender**. +2. Auf **Microsoft 365 verbinden** beziehungsweise **Google verbinden**. +3. Beim Anbieter anmelden und der Berechtigung zustimmen. +4. Du landest wieder hier, und der erste Abgleich läuft sofort. + +> **Warum fragt der Planer auch nach Schreibrechten?** Weil er ab M4 +> Zeitblöcke in den Kalender schreibt. Würde er jetzt nur Leserechte +> anfordern, müsstest du später jedem Konto erneut zustimmen — und genau +> daran scheitert Einrichtung erfahrungsgemäß. + +### Welche Anbieter es gibt + +Das hängt an der Instanz und ist nicht verhandelbar: + +| | Firma | Daheim | +| ------------- | -------- | ------ | +| Microsoft 365 | ja | ja | +| Google | **nein** | ja | + +In der Firmeninstanz gibt es die Google-Seite nicht — der Aufruf endet dort +mit „Diese Seite gibt es nicht". Das ist Absicht, kein Fehler. + +## Der Abgleich + +Alle 15 Minuten läuft der Abgleich von selbst. Mit **Jetzt abgleichen** +stößt du ihn von Hand an. + +Beide Anbieter liefern nur die **Änderungen** seit dem letzten Mal, nicht +jedes Mal alle Termine. Läuft dieser Merker ab — das passiert nach längerer +Pause —, liest der Planer den ganzen Zeitraum neu ein. Davon merkst du außer +einem etwas längeren Abgleich nichts. + +## Die Belegung + +Auf der Startseite zeigt jeder Bereich, wie voll der heutige Tag schon ist: +Termine aus **allen** verbundenen Kalendern gegen die Tageskapazität dieses +Bereichs. + +> **Warum zählen alle Kalender gegen jeden Bereich?** Weil ein Termin die +> Zeit ohnehin blockiert, ganz gleich, zu welchem Lebensbereich er gehört. +> Wer um 14 Uhr beim Zahnarzt sitzt, kann zur selben Zeit nichts für die +> Firma tun. + +Gezählt wird nur, was **ins Arbeitsfenster** des Bereichs fällt. Ein Termin +um 22 Uhr verkleinert die Kapazität eines Bereichs nicht, dessen Fenster um +17 Uhr endet. + +Überlappende Termine werden zusammengelegt — eine Doppelbuchung zählt nur +einmal. + +## Ein Konto umbenennen + +Die Bezeichnung ist frei wählbar. „Firma Outlook" oder „Gewerbe" liest sich +besser als die Mailadresse, gerade wenn du mehrere Konten desselben +Anbieters hast. + +## Ein Konto trennen + +Der rote Knopf, mit Rückfrage. Die gespiegelten Termine verschwinden dabei — +**im Kalender selbst bleibt alles unangetastet**. Der Planer hat bis M4 +ohnehin nur gelesen. + +Zusätzlich solltest du die Berechtigung beim Anbieter selbst zurückziehen: + +- Microsoft: → _Zugriff verwalten_ +- Google: + +## Wenn es klemmt + +| Was du siehst | Was dahintersteckt | +| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| „Der Anbieter hat kein Aktualisierungstoken herausgegeben." | Meist bei Google, wenn du schon einmal zugestimmt hattest. Berechtigung im Google-Konto entziehen und neu verbinden. | +| „Die Anfrage war ungültig oder zu alt." | Zwischen Klick und Zustimmung lagen mehr als 30 Minuten. Einfach neu beginnen. | +| „Kein Aktualisierungstoken hinterlegt." | Das Konto muss neu verbunden werden. Das passiert auch, wenn `AUTH_SECRET` gewechselt wurde. | +| „Der Anbieter antwortete mit 401." | Die Berechtigung wurde beim Anbieter entzogen. Neu verbinden. | +| „unerwartete Zeitzone …, erwartet wurde UTC" | Ein Konfigurationsfehler auf Serverseite. Bitte melden — der Planer bricht hier bewusst ab, statt Termine um Stunden zu verschieben. | +| Die Belegung bleibt leer | Kein Kalender verbunden, oder der erste Abgleich lief noch nicht. Die Karte zeigt die Belegung erst, wenn wirklich ein Kalender dranhängt. | + +## Sicherheit + +Die Zugangstoken liegen **verschlüsselt** in der Datenbank (AES-256-GCM). +Wer eine Sicherung in die Hände bekommt, kann damit nichts anfangen, solange +er nicht auch `AUTH_SECRET` hat. + +Wird `AUTH_SECRET` gewechselt, müssen alle Kalenderkonten neu verbunden +werden. Das ist gewollt: mit dem alten Geheimnis sind auch alle Sitzungen +ungültig. + +Für die Einrichtung auf Serverseite — was in Entra ID und in der Google +Cloud Console anzulegen ist — siehe +[README, Abschnitt Kalenderanbieter](../../README.md#kalenderanbieter-einrichten). diff --git a/prisma/demo-seed.ts b/prisma/demo-seed.ts index 068b213..8b960f7 100644 --- a/prisma/demo-seed.ts +++ b/prisma/demo-seed.ts @@ -327,6 +327,52 @@ async function main(): Promise { }) } + // --- Kalenderkonto und erfundene Termine --------------------------------- + // Die Tokens sind absichtlich Unsinn: die Screenshot-Instanz spricht mit + // keinem Anbieter, sie soll die Seite nur nicht leer zeigen. + await db.kalenderkonto.deleteMany({}) + const kalenderkonto = await db.kalenderkonto.create({ + data: { + benutzerId: benutzer.id, + anbieter: 'GRAPH', + anzeigename: 'Firma Outlook', + extKontoId: 'martina.beispiel@firma.invalid', + letzterAbgleich: new Date(DEMO_JETZT.getTime() - 6 * 60_000), + }, + }) + + const TERMINE = [ + { betreff: 'Wochenbesprechung', vonStunde: 8, bisStunde: 9 }, + { betreff: 'Kundengespräch Sonnenberg', vonStunde: 10, bisStunde: 11 }, + { betreff: 'Mittagspause', vonStunde: 11, bisStunde: 12, frei: true }, + { betreff: 'Abnahme Baustelle', vonStunde: 13, bisStunde: 15 }, + ] + + for (const eintrag of TERMINE) { + await db.termin.create({ + data: { + kalenderkontoId: kalenderkonto.id, + extId: `demo-${eintrag.betreff.toLowerCase().replace(/[^a-z]+/g, '-')}`, + betreff: eintrag.betreff, + beginn: new Date(Date.UTC(2026, 2, 17, eintrag.vonStunde - 1, 0)), + ende: new Date(Date.UTC(2026, 2, 17, eintrag.bisStunde - 1, 0)), + zeigtAls: eintrag.frei ? 'FREI' : 'GEBUCHT', + }, + }) + } + + await db.termin.create({ + data: { + kalenderkontoId: kalenderkonto.id, + extId: 'demo-brueckentag', + betreff: 'Brückentag', + beginn: new Date(Date.UTC(2026, 2, 20, 0, 0)), + ende: new Date(Date.UTC(2026, 2, 21, 0, 0)), + ganztaegig: true, + zeigtAls: 'ABWESEND', + }, + }) + // Eine Aufgabe, die von außen kam – zeigt die Herkunft in der Liste. await db.aufgabe.create({ data: { diff --git a/screenshots/bilder.spec.ts b/screenshots/bilder.spec.ts index a55d80b..9f70913 100644 --- a/screenshots/bilder.spec.ts +++ b/screenshots/bilder.spec.ts @@ -34,6 +34,7 @@ const AUFNAHMEN: Aufnahme[] = [ { name: 'projekte', pfad: '/projekte' }, { name: 'ideen', pfad: '/ideen' }, { name: 'bereiche', pfad: '/bereiche' }, + { name: 'kalenderkonten', pfad: '/kalenderkonten' }, { name: 'einstellungen', pfad: '/einstellungen' }, { name: 'protokoll', pfad: '/verwaltung/protokoll' }, ]