Files
tickte-system/docs/superpowers/plans/2026-10-05-weisses-design-p3-planboard.md

31 KiB
Raw Permalink Blame History

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

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
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 <noreply@anthropic.com>"

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

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
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 <noreply@anthropic.com>"

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:

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 <div className="page">
Kopf <PageHeader title="Planboard" subtitle={planboardSubtitle({ weekStart: currentWeek, count: weekCount, loading })} actions={…} />
Aktionen drei Knöpfe in dieser Reihenfolge: <Button variant="quiet" aria-label="Vorherige Woche">←</Button>, <Button>Heute</Button>, <Button variant="quiet" aria-label="Nächste Woche">→</Button>. Kein schwarzer Knopf im Kopf
Woche <div className="week"> mit sieben <div className="day">. 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 <div className="day-head"><span className="wd">{labels.weekday}</span><span className="dn">{labels.number}</span></div>
Leerer Tag <div className="none">Keine Termine</div>
Termin <Link className={appointmentClass(entry.type)} to={ticketPath(ticketRef({ woid: entry.woid, $id: entry.id }))} title={appointmentTitle(entry)}> 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

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 <noreply@anthropic.com>"

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.