# WOMS „Weißes Design“ P3: Planboard – Umsetzungsplan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Nach P3 zeigt das Planboard eine Wochenansicht ohne Karten: - sieben Tagesspalten mit Haarlinien, Wochenende in `--bg-subtle`, heutiger Tag als schwarze Datumspille - Termine als Links auf die Ticketseite, Akquise-Termine hell orange, stornierte Termine weiter sichtbar - Kopf „Kalenderwoche N · Datum bis Datum · n Termine“ mit den Aktionen ← · Heute · → - unter 860 px eine Spalte Die Logik in `lib/planboard.js` bleibt unverändert. **Architecture:** - `.week`, `.day`, `.appt` und ihre Unterklassen kommen als eigener Block „Planboard (P3)“ in `src/styles/global.css`, vor den KOMPAT-Block. Die Werte stammen aus `quelle/styles.css` (Zeilen 379–397 und 535–537). - Die reine Anzeigelogik (Kopfzeile, Terminzeile, Klassen, Linkziel) steht in der neuen `src/lib/planboardView.js` mit Tests. `PlanboardPage.jsx` besteht danach nur noch aus Markup. - Die Seite nutzt `PageHeader` und `Button` aus P1 sowie `ticketPath` und `ticketRef` aus P2. Sie enthält keine Icons und keine Inline-Styles. **Tech Stack:** React 18, react-router-dom 6, date-fns 2.30 (`de`, `getISOWeek`), reines CSS. Tests mit `node --test`, Screenshot-Werkzeug aus P0 (Playwright 1.62, `page.clock`). **Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Bausteine“ (Seitenkopf, Knöpfe, Links), „Ticket als eigene Seite / Route und Links“, „Texte“ und Paket P3. Detail im Anhang `2026-10-04-weisses-design-umbaukarte.md`, Abschnitt 5 (P3), 3 und 8.1 (Punkt 8). Globale Regeln und gemeinsame Schnittstellen: `2026-10-05-weisses-design-00-uebersicht.md`. ## Global Constraints Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&`. Danach gelten `$WT` (Worktree, aktuelles Verzeichnis), `$W` (Werkzeug) und `$TMPDIR`. Tests und Build laufen nur über `"$W/check.sh"`, Browser-Läufe nur über `"$W/shots.sh"` (Regeln der Übersicht). Es gelten die Regeln der Übersicht. Für P3 kommt hinzu: - Zeilenangaben gelten für den Stand `603d08c`. Nach P1 bis P2 können sie leicht verschoben sein, maßgeblich sind die genannten Namen. - Voraussetzung ist das Tag `design-p2` mit `src/lib/ticketRoutes.js` (`ticketPath`, `ticketRef`). - Geändert werden nur `src/pages/PlanboardPage.jsx` und `src/styles/global.css` (ein neuer Block). Neu sind `src/lib/planboardView.js` und zwei Testdateien. `src/lib/planboard.js`, `useWorkorders` und `useEmployees` bleiben unverändert. - Sichtbare Texte bleiben wörtlich: „Planboard“, „Heute“, „Keine Termine“, „ohne Uhrzeit“, „niemand zugewiesen“, Tooltip „Ticket #N öffnen“. Neu ist nur, was die Spec unter „Planboard-Kopf“ nennt: die Kopfzeile. Die `aria-label`-Texte „Vorherige Woche“ und „Nächste Woche“ sind nicht sichtbar. - **Erreichbar bleiben** alle heutigen Bedienelemente: Wochenwechsel (zwei Pfeilknöpfe), „Heute“ und jeder Termin als Link auf sein Ticket. Es gibt keine `data-testid` und keine `window.confirm` in der Seite. - **Entscheidung:** Der Tageskopf behält die heutigen Texte (voller Wochentag, Datum als `dd.MM`). Die Spec ist dort still, der Prototyp kürzt auf „Mo 5.“. Die Großbuchstaben des Wochentags setzt das CSS (Spec). Weil die Spalten bei 861–1100 px unter 120 px breit sind, darf `.day-head` umbrechen. Das volle Datum steht zusätzlich im `aria-label` der Spalte. - **Entscheidung:** Kein Fußtext und keine Legende „Akquise“. Der Prototyp hat beides, die Spec nennt es nicht. - **Entscheidung:** Stornierte Termine sind sichtbar und zählen im Kopf mit, ohne eigene Optik (wie heute). - **Entscheidung:** Das Linkziel ist `ticketPath(ticketRef({ woid: entry.woid, $id: entry.id }))`, also die `woid` und ohne sie die `$id`. Die Zeile eines Termins ohne `woid` zeigt kein leeres „#“ mehr, sondern nur den Mitarbeiter, sein Tooltip lautet „Ticket öffnen“. - **Entscheidung:** Während die Tickets laden, endet die Kopfzeile auf „Wird geladen …“ statt auf einer Terminzahl. - **Entscheidung:** Akquise wird wie heute über `String(type).toLowerCase() === 'akquise'` erkannt. Die grünen und grauen Typfarben entfallen, alle anderen Termine bekommen `--bg-hover`. - **Entscheidung:** Der Inhalt eines Termins behält die heutige Reihenfolge (Zeit, Kunde oder Betreff, Betreff, Nummer · Mitarbeiter), damit keine Information wandert. - **Entscheidung:** Auf Wochenendspalten bekommen Termine einen weißen Grund, sonst verschwinden sie im `--bg-subtle`. ## Review Focus Alle fünf Punkte der Übersicht treffen das Planboard. Jede Prüfung hat eine Ansicht in Task 4. 1. **Altdaten:** Ansicht `planboard-altdaten`. - `2.5.2026` erscheint am Samstag der Woche 18. - `31.02.2026`, ein ISO-Datum und ein leeres Datum ergeben weder einen Eintrag noch einen Fehler. - Ein Ticket ohne `woid` verlinkt über die `$id` und zeigt kein leeres „#“. - Im Text steht weder `undefined` noch `null` noch `NaN`. 2. **Lange Inhalte:** Ansicht `planboard`. Am Samstag stehen ein Kundenname mit 60 und ein Betreff mit 120 Zeichen. Es gibt kein waagerechtes Scrollen (`overflow-x`), und kein `.day` ist breiter als seine Spalte (`scrollWidth <= clientWidth`). 3. **Teilausfall:** Ansicht `planboard-fehler`. Antwortet die Mitarbeiterliste mit 500, erscheinen alle 13 Termine. Der Mitarbeiter steht dann aus `assignedName`. 4. **Nur Tastatur:** Ansicht `planboard-tastatur`. Der erste Termin ist per Tab erreichbar, trägt den schwarzen Fokusring und öffnet mit Enter `/tickets/100201`. 5. **Viele Einträge:** Der Donnerstag hat sechs Termine. Die Spalte wächst nach unten, nichts überlappt, die anderen Spalten bleiben unverändert hoch. --- ### Task 1: Paketstart und Ausgangswerte **Files:** keine Änderung (nur Lesen und Prüfen). **Interfaces:** - Consumes: Tag `design-p2`, `ticketPath(ref, tab)` und `ticketRef(ticket)` aus `src/lib/ticketRoutes.js`. - Produces: die Ausgangs-Testzahl N0 für die Übergabe. - [ ] **Step 1: Paketstart** Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p2 HEAD && git rebase origin/test && "$W/check.sh" && git status --short` Expected: - Alle Befehle enden mit Exit 0, `git status` gibt keine Zeile aus. Schlägt `merge-base` fehl, ist `design-p2` nicht im Branch: Stopp und melden, P3 darf erst nach P2 laufen. - `check.sh` zeigt `# fail 0`. Die Zahl `# pass` ist N0 (Soll nach P3: N0 + 9). - [ ] **Step 2: Parallele Arbeit an `feature/terminplaner-tp2`** Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..feature/terminplaner-tp2 -- src/pages/PlanboardPage.jsx src/lib/planboard.js src/lib/planboardView.js src/styles/global.css` Expected: keine Ausgabe. Gibt der Befehl Zeilen aus, stoppt der Agent und fragt den Eigentümer. Er empfiehlt, tp2 zuerst nach `test` zu bringen. - [ ] **Step 3: `ticketPath` und `ticketRef` sind da** Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "export function (ticketPath|ticketRef)" src/lib/ticketRoutes.js` Expected: zwei Treffer. Sonst Stopp: P3 darf erst nach P2 laufen. - [ ] **Step 4: Stand der Seite und des CSS** Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "ticketPath|react-icons|style=\{\{" src/pages/PlanboardPage.jsx; rg -n "^\s*\.(week|day|appt)\b" src/styles/global.css` Expected: - In der Seite: ein Treffer `ticketPath` (P2 hat die Linkzeile umgestellt), dazu die Icon-Importe und die Inline-Styles. - Im CSS: keine Treffer. Findet P1 dort schon Mobil-Regeln für `.week` oder `.day`, werden sie in Task 5 Step 1 in den P3-Block verschoben. ### Task 2: Charakterisierungstests für `lib/planboard.js` Die Logik hat bisher keinen Test. P3 benutzt sie unverändert, die Altwerte aus Review Focus 1 werden jetzt festgeschrieben. **Files:** - Test: `tests/unit/planboard.test.mjs` (neu) **Interfaces:** - Consumes: `parseGermanDate`, `formatTime`, `dayKey`, `groupAppointmentsByDay`, `countInRange` aus `src/lib/planboard.js`, unverändert. - Produces: vier Tests. - [ ] **Step 1: Tests schreiben** ```js const { parseGermanDate, formatTime, dayKey, groupAppointmentsByDay, countInRange } = await loadModule('src/lib/planboard.js') test('parseGermanDate: ein- und zweistellig, Altwerte ergeben null', () => { assert.equal(parseGermanDate('2.5.2026').getTime(), new Date(2026, 4, 2).getTime()) assert.equal(parseGermanDate(' 07.10.2026 ').getTime(), new Date(2026, 9, 7).getTime()) for (const v of ['31.02.2026', '0.1.2026', '2026-10-07T09:00:00.000Z', '2026-10-07', '7.10.26', '', undefined, null, 'morgen']) { assert.equal(parseGermanDate(v), null, String(v)) } }) test('formatTime: hhmm und hh:mm, ungültige Werte ergeben leer', () => { for (const [v, out] of [['0900', '09:00'], ['900', '09:00'], ['9:00', '09:00'], ['09:30', '09:30'], ['2359', '23:59']]) { assert.equal(formatTime(v), out, v) } for (const v of ['2400', '1260', '9', 'abc', '', undefined]) assert.equal(formatTime(v), '', String(v)) assert.equal(dayKey(new Date(2026, 0, 5)), '2026-01-05') }) const employees = [{ userId: 'u1', displayName: 'Kenso Grimm', shortcode: 'KNSO' }] const workorders = [ { $id: 'a', woid: '1', startDate: '7.10.2026', startTime: '1400', endTime: '1500', topic: 'Spät', type: 'Webpage', status: 'Open', customerName: 'Alpha', assignedTo: 'u1' }, { $id: 'b', woid: '2', startDate: '07.10.2026', startTime: '', topic: 'Ohne Uhrzeit', type: 'Akquise', status: 'Open', assignedName: 'Ehemalige' }, { $id: 'c', woid: '3', startDate: '7.10.2026', startTime: '0830', topic: 'Früh', type: 'Webpage', status: 'Cancelled' }, { $id: 'd', startDate: '8.10.2026', startTime: '1000', title: 'Nur Titel' }, { $id: 'h', startDate: '9.10.2026' }, { $id: 'e', woid: '5', startDate: '31.02.2026', startTime: '1000', topic: 'Ungültig' }, { $id: 'f', woid: '6', startDate: '2026-10-07T09:00:00.000Z', topic: 'ISO' }, { $id: 'g', woid: '7', startDate: '', topic: 'Leer' }, ] test('groupAppointmentsByDay: nach Uhrzeit, ohne Uhrzeit am Ende, Stornierte bleiben, Altwerte fallen weg', () => { const byDay = groupAppointmentsByDay(workorders, employees) assert.deepEqual(Object.keys(byDay).sort(), ['2026-10-07', '2026-10-08', '2026-10-09']) assert.deepEqual(byDay['2026-10-07'].map((a) => a.id), ['c', 'a', 'b']) assert.equal(byDay['2026-10-07'][0].status, 'Cancelled') assert.equal(byDay['2026-10-07'][1].employeeShort, 'KNSO') assert.equal(byDay['2026-10-07'][1].employeeName, 'Kenso Grimm') assert.equal(byDay['2026-10-07'][2].employeeName, 'Ehemalige') assert.equal(byDay['2026-10-07'][2].employeeShort, '') assert.equal(byDay['2026-10-08'][0].woid, '') assert.equal(byDay['2026-10-08'][0].topic, 'Nur Titel') assert.equal(byDay['2026-10-09'][0].topic, 'Ohne Betreff') }) test('countInRange: zählt nur die genannten Tage', () => { const byDay = groupAppointmentsByDay(workorders, employees) const week = (day) => Array.from({ length: 7 }, (_, i) => new Date(2026, 9, day + i)) assert.equal(countInRange(byDay, week(5)), 5) assert.equal(countInRange(byDay, week(12)), 0) assert.equal(countInRange(byDay), 0) }) ``` - [ ] **Step 2: Tests laufen lassen** Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/planboard.test.mjs` Expected: PASS, 4 Tests. Die Tests halten den Bestand fest, es ändert sich kein Produktivcode. Schlägt einer fehl, wurde das Verhalten falsch gelesen. Dann wird der Test korrigiert, nicht `planboard.js`. - [ ] **Step 3: Commit** ```bash git add tests/unit/planboard.test.mjs git commit -m "Planboard: Tests halten Datums- und Terminlogik fest (Altwerte, Stornierte, ohne Uhrzeit)" -m "Co-Authored-By: Claude Opus 5.5 " ``` ### Task 3: `planboardView.js` (Anzeigelogik) **Files:** - Create: `src/lib/planboardView.js` - Test: `tests/unit/planboardView.test.mjs` (neu) **Interfaces:** - Consumes: `date-fns` (`format`, `getISOWeek`, `addDays`) und `de`. - Produces (Signaturen, die `PlanboardPage` benutzt): - `planboardSubtitle({ weekStart: Date, count: number, loading?: boolean }) → string` - `dayLabels(day: Date) → { weekday: string, number: string, full: string }` - `appointmentClass(type) → 'appt' | 'appt akquise'` - `appointmentTimeLabel({ time, endTime }) → string` - `appointmentOwnerLabel({ woid, employeeName, employeeShort }) → string` - `appointmentTitle({ woid }) → string` - [ ] **Step 1: Failing Tests schreiben** ```js const { planboardSubtitle, dayLabels, appointmentClass, appointmentTimeLabel, appointmentOwnerLabel, appointmentTitle } = await loadModule('src/lib/planboardView.js') test('planboardSubtitle: Kalenderwoche, Zeitraum, Terminzahl', () => { const mo = new Date(2026, 9, 5) assert.equal(planboardSubtitle({ weekStart: mo, count: 13 }), 'Kalenderwoche 41 · 5. Oktober bis 11. Oktober · 13 Termine') assert.equal(planboardSubtitle({ weekStart: mo, count: 1 }), 'Kalenderwoche 41 · 5. Oktober bis 11. Oktober · 1 Termin') assert.equal(planboardSubtitle({ weekStart: mo, count: 0 }), 'Kalenderwoche 41 · 5. Oktober bis 11. Oktober · 0 Termine') assert.equal(planboardSubtitle({ weekStart: mo, count: 0, loading: true }), 'Kalenderwoche 41 · 5. Oktober bis 11. Oktober · Wird geladen …') assert.equal(planboardSubtitle({ weekStart: new Date(2026, 11, 28), count: 2 }), 'Kalenderwoche 53 · 28. Dezember bis 3. Januar · 2 Termine') assert.equal(planboardSubtitle({ weekStart: new Date(2027, 0, 4), count: 2 }), 'Kalenderwoche 1 · 4. Januar bis 10. Januar · 2 Termine') }) test('dayLabels: Wochentag, Datum, volles Datum', () => { assert.deepEqual(dayLabels(new Date(2026, 9, 5)), { weekday: 'Montag', number: '05.10', full: 'Montag, 5. Oktober' }) assert.deepEqual(dayLabels(new Date(2026, 9, 11)), { weekday: 'Sonntag', number: '11.10', full: 'Sonntag, 11. Oktober' }) }) test('appointmentClass und appointmentTimeLabel', () => { for (const t of ['Akquise', 'akquise', 'AKQUISE']) assert.equal(appointmentClass(t), 'appt akquise', t) for (const t of ['Webpage', 'Project', 'Migration', '', undefined, null]) assert.equal(appointmentClass(t), 'appt', String(t)) assert.equal(appointmentTimeLabel({ time: '08:00', endTime: '09:30' }), '08:00–09:30') assert.equal(appointmentTimeLabel({ time: '08:00', endTime: '' }), '08:00') assert.equal(appointmentTimeLabel({ time: '', endTime: '09:30' }), 'ohne Uhrzeit') assert.equal(appointmentTimeLabel({ time: '', endTime: '' }), 'ohne Uhrzeit') }) test('appointmentOwnerLabel: Nummer und Mitarbeiter', () => { assert.equal(appointmentOwnerLabel({ woid: '100201', employeeName: 'Kenso Grimm', employeeShort: 'KNSO' }), '#100201 · KNSO') assert.equal(appointmentOwnerLabel({ woid: '100201', employeeName: 'Ehemalige', employeeShort: '' }), '#100201 · Ehemalige') assert.equal(appointmentOwnerLabel({ woid: '100201', employeeName: '', employeeShort: '' }), '#100201 · niemand zugewiesen') assert.equal(appointmentOwnerLabel({ woid: '', employeeName: 'Kenso Grimm', employeeShort: 'KNSO' }), 'KNSO') assert.equal(appointmentOwnerLabel({ woid: '', employeeName: '', employeeShort: '' }), 'niemand zugewiesen') }) test('appointmentTitle: Tooltip mit und ohne woid', () => { assert.equal(appointmentTitle({ woid: '100201' }), 'Ticket #100201 öffnen') assert.equal(appointmentTitle({ woid: '' }), 'Ticket öffnen') }) ``` - [ ] **Step 2: Test laufen lassen** Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/planboardView.test.mjs` Expected: FAIL, `Could not resolve "src/lib/planboardView.js"` bzw. Funktionen fehlen. - [ ] **Step 3: `planboardView.js` implementieren** Festgelegte Werte: - Zeitraum: `format(d, 'd. MMMM', { locale: de })` für `weekStart` und `addDays(weekStart, 6)`, Verbindung „ bis “. - Kopfzeile: `Kalenderwoche ${getISOWeek(weekStart)} · ${von} bis ${bis} · ${rest}`. `rest` ist `Wird geladen …` bei `loading`, sonst `${count} Termin` (genau 1) oder `${count} Termine`. - `dayLabels`: `weekday` = `format(day, 'EEEE', { locale: de })`, `number` = `format(day, 'dd.MM')`, `full` = `format(day, 'EEEE, d. MMMM', { locale: de })`. - Zeitbereich in `appointmentTimeLabel` mit Halbgeviertstrich „–“ (U+2013), wie heute. - Die Datei importiert `date-fns`. Der Test-Lader bündelt es (geprüft mit `getISOWeek` und `date-fns/locale`), sie darf nur keinen Appwrite-Import bekommen. - `appointmentOwnerLabel`: wie heute entscheidet `employeeName`, ob ein Name oder „niemand zugewiesen“ erscheint. Mit `woid` steht `#woid · …`, ohne `woid` nur der Teil nach dem Punkt. - [ ] **Step 4: Test grün** Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/planboardView.test.mjs && "$W/check.sh" --nur-tests` Expected: PASS, Exit 0, `# fail 0`, `# pass` = N0 + 9. - [ ] **Step 5: Commit** ```bash git add src/lib/planboardView.js tests/unit/planboardView.test.mjs git commit -m "Planboard: Anzeigelogik fuer Kopfzeile, Tageskopf und Terminzeile mit Tests" -m "Co-Authored-By: Claude Opus 5.5 " ``` ### Task 4: Testdaten, Ansichten und Gate (Prüfung vor dem Umbau) Alles liegt im Werkzeug außerhalb des Repos, deshalb gibt es keinen Commit. **Files** (unter `$W/`): - Create: `fixtures/planboard.mjs` - Modify: `views.mjs`, `gates.mjs`, bei Bedarf `backend.mjs` **Interfaces:** - Consumes: `createBackend`, `VIEWS`, `GATES` aus P0. - Produces: die Ansichten `planboard`, `planboard-woche-vor`, `planboard-leer`, `planboard-altdaten`, `planboard-fehler`, `planboard-tastatur` und das Gate `p3`. - [ ] **Step 1: `fixtures/planboard.mjs` anlegen** Aufbau wie `altdaten.mjs`: Es importiert `standard.mjs` und ersetzt `workorders` durch genau diese Tickets. Alle zugewiesenen Tickets tragen zusätzlich `assignedName` (voller Name). Ein Kürzel steht für `assignedTo` = `userId` des Mitarbeiters mit diesem Kürzel aus `standard`. Tickets „ohne `woid`“ haben kein Feld `woid` und die feste `$id` aus der Tabelle. | woid | startDate | Zeit | Art | Status | Kunde | Betreff | Zuständig | |---|---|---|---|---|---|---|---| | 100201 | `5.10.2026` | 0900–1030 | Webpage | Occupied | Bäckerei Sonnenschein | Öffnungszeiten für die Feiertage anpassen | KNSO | | 100202 | `06.10.2026` | 1400, ohne Ende | Akquise | In Kontakt | Gärtnerei Grünblick | Erstgespräch Rechnungsanalyse | ANDJ | | 100203 | `7.10.2026` | 0830–0915 | Webpage | Occupied | Kanzlei Weber & Partner | Kontaktformular landet im Spam | JUSN | | 100204 | `7.10.2026` | ohne Zeit | Migration | Open | ohne Kunde | Umzug ohne Uhrzeit und ohne Kunde | niemand, ohne `assignedName` | | 100205 | `7.10.2026` | 1500–1600 | Webpage | Cancelled | Fahrschule Roth | Storniertes Gespräch | KNSO | | 100211–100216 | `8.10.2026` | 0800, 0900, 1000, 1300, 1400, 1530 | 100214 ist Akquise, sonst Webpage | gemischt | Testkunde 1–6 | Termin 1–6 | gemischt | | ohne `woid`, `$id: 'ohne-woid-fr'` | `9.10.2026` | 1100–1115 | Project | Open | Physiotherapie am Markt | Ticket ohne Ticketnummer | NIRE | | 100206 | `10.10.2026` | 1000–1100 | Webpage | Occupied | `Müller-Lüdenscheidt Metallbau- und Anlagentechnik GmbH & Co.` (60 Zeichen) | Betreff mit 120 Zeichen (Wert aus `altdaten.mjs`) | `assignedTo: 'ehemalig'` (nicht in der Mitarbeiterliste), `assignedName: 'Ehemalige Kollegin'` | | 100230 | `12.10.2026` | 0900–1000 | Webpage | Open | Café Mühlental | Termin nächste Woche | JUSN | Dazu die Altwerte, die nie im Planboard erscheinen dürfen oder in einer anderen Woche liegen: | woid | startDate | Zeit | Betreff | Erwartung | |---|---|---|---|---| | 100301 | `2.5.2026` | 1000–1100 | Altwert einstellig (Kunde „Altkunde GmbH“, KNSO) | erscheint am Samstag der Woche 18 | | ohne `woid`, `$id: 'ohne-woid-alt'` | `1.5.2026` | 0900 | Ticket ohne Nummer im Altbestand (Altkunde GmbH, niemand) | erscheint am Freitag der Woche 18 | | 100302 | `31.02.2026` | 0900 | Ungültiges Datum | nie sichtbar | | 100303 | `2026-10-07T09:00:00.000Z` | – | ISO-Datum | nie sichtbar | | 100304 | leer | – | Ohne Datum | nie sichtbar | Soll: In der Woche 5.–11. Oktober stehen 13 Termine (1 + 1 + 3 + 6 + 1 + 1), davon 2 Akquise (100202, 100214). In der Woche 12.–18. Oktober steht 1 Termin, in der Woche 27. April–3. Mai stehen 2. - [ ] **Step 2: Szenario `fehler` deckt beide Mitarbeiter-Wege ab** `useEmployees` fragt zuerst `GET https://portal.test/api/admin/employees` und fällt bei einem Fehler auf die Appwrite-Sammlung `employees` zurück. Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "fehler" "$W/backend.mjs"` Expected: In `fehler` antworten beide Wege mit 500. Fehlt einer, wird er in `backend.mjs` ergänzt. Sonst lädt die Liste trotz Szenario, und Review Focus 3 prüft nichts. - [ ] **Step 3: Ansichten in `views.mjs` ergänzen** Die Schritte haben die Signatur `steps(page, ctx)` aus der Übersicht. Alle Ansichten setzen zuerst die Uhr (`page.clock.setFixedTime`, ohne die Zeit einzufrieren) und klicken dann „Heute“. Das ist nötig, weil die Seite die aktuelle Woche beim ersten Rendern bestimmt. Eine Zusicherung, die nicht stimmt, wirft einen Fehler (Verstoß `step-failed`, in jedem Gate aktiv). | ID | Daten | Schritte | Zusicherung | |---|---|---|---| | `planboard` | `planboard` | Uhr `2026-10-07T10:00:00+02:00`, „Heute“ | Kopf `Kalenderwoche 41 · 5. Oktober bis 11. Oktober · 13 Termine`. 7 `.day`, genau 2 `.day.weekend`, `.day.today .dn` = „07.10“, 13 `a.appt`, 2 `a.appt.akquise`, 1 Mal „Keine Termine“ (Sonntag). Je `.day` gilt `scrollWidth <= clientWidth`. Der Donnerstag hat 6 `a.appt`, jeder beginnt unter dem Ende des vorigen | | `planboard-woche-vor` | `planboard` | wie oben, dann „Nächste Woche“ | Kopf `Kalenderwoche 42 · 12. Oktober bis 18. Oktober · 1 Termin` | | `planboard-leer` | `planboard` | wie oben, zweimal „Nächste Woche“ | Kopf `Kalenderwoche 43 · 19. Oktober bis 25. Oktober · 0 Termine`, 7 Mal „Keine Termine“ | | `planboard-altdaten` | `planboard` | Uhr `2026-05-04T10:00:00+02:00`, „Heute“, „Vorherige Woche“ | Kopf `Kalenderwoche 18 · 27. April bis 3. Mai · 2 Termine`. Der Text enthält weder `undefined` noch `null` noch `NaN`. Der Freitagstermin hat einen `href`, der auf `/tickets/ohne-woid-alt` endet, und sein Text enthält kein „#“ | | `planboard-fehler` | `planboard`, Szenario `fehler` | wie `planboard` | 13 `a.appt`. Der Montagstermin zeigt „Kenso Grimm“ statt „KNSO“, der Samstagstermin „Ehemalige Kollegin“. `ctx.backend.calls` enthält je einen GET auf `…/api/admin/employees` und auf `…/collections/employees/documents` (beide Wege wurden versucht) | | `planboard-tastatur` | `planboard` | wie `planboard`. Tab drücken (höchstens 40 Mal), bis `document.activeElement` ein `a.appt` ist | Die Outline des fokussierten Elements ist `solid` mit 2 px. Enter führt zu `/tickets/100201`. Danach „Zurück“ und Warten auf `.week` | - [ ] **Step 4: Gate `p3` in `gates.mjs` anlegen** `p3` ist `p2` plus die Ansichten dieses Pakets im bereichsbezogenen Gate `icon-svg` (Schema der Übersicht). `step-failed`, `external-request` und `console-error` sind ohnehin aktiv, `ignore` von `p2` bleibt erhalten: ```js GATES.p3 = { ...GATES.p2, scoped: { ...GATES.p2.scoped, 'icon-svg': { ...GATES.p2.scoped['icon-svg'], views: [...GATES.p2.scoped['icon-svg'].views, 'planboard', 'planboard-*'] } } } ``` - [ ] **Step 5: Vorher-Lauf (muss scheitern)** Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p3-vorher && "$W/shots.sh" p3-vorher "$W/shots/p3-vorher" --only planboard --gate p3` Expected: Exit 1, `icon-svg` findet die zwei Chevron-Icons (dazu `step-failed`, weil die neue Kopfzeile fehlt). Die Bilder `shots/p3-vorher/1440/planboard.png` und `390/planboard.png` sind die Vorher-Bilder mit den neuen Testdaten. Die anderen fünf Ansichten brechen vor dem Umbau an den fehlenden Knöpfen und werden hier nicht gestartet. ### Task 5: Planboard-CSS und Seite **Files:** - Modify: `src/styles/global.css` (neuer Block „Planboard (P3)“ unmittelbar vor `/* ===== KOMPAT (bis P10) ===== */`) - Modify: `src/pages/PlanboardPage.jsx` (Zeilen 1–119 ersetzen) **Interfaces:** - Consumes: - `PageHeader({ title, subtitle, actions })`, `Button({ variant })` aus P1. - `ticketPath(ref)` und `ticketRef(ticket)` aus P2. - `planboardView.js` aus Task 3. - `groupAppointmentsByDay`, `countInRange`, `dayKey` aus `lib/planboard.js`. - Tokens `--bg-hover`, `--bg-subtle`, `--appt-hover`, `--appt-akquise`, `--appt-akquise-hover`, `--accent`. - Produces: die Klassen `.week`, `.day` (`.today`, `.weekend`), `.day-head` (`.wd`, `.dn`), `.appt` (`.akquise`, `.tm`, `.tt`, `.cu`), `.none`. Spätere Pakete benutzen sie nicht. - [ ] **Step 1: CSS-Block anlegen** Alle Werte aus `quelle/styles.css`, Zeilen 379–397, mit den Tokens statt der Literale `#eef1f4`, `#fdf4e3` und `#fbeccd`. Zusätzlich gelten diese Ergänzungen: | Selektor | Wert | |---|---| | `.day-head` | zusätzlich `flex-wrap: wrap; row-gap: 0` | | `.appt` | zusätzlich `overflow-wrap: anywhere` | | `.day.weekend .appt:not(.akquise)` | `background: var(--bg)` | | `.day.weekend .appt:not(.akquise):hover` | `background: var(--appt-hover)` | | `@media (max-width: 860px)` | am Ende des Blocks: `.week { grid-template-columns: 1fr; border-top: 0; min-height: 0 }`, `.day { border-left: 0; padding: 12px 0; border-top: 1px solid var(--line) }`, `.day.weekend { background: none }`, `.day.weekend .appt:not(.akquise) { background: var(--bg-hover) }` (Quelle: Zeilen 535–537) | Der Block beginnt mit `/* ===== Planboard (P3) ===== */`. Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && sed -n '/Planboard (P3)/,/KOMPAT (bis P10)/p' src/styles/global.css | rg -n "!important|#[0-9a-fA-F]{3,8}\b"` Expected: nur ein Treffer, `#fff` an `.day.today .dn`. Gab es in Task 1 Step 4 Mobil-Regeln aus P1 für `.week` oder `.day`, sind sie jetzt aus dem P1-Block entfernt, sodass sie nur noch hier stehen (`rg -n "^\s*\.(week|day)\b" src/styles/global.css` zeigt nur Zeilen im P3-Block). - [ ] **Step 2: `PlanboardPage.jsx` neu schreiben** Der Code der Seite (State, `useWorkorders({ limit: 500 })`, `useEmployees`, `byDay`, `weekCount`, `navigateWeek`, `goToday`) bleibt. Das Markup wird: | Element | Umsetzung | |---|---| | Wurzel | `
` | | Kopf | `` | | Aktionen | drei Knöpfe in dieser Reihenfolge: ``, ``, ``. Kein schwarzer Knopf im Kopf | | Woche | `
` mit sieben `
`. Klassen `today` bei `isSameDay(day, new Date())` und `weekend` ab dem sechsten Tag. Attribute `role="group"`, `aria-label={labels.full}`, `aria-current="date"` nur bei heute | | Tageskopf | `
{labels.weekday}{labels.number}
` | | Leerer Tag | `
Keine Termine
` | | Termin | `` mit `.tm` (`appointmentTimeLabel`), `.tt` (`entry.customerName \|\| entry.topic`), einem `.cu` mit `entry.topic` (nur bei vorhandenem Kundennamen) und einem `.cu` mit `appointmentOwnerLabel(entry)` | Es entfallen: alle Icon-Importe, `Card`, `useNavigate`, `TYPE_COLORS`, `format` für die Kopfzeile und jedes `style`. - [ ] **Step 3: Hygiene prüfen** Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{\{|navigate\(|Card|TYPE_COLORS" src/pages/PlanboardPage.jsx` Expected: keine Treffer. - [ ] **Step 4: Bauen und Gate** Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p3 && "$W/shots.sh" p3 "$W/shots/p3" --gate p3` Expected: - Exit 0, alle Zusicherungen aus Task 4 Step 3 erfüllt, `icon-svg` ist für die `planboard*`-Ansichten sauber. - In `shots/p3/390x844/planboard.png` stehen die Tage untereinander, der Mittwoch trägt die schwarze Zahl, und es gibt keine Spaltenlinien. - In `shots/p3/1440x900/planboard.png` sind Samstag und Sonntag hell abgesetzt, die Donnerstagsspalte ist länger, und die Akquise-Termine sind hell orange. - [ ] **Step 5: Commit** ```bash git add src/pages/PlanboardPage.jsx src/styles/global.css git commit -m "Planboard: Wochenansicht ohne Karten (Tagesspalten, heute als Pille, Termine als Links)" -m "Co-Authored-By: Claude Opus 5.5 " ``` ### Task 6: Gate P3, Review, Tag - [ ] **Step 1: Rebase** Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"` Expected: Exit 0, `# fail 0`, `# pass` = N0 + 9. Liegen neue Commits auf `test`, kann die Zahl höher sein; sie wird notiert. - [ ] **Step 2: Vollständiger Gate-Lauf** Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p3 && "$W/shots.sh" p3 "$W/shots/p3" --gate p3` Expected: - Gate `p3` Exit 0. - Keine Verstöße gegen `dark-surface`, `low-contrast`, `overflow-x`, `console-error`, `external-request` und `step-failed`, auch auf allen übrigen Ansichten. - Die Planboard-Ansichten haben kein `icon-svg`. - [ ] **Step 3: Bundle-Größe** Die Größen stehen in der Ausgabe von `check.sh` (Step 1). Sie werden mit dem Stand von `design-p2` verglichen. Das JS wird kleiner (zwei Icon-Importe weniger), das CSS wächst um rund 1 KB. - [ ] **Step 4: Code-Review (sonnet)** Geprüft wird der Diff `design-p2..HEAD` gegen die Spec-Abschnitte „Gestaltungsregeln“, „Bausteine“ und Paket P3. Schwerpunkte: - keine Icons, keine `style={{`, kein Farbliteral in der Seite - `ticketPath(ticketRef(…))` ist das einzige Linkziel - das `aria-label` der Spalten und der Pfeilknöpfe - der CSS-Block benutzt nur Tokens, kein `!important` - `lib/planboard.js` ist unverändert (`git diff design-p2 -- src/lib/planboard.js` ist leer) Wichtige Befunde werden behoben, Kleinigkeiten notiert. Danach kommt der Rebase aus Step 1 noch einmal und Step 2 als Kurzlauf mit `--only planboard,planboard-fehler`. - [ ] **Step 5: Tag und Übergabe** Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p3 && git log --oneline origin/test..design-p3 && git rev-parse design-p3` Der Eigentümer bekommt: - die Screenshots aus `shots/p3/1440x900` und `shots/p3/390x844` (Auswahl: `planboard`, `planboard-woche-vor`, `planboard-fehler`) und den Vorher-Vergleich aus `shots/p3-vorher` - den Hinweis, dass der Tageskopf die heutigen Texte behält (der Prototyp kürzt auf „Mo 5.“) und der Prototyp-Fußtext samt Legende fehlt - die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p3^{commit}:refs/heads/test'` - die Rücknahme: `git revert --no-commit design-p2..design-p3`, Commit und Push durch den Eigentümer (gilt nur, solange P3 das jüngste Paket ist) - [ ] **Step 6: Nach dem Push (erst, wenn der Eigentümer gepusht hat)** - Im Log von `webklar-ticket-build` steht „Deployment completed successfully“. - `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` entspricht `dist/index.html` des Tags. - Ein lesender Sichttest im Chrome des Eigentümers: Planboard öffnen, Woche vor und zurück, ein Termin öffnet das Ticket.