diff --git a/CHANGELOG.md b/CHANGELOG.md index b741729..4a5b36d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,34 @@ die Versionierung [Semantic Versioning](https://semver.org/lang/de/). ## [Unveröffentlicht] +### Hinzugefügt + +- **Wochenansicht** mit Ziehen und Ablegen: Aufgaben aus der linken Spalte in + ein Zeitfenster ziehen legt einen Zeitblock an und schreibt ihn in den + Kalender. Die Dauer kommt aus dem geschätzten Aufwand der Aufgabe. +- Zeitblöcke verschieben und ausplanen; der Kalendertermin zieht mit. +- **Rückweg**: Wird ein Zeitblock im Kalender verschoben, übernimmt der Planer + das beim nächsten Abgleich. Wird er dort gelöscht, verschwindet auch der + Zeitblock — die Aufgabe bleibt und fällt zurück auf offen. +- Kapazität und Belegung je Wochentag über jeder Spalte; überbuchte Tage + werden rot, sperren aber nichts. +- Eine eingeplante Aufgabe wechselt selbsttätig von _Offen_ auf _Geplant_ und + wieder zurück, wenn nichts mehr für sie geplant ist. +- Handbuchseite zur Wochenplanung. + +### Sicherheit + +- **Schleifenschutz** über `hash = sha256(startISO|endeISO)`: Der Planer + schreibt nur bei eigener Änderung und übernimmt aus dem Kalender nur, wenn + der Fingerabdruck abweicht. Ohne das würden App und Kalender einander + endlos hinterherschreiben. +- Jeder Schreibzugriff auf einen bestehenden Termin geht mit `If-Match` + heraus. Bei HTTP 412 **gewinnt der Kalender**: der Planer schreibt nicht, + verwirft seinen Fingerabdruck und überlässt dem nächsten Abgleich, den + fremden Stand zu holen. +- Zeitblöcke gehören dem Benutzer über sein Kalenderkonto; die Kennung geht + in jede where-Bedingung, nicht in einen nachträglichen Vergleich. + ## [0.3.0] – 2026-09-10 ### Hinzugefügt diff --git a/README.md b/README.md index a77dcea..564a02f 100644 --- a/README.md +++ b/README.md @@ -7,9 +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 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 +> **Stand:** Meilenstein 4 von 8. Die Kernidee steht: Aufgaben lassen sich +> per Ziehen und Ablegen als Zeitblöcke in den Kalender schreiben, und +> Verschiebungen von dort kommen zurück. Es folgen Benachrichtigungen, +> Integrationen, Schnellerfassung vom Handy und ein MCP-Server. Der > [Fahrplan](ROADMAP.md) sagt, was wann folgt. - **[Schnellstart](docs/SCHNELLSTART.md)** – in zehn Minuten von `git clone` @@ -30,6 +31,8 @@ wird. - **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 +- **Wochenplanung** per Ziehen und Ablegen: Aufgaben werden zu echten + Kalenderterminen, Verschiebungen im Kalender kommen zurück - 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 @@ -40,7 +43,6 @@ wird. ### Geplant -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). @@ -51,6 +53,7 @@ MCP-Server. Siehe [ROADMAP.md](ROADMAP.md). | 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) | +| Woche | ![Woche, hell](docs/bilder/woche-hell-schreibtisch.png) | ![Woche, dunkel](docs/bilder/woche-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) | diff --git a/ROADMAP.md b/ROADMAP.md index 61427dc..3a1e471 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -8,7 +8,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 | erledigt | -| [M4](#m4--wochenplanung-und-zeitblöcke) | Wochenplanung und Zeitblöcke | geplant | +| [M4](#m4--wochenplanung-und-zeitblöcke) | Wochenplanung und Zeitblöcke | erledigt | | [M5](#m5--benachrichtigungen) | Benachrichtigungen | geplant | | [M6](#m6--integrationen) | Integrationen | geplant | | [M7](#m7--schnellerfassung-und-api) | Schnellerfassung und API | geplant | @@ -76,15 +76,21 @@ später" und wartet darauf, ob es im Alltag wirklich fehlt. ## M4 – Wochenplanung und Zeitblöcke +**Erledigt am 10.09.2026 mit Version 0.4.0.** + **Ziel:** Aufgaben per Ziehen und Ablegen in den Kalender schreiben — und Verschiebungen von dort zurückholen. -- [ ] `Zeitblock` an genau einer Aufgabe und einem Kalenderkonto -- [ ] Wochenansicht mit `@dnd-kit/core` -- [ ] Schreiben in den Kalender, Rückweg beim Verschieben -- [ ] Schleifenschutz über `hash = sha256(startISO|endeISO)` -- [ ] Konflikte: der Kalender gewinnt (HTTP 412 auf If-Match) -- [ ] Belegung gegen die Tageskapazität des Bereichs +- [x] `Zeitblock` an genau einer Aufgabe und einem Kalenderkonto +- [x] Wochenansicht mit `@dnd-kit/core` +- [x] Schreiben in den Kalender, Rückweg beim Verschieben +- [x] Schleifenschutz über `hash = sha256(startISO|endeISO)` +- [x] Konflikte: der Kalender gewinnt (HTTP 412 auf If-Match) +- [x] Belegung gegen die Tageskapazität des Bereichs + +**Bewusst offen gelassen:** Die Dauer lässt sich nicht am Balken ziehen, und +ein Zeitblock landet immer im ersten verbundenen Kalender. Beides steht unter +"Ideen für später" — erst zeigt der Alltag, ob es wirklich fehlt. ## M5 – Benachrichtigungen diff --git a/docs/handbuch/README.md b/docs/handbuch/README.md index 09cc555..38933a0 100644 --- a/docs/handbuch/README.md +++ b/docs/handbuch/README.md @@ -18,10 +18,12 @@ Anleitung für den Alltag mit dem Planer. Eine Seite je Thema. Fortschritt 6. **[Kalenderkonten](kalenderkonten.md)** – Microsoft 365 und Google verbinden, Belegung verstehen +7. **[Wochenplanung](woche.md)** – Aufgaben per Ziehen und Ablegen in den + Kalender legen ## Verwaltung -7. **[Protokoll](protokoll.md)** – nachsehen, wer wann was geändert hat +8. **[Protokoll](protokoll.md)** – nachsehen, wer wann was geändert hat ## Was noch kommt @@ -29,7 +31,6 @@ Diese Seiten entstehen mit den nächsten Meilensteinen: | Thema | Meilenstein | | ------------------------------- | ----------- | -| Wochenplanung und Zeitblöcke | M4 | | Benachrichtigungen | M5 | | Integrationen | M6 | | Schnellerfassung vom Handy, API | M7 | diff --git a/docs/handbuch/woche.md b/docs/handbuch/woche.md new file mode 100644 index 0000000..beb6814 --- /dev/null +++ b/docs/handbuch/woche.md @@ -0,0 +1,106 @@ +# Wochenplanung und Zeitblöcke + +Hier wird aus einer Aufgabe ein Termin. Du ziehst sie in ein Zeitfenster, und +der Planer legt sie als **Zeitblock** in deinen echten Kalender. + +![Die Wochenansicht](../bilder/woche-hell-schreibtisch.png) + +## Der Aufbau + +| Wo | Was | +| --------------------- | ------------------------------------------------------------------------------------------------------------- | +| **Links** | Alles, was noch eingeplant werden kann — offen, geplant oder wartend. Ideen bleiben draußen | +| **Rechts** | Sieben Tage im Viertelstundenraster | +| **Über jeder Spalte** | Belegte Zeit gegen die Kapazität dieses Tages | +| **Graue Balken** | Termine aus deinem Kalender. Sie gehören dem Kalender, nicht dem Planer — hier lassen sie sich nicht anfassen | +| **Farbige Balken** | Deine Zeitblöcke, in der Farbe ihres Bereichs | + +Das sichtbare Stundenfenster ergibt sich aus den Arbeitsfenstern **aller** +Bereiche, mit einer Stunde Luft nach oben und unten. + +## Eine Aufgabe einplanen + +Aufgabe links am Griff packen und in ein Zeitfenster ziehen. Fertig — der +Zeitblock steht sofort im Raster und wenige Augenblicke später in deinem +Kalender. + +> **Die Dauer kommt aus dem geschätzten Aufwand.** Wer bei der Aufgabe +> „90 Minuten" einträgt, bekommt einen 90-Minuten-Block. Ohne Schätzung sind +> es 60 Minuten, die du danach anpassen kannst. + +Eine eingeplante Aufgabe wechselt automatisch von _Offen_ auf _Geplant_. + +## Verschieben + +Am Griff im Block packen und woandershin ziehen. Der Planer zieht den +Kalendertermin nach. + +## Ausplanen + +Das Kreuz im Block. Der Termin verschwindet aus dem Kalender; die Aufgabe +bleibt und fällt zurück auf _Offen_, wenn nichts anderes mehr für sie geplant +ist. + +## Wenn du im Kalender verschiebst + +Das darfst du — und der Planer zieht nach. Beim nächsten Abgleich (spätestens +nach 15 Minuten) steht der Block dort, wo du ihn im Kalender hingelegt hast. + +> **Der Kalender gewinnt.** Immer. Wer dort etwas verschiebt, meint es so; +> säße der Planer am längeren Hebel, würde er dir den Termin wieder wegziehen +> und du wüsstest nicht, warum. +> +> Löschst du den Termin im Kalender, verschwindet auch der Zeitblock. Die +> Aufgabe selbst bleibt. + +### Warum sich das nicht aufschaukelt + +Der Planer merkt sich zu jedem Zeitblock einen Fingerabdruck des Zeitraums, +den er zuletzt geschrieben hat. Findet er beim Abgleich genau diesen +Fingerabdruck wieder, war es seine eigene Änderung — er tut nichts. Weicht er +ab, hat jemand anderes verschoben. + +Ohne diesen Kniff würden App und Kalender einander endlos hinterherschreiben. + +## Das Warnzeichen im Block + +Ein kleines Dreieck heißt: **noch nicht im Kalender**. Meist war der Anbieter +gerade nicht erreichbar. + +Der Zeitblock bleibt trotzdem stehen — deine Planung soll nicht verlorengehen, +bloß weil ein Server hustet. Verschiebe den Block einmal, dann trägt der +Planer ihn nach. + +## Die Zahl über der Spalte + +`3 h / 6 h` heißt: sechs Stunden Kapazität an diesem Tag, drei davon schon +belegt. Gezählt werden Kalendertermine **und** deine Zeitblöcke. + +Wird die Zahl rot, ist der Tag überbucht. Das ist keine Sperre — der Planer +lässt dich weiterplanen. Es ist ein Hinweis, dass der Tag nicht aufgeht. + +Ganztägige Termine zählen nicht mit. Ein Urlaubstag im Kalender blockiert +sonst rechnerisch den ganzen Tag, obwohl er gar keine Arbeitszeit kostet. + +## Blättern + +**Zurück**, **Diese Woche** und **Weiter**. Die Woche steht in der Adresse — +eine bestimmte Woche lässt sich also als Lesezeichen ablegen. + +## Wenn es klemmt + +| Was du siehst | Was dahintersteckt | +| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| „Ohne verbundenen Kalender lässt sich nichts einplanen." | Erst unter [Kalender](kalenderkonten.md) ein Konto verbinden | +| „Eingeplant, aber nicht in den Kalender geschrieben" | Der Anbieter war nicht erreichbar. Der Block steht, verschiebe ihn einmal zum Nachtragen | +| „Der Termin wurde inzwischen im Kalender geändert." | Jemand war schneller. Der Kalender gewinnt; der nächste Abgleich holt den dortigen Stand | +| „Zeitblöcke rasten in 15-Minuten-Schritten ein." | Das Raster ist Absicht. Genauer zu planen bringt nichts | +| Ein Block ist nicht zu sehen | Er liegt außerhalb des sichtbaren Stundenfensters. Das Fenster folgt den Arbeitsfenstern der Bereiche — siehe [Bereiche](bereiche.md) | + +## Was noch nicht geht + +- **Ein Zeitblock landet immer im ersten verbundenen Kalender.** Die Auswahl + je Bereich ist im Datenmodell vorbereitet, aber noch nicht in der + Oberfläche. +- **Die Dauer lässt sich nicht am Balken ziehen.** Wer eine andere Dauer will, + ändert den Aufwand der Aufgabe und plant neu ein. diff --git a/prisma/demo-seed.ts b/prisma/demo-seed.ts index 8b960f7..fee602e 100644 --- a/prisma/demo-seed.ts +++ b/prisma/demo-seed.ts @@ -373,6 +373,37 @@ async function main(): Promise { }, }) + // --- Zeitblöcke: eingeplante Aufgaben im Kalender ------------------------- + await db.zeitblock.deleteMany({}) + const einzuplanen = await db.aufgabe.findMany({ + where: { + titel: { + in: [ + 'Wochenbericht an die Geschäftsführung', + 'Angebot Heizungstausch nachfassen', + ], + }, + }, + select: { id: true, titel: true, aufwandMinuten: true }, + }) + + let blockStunde = 15 + for (const aufgabe of einzuplanen) { + const beginn = new Date(Date.UTC(2026, 2, 17, blockStunde - 1, 0)) + await db.zeitblock.create({ + data: { + aufgabeId: aufgabe.id, + kalenderkontoId: kalenderkonto.id, + beginn, + ende: new Date(beginn.getTime() + (aufgabe.aufwandMinuten ?? 60) * 60_000), + extId: `demo-block-${aufgabe.id}`, + hash: 'demo', + }, + }) + await db.aufgabe.update({ where: { id: aufgabe.id }, data: { status: 'GEPLANT' } }) + blockStunde += 2 + } + // 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 9f70913..573fa12 100644 --- a/screenshots/bilder.spec.ts +++ b/screenshots/bilder.spec.ts @@ -31,6 +31,7 @@ const AUFNAHMEN: Aufnahme[] = [ { name: 'start', pfad: '/' }, { name: 'inbox', pfad: '/inbox' }, { name: 'aufgaben', pfad: '/aufgaben' }, + { name: 'woche', pfad: '/woche' }, { name: 'projekte', pfad: '/projekte' }, { name: 'ideen', pfad: '/ideen' }, { name: 'bereiche', pfad: '/bereiche' },