Konzept: weisses Design fuer WOMS (Spec + Umbaukarte)

Steriles Weiss, kaum Rahmen, kaum Icons, Akzent Schwarz, Ticket als
eigene Seite. Pakete P0-P10, jedes einzeln ueber ein Tag live.
Gegen den Code geprueft (4 Pruefer + Gegenpruefung, 66 Befunde
eingearbeitet).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-06 06:02:54 +00:00
1 parent 00fa229235
commit 9d604aa550
2 files changed
+1235

No files matched your search

@@ -0,0 +1,541 @@
# WOMS „Weißes Design“ – Konzept
Stand 2026-10-04. Freigegeben im Gespräch:
- Prototyp angesehen, Akzentfarbe „Schwarz passt“.
- Ticket öffnet als eigene Seite.
- Paketablauf und alle Standardentscheidungen mit „ja passt“.
Danach gegen den Code geprüft: 4 Prüfer, Gegenprüfung jedes Befunds, 66 bestätigte Befunde eingearbeitet.
Grundlage:
- Klick-Prototyp im Repo `WEBklar/ticketsystem-design`, Vorschau `https://ticketsystem-design.project.webklar.com` mit Mitarbeiter-Login. Kopie unter `/root/reports/ticketsystem-design-2026-10-04/quelle/`.
- Bestandsaufnahme aller 104 Quelldateien in `2026-10-04-weisses-design-umbaukarte.md` (gleicher Ordner). **Bei Widersprüchen gilt dieses Dokument.**
## Ziel
Das ganze Ticketsystem (ticket.webklar.com) bekommt ein neues Aussehen:
- steriles Weiß, sehr minimalistisch,
- kaum Rahmen, kaum Icons,
- angelehnt an das Stripe-Dashboard und trotzdem übersichtlich.
Das dunkle Design entfällt vollständig. Es gibt keinen Umschalter.
Der Umbau ist **rein visuell**, mit zwei Ausnahmen:
- die in „Bewusste Änderungen am Verhalten“ genannten Punkte,
- vor allem: Ein Ticket öffnet als **eigene Seite** mit eigenem Link (`/tickets/<Nummer>`), statt in der Liste aufzuklappen.
Erfolg heißt:
- Jede Seite folgt den Gestaltungsregeln.
- Keine heutige Funktion geht verloren, außer den ausdrücklich genannten.
- Alle Tests sind grün.
- Jedes Paket geht einzeln live.
- Das jüngste Paket lässt sich per Revert zurücknehmen.
Nicht im Umfang:
- Dark Mode, neue Funktionen
- Änderungen am Datenmodell, an Rechten oder am Server
- das Kundenportal, auch nicht dessen Chat-Composer
- Terminplaner TP2
- flächige Rechtschreibkorrekturen (ae/ue) in Bestandstexten
## Gestaltungsregeln
1. **Weiß überall.**
- Seite, Menü und Dialoge haben `#ffffff`.
- Es gibt keine Verläufe, Leuchteffekte, Glas-Effekte oder Hintergrundanimationen.
- Einzige Ausnahme sind Bildansicht und Kamera des fremden Chat-Composers. Sie bleiben dunkel, weil sie Medienbetrachter sind.
2. **Kaum Rahmen.**
- Struktur entsteht durch Weißraum, Schriftgrößen und einzelne 1-px-Haarlinien `#ebeef1`.
- Haarlinien stehen zwischen Tabellen- und Listenzeilen, unter Reitern, rechts neben dem Menü, zwischen Schlüsselwerten (`.fact`) und zwischen den Tagesspalten im Planboard.
- Karten oder Kästen um Inhalte gibt es nicht.
- Eingabefelder und Zweitknöpfe tragen statt Rahmen einen feinen Ring-Schatten.
3. **Kaum Icons.**
- Menü, Knöpfe, Überschriften und Badges tragen keine Icons.
- Handelnde Zeichen werden als Schriftzeichen gesetzt, nicht als Icon-Komponente: Schließen „ד, Pfeile „← →“, Sortierung „↑ ↓“.
- Am Ende fliegen `react-icons` und `@tabler/icons-react` aus `package.json`.
4. **Eine Akzentfarbe: Schwarz `#111111`.**
- Sparsam eingesetzt: Primärknöpfe, aktiver Reiter, aktiver Menüpunkt, heutige Datumspille im Planboard.
- Pro Seitenkopf höchstens ein schwarzer Knopf.
- Links sind am feinen Unterstrich erkennbar, nicht an der Farbe.
5. **Statusfarben** sind leicht getönte Pillen ohne Rahmen: Radius 4 px, Höhe 20 px, Schrift 12 px/500.
| Ton | Hintergrund | Schrift | Ticketstatus (gespeicherte Werte) |
|---|---|---|---|
| grau | #ebeef1 | #414552 | Offen (`Open`), leer oder unbekannt |
| blau | #e0f2fe | #075985 | In Arbeit (`Occupied`, `Assigned`, `Added Info`, `In Kontakt`) |
| orange | #fcedb9 | #a82c00 | Wartet auf Kunde (`Awaiting`, `Halted`) |
| violett | #efeafe | #5b21b6 | Prüfung (`In Test`) |
| grün | #d7f7c2 | #006908 | Erledigt (`Closed`, `Zugesagt`) |
| plain | – | #5e6678 | Storniert (`Cancelled`, `Aborted`, `Abgesagt`) |
| rot | #ffe7f2 | #b3093c | kein Status: Badge „Dringend“ |
6. **Schrift.**
- Systemschrift `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif`. Lato und `src/assets/fonts` entfallen.
- Größen: h1 28/34 700 (Handy 24/30), Abschnitt-h2 16/24 600, Dialogtitel 18/26 600, Text 14/20, klein 13, sehr klein 12/16, Kennzahl 22/30 600.
- Gewicht 800 entfällt. Großbuchstaben mit Sperrung gibt es nur noch für die Wochentage im Planboard.
- Zahlen und Beträge nutzen `tabular-nums`.
7. **Breakpoints der WOMS-Seiten: 1100 px und 860 px.**
- Unter 1100 px werden Nebenspalten ausgeblendet (`.hide-md`).
- Unter 860 px:
- Das Menü wird zur Schublade mit Menü-Knopf.
- Seitenköpfe stapeln sich.
- Tabellen zeigen nur Kernspalten (`.hide-sm`).
- Dialoge erscheinen als Bottom-Sheet.
- Der Chat wechselt zwischen Liste und Verlauf (heute 720 px).
- Ausgenommen:
- die Container-Queries der Finanztabellen (560/640/860)
- die Breakpoints des fremden Chat-Composers (640/880)
8. **Lesbarkeit.**
- Text `#1a1f36`, Nebentext `#5e6678` (5,8:1).
- Tertiärtext `#8a93a3` (3,1:1) nur für Nicht-Essenzielles, nämlich Nummern, „–“ und leere Zustände. Inhalte und Status nutzen ihn nie.
- Fehlerrot `#c0123c`.
- Fokus ist immer sichtbar:
- Knöpfe und Links: 2 px durchgezogen `#111111` mit 2 px Abstand.
- Felder: 1 px `#111111` plus Ring `rgba(17,17,17,.14)`.
- Deaktiviert: Deckkraft .55, Cursor `not-allowed`. Schreibgeschützte Felder bekommen den Hintergrund `#f8f9fb`.
## Design-Tokens
Die Tokens stehen in `:root` in `src/styles/global.css`. Quelle ist der Prototyp mit festem Akzent, ohne `html[data-accent]` und ohne Prototyp-Leiste.
```css
:root {
--bg: #ffffff; --bg-hover: #f6f8fa; --bg-subtle: #f8f9fb;
--text: #1a1f36; --text-2: #5e6678; --text-3: #8a93a3;
--line: #ebeef1; --line-strong: #d8dde3;
--accent: #111111; --accent-hover: #2b2b2b; --accent-ink: #111111;
--accent-soft: rgba(17,17,17,.06); --accent-ring: rgba(17,17,17,.14);
--grey-bg: #ebeef1; --grey-fg: #414552; --blue-bg: #e0f2fe; --blue-fg: #075985;
--orange-bg: #fcedb9; --orange-fg: #a82c00; --violet-bg: #efeafe; --violet-fg: #5b21b6;
--green-bg: #d7f7c2; --green-fg: #006908; --red-bg: #ffe7f2; --red-fg: #b3093c;
--danger: #c0123c; --danger-ring: rgba(192,18,60,.18);
--radius: 6px; --radius-lg: 12px;
--ring: 0 0 0 1px rgba(60,66,87,.16), 0 1px 1px rgba(0,0,0,.06);
--shadow-btn: 0 1px 1px rgba(0,0,0,.12), 0 2px 5px rgba(60,66,87,.08);
--shadow-pop: 0 0 0 1px rgba(60,66,87,.06), 0 12px 32px rgba(26,31,54,.14), 0 2px 6px rgba(26,31,54,.06);
--overlay: rgba(26,31,54,.32); --scrim: rgba(26,31,54,.24);
--nav-dot: #e5a000;
--appt-akquise: #fdf4e3; --appt-akquise-hover: #fbeccd; --appt-hover: #eef1f4;
--sidebar-w: 232px;
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
html { color-scheme: light; }
```
**Alte Token-Namen bleiben bis P10 als Aliase.** Fremd-CSS und JSX benutzen sie noch, so wird Bestandscode sofort weiß.
| Alter Name | Neuer Wert |
|---|---|
| `--surface-1`, `--surface-3`, `--surface`, `--dark-bg`, `--dark-card-bg` | `--bg` |
| `--surface-2`, `--surface-hover` | `--bg-hover` |
| `--border`, `--gray-700`, `--admin-line` | `--line` |
| `--border-strong`, `--admin-line-strong` | `--line-strong` |
| `--border-accent` | `--accent-ring` |
| `--text-muted`, `--text-faint`, `--gray-800` | `--text-2` (Altcode setzt `--text-faint` auch für Inhalte ein) |
| `--dark-text`, `--gray-900` | `--text` |
| `--accent-2`, `--green-secondary` | `--accent-hover` |
| `--green-primary` | `--accent` |
| `--info` | `--blue-fg` |
| `--warn` | `--orange-fg` |
| `--ok` | `--green-fg` |
| `--r-sm` | `--radius` |
| `--r-md`, `--r-lg` | `--radius-lg` |
| `--r-pill` | 4px |
| `--shadow-1` | `none` |
| `--shadow-2` | `--shadow-btn` |
| `--shadow-3` | `--shadow-pop` |
| `--sp-1…6` | bleiben unverändert |
| `--sidebar-w-collapsed` | entfällt |
**Diagrammfarben.** Canvas kann kein `var()` lesen. Deshalb stehen die Farben der Diagramme (uPlot) als Konstanten in der neuen Datei `src/components/serverpuls/chartColors.js`. Sie wird schon in P1 angelegt und von `Chart.jsx` und `ServerpulsPage` genutzt.
| Element | Farbe |
|---|---|
| Linie | `#111111` |
| Füllung | `rgba(17,17,17,.06)` |
| Spitze | `#8a93a3`, gestrichelt |
| Achsen | `#5e6678` |
| Raster | `#ebeef1`, nur horizontal |
| Ticks | `#d8dde3` |
## Bausteine
Die Klassennamen folgen dem Prototyp (`styles.css`). Wo eine Komponente existiert, behält sie ihre Schnittstelle, außer wo unten anders vermerkt. Es ändern sich Markup und Klassen.
| Baustein | Umsetzung |
|---|---|
| **App-Gerüst** | `.shell` mit Menü `<aside class="sidebar">` (fest, 232 px) und `.main`. In `.main` liegen `.mobile-bar` und `<main id="inhalt">` mit den Routen, ohne eigene Klasse `.page`. Jede Seite behält ihre Wurzel mit Klasse `page`, die den Prototyp-Look bekommt (max. 1160 px, Abstand 36/48/80). `ProjectsPage` wechselt in P1 von `main-content` auf `page`. `AdminPage` wechselt in P1 von `<main className="page admin-page">` auf `<div className="page admin-page">`, die maximale Breite 1480 px von `.admin-page` bleibt. Gescrollt wird das Dokument, ein innerer Scroll-Container entfällt. Der Footer entfällt, sein Text steht künftig im Login-Fuß. Unter 860 px ist das Menü eine Schublade mit `.scrim`. Sie schließt bei Klick daneben, bei Esc (Listener nur aktiv, solange sie offen ist) und bei Wechsel des Pfads. Die Seite springt nur bei Wechsel von `location.pathname` nach oben, nie bei Änderungen am Suchteil der URL. |
| **Menü** | Reine Textliste `.nav-item` mit Unterpunkten `.sub` und Zählern `.nav-count`. Chat-Ungelesene bekommen `.hot` (neues Feld `hot` am Chat-Eintrag in `navLinks.js`). „Leads“ erscheint grau mit „pausiert“. Serverpuls-Punkt: `.nav-dot` gelb bei `medium`, `.nav-dot.critical` rot bei `critical`. `data-testid="sp-nav-dot"`, Titel, 5-Minuten-Abfrage und `useSidebarCounts` bleiben. Fuß `.sidebar-foot`: Name (gekürzt mit Ellipsis) und „Abmelden“. Das Einklappen entfällt samt `lib/sidebar.js` und Test. Reihenfolge, Texte und Ziele in `navLinks.js` bleiben. |
| **Logo** | Neue `components/Logo.jsx` mit dem SVG-Zeichen aus dem Prototyp, 24 px im Menü und in der Mobil-Leiste, 40 px im Login. Dazu `public/favicon.svg` und `<link rel="icon">` in `index.html`. `<title>` bleibt. |
| **Seitenkopf** | `PageHeader` rendert `.page-head` mit h1, `.sub` und `.actions`. Neue optionale Props `eyebrow` und `crumb`. Brotkrumen `.crumb`, z. B. „Kunden / Name“. |
| **Abschnitt** | `Card` rendert `<section>` mit `<h2 class="section">`: Titel links, Aktion rechts, ohne Hintergrund, Rand, Schatten und Innenabstand. `pad`, `bodyStyle` und `style` wirken nicht mehr. `className` bleibt, Finanzen braucht es für Container-Queries. |
| **Knöpfe** | `Button`: Standard und `ghost` werden `.btn` (weiß mit Ring), `primary` wird `.btn.btn-primary` (schwarz). Neu ist `quiet` als `.btn.btn-quiet`. `danger` wird `.link-danger` (roter Textlink). Die Prop `size` wird angenommen, wirkt aber nicht: Alle Knöpfe sind 32 px hoch, nur der Login-Knopf 36 px. Zeilenaktionen in Tabellen sind Textlinks `.link`. Während einer Anfrage sind Knöpfe `disabled` und zeigen Text („Speichert …“), keinen Spinner. |
| **Links** | `a.link` mit feinem Unterstrich (`--line-strong`, beim Überfahren `currentColor`). Basisregel `a:not([class])` mit demselben Unterstrich, damit auch klassenlose Links (Rechtstext-Dateien, AVV-Link der Analyse-Zustimmung u. a.) erkennbar bleiben. Neuer Code vergibt immer eine Klasse. |
| **Badge** | `<span class="badge {ton}">`. Die Ton-Namen der Logik (`muted, info, ok, warn, danger`) bleiben, Tests pinnen sie. `Badge.jsx` übersetzt: muted → grau, info → blau, ok → grün, warn → orange, danger → rot. Neu sind `violet` und `plain`. Die Prop `dot` entfällt. |
| **Ticketstatus** | Neue Funktion `statusTone(status)` in `lib/ticketStatus.js` mit den Rückgaben `'' \| 'blue' \| 'orange' \| 'violet' \| 'green' \| 'plain'` nach der Tabelle oben. Leer, `undefined` und unbekannt ergeben `''`. Der Test deckt jeden gespeicherten Wert ab. `StatusPill` rendert `badge {ton}` direkt, ohne das Mapping in `Badge.jsx`. `UrgentPill` rendert „Dringend“ in Rot. `statusClass` entfällt mit P2. |
| **Reiter** | `Tabs` rendert `.tabs` und `.tab` mit 2-px-Unterstrich und optionalem Zähler `<span class="count">`. Auf dem Handy scrollen die Reiter waagerecht. Zähler werden beim Rendern ergänzt, nie in `lib/customerTabs.js`. |
| **Umschalter** | `.seg`: graue Schiene, der aktive Teil ist weiß mit Ring, dazu `aria-pressed`. |
| **Tabelle** | `.table` mit Kopf 12 px/500 und Haarlinien. `tr.row` ist klickbar und wird beim Überfahren `--bg-subtle`. Weitere Klassen: `.cell-title`, `.cell-sub`, `.id`, `.right`, `.num`, `.table-foot`, `.hide-md`, `.hide-sm`. Die erste Zelle enthält immer ein echtes Bedienelement für Tastatur und Mittelklick: einen Link oder, wenn die Zeile einen Dialog öffnet, einen `<button class="link">`. Klicks auf `a`, `button`, `select`, `input` und `label` in der Zeile lösen keine Zeilenaktion aus. |
| **Liste** | `.list-row` mit Haarlinie. Bei Link-Zeilen färbt sich beim Überfahren der Titel. |
| **Schlüsselwerte** | `.facts`/`.fact` für den Ticketkopf. `.stats`/`.stat` für Kennzahlen: `.v.warn` orange, neu `.v.neg` rot für negative Beträge. `.kv` für Beschriftung und Wert, mobil einspaltig. Dazu `.two-col` und `.prose` (Fließtext mit Zeilenumbrüchen). Neue Komponente `Stat` mit Prop `tone`, ohne Icon und ohne Farbstreifen. `Kpi` bleibt bis P7 als Export erhalten und rendert `Stat`: `accent` = `var(--danger)` wird `neg`, alle anderen `accent` werden ignoriert. |
| **Formular** | `.field > label` (500) mit `.input`, `.select` oder `.textarea` (32 px, Ring statt Rand, zweistufiger Fokus) und `.hint`. Mehrspaltige Formulare nutzen `.field-row` und `.field-row-3`, Kästchen `.check`. Fehler zeigt ein roter Ring (`--danger`, `--danger-ring`), auch bei `[aria-invalid="true"]`. `Field`, `FormField`, `DateTimeInput`, `DurationSelect` und `AutocompleteInput` übernehmen die Klassen. Die Vorschlagsliste ist ein weißes Popover mit `--shadow-pop`. |
| **Dialog** | Neue `ui/Dialog.jsx` mit den Props `title`, `onClose`, `busy`, `wide`, `footer`. Aufbau: `.overlay` (`--overlay`) mit `.dialog` (weiß, Radius 12, `--shadow-pop`, 520 px, mit `wide` 760 px). Der Kopf enthält h3 und den Knopf „ד (`aria-label="Schließen"`), danach folgen `.dialog-body` (scrollt) und `.dialog-foot`. Attribute: `role="dialog"`, `aria-modal`, `aria-labelledby`. Den Fokus regelt `useDialogFocus`. Unter 860 px erscheint der Dialog als Bottom-Sheet. **Schließen geht nur über × und Esc.** Ein Klick auf den Hintergrund schließt nicht, damit lange Eingaben nicht verloren gehen (Abweichung vom Prototyp). Esc greift nur, wenn `!e.defaultPrevented`, so schließt eine offene Vorschlagsliste zuerst sich selbst. Solange `busy` gilt, wirken × und Esc nicht, und × ist `disabled`. Beide Wege rufen genau das `onClose` des Aufrufers. `AssignDialog` übergibt sein `close()` samt `refresh`. Die eigenen Esc-Listener in `InvoiceCreateModal` und `AssignDialog` entfallen. `lockRef` und `busyRef` bleiben als Doppelklick-Sperre. Die native `required`-Prüfung bleibt. |
| **Leerer Zustand** | `<div class="empty"><strong>Titel</strong>Hinweis</div>` mit optionaler Aktion. Die Prop `icon` entfällt. |
| **Meldungen** | Ohne Kasten: `.msg.err` (rot, `role="alert"`), `.msg.ok` (grün), `.msg.warn` (orange), alle 13 px. Flüchtige Bestätigungen dürfen `.toast` nutzen. |
| **Laden** | `.loading` mit Text „Wird geladen …“. `AdminRoute` behält wörtlich `<p>Wird geladen …</p>` ohne Klasse, weil ein Test darauf prüft. Der Seitenabstand kommt dort vom Elternelement. |
| **Dringend-Schalter** | `UrgentToggle` rendert den kleinen Schalter `.sw` mit dem Wort „Dringend“. Eingeschaltet ist er rot. `aria-pressed` und die Logik bleiben. |
| **Login** | Aufbau: `.login` > `.login-box` (360 px), Logo, h1 „Anmelden“, `.sub` „Ticketsystem für das WEBklar-Team“, Felder „E-Mail“ und „Passwort“ mit `autocomplete`, Knopf „Weiter“. Danach die Schritte „Code senden“ und „Code eingeben“ (h1 „Code eingeben“, `.code-input`, Links „Code erneut senden“ · „Abbrechen“). Die erklärenden Texte der Code-Schritte bleiben wörtlich wie heute: „Wir schicken dir einen Code an deine E-Mail-Adresse.“, „Wir haben dir einen Code an {E-Mail} geschickt.“, „Code aus der E-Mail“, „Neuer Code ist unterwegs.“. Die Prototyp-Hinweise zum Testbetrieb kommen nicht ins echte System. `.login-foot` zeigt „Webklar Ticket-Plattform · © {Jahr}“. Die Logik (`login`, `sendCode`, `verifyCode`, `cancelMfa`, `NO_STAFF_ACCESS`, `safeReturnPath`) bleibt unverändert. `LetterGlitch.jsx` entfällt. |
### Übergangsschicht KOMPAT (P1 bis P10)
Ein markierter Block „KOMPAT“ in `global.css` bildet alte Klassen auf den neuen Look ab. So sind noch nicht umgebaute Seiten weiß, kastenlos und lesbar. Er gilt von P1 bis P10.
| Alte Klasse | Darstellung im neuen Look |
|---|---|
| `.ui-card*` | Abschnitt ohne Kasten, `.ui-card-head h3` wie `h2.section` |
| `.ui-btn*` | wie `.btn` / `.btn-primary` / `.link-danger` |
| `.form-group`, `.form-label`, `.form-control` | wie `.field`, Label, `.input` / `.select` / `.textarea` |
| `.badge-*`, `.status-*`, `.priority-*` | Badge-Töne |
| `.overlay` (Altform) | behält `overflow-y: auto`, `align-items: flex-start` und 24/16 px Abstand, Hintergrund `--overlay` |
| `.overlay-content` | weiße Box: `width: 100%`, `max-width: 520px` (Inline-`maxWidth` der Aufrufer wirkt weiter, `.fin-dialog` 760 px), `margin: 0 auto`, Radius 12, `--shadow-pop`, Abstand 24, Schrift `--text` |
| `.overlay-close` | fest oben rechts im Fenster, 32×32 px, weißer runder Knopf mit „ד in `--text-2` |
| `.seg button.active` | wie `.seg button.on` |
| `.bg-red` (mit oder ohne `.text-white`) | kein Hintergrund, Schrift `--red-fg`, 13 px |
| `.text-white` allein | keine Regel |
| `.text-grey` | `--text-2` |
| `.text-red` | `--red-fg` |
| `.side-count` | kleine graue Zahl, bis P6 (Chat-Liste) |
| `.side-logo` | Initiale ohne Verlauf, bis P4 (Kundenliste) |
| `.btn-dark`, `.btn-teal` | wie `.btn-primary` |
| `.card*`, `.row`, `.col*`, `.dropdown*` | neutral |
| `.spinner` | neutraler grauer Ring |
| `.page .page`, `.page .main-content` | Abstand und Breite neutralisiert, falls eine Seite verschachtelt |
| Links in Bestandsklassen, die bisher `--accent` als Farbe nutzten (`.fin-link`, `.legal-files a`, `.credit-cols a`, `.area-rows a`) | feiner Unterstrich |
Alle `!important`-Regeln des Altbestands werden gelöscht, nicht überschrieben. Tote Klassen löscht P1 nur nach einer Liste aus einem Abgleich-Skript:
- Das Skript vergleicht alle Klassen in `global.css` mit Klassen-Literalen in JSX, mit Template-Präfixen (z. B. `fin-fixed-${…}`, `legal-tone-${…}`) und mit Zeichenketten in `lib/*.js`.
- Gelöscht wird erst nach Gegenprobe mit `rg -w <name> src --glob '!*.css'`.
- Dynamisch gebaute Klassen gelten nicht als tot. Beispiele: `fin-neg`, `fin-fixed-off`, `legal-tone-*`, `legal-version-*`, `badge-*`, `status-*`, `ticket-tab-active`, `ui-btn-*`.
- Die Liste steht im Plan.
P10 entfernt den KOMPAT-Block.
## Ticket als eigene Seite
### Route und Links
- Route `/tickets/:ref/:tab?`.
- Sie ist wie alle anderen eine `ProtectedRoute` und steht vor dem Fallback `*`.
- `tab` ist `details` (Standard) oder `arbeitsblaetter`. Ein unbekannter `tab` leitet mit `replace` auf Details um.
- Das Menü markiert „Tickets“ auch auf Unterseiten.
- Neue `lib/ticketRoutes.js` mit `ticketPath(ref, tab)` und Test.
- `ref` ist `ticket.woid || ticket.$id`. Reine woid-Aufrufer übergeben die woid.
- Rückgabe ist `/tickets/` + `encodeURIComponent(ref)`, bei `arbeitsblaetter` mit `/arbeitsblaetter`.
- Ein leeres `ref` ergibt `/tickets`.
- Die angezeigte Kurznummer `$id.slice(-5)` ist nie Linkziel.
- Bestehende Links auf `/tickets?woid=N` gibt es heute an sechs Stellen. Alle nutzen künftig `ticketPath`:
- `PlanboardPage`
- `AssignProjectPage` (2×)
- `ServicesPage`
- `ProjectContextPanel`
- `CustomerFunktionen`
- Neue Links:
- Zeilen der Ticketliste (P2)
- Tickets-Reiter der Kundenseite (P4, heute ohne Link)
- Ticket-Spalte der Projekte (P5, `project.ticketId` ist eine `$id`)
- Alte Links `/tickets?woid=N` leiten mit `replace` auf `/tickets/N` um.
- Das bleibt dauerhaft, auch nach P10, weil Portal-Mails so verlinken.
- Die Umleitung liegt innerhalb der geschützten Route, damit `safeReturnPath` samt Suchteil nach dem Login greift.
### Laden
Neuer Hook `useWorkorder(ref)`:
1. Zuerst sucht er per `Query.equal('woid', [String(ref)])`, `Query.orderDesc('$createdAt')`, `Query.limit(1)`.
2. Findet er nichts, holt er das Ticket per `getDocument(ref)` als `$id`.
3. 400 und 404 gelten beide als „Ticket nicht gefunden“.
Der Hook liefert dasselbe Schreib-API wie `useWorkorders` (`updateWorkorder`). Er kennt die Zustände Laden, Fehler und „Ticket nicht gefunden“.
Arbeitsblätter und `CreateWorksheetModal` nutzen immer `ticket.woid` des geladenen Tickets, nie den URL-Parameter. Für Tickets ohne woid zeigt der Reiter Arbeitsblätter den Hinweis „Für dieses Ticket gibt es keine Ticketnummer, Arbeitsblätter sind nicht möglich.“ statt eines Fehlers. `worksheets.woid` ist Pflicht.
### Seite `pages/TicketPage.jsx`
- **Kopf:**
- Brotkrumen „Tickets / Nummer“. Der Link „Tickets“ führt mit dem Suchteil zurück, den die Liste per Link-State `listSearch` mitgibt. Ohne State führt er auf `/tickets`.
- Eyebrow „Nummer · Art“. Die Art kommt aus `ticketTypeLabel`, „Webpage“ bleibt „Webpage“.
- h1 mit dem Betreff (Rückfall „Kein Betreff“), `.sub` mit dem Kunden als Link und dem Ort.
- Aktionen: Dringend-Schalter, Status-Auswahl und der schwarze Knopf „Arbeitsblatt hinzufügen“. Der Knopf erscheint für **alle** Ticketarten, auch Akquise, wie heute. Der Prototyp blendet ihn bei Akquise aus, das wird nicht übernommen.
- **`.facts`:**
- Zuständig als Auswahl
- Termin: `startDate` mit `startTime`–`endTime`, ohne Rückfall auf das Erstelldatum, leer „Kein Termin“
- Fällig: `deadline`, leer „–“
- Einsatz: `serviceTypeLabel`
- Angefragt von, leer „–“
- Erstellt: `formatDistanceToNow`, deutsch, wie heute
- **Datumsfelder sind Freitext.**
- Angezeigt wird über `parseGermanDate` und `formatTime` aus `lib/planboard.js`.
- Ist ein Wert nicht lesbar, steht der Rohtext da.
- Nie wird `new Date()` oder `format()` direkt auf den gespeicherten Text angewendet.
- **Reiter:** „Details“ und „Arbeitsblätter (n)“.
- Details zeigt die Beschreibung als `.prose`, den Projekt-Abschnitt (`TicketProjects`, optisch bis P5 unverändert) und „Weitere Tickets von {Kunde}“ (`ProjectContextPanel`, Sichtbarkeitsregel wie heute).
- Arbeitsblätter zeigt `.stats` und die Arbeitsblatt-Zeilen.
- **Status-Auswahl** (natives `<select>`):
- Sie bietet die sechs Werte aus `TICKET_STATUSES` an, für Akquise ebenfalls.
- Ist der aktuelle Wert nicht dabei (Akquise- oder Altwert), steht er zusätzlich als gewählte Option darin.
- Geschrieben wird nur `{ status }`.
- **Zuständig** (natives `<select>`):
- Optionen: „Nicht zugewiesen“ und die Mitarbeiter mit vollem Namen.
- Ist `assignedTo` nicht in der Mitarbeiterliste, steht zusätzlich eine gewählte Option mit `assignedName`, sonst „Unbekannte Person“.
- Solange die Liste lädt, ist die Auswahl deaktiviert.
- Geschrieben wird nur `{ assignedTo }`, für niemanden ein leerer String.
- `useEmployees` läuft einmal pro Seite.
- **`TicketCard.jsx` entfällt.**
- Vorher wandert die ungetestete Logik aus `handleCreateWorksheet` in eine Lib-Funktion mit Test: Status zurückschreiben und Akquise-Ergebnis setzen.
- Testfälle: Akquise + Zugesagt → Kunde fest, Akquise + Abgesagt → verloren, Akquise + In Kontakt → keine Änderung, Nicht-Akquise → nie eine Änderung.
- `useWorksheets(ticket.woid)` und `useCustomerAbo` laufen einmal pro Seite.
- `aboHoursAllowed`, `onTogglePlan`, `canEditWorksheetTimes`, `canAddOwnSupport` und die drei Dialoge werden unverändert verdrahtet.
### Ticketliste `TicketsPage`
- **Reiter mit Zählern:** Aktiv · Offen · In Arbeit · Wartet auf Kunde · Prüfung · Erledigt · Storniert · Alle.
- Aktiv = `DEFAULT_STATUS_GROUPS` (offen, in Arbeit, wartend, Prüfung).
- Die übrigen Reiter entsprechen je einer Gruppe aus `STATUS_GROUPS`.
- Alle bedeutet: kein Statusfilter.
- „Storniert“ ist neu gegenüber dem Prototyp, damit die heutige Auswahl „nur stornierte“ nicht verloren geht.
- Startzustand: Reiter Aktiv, Art Aufträge, „Nur dringende“ aus.
- **Art-Auswahl** (`.select-quiet`) mit allen sechs heutigen `TYPE_FILTERS`: Aufträge (Standard, ohne Akquise) · Webpage · Migration · Projekt · Akquise · „Aufträge und Akquise“ (heute „Alle“).
- **Kästchen** „Nur dringende“.
- **Suche:**
- Sie läuft im Browser über `ticketMatches`, wie heute.
- Sie startet 300 ms nach der letzten Eingabe oder per Enter.
- Der Knopf „Anwenden“ entfällt.
- **Veraltete Antworten:** `useWorkorders` darf als einzige Hook-Änderung veraltete Antworten verwerfen (Anfrage-Zähler). So kann eine ältere Antwort nie eine neuere überschreiben.
- **URL-Zustand** (`useSearchParams`):
- Reiter, Art, Dringend, Suche und `limit` (Anzahl geladener Tickets, keine Seiten) stehen in der URL.
- Geschrieben wird immer mit `replace`, Standardwerte werden nicht geschrieben.
- Ungültige Werte fallen auf den Standard zurück. `limit` wird auf 10 bis 500 begrenzt.
- Jede Filteränderung setzt `limit` auf 10 zurück.
- `?woid=` hat Vorrang und leitet um.
- Die Helfer stehen in neuer `lib/ticketListUrl.js` mit Test.
- **Nachladen:**
- Beim Nachladen bleiben die alten Zeilen sichtbar, im Fuß steht dezent „Wird geladen …“.
- Den leeren Ladezustand gibt es nur beim allerersten Laden.
- „Mehr laden“ bleibt (+10), ohne Suche wie heute.
- **Zähler:**
- Je Reiter läuft eine Abfrage mit `Query.limit(1)`, die `total` liest. Sie nutzt dieselbe Art und denselben Dringend-Filter wie die Liste, nie den Suchtext.
- Die Zähler laden beim Öffnen, bei Wechsel von Art oder Dringend und nach dem Anlegen eines Tickets, nicht bei Reiterwechsel oder Tastendruck.
- Bei aktiver Suche werden sie ausgeblendet, der Fuß zeigt dann die Trefferzahl.
- Schlägt eine Abfrage fehl, fehlt nur der Zähler.
- Eigener Hook, `useWorkorders` bleibt dafür unberührt.
- **Tabellenspalten:**
- **Ticket:** Nummer, Betreff, Badge „Dringend“, darunter Kunde · Ort.
- **Status.**
- **Zuständig:** „Vorname N.“ aus der Mitarbeiterliste, Rückfall `assignedName`, sonst „Unbekannt“. Ohne Zuweisung steht „Niemand“ in Grau.
- **Termin:** wie in den Facts, Fehlwert „–“.
- **Zeit:** siehe unten.
- **Erstellt.**
- **Spalte „Zeit“:**
- Wert ist die Summe von `worksheetMinutes` über alle Arbeitsblätter ohne Kommentar. Das ist dieselbe Regel wie „Arbeitszeit gesamt“ auf der Ticketseite. Formatiert wird mit `stepLabel`.
- Geladen wird für die geladenen Zeilen in Blöcken von höchstens 100 woids: `Query.equal('woid', block)`, `Query.select([...])`, `Query.limit(5000)`, höchstens 3 Blöcke gleichzeitig.
- Das Ergebnis wird je woid zwischengespeichert, „Mehr laden“ holt nur neue Zeilen.
- Die Zeit wird höchstens für die ersten 200 Zeilen berechnet. Sind mehr geladen, nennt der Fuß das.
- Ein Block mit 5000 Treffern könnte abgeschnitten sein. Er und ein Fehler zeigen „–“, nie eine falsche Zahl. Die Liste bleibt bedienbar.
- Tickets ohne woid zeigen „–“.
- Die Summenfunktion bekommt einen Test.
- **Sortierung:** Sie bleibt `$createdAt` absteigend, der Fuß sagt „Neueste zuerst“.
- **Navigation:**
- Die Zeilen verlinken mit `state={{ listSearch: location.search }}`.
- Nach „Neues Ticket“ springt die App auf die neue Ticket-Seite (`createWorkorder` liefert die woid).
### Arbeitsblätter (`WorksheetList` neu)
- **Aufbau:** flache Zeilen `.ws` in drei Bereichen:
- links Datum und Uhrzeit
- in der Mitte Person, Einsatz (bei Kommentaren das Wort „Kommentar“), Nummer, Badges, Text, Unterstützung, „Status danach: X“ und Aktionen
- rechts die Zeit
- Es gibt kein Aufklappen, keine Verläufe und keine Animation.
- Kommentare stehen in Nebentextfarbe.
- **Git-Push:** ohne Zeit Badge „Zeit offen“ (orange), mit Zeit „Zeit eingetragen“ (grün).
- **Abo:** Der Umschalter heißt „Abo“ beziehungsweise „+ Abo“. Seine Sichtbarkeitsregel bleibt exakt: `onTogglePlan && !isComment && totalTime > 0 && (aboHoursAllowed || countsToPlan)`.
- **Bedingungen, die bleiben:** `canEditTimes`, `canAddSupport`, `isGit` und `needsTimeEntry`.
- **Texte, die bleiben:** „Zeit nachtragen“, „Zeiten bearbeiten“, „Ich habe unterstützt“ und „Meine Unterstützung bearbeiten“. Letzteres erscheint, sobald man schon unterstützt. `OwnSupportModal` behält „Eintrag entfernen“ und `maxLength` 200.
- **Reihenfolge:** neueste zuerst.
- **Zeitformat:** überall `stepLabel` („1 h 30 min“, heute „1h 30min“).
- **Kennzahlen:**
- Arbeitszeit gesamt = Summe `worksheetMinutes` ohne Kommentare
- davon Abo-Stunden = dieselbe Summe über `countsToPlan && !isComment`
- Zeiten offen = Anzahl `needsTimeEntry`, orange bei mehr als 0
## Bewusste Änderungen am Verhalten
| Bisher | Künftig |
|---|---|
| Ticket klappt in der Liste auf | eigene Seite mit Link |
| Statusfilter als Mehrfachauswahl mit „Anwenden“ | Reiter (Einfachwahl) |
| Suche nach Klick auf „Anwenden“ | Suche nach Tipp-Pause |
| Footer auf jeder Seite | Footer entfällt, Text steht im Login-Fuß |
| Menü einklappbar | nicht mehr einklappbar, auf dem Handy eine Schublade |
| Dialoge schließen nur mit × | Dialoge schließen mit × und neu mit Esc |
| Statusfarben: Offen grün, Erledigt grau | Farben wie in der Tabelle oben. Das Team wird beim Live-Gang von P1 informiert. |
| Kein Link auf die Tickets einer Kundenseite oder in der Projektliste | beide verlinkt |
Alle anderen Funktionen bleiben erhalten.
## Pakete
### Arbeitsweise
- **Branch:** Gearbeitet wird im Worktree `dev-design` auf `feature/design-weiss`, abgezweigt von `test` 603d08c.
- **Commits:** Jedes Paket ist ein eigener Commit-Stapel. `git add` nur mit ausdrücklichen Pfaden.
- **Rebase:** Vor jedem Paket und unmittelbar vor jeder Abnahme läuft `git fetch origin && git rebase origin/test`, kein Merge.
- **Tag:** Erst danach wird das Tag `design-p<N>` auf den abgenommenen Stand gesetzt und nicht mehr bewegt. Korrekturen nach dem Push sind Folge-Commits mit eigenem Tag `design-p<N>-fix<k>`.
- **Live-Gang:** Live geht ein Paket nur über das Tag, nie über die Branch-Spitze. Der Eigentümer führt aus:
`git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p<N>^{commit}:refs/heads/test'`
Das ist ein reiner Fast-Forward. Seit 2026-09-30 deployt nur `test`. Vor dem Push nennt der Agent die genaue Zeile, den SHA und `git log --oneline origin/test..design-p<N>`.
- **Nach dem Push:**
- Der Agent prüft im Log von `webklar-ticket-build` die Zeile „Deployment completed successfully“.
- Er vergleicht den Asset-Hash von `https://ticket.webklar.com/` mit `dist/index.html`.
- Er macht einen kurzen Sichttest im Chrome des Eigentümers, nur lesend.
- **Zurücknehmen:** Per Revert-Commit, immer das jüngste Paket. Ein früheres Paket geht nur zusammen mit allen späteren: `git revert --no-commit design-p<N-1>..design-p<N>`.
- **Reihenfolge:**
- Live gehen die Pakete in der Reihenfolge P1 bis P10.
- P3 bis P5 brauchen `ticketPath` aus P2.
- P4 liegt vor P5, weil P4 den Aufruf von `CreateCustomerForm` in `AssignProjectPage` umstellt.
- P7, P8 und P9 hängen fachlich nicht voneinander ab, gehen aber auf demselben Branch in dieser Reihenfolge live.
- **Parallelarbeit:** Am nächsten Paket wird erst nach dem Tag des vorigen gearbeitet. Die Commits liegen hinter dem Tag und gehen deshalb nicht mit live.
### Inhalt
| Paket | Umfang | Danach sichtbar |
|---|---|---|
| **P0** Vorbereitung | `LeadsPage.jsx` und `hooks/useLeads.js` löschen. Beide sind toter Code. Die `/leads`-Umleitung, der pausierte Menüpunkt und `lib/leads.js` bleiben. `node_modules` wird aus Git genommen (`git rm -r --cached node_modules`; `.gitignore` führt es schon). Der Deploy installiert ohnehin neu. Im Worktree folgt ein echtes `npm ci` unter `systemd-run` mit Speichergrenze, ohne Symlink. Danach muss `git status --short` ohne `??`-Zeilen leer sein. Das Screenshot-Werkzeug entsteht außerhalb des Repos (siehe Abnahme), und es werden Vorher-Bilder erzeugt. Fertig, wenn `npm test` und `npm run build` im Worktree grün laufen und ein Befehl alle Vorher-Bilder erzeugt. | nichts |
| **P1** Fundament | Tokens, Aliase und KOMPAT, Basis, Gerüst, Menü, Logo, Favicon. Alle `ui/*`-Bausteine samt `Dialog` und `Stat`, außerdem `statusTone` und der neue Login. Löschen: Lato, `Footer.jsx`, `LetterGlitch.jsx`, `lib/sidebar.js` samt Test und tote CSS-Regeln laut Skript-Liste. Pflicht-Fixes gegen Unlesbares: Chat-Variablen in `chat.css` unter `.woms-chat` (`--chat-on-accent: #fff`, `--chat-line: var(--line-strong)`, `--chat-muted: var(--text-2)`, Fehler- und Hinweistexte `--red-fg`/`--text-2`). `.admin-primary-button` bekommt weiße Schrift, Hover `--accent-hover`. Farbliterale umstellen in `AssignedProjectCard`, `PreviewBlock`, `PreviewLinkButton`, `GiteaLinkButton`, `AnalyseRow`, `CreateWorksheetModal` und `WorksheetTimesModal` (`#a0aec0`) sowie die helle Warn- und Fehlerschrift in `admin-areas.css`, `serverpuls.css`, `AdminPage.css` und im Finanz- und Rechtlich-Teil von `global.css`. `chartColors.js` wird angelegt und in `Chart.jsx` und `ServerpulsPage` benutzt. Abschluss-Prüfung: `rg` über alle Farbliterale in `src`. Jedes Literal mit Kontrast unter 3:1 auf Weiß, jede dunkle Schrift auf `--accent` und jedes Weiß-Alpha ist entweder behoben oder im Plan mit Zielpaket als bewusst offen eingetragen. | ganze App weiß, Menü als Text, neues Login, alle Dialoge weiß; Innenlayouts noch alt, aber lesbar |
| **P2** Tickets | Ticketliste, Ticket-Seite, Route, `ticketRoutes.js`, `ticketListUrl.js` und Arbeitsblätter. Dialoge `CreateTicketModal`, `CreateWorksheetModal`, `WorksheetTimesModal`, `OwnSupportModal`; `SupportersEditor` als Feldblock in den Arbeitsblatt-Dialogen. Native Auswahlfelder statt `StatusDropdown`/`EditorDropdown`, dazu der Dringend-Schalter und „Weitere Tickets“. Die sechs Link-Stellen wechseln auf `ticketPath`. Altklassen `.tcard*`, `.dropdown*`, `.ticket-tab*`, `.worksheet-summary` und `.chevron` löschen. „Neues Ticket“ bleibt inhaltlich wie heute, also automatische Zuweisung und kein Einsatzfeld. | Ticket-Arbeitsplatz wie im Prototyp |
| **P3** Planboard | `.week` mit sieben Spalten, Haarlinien und Wochenende `--bg-subtle`. Heute als schwarze Datumspille, Termine als Links auf `ticketPath`. Akquise-Termine hell orange, stornierte bleiben sichtbar wie heute. Kopf: „Kalenderwoche N · Datum bis Datum · n Termine“, Aktionen ← · Heute · →. Unter 860 px eine Spalte. | Planboard ohne Karten |
| **P4** Kunden | Kundenliste mit Reitern Feste Kunden · Leads · Archiviert · Alle. Die Zähler nutzen dieselbe Filterfunktion wie die Liste (`lib/leads.js`). Dazu Suche und Tabelle mit Unternehmen und Ansprechpartner, Kundennummer, Standort, E-Mail, Telefon. Badges „Lead“, „Abgesagt“, „Archiviert“ neben dem Namen wie heute; „Wichtig“ als Badge statt Stern. Kopf: für Admins „Neuer Kunde“ schwarz und „Neuer Lead“ als Standardknopf, für andere nur „Neuer Lead“ als Standardknopf. `CreateCustomerForm` und `CreateLeadForm` werden Dialoge, Hinweis- und Knopftexte bleiben; der Aufruf in `AssignProjectPage` wird mit umgestellt. Kundenseite mit Brotkrumen, Eyebrow und Reitern; ihr Tickets-Reiter verlinkt die Tickets. Übersicht als `.two-col` mit `.kv`, Stammdaten bleiben direkt bearbeitbar. `CustomerFunktionen` als Tabelle, die weiter `abgeschaltetText` zeigt (kommt aus tp2, siehe Risiken). Außerdem `AboStatusCard`, `CustomerViewLog`, `KundenansichtButton` (Standardknopf) und Grundstil von `admin-areas.css`. | Kundenbereich wie im Prototyp |
| **P5** Projekte | `ProjectsPage` als eine Tabelle: Projekt + Repo, Kunde (Auswahl je Zeile wie heute, deaktiviert bei archiviert oder während der Zuordnung, Rückfrage beim Entfernen), Art, Adresse, Ticket als Link, Status, Zuletzt geändert = `$updatedAt`, Aktionen. Die Aktionen sind Textlinks, nur wenn zutreffend: „Vorschau öffnen“, „In Gitea öffnen“, „Hosten“ (mit Ladezustand), „Aktualisieren“. Die erste Zelle ist ein `<button class="link">` mit dem Projektnamen und öffnet „Projekt bearbeiten“. Auf dem Handy bleiben Projekt, Kunde, Status und Aktionen sichtbar. Die heutigen sechs Ansichten werden 1:1 Reiter: Aktive Projekte · Unzugeordnet · Zugeordnet · Vorlagen (nur Admins) · Archiviert · Alle inkl. Archiv. Dazu Suche, Auswahl Kunde und Mitarbeiter, „Weitere Treffer“ und Hinweiszeile. „Neues Projekt“ als Dialog mit Repo-Name. `PreviewBlock` steht außerhalb des Formulars, der Dialog ist `wide`. Die Liste zeigt „Wird geladen …“ nur beim ersten Laden. Außerdem: `AssignProjectPage` mit Schritten als Abschnitten und Auswahl auch als Text markiert; `ServicesPage`; `TicketProjects`; `AssignedProjectCard`; `AnalyseRow` (Zustimmung als Dialog, Wortlaut unverändert); `PreviewBlock`; `PreviewLinkButton` („Vorschau öffnen“); `GiteaLinkButton` („In Gitea öffnen“); `CopyableCredential` („Anzeigen“/„Verbergen“, „Kopieren“/„Kopiert“). | Projektbereich wie im Prototyp |
| **P6** Kunden-Unterseiten | Chat: Liste als Haarlinien-Zeilen. Alle Regeln, die Elemente im Verlauf und in der Eingabezeile gestalten, tragen das Präfix `.woms-chat`, bei Bedarf `.woms-chat.woms-chat`, nie `!important`. Das Seitenlayout (`.chat-page`, `.chat-side`, `.chat-list*`, `.chat-main`, `.chat-back`) bleibt ohne Präfix. Fremde Blase `--bg-hover`, eigene `--grey-bg`. `loadChatComposer.js`, sein Test und die Container in `ChatThread.jsx` bleiben unverändert. Gutschriften: Filter als Reiter, Tabelle mit Spaltenpriorität, Detail als `.two-col`, Knöpfe Genehmigen (schwarz), Rückfrage (Standard), Ablehnen (rot als Text). Abo-Angebote: Tabelle, Formular als Dialog. E-Mail-Speicher: Tabelle mit `data-storage-done`. | Chat und Kunden-Unterseiten hell |
| **P7** Finanzen | Finanz-CSS neu (ca. 320 Zeilen), alle Finanzkomponenten. `LiquidityHead` wechselt von `Kpi` auf `Stat`, der `Kpi`-Export wird gelöscht. Dialoge `AssignDialog` und `InvoiceCreateModal` (`wide`). Bleiben: die Anker `INVOICES_ANCHOR` und `finanzen-buchungen` samt `scroll-margin-top`, die Klassen-Hooks, `data-label`, `role`-Attribute und Container-Queries; `container-type` wandert auf die Wrapper. Beträge behalten ihr Vorzeichen, negative sind rot. | Finanzen ohne Karten |
| **P8** Rechtliches, Admin | `LegalPage`, `LegalTextsTab`, `ProviderContractsTab` als Tabelle, `ContractForm`/`VersionForm` als Dialoge. `AdminPage` mit Tabelle, Formular als Dialog und Textaktionen „Bearbeiten“/„Löschen“. Fehler erscheinen als `.msg.err` im Dialog, der erst bei Erfolg schließt. Die einmal angezeigten Zugangsdaten bleiben ein Abschnitt auf der Seite. `AdminPage.css` wird neu geschrieben; `.credential*`, `.sr-only` und `.spin` ziehen nach `global.css` um. | Rechtliches und Admin ohne Karten |
| **P9** Serverpuls | Kopf mit Text-Badge statt Emoji. Warnungen als Listenzeilen mit Stufen-Badge, Kacheln als `.stats` mit Sparkline. Deltas als Textfarbe (rot = gestiegen, grün = gesunken), Umschalter `.seg`. Alle `data-testid` bleiben. | Serverpuls hell |
| **P10** Aufräumen | `react-icons` und `@tabler/icons-react` deinstallieren, sobald `rg "react-icons\|@tabler" src` 0 Treffer liefert. KOMPAT-Block, Aliase und tote Klassen löschen. `previewEnv.dotColor` samt Test entfernen. Die Kommentare zu Icons in `navLinks.js` und im Test anpassen. Die Bundle-Größe vorher und nachher dokumentieren. Die `?woid=`-Umleitung bleibt. | nichts Neues, kleineres Bundle |
### Seiten ohne Prototyp-Vorlage
Gemeint sind P6 bis P9, `ServicesPage` und `AssignProjectPage`. Sie entstehen aus denselben Bausteinen nach festen Ersetzungen:
- Karte → Abschnitt
- Kachelraster → Tabelle oder Liste
- farbiger Streifen → Badge
- Icon-Knopf → Textknopf oder Textlink
Der Eigentümer nimmt diese Pakete anhand von Screenshots ab, bevor er pusht.
### Texte
Texte bleiben wörtlich erhalten. Ausnahmen sind nur diese neuen oder geänderten Texte:
**Login:** „Anmelden“, „E-Mail“, „Passwort“, „Weiter“, „Code eingeben“, „Code erneut senden“, „Abbrechen“ sowie der Untertitel „Ticketsystem für das WEBklar-Team“.
**Menü und Laden:** „Abmelden“, „Menü“, „Wird geladen …“ statt „Loading...“.
**Ticketliste:**
- Reiter und Art-Eintrag „Aufträge und Akquise“
- Fuß „Neueste zuerst“
- „Niemand“, „Unbekannt“
**Ticketseite:**
- „Nicht zugewiesen“ (statt „(Unassigned)“), „Unbekannte Person“
- „Kein Termin“
- „Weitere Tickets von {Kunde}“
- „Status danach: X“
- Hinweis für Tickets ohne Ticketnummer
- Zeitformat `stepLabel`
**Weitere Seiten:**
- Planboard-Kopf
- Badge „Wichtig“
- Spaltenköpfe der neuen Tabellen
**Knöpfe:** Textknöpfe, die bisher Icons waren:
- „Vorschau öffnen“, „In Gitea öffnen“
- „Bearbeiten“, „Löschen“, „Entfernen“, „Lösen“
- „Anzeigen“, „Verbergen“, „Kopieren“, „Kopiert“
- „Erneut versuchen“, „Aktualisieren“, „Hosten“
**Ladetexte in Knöpfen:** „Speichert …“, „Sendet …“, „Wird geladen …“.
## Was gleich bleibt
**Verhalten:**
- Alle Hooks und alle Lib-Logik. Einzige Ausnahme: das Verwerfen veralteter Antworten in `useWorkorders`. Neue Hooks sind erlaubt (`useWorkorder`, Zähler, Zeitsummen).
- Auth und 2FA.
- Alle Abfrage-Intervalle.
- Pflichtfelder und 15-Minuten-Schritte.
- Doppelklick-Sperren.
- Alle `window.confirm`-Rückfragen.
- Alle `data-testid`.
- Die URL-Parameter `?tab=`, `?antrag=`, `?kunde=`.
**Daten:**
- Appwrite-Schema, Rechte und gespeicherte Werte (englische Status, die das Kundenportal auswertet).
- Server-APIs.
- Portal-Repo.
- `.env*`: Latin-1, nicht anfassen.
**Texte:**
- Alle rechtlich relevanten Texte:
- Zustimmung zur Website-Analyse
- Abo-, Gutschrift- und § 19-Hinweise
- Steuer-Hinweis
- Mail- und Chat-Vorlagen.
- Fehlermeldungen.
## Risiken und Gegenmaßnahmen
| Risiko | Gegenmaßnahme |
|---|---|
| Ein einziges CSS-Bündel: umbenannte Klassen brechen Seiten, die gerade niemand ansieht | KOMPAT ab P1. Am Ende von P1 ein Skript, das alle in JSX und `lib/*.js` benutzten Klassen gegen die CSS-Regeln abgleicht; Ergebnis im Plan. Screenshot-Pflichtliste nach jedem Paket. |
| Namensgleiche Klassen mit anderer Bedeutung: `.btn`, `.badge`, `.page`, `.field`, `.seg`, `.toolbar`, `.empty`, `.list-row`, `.urgent-toggle`, `.overlay`, `.id` | in P1 bewusst ersetzen, jede Altverwendung im Screenshot prüfen |
| 79 `!important` im Altbestand | löschen statt überschreiben |
| Alt-Dialoge bleiben bis P2/P5/P7/P8 in Betrieb | KOMPAT-Regeln für `.overlay`, `.overlay-content`, `.overlay-close`. P1-Abnahme: jedes der 10 Alt-Dialoge bei 390×700 und 1440×700, Speichern-Knopf erreichbar. |
| Fremder Chat-Composer, sein CSS lädt nach dem WOMS-CSS | Präfix-Regel und `--chat-*`-Variablen, siehe P1 und P6 |
| Diagramme auf Canvas | Farbkonstanten statt Variablen, ab P1 hell |
| Statusfarben ändern ihre Bedeutung | Hinweis an das Team beim Live-Gang von P1 |
| Dokument-Scroll statt innerem Scroller: `position: sticky` in AdminPage, Scroll-Anker der Finanzen | in der Abnahme gezielt prüfen |
| Vorschlagslisten in scrollenden Dialogen werden abgeschnitten | in P2, P6 und P7 prüfen |
| Freitext-Datumsfelder, keine Fehlergrenze: ein Parserfehler legt die App lahm | nur über `parseGermanDate`/`formatTime` anzeigen. Testdaten mit Altwerten: `2.5.2026`, `31.02.2026`, ISO-Datum, leer, ohne woid, unbekannter Status. |
| Paralleler Branch `feature/terminplaner-tp2`: Commit 6f03712 ändert `CustomerFunktionen.jsx`, `lib/funktionenView.js` (neue Funktion `abgeschaltetText`) und den Test (+4 Tests) | vor P4 nach `test` bringen (Eigentümer) oder per `git cherry-pick 6f03712` in `dev-design` holen. Die Testzahl steigt dann um 4. |
| `node_modules` aus Git nehmen (P0) | betrifft nur Git, nicht den Deploy, weil `deploy.sh` löscht und neu installiert. Andere Worktrees mit Symlink sind unkritisch. |
| Speicher knapp | Installation, Build und Screenshots unter `systemd-run` mit Speichergrenze |
## Abnahme
Ein Paket ist fertig, wenn alles Folgende erfüllt ist:
1. **Tests:** `npm test` läuft im Worktree grün. Die Testzahl wird je Paket notiert. Ausgangsstand 312; P1 minus 3 (`sidebar`) plus neue Tests; nach tp2 plus 4. Neue Tests gibt es für `statusTone`, `ticketPath`, `ticketListUrl`, die ausgelagerte Arbeitsblatt-Logik und die Zeitsummen.
2. **Build:** `npm run build` ist grün, die Bundle-Größe wird notiert.
3. **Screenshots** mit 1440 px und 390 px über die Pflichtliste:
- Login inklusive beider Code-Schritte
- Ticketliste (mit und ohne Suche), Ticket mit beiden Reitern, alle Ticket-Dialoge
- Planboard
- Kundenliste und Kundenseite mit allen Reitern
- Chat
- Gutschriften, Abo-Angebote, E-Mail-Speicher
- Projekte mit beiden Dialogen, Zuordnen, Services
- Zustimmung der Website-Analyse
- Finanzen mit Dialogen
- Rechtliches
- Admin inklusive „Mitarbeiter anlegen“
- Serverpuls mit Diagrammen
- in P1 zusätzlich die 10 Alt-Dialoge
4. **Screenshot-Werkzeug:**
- Es liegt unter `/root/reports/ticketsystem-design-2026-10-04/werkzeug/`, außerhalb des Repos, und ändert weder `package.json` noch Lock noch `tests/`.
- Aufbau: esbuild-Bündel und Playwright Headless mit nachgebildetem Appwrite. Es nutzt nur Testdaten, inklusive der Altwerte aus der Risikotabelle, keine Produktionsdaten und keine echten Logins.
- Es läuft unter `systemd-run` mit `MemoryMax`.
- Die Ausgabe liegt je Paket unter `shots/p<N>/1440` und `shots/p<N>/390`.
5. **Code-Hygiene im umgebauten Bereich:**
- keine Icon-Importe
- keine neuen `style={{…}}`, außer für Werte, die erst zur Laufzeit feststehen (z. B. berechnete Breiten)
- Texte nur wie unter „Texte“ geändert
6. **Code-Review:** Ein Review (sonnet) ist gelaufen, wichtige Befunde sind behoben.
7. **Freigabe:** Der Eigentümer hat die Screenshots gesehen, bevor er das Tag pusht.
@@ -0,0 +1,694 @@
> Anhang zu `2026-10-04-weisses-design-design.md`: Ergebnis der Bestandsaufnahme vom 2026-10-04 (6 Leser + Synthese, nur lesend). Bei Widersprüchen gilt das Konzept. Entschieden sind inzwischen: Akzent Schwarz, Ticket als eigene Seite (Abschnitt 6) und alle Standards aus Abschnitt 8.1, außer wo das Konzept etwas anderes festlegt (Tab-Zähler und Spalte "Zeit" werden mitgeliefert, Planboard zeigt Stornierte, Kundenliste ohne "Offene Tickets", Projekte-Spalte "Zuletzt geändert" = `$updatedAt`).
# Konvertierungskarte WOMS: dunkel zu weiß (Prototyp, Akzent Schwarz #111)
Stand 2026-10-04. Repo `/home/webklar/apps/ticket.webklar.com/build` (Branch `test` = `603d08c` = `origin/test`).
Quellen sind der Prototyp (`/root/reports/ticketsystem-design-2026-10-04/quelle/styles.css`, 545 Zeilen, vollständig gelesen, und `app.js`), das Leser-Inventar (104 Dateien) und eigene Stichproben. Was ich selbst nachgeprüft habe, steht unter 0 und ist im Text mit [geprüft] markiert. Der Eigentümer hat Schwarz bestätigt ("schwarz passt"). Es gibt keine Akzent-Varianten (`html[data-accent]`) und keine `.proto`-Leiste.
---
## 0. Befunde aus der Gegenprüfung, die die Planung ändern
1. **Der Deploy-Webhook deployt nur `test`.** Die Aufgabenstellung und die Projektnotiz `woms-ticketsystem.md` gehen noch von "jeder gepushte Branch geht live" aus [geprüft].
- `/home/webklar/apps/ticket.webklar.com/webhook-server.js` hat seit 2026-09-30 12:51:29 UTC `DEPLOY_BRANCH = "test"` ("ignoring push to X (only test deploys)").
- Der Container `webklar-ticket-build` startete 12:51:56 UTC und mountet die Datei `:ro`, läuft also mit der neuen Version.
- In den Logs der letzten 72 h stehen nur Pushes auf `test`. Ein Push auf einen Feature-Branch wurde nie getestet.
- Empfehlung: Feature-Branches weiter nicht pushen, bis ein Probe-Push den Lock bestätigt hat. Der Eigentümer bringt Pakete live mit `git push origin <branch>:test`.
2. **`build/` ist das Live-Checkout.** Es ist im Container als `/app` eingehängt [geprüft].
- `deploy.sh` macht dort `git reset --hard origin/test`, `git clean -fd`, `rm -rf node_modules` und `npm install`.
- Dort wird nie entwickelt. Entwicklung läuft in einem Worktree, zum Beispiel `dev-design` auf `feature/design-weiss` (von `test` abgezweigt), wie die vorhandenen `dev-*`-Worktrees.
- Es gibt kein Staging. Jeder Push auf `test` ist Produktion, ein Poll alle 15 Minuten baut zusätzlich neue Commits auf `test`.
3. **`feature/terminplaner-tp2` ist divergiert, nicht nur "1 voraus"** [geprüft].
- Der Branch liegt 1 Commit voraus und 3 hinter `test`. Alle anderen `dev-*`-Branches sind vollständig in `test` enthalten (ahead = 0).
- Der eine Commit `6f03712` ("Funktionen-Reiter: Abschaltgrund und Löschdatum") ändert `src/components/customer/CustomerFunktionen.jsx` (3 Zeilen), `src/lib/funktionenView.js` (+21) und `tests/unit/funktionenView.test.mjs` (+36).
- Konfliktpotenzial hat nur die 3-Zeilen-Änderung in `CustomerFunktionen.jsx`, und die wird in Paket 4 neu gebaut. Siehe Abschnitt 7.
4. **Ticket als eigene Seite ist laut Projektnotiz `woms-theme-weiss.md` schon entschieden.** Dort steht: "User decided: a ticket opens as its OWN PAGE (route per ticket, tabs Details/Arbeitsblätter), not inline expand." Die Aufgabe nennt die Frage noch offen. Abschnitt 6 begründet die Empfehlung unabhängig davon. Falls die Entscheidung nur mündlich fiel, bitte kurz bestätigen lassen.
5. **Latin-1 gibt es nur in `.env.development` und `.env.production`** (ISO-8859, getrackt, nur Kommentarzeilen mit "Öffentliche … für") [geprüft]. Alle 93 JSX- und 65 JS-Dateien, Tests, CSS und `index.html` sind UTF-8 ohne BOM und ohne CRLF. Die `.env*` bleiben unangetastet.
6. **`node_modules` ist teilweise in Git getrackt**: 8.890 von 9.117 getrackten Dateien (`date-fns` 5.722, `caniuse-lite` 837, `react-icons` 147, `@tabler` 0) [geprüft].
- Für den Build ist das egal, weil `deploy.sh` `node_modules` löscht und neu installiert.
- Praktische Folge: In Worktrees nur mit expliziten Pfaden `git add` und nie `git add -A`. Ein `npm uninstall` in einem Worktree zeigt sonst 147 gelöschte getrackte Dateien.
7. **Baseline:** `npm test` = 312 Tests, 312 grün, 0,8 s [geprüft]. Bundle heute: JS 680.818 B, CSS 64.433 B, Schrift `lato-regular.ttf` 96.184 B.
8. **`LeadsPage.jsx` ist toter Code** [geprüft]. Sie wird nirgends importiert, `/leads` leitet auf `/tickets` um, und `hooks/useLeads.js` wird nur von ihr benutzt. `lib/leads.js` dagegen lebt (Kunden-Seiten) und bleibt.
9. **Das Test-Rezept für UI** steht in `woms-ticketsystem.md`. Vite dev wird per OOM getötet. Stattdessen gilt: esbuild-Harness unter `systemd-run` mit `MemoryMax=400M`, dann Playwright-Headless mit gemocktem Appwrite. Die Skripte sind weg und müssen in Paket 0 neu gebaut werden.
---
## 1. Ausgangslage in Zahlen
### 1.1 JSX nach Bereich (eigene Zählung, `style={`, Farbliterale `#rrggbb`/`#rgb`/`rgb(a)(`, Icon-Tags `<Fa…/Icon…/Si…>`)
| Bereich | JSX-Dateien | Zeilen | Inline-`style` | Farbliterale | Icon-Tags | Dateien mit Icon-Import |
|---|---|---|---|---|---|---|
| 1 Grundgerüst/UI/Login (App, Navbar, Footer, AdminRoute, LoginPage, LetterGlitch, `ui/*` 12) | 19 | 1.086 | 39 | 17 | 17 | 1 |
| 2 Tickets, Arbeitsblätter, Projekt-Block, Planboard | 20 | 2.766 | 160 | 82 | 42 | 16 |
| 3 Kunden, Chat, Gutschriften, Angebote, Speicher, Leads | 18 | 2.524 | 107 | 14 | 68 | 15 |
| 4 Projekte, Zuordnen, Services, AnalyseRow | 4 | 1.419 | 95 | 4 | 25 | 3 |
| 5 Admin, Finanzen, Rechtliches, Serverpuls | 31 | 3.726 | 7 | 14 | 35 | 16 |
| AuthContext | 1 | 272 | 0 | 0 | 0 | 0 |
| **Summe** | **93** | **11.793** | **408** | **131** | **187** | **51** |
| davon LeadsPage (wird gelöscht) | 1 | 614 | 53 | 14 | 25 | 1 |
| **Summe ohne LeadsPage** | **92** | **11.179** | **355** | **117** | **162** | **50** |
Die größten Inline-Style-Träger sind `ProjectsPage` (37), `WorksheetList` (33), `AssignProjectPage` (31), `PreviewBlock` (30), `LoginPage` (19), `AssignedProjectCard` (18), `AnalyseRow` (16), `TicketsPage` und `CustomersPage` (je 13) und `PlanboardPage` (12). Fast alles ist Layout (flex, gap, margin, fontSize). Echte Farben stehen nur in `WorksheetList` (Gradienten, Weiß-Alphas, `#9d3dfe`), `AssignedProjectCard`, `PlanboardPage`, `PreviewBlock`, `LoginPage`, den Link-Buttons und `LeadsPage`.
### 1.2 CSS und Sonstiges
| Datei | Zeilen | Besonderheit |
|---|---|---|
| `src/styles/global.css` | 1.476 | 568 Regeln, 378 Klassennamen, 272 `var()`-Zeilen, 79 `!important`, 18 Gradienten, 104 Hex + 90 rgb(a); 3 Schichten: L1-794 Legacy-Dunkel, L796-1120 "Redesign auf dunklen Tokens", L1122-1476 Rechtliches (L1122-1153) + Finanzen (L1155-1476) |
| `src/pages/AdminPage.css` | 416 | 16 Farbliterale, `--admin-line` als Weiß-Alpha; enthält auch geteilte `.credential*`, `.sr-only`, `.spin` |
| `src/styles/serverpuls.css` | 74 | 9 Literale; L70-74 `.side-dot*` gehören zur Sidebar |
| `src/styles/admin-areas.css` | 61 | 5 Literale, 7 Importeure |
| `src/styles/chat.css` | 52 | grün verdrahtet, `var(--surface)` ist undefiniert (Altfehler, L13) |
| `src/lib/previewEnv.js` | 29 | 4 Hex (6 Vorkommen) in `dotColor()` |
| `src/assets/fonts` | 2 Dateien | Lato 400 (96 KB) + `OFL-Lato.txt` |
- Tote CSS-Klassen: 66 Namen, etwa 88 Regeln und 285 Zeilen ohne jede Verwendung, dazu 3 tote Keyframes (`slideDown`, `fadeIn`, `slideIn`).
- Klassen ohne Regel: `p-4`, `btn-sm` (sitzt in 7 JSX-Dateien!), `status-filter`, `supporters-editor`, `timeline` und rund 35 Bootstrap-Namen in `WorksheetList.jsx` (`d-flex`, `py-3`, `col-md-6`, …). Sie tun heute nichts.
- Breakpoints heute: 640, 700, 720 (x3 plus Sidebar-Auto-Collapse), 760, 768, 900, 1060, 1100 und Container Queries 560, 640, 860 und 960. Der Prototyp kennt nur 1100 und 860.
- Tests: 34 Dateien, 312 Tests, nur reine Logik (`node --test`, kein jsdom). Genau ein Test greift Markup (`adminRoute.test.mjs`).
### 1.3 Aufwandsklassen der 104 betrachteten Dateien (JSX, JS, CSS, `index.html`, fremde Chat-Datei)
| Bereich | trivial | klein | mittel | groß |
|---|---|---|---|---|
| Grundgerüst/UI/Login | 13 | 7 | 4 | 1 (`global.css`) |
| Tickets/Planboard | 4 | 7 | 6 | 3 (`TicketsPage`, `TicketCard`, `WorksheetList`) |
| Kunden/Chat/… | 7 | 5 | 8 | 2 (`CustomerDetailPage`, `CreditDetail`) |
| Projekte | 0 | 2 | 1 | 1 (`ProjectsPage`) |
| Admin/Finanzen/Recht/Puls | 9 | 13 | 10 | 1 (`InvoiceCreateModal`) |
| **Summe** | **33** | **34** | **29** | **8** |
**Triviale Dateien** (nur Klassen oder Text, kein Layout):
- Grundgerüst: `main.jsx`, `index.html` (+Favicon), `Footer.jsx` (löschen), `LetterGlitch.jsx` (löschen), `AuthContext.jsx` (keine Änderung), `lib/navLinks.js`, `ui/index.js`, `DateTimeInput`, `DurationSelect`, `EmptyState`, `Field`, `PageHeader`, `AdminRoute` (testgebunden, siehe 7.4).
- Tickets: `UrgentToggle`, `PreviewLinkButton`, `GiteaLinkButton`, `CopyableCredential`.
- Kunden: `KundenansichtButton`, `CreditsPage`, `CreditsView`, `OffersPage`, `OffersView`, `LeadsPage` (löschen), `loadChatComposer.js` (nicht anfassen).
- Admin: `FinanceSettingsPage`, `BankCallbackPage`, `LegalPage`, `AddressWarning`, `CustomerInvoices`, `FormField`, `Pager`, `legalUi`, `Sparkline`.
- Logik: alle `src/hooks/*`, alle `src/lib/*` bis auf die vier in 5.P1 genannten.
---
## 2. Design-Tokens
### 2.1 Finaler `:root`-Block für WOMS
Quelle ist `styles.css` L5-44 mit festem Akzent Schwarz. Dazu kommen wenige Ergänzungen für Stellen, an denen der Prototyp nur Literale benutzt.
```css
:root {
--bg: #ffffff;
--bg-hover: #f6f8fa;
--bg-subtle: #f8f9fb;
--text: #1a1f36;
--text-2: #5e6678;
--text-3: #8a93a3;
--line: #ebeef1;
--line-strong: #d8dde3;
--accent: #111111;
--accent-hover: #2b2b2b;
--accent-ink: #111111;
--accent-soft: rgba(17, 17, 17, 0.06);
--accent-ring: rgba(17, 17, 17, 0.14);
--grey-bg: #ebeef1; --grey-fg: #414552;
--blue-bg: #e0f2fe; --blue-fg: #075985;
--orange-bg: #fcedb9; --orange-fg: #a82c00;
--violet-bg: #efeafe; --violet-fg: #5b21b6;
--green-bg: #d7f7c2; --green-fg: #006908;
--red-bg: #ffe7f2; --red-fg: #b3093c;
--danger: #c0123c;
--radius: 6px;
--radius-lg: 12px;
--ring: 0 0 0 1px rgba(60, 66, 87, 0.16), 0 1px 1px rgba(0, 0, 0, 0.06);
--shadow-btn: 0 1px 1px rgba(0, 0, 0, 0.12), 0 2px 5px rgba(60, 66, 87, 0.08);
--shadow-pop: 0 0 0 1px rgba(60, 66, 87, 0.06), 0 12px 32px rgba(26, 31, 54, 0.14), 0 2px 6px rgba(26, 31, 54, 0.06);
--sidebar-w: 232px;
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
/* Ergänzungen WOMS (im Prototyp nur als Literale) */
--nav-dot: #e5a000; /* .nav-dot */
--danger-ring: rgba(192, 18, 60, 0.18); /* aria-invalid statt Rahmenfarbe */
--overlay: rgba(26, 31, 54, 0.32);
--scrim: rgba(26, 31, 54, 0.24);
--appt-akquise: #fdf4e3; --appt-akquise-hover: #fbeccd; --appt-hover: #eef1f4;
}
html { color-scheme: light; } /* ersetzt `color-scheme: dark` an .datetime-pair (global.css L1077) */
```
**Basis:** `body` 14px/20px, `font-family: var(--font)`, `-webkit-font-smoothing: antialiased`. Dazu `button,input,select,textarea{font:inherit}` (fehlt heute, Native-Controls rendern in der Browserschrift), `a{color:inherit;text-decoration:none}`, `:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px}`.
**Typo-Skala:**
- h1 28/34 700 (mobil 24/30), Login-h1 24/32 700, Section-h2 16/24 600, Dialog-h3 18/26 600.
- Body 14/20, `.small` 13, `.xs` 12/16, `.stat .v` 22/30 600 (mobil 18).
- `.num` setzt tabular-nums explizit, weil `body` `tnum 0` hat. Alle `.fin-amount` und Betragsspalten behalten tabular-nums.
- Alle heutigen Gewichte 800 (Kpi 28/800, `.fin-fact-big`, `.fin-low`, `.sp-tile-value`, `.page-title` 24/800) werden auf 600/700 gesetzt. Mit echter Systemschrift wirkt 800 viel schwerer als mit der synthetisch gefetteten Lato.
- Großbuchstaben mit Letter-spacing (`.sp-tile-label`, `.fin-age-label`, `.ui-table th`, `.area-row.head`, `.admin-table th`, `.admin-eyebrow`, `.sp-alert-level`) entfallen. Kopfzellen sind 12px/500 in `--text-2`. Einzige Ausnahme im Prototyp ist `.day-head .wd` im Planboard.
### 2.2 Mapping alter Tokens und Legacy-Aliase auf neue Werte
Die Spalte "Verwendungen" zählt `var(--x)` außerhalb von `global.css` ([geprüft], Python-Zählung). Weil diese 4 Fremd-CSS und 11 JSX-Dateien die alten Namen nutzen, bleiben sie als Aliase bestehen und werden erst in Paket 10 gelöscht.
| Alter Token | Heute (dunkel) | Neu | Verwendungen außerhalb global.css |
|---|---|---|---|
| `--bg` | #0e131a | `#ffffff` (Name bleibt) | 0 (global 3) |
| `--surface-1` | #151b23 | `var(--bg)` | 6 |
| `--surface-2` | #1b2430 | `var(--bg-hover)` | 16 (11 CSS + 5 JSX, u.a. `AssignProjectPage` Auswahlzeile) |
| `--surface-3` | #222d3a | `var(--bg)` (Popover bekommt `--shadow-pop`) | 0 |
| `--surface-hover` | #243040 | `var(--bg-hover)` | 0 |
| `--surface` (nirgends definiert, `chat.css` L13) | transparent (Bug) | `var(--bg)` | 1 |
| `--border` | weiß 8 % | `var(--line)` | 20 |
| `--border-strong` | weiß 14 % | `var(--line-strong)` | 0 |
| `--border-accent` | grün 35 % | `var(--accent-ring)` | 1 (`serverpuls.css`) |
| `--text` | #e6edf3 | #1a1f36 | 17 |
| `--text-muted` | #9aa7b4 | `var(--text-2)` | 31 |
| `--text-faint` | #6b7785 | `var(--text-3)` | 7 |
| `--accent` | #10b981 | #111111 | 19 |
| `--accent-2` | #059669 | `var(--accent-hover)` | 1 (`chat.css`) |
| `--accent-soft` | grün 14 % | `rgba(17,17,17,.06)` | 6 |
| `--info` | #3b82f6 | `var(--blue-fg)` | 1 |
| `--warn` | #f59e0b | `var(--orange-fg)` (Text; als Fläche entfällt es mit den Kpi-Streifen) | 3 |
| `--danger` | #ef4444 | #c0123c (6,19:1 auf Weiß; #ef4444 hatte 3,76:1) | 9 |
| `--ok` | #10b981 | `var(--green-fg)` | 5 |
| `--sp-1..6` | 4/8/12/16/24/32 | unverändert | 38 |
| `--r-sm` / `--r-md` / `--r-lg` / `--r-pill` | 8 / 12 / 16 / 999 | `var(--radius)` / `var(--radius-lg)` / `var(--radius-lg)` / 4px (Badge) | 6 / 4 / 0 / 2 |
| `--shadow-1..3` | schwarz 30-40 % | `none` / `var(--shadow-btn)` / `var(--shadow-pop)` | 0 |
| `--sidebar-w` | 248px | 232px | 0 |
| `--sidebar-w-collapsed` | 72px | entfällt (Rail gibt es nicht mehr) | 0 |
| Legacy: `--dark-bg`, `--dark-card-bg`, `--dark-text`, `--green-primary`, `--green-secondary`, `--gray-700/800/900` | Aliase auf Dunkelwerte | `--dark-bg`/`--dark-card-bg` = `var(--bg)`, `--dark-text` = `var(--text)`, `--green-primary` = `var(--accent)`, `--green-secondary` = `var(--accent-hover)`, `--gray-700` = `var(--line-strong)`, `--gray-800` = `var(--text-2)`, `--gray-900` = `var(--text)` | 0 (nur 13 + 6 + 1 Stellen innerhalb alter Legacy-Blöcke in global.css, die Paket 1 löscht) |
Zwei Hinweise zu den Aliasen:
- `--text-3` (#8a93a3) hat auf Weiß nur 3,1:1. Der Prototyp nutzt es nur für Tertiäres (IDs, "–", leere Zustände). Hinweise in Formularen laufen über `.hint` mit `--text-2`. Die Klasse `.faint` darf in Bestandscode bleiben, soll aber nur nicht-essenziellen Text tragen.
- `--accent` ist heute oft Linkfarbe (`.fin-link`, `.legal-files a`, `.credit-cols a`, `.area-rows a:hover`, Ticketnummer in `CustomerDetailPage`). Mit Schwarz geht die Link-Erkennbarkeit verloren, und der Hover auf Schwarz wäre unsichtbar. Im Kompat-Block bekommen diese Links `text-decoration: underline; text-decoration-color: var(--line-strong)`, wie `.link` im Prototyp.
### 2.3 Wie Altcode auf Weiß richtig aussieht (Neutralisierung)
1. Der `:root` wird ersetzt und der Alias-Block nach 2.2 darunter gelegt. Alles, was nur `var()` benutzt (laut Inventar etwa 94 % der Redesign-Schicht), kippt automatisch.
2. Dunkelspezifisches wird nicht über Variablen gerettet, sondern gelöscht oder neu geschrieben (Tabelle 2.4).
3. Kompat-Block (Paket 1, wird in Paket 10 gelöscht). Er bildet Altklassen auf den neuen Look ab, damit noch nicht migrierte Dateien lesbar und boxlos sind:
- `.ui-card`, `.ui-card.pad`, `.ui-card-head`, `.ui-card-body`: kein Hintergrund, kein Rand, kein Schatten, kein Padding. `.ui-card-head h3` bekommt den Stil von `h2.section`.
- `.ui-btn`, `-primary`, `-ghost`, `-danger`, `-sm`: wie `.btn`, `.btn-primary`, `.btn`, `.link-danger`, ohne Größenvariante. Sie sind nötig, weil 7 Stellen die Klassen von Hand schreiben (`TicketProjects` x4, `TicketCard` L137 u.a.).
- `.form-control`, `.form-group`, `.form-label` entsprechen `.input`/`.select`/`.textarea`/`.field`/`.field > label`. `select.form-control` bekommt das Chevron, `textarea.form-control` die Textarea-Höhe. Die `!important`-Regeln (global.css L308-346, L1046-1047) werden ersatzlos gestrichen.
- `.btn`/`.btn-dark`/`.btn-green`/`.btn-teal`: die Namen kollidieren mit dem Prototyp-`.btn` (weiß mit Ring). Im Bestand ist `.btn` (43x in 13 Dateien) ein graues Gradient-Element mit `!important`. Es wird komplett durch die Prototyp-`.btn`-Regel ersetzt, `.btn-dark` und `.btn-teal` werden `.btn.btn-primary`. `.btn-green` stirbt mit der Login-Seite.
- `.badge-muted|info|ok|warn|danger` entsprechen `.badge`, `.blue`, `.green`, `.orange`, `.red`. `.status-open|working|awaiting|assigned|closed` und `.priority-critical` (nur bis `StatusPill` neu ist, in P1 sofort ersetzt).
- `.overlay`, `.overlay-close`, `.overlay-content`: `.overlay` wird zum Prototyp-Backdrop (`--overlay`, zentriert). `.overlay-content` wird zur weißen Box (radius 12, `--shadow-pop`, Padding 24, Text `--text` statt `#fff`). `.overlay-close` wird ein kleines graues "×" oben rechts. Alle 10 handgebauten Modals sehen damit sofort weiß aus. Die finalen Dialoge kommen in den Bereichspaketen (3).
- `.card`, `.card-header`, `.card-body`, `.row`, `.col`, `.col-6`, `.dropdown*` (bis P2), `.spinner` (neutraler grauer Ring), `.text-grey` (`--text-2`), `.text-red` (`--red-fg`), `.text-white`, `.bg-red` (rot getönte Textzeile ohne Kasten), `.ticket-projects*`.
- Utilities `.p-1/.p-2/.mt-2/.mt-3/.mb-2/.text-center/.flex/.gap-2/.gap-3/.items-center/.justify-between/.grow/.wrap/.muted/.faint/.mono/.text-ellipsis` bleiben.
4. Chat-Mindestfix in P1, damit der Senden-Knopf lesbar bleibt. `chat.css` bekommt `--chat-accent: var(--accent)`, `--chat-accent-hover: var(--accent-hover)`, `--chat-accent-soft`, `--chat-accent-line: rgba(17,17,17,.14)`, `--chat-accent-text: var(--accent)`, **`--chat-on-accent: #ffffff`** (heute `#04130d`, auf Schwarz wäre die Schrift unsichtbar) und `--chat-surface: var(--bg)`. Der Rand der eigenen Blase wird `var(--line)`, und `.chat-messages` bekommt `background: var(--bg)`, womit der `--surface`-Fehler weg ist.
### 2.4 `global.css`: Blöcke und Aktion
| Zeilen | Inhalt | Aktion |
|---|---|---|
| 1-10 | `@font-face` Lato | löschen, plus `src/assets/fonts/*` |
| 18-71 | `:root` | ersetzen (2.1 + 2.2) |
| 73-106 | `body`-Glows, `overflow:hidden`, Scrollbar, `::selection` | löschen; `body` auf Prototyp-Basis. Scrollbar und Selection bleiben im System-Standard |
| 108-126, 128-140, 142-185 | `.bg-*`/`.text-*` (17 Klassen, 13 tot), `.container`, `.navbar*` | nur `.bg-red`, `.text-red/white/grey` als Kompat behalten, Rest löschen |
| 187-233 | `.btn`-Familie (Gradienten, grüner Glow) | ersetzen (Prototyp `.btn`) |
| 235-305 | `.table`, `.card` (tot bzw. 2 Verwendungen) | löschen / Kompat |
| 307-351 | `.form-control` mit `!important` | ersetzen (Prototyp `.input`, Alias) |
| 353-382 | `.row/.col`, Utilities | behalten (Kompat), tote entfernen |
| 384-494 | `.ticket-row`, `.worksheet-expansion` usw. | löschen (komplett tot) |
| 496-509 | `.status-*`, `.priority-*` | löschen, durch `.badge`-Töne ersetzen (StatusPill) |
| 511-560 | `.dropdown*` | bis P2 behalten, dann löschen |
| 562-605 | `.overlay*`, `.modal-content` | umbauen (2.3), Rest in P10 löschen |
| 607-754 | `.slider`, `.drop-zone`, `.modern-card`, `.footer`, `.spinner` | löschen, außer `.spinner` (neutral) |
| 756-787, 1035-1039 | `.ticket-tab*` | durch `.tabs`/`.tab` ersetzen |
| 789-794 | Responsive Legacy | löschen |
| 796-864 | App-Shell, `.side*` | ersetzen (Prototyp `.shell/.sidebar/.nav-item/...`) |
| 866-1033 | Page-Header, `.ui-card`, `.ui-btn`, `.badge-*`, `.kpi*`, `.tcard*`, `.list*`, `.ui-table`, `.seg`, `.empty`, `.field` | ersetzen bzw. in Bereichspaketen löschen (`.tcard*` in P2, `.kpi*` in P7) |
| 1050-1120 | Utilities, `.automatic-assignment-note`, `.datetime-pair`, `.autocomplete*` | behalten, neu einfärben; `color-scheme:dark` (L1077) löschen |
| 1122-1153 | Rechtliches | in P8 umbauen |
| 1155-1476 | Finanzen (26 Dunkel-Literale: `#fecaca` L1377, `#fbbf24` L1379/1383, `rgba(6,9,14,.98)` L1372, `#f87171` L1400, `#b45309` L1144, rgba-Tönungen) | P1 nur Literale entfernen, Umbau in P7 |
---
## 3. Bausteine
Zeile = Baustein. "Verbraucher" nennt Dateien und, wo vom Inventar gezählt, die Anzahl.
| Baustein | Heute | Neu / Änderung | Verbraucher |
|---|---|---|---|
| **App-Shell** | `.app-shell` 100vh + `overflow:hidden`, innerer Scroller `.app-scroll`, `Footer` darunter, Sidebar 248px sticky im Flex | `.shell` > `<aside class="sidebar">` (fixed 232px) + `.main` (`margin-left: var(--sidebar-w)`) > `.mobile-bar` + `.page`. Dokument-Scroll (Body scrollt). Footer entfällt. `menuOpen`-State in `AppContent`, Schließen bei Routenwechsel (`useLocation`), Esc und Klick auf `.scrim`. `ScrollToTop`-Effekt bei Routenwechsel (fehlt heute komplett, `rg scrollTo` = 0). Ladezustand: `.loading`-Klasse statt Inline-Style, Text "Wird geladen …" | `App.jsx`, `Navbar.jsx`, `Footer.jsx` (löschen), `AdminRoute.jsx` |
| **Sidebar** | Icons (17 Tabler), Gruppen, Collapse-Rail 72px, Avatar-Verlauf, `.side-count` grün | Textliste (`.nav-item`, `.sub`, `.nav-count`, `.hot` für Chat-Ungelesene, `.paused` mit "pausiert", `.nav-dot`), `.sidebar-foot` mit `.who` (Name, Ellipsis) + "Abmelden". Collapse und `lib/sidebar.js` entfallen, Drawer unter 860px. Serverpuls-Punkt: `.nav-dot` (#e5a000) für Stufe medium, zusätzlich `.nav-dot.critical` (`--danger`), da das Live-System zwei Stufen hat. `data-testid="sp-nav-dot"`, `title`, `aria-disabled`, 5-min-Polling und `useSidebarCounts` bleiben | `Navbar.jsx`, `lib/navLinks.js` (nur optional `hot:true` am Chat-Kind), `serverpuls.css` L70-74 |
| **Logo** | grüne Kachel mit "W" | neue `components/Logo.jsx` mit dem SVG aus `app.js` (Konstante `LOGO`, Z. 115); 24px in Sidebar/Mobile-Bar, 40px im Login; `public/favicon.svg` aus `quelle/favicon.svg` plus `<link rel="icon">`, `noindex`, `color-scheme` in `index.html` | `Navbar`, `LoginPage`, `index.html` |
| **Seitenkopf** (`PageHeader`) | `.page-header/.page-title/.page-subtitle/.page-actions`, 24/800 | Klassen `.page-head`, h1 28/700, `.sub`, `.actions`; neue Props `eyebrow` und `crumb`; mobil gestapelt unter 860px | 12 Verwendungen in 9 Dateien; `ProjectsPage` hat einen eigenen zentrierten `<h2>`-Kopf und muss umgestellt werden |
| **Breadcrumb** | gibt es nicht ("Zurück"/"Alle Kunden" mit Pfeil-Icons) | `.crumb` (13px, `--text-2`, Slash in `--text-3`) | Ticket-Seite, `CustomerDetailPage`, `FinanceSettingsPage`, `AssignProjectPage` |
| **Toolbar/Suche** | `.toolbar` (2x), `.search-input` (4x), Filterkarte mit "Anwenden" | `.toolbar` + `.input.search` (300px, mobil 100%) + `.select-quiet` + `.check`; Suche live (Debounce oder Enter), "Anwenden" entfällt | `TicketsPage`, `CustomersPage`, `ProjectsPage`, `AppsTable`, `CustomerChatPage`, `ProviderContractsTab` |
| **Tabs** | `Tabs.jsx`, `.ticket-tabs/.ticket-tab` (gefüllt, grüner Verlauf), kein Zähler | `.tabs` (Unterstrich 2px `--accent`, `overflow-x:auto`) + `.tab` + `<span class="count">`; neue Prop `t.count`. Auch als Filter-Tabs: Tickets, Kunden, Projekte, Gutschriften, Angebote | 3 Aufrufer heute (`TicketCard`, `CustomerDetailPage`, `LegalPage`); neu mehr. **Achtung:** `customerTabs.js` testet `Object.keys(entry) = [id, label]`, Zähler also nicht in diese Funktion, sondern beim Rendern anfügen |
| **Segmented Control** | `.seg` (dunkle Schiene, 4 Aufrufer), `.sp-pill` | `.seg` (graue Schiene, aktiver Chip weiß mit Ring), `aria-pressed` | `CustomersPage` (wird Tabs), `InvoiceTable`, `CreditList`, `OfferList`, Serverpuls-Pills, neu: Einsatz Fernwartung/Vor Ort (`CreateWorksheetModal`), Art-Wahl `AssignProjectPage` |
| **Tabelle** | `.ui-table` (4x), div-Raster `.area-row` (4 Ansichten), `.tcard`-Liste, `.list-row` (6x), `.sp-table`, `.admin-table`, `.legal-contracts` | `.table` (Haarlinien, Kopf 12px/500), `tr.row` (klickbar, Hover `--bg-subtle`), `.cell-title`/`.cell-sub`/`.id`, `.right`, `.num`, `.table-foot`, `.hide-sm` (<860) und `.hide-md` (<1100). Container-Query-Stapelung der Finanztabellen bleibt (Wrapper statt Card trägt `container-type`) | Tickets, Kunden, Projekte, Gutschriften, Angebote, E-Mail-Speicher, Funktionen, Finanzen (3), Rechtliches, Admin, Serverpuls |
| **Listenzeile** | `.list`/`.list-row` mit Hover-Fläche | Haarlinie, `a.list-row:hover .cell-title { color: var(--accent-ink) }`, erste Zeile `border-top` | `ProjectContextPanel`, `TicketProjects`, `AssignProjectPage`, `ServicesPage`, `CustomerViewLog`, `CustomerChatPage` |
| **Facts** | `Ticketkopf` als Meta-Zeile | `.facts`/`.fact` (`.k` 13px, `.v` 500), mit `.select-quiet` für "Zuständig" | Ticket-Seite (neu) |
| **Stats** | `Kpi` (Box mit Farbstreifen, 28/800), `WorksheetSummary`-Streifen, `.fin-stats`, `.fin-age` | `.stats`/`.stat` (`.k` 13px, `.v` 22/600, tabular), `.v.warn` orange, **neu `.v.neg` rot** (negatives Geld soll rot, nicht orange, sein); `Kpi` wird `Stat` (Prop `tone` statt `accent`), Icon-Prop und Streifen entfallen | `LiquidityHead` (3), `WorksheetSummary`, `AboCard`, Finance-Altersstreifen, `TaxCard` |
| **kv / two-col / prose** | `.area-kv`, `.credit-cols`, `.fin-cols`, `.fin-bank-facts` | `.kv` (180px/1fr, mobil 1 Spalte), `.two-col` (1fr 1fr, Gap 56, mobil 1 Spalte), `.prose` (`pre-wrap`, max 680px) | Kundenübersicht, `CreditDetail`, `AboStatusCard`, `BankAccessCard`, `TaxCard`, Beschreibung des Tickets (die heutige `description` bekommt `pre-wrap`) |
| **Badge** | `Badge.jsx` Pille (999px), Tone `muted|info|ok|warn|danger`, `dot`-Prop (5 Aufrufer) | `<span className="badge {tone}">`; Mapping `muted`→(grau), `info`→`blue`, `ok`→`green`, `warn`→`orange`, `danger`→`red`; neu `violet`, `plain`; 20px hoch, Radius 4, kein Rand, `dot` entfällt. Die Töne in `lib/*Format.js` bleiben **unverändert** (Tests deepEqual-en sie), das Mapping sitzt nur in `Badge.jsx` | 44 Verwendungen in 20 Dateien |
| **Status-Töne** | `statusClass()` → `.status-*` (Abschnitt unten) | `StatusPill` rendert `badge {tone}`; `UrgentPill` = `badge red` "Dringend" | `TicketCard`, `CustomerDetailPage`, `ProjectContextPanel` |
| **Button** | `Button.jsx` (`ui-btn`, 4 Varianten, `size="sm"`), 114 Verwendungen in 35 Dateien (43 ghost, 43 sm, 36 primary, 5 danger) | default und ghost → `.btn`; primary → `.btn.btn-primary`; neue Variante `quiet` → `.btn.btn-quiet`; danger → `.link-danger` (Textlink, im Prototyp gibt es keinen roten Knopf); `size` entfällt (alle 32px, `Button` ignoriert `size` zunächst); Zeilenaktionen in Tabellen werden `.link` | alle; `TicketProjects` u.a. mit handgeschriebenem `ui-btn` |
| **Card** | `.ui-card` Box, 57 Verwendungen in 25 Dateien, `bodyStyle`, `pad` | `<section className={className}>` + `<h2 class="section">{title}{actions}</h2>` + Kinder; `pad`/`bodyStyle`/`style` werden No-ops; `className` bleibt (Finance-Wrapper), Container-Queries brauchen das Element | alle |
| **Dialog** | 10 handgebaute Overlays (siehe 3.1) + 2 `fin-overlay`; schwarz, weißes 48px-"×" als `<span>`, kein `role`, kein Esc | neue `ui/Dialog.jsx`: `.overlay` > `form|div.dialog` (max 520, Variante breit ~760 für Rechnung) mit `.dialog-head` (h3 + `<button class="dialog-close" aria-label="Schließen">×</button>`), `.dialog-body` (Flex-Spalte, Gap 16, `overflow-y:auto`), `.dialog-foot`; `role="dialog" aria-modal aria-labelledby`, Esc, Klick aufs Backdrop, mobil Bottom-Sheet. Bindet `useDialogFocus` ein (Overlay `onKeyDown`, innerer Dialog mit `ref` + `tabIndex=-1`) | siehe 3.1 |
| **Form-Felder** | `.form-group/.form-label/.form-control` (49/49/118), `Field` (32x in 4 Dateien), `.datetime-pair`, `FormField` | `.field > label` (500) + `.input`/`.select`/`.textarea` (32px, Ring-Schatten, Fokus-Ring 2-stufig, **kein Rand**) + `.hint` (13px `--text-2`) + `.field-row`/`.field-row-3`; Fehler: `box-shadow: 0 0 0 1px var(--danger), 0 0 0 4px var(--danger-ring)`; `.check` für Checkbox/Radio | alle Formulare |
| **Urgent-Schalter** | `.urgent-toggle` gestrichelte Pille, `.on` | gleiche Klassennamen, aber Markup: `<span class="sw"/>` vor dem Label (28x16 Schalter, `.on` rot); `aria-pressed` bleibt; Logik unverändert | `UrgentToggle.jsx` (verbraucht nur von `TicketCard` bzw. neuer Ticket-Seite) |
| **Empty-State** | `EmptyState` mit `icon`-Prop (keiner übergibt es), 3 Inline-Styles | `<div class="empty"><strong>Titel</strong>Hinweis</div>` + Aktion; `icon`-Prop und `.empty-icon` entfernen | 16 Verwendungen in 13 Dateien |
| **Autocomplete** | `.autocomplete-list` auf `--surface-3` mit Schatten | weißes Popover (`--bg`, `--shadow-pop`, Radius 8), aktiver/Hover-Eintrag `--bg-hover`, `form-control`→`input`. **Achtung** Clipping im scrollenden `.dialog-body` | `CreateTicketModal`, `InvoiceCreateModal`, `OfferForm`, `CustomerChatPage` |
| **Meldungen** | `.bg-red.text-white` (9x in 6 Dateien), `.legal-error`, `.fin-dialog-error`, `.area-error/-message/-summary`, `.sp-error`, `.admin-notice` | **Kein Kasten.** Neue Klassen `.msg`, `.msg.err` (13px `--red-fg`), `.msg.ok` (`--green-fg`), `.msg.warn` (`--orange-fg`); transiente Meldungen optional als `.toast` (dunkle Pille, Prototyp) | siehe Spalte links; im Prototyp gibt es keine Banner, deshalb Text-only |
| **Lader** | `.spinner` (40x in 25 Dateien, grüner Ring) + doppeltes `FaSpinner` | Text "Wird geladen …"/"Speichert …" über `.loading` (`color: var(--text-2)`); `.spinner` bleibt nur als neutraler Kompat-Ring bis P10 | alle |
| **Login** | Glas-Karte über `LetterGlitch`-Canvas, 19 Inline-Styles | `.login > .login-box` (360px, Logo 40px, h1, `.sub`, `.field`, `.btn-primary.btn-block`), Code-Schritt mit `.code-input`, `.login-links`, `.login-foot` (siehe 5.P1) | `LoginPage.jsx` |
| **Tab-Inhalt Ticket** | `.ticket-tab-content` | entfällt (Reiterinhalt direkt unter `.tabs`) | `TicketCard` |
### 3.1 Dialog-Verbraucher (10 handgebaute Overlays + Sonderfälle)
`CreateTicketModal`, `CreateWorksheetModal`, `WorksheetTimesModal`, `OwnSupportModal`, `TicketProjects` (Art-Frage "Was ist das?"), `ProjectsPage` (2x: Neues Projekt, Projekt bearbeiten), `finance/AssignDialog`, `finance/InvoiceCreateModal`, `legal/ContractForm`, `legal/VersionForm`.
Weitere Kandidaten:
- `CreateCustomerForm` (aus `CustomersPage.jsx` exportiert, in `AssignProjectPage.jsx` L12/L277 inline benutzt).
- `OfferForm` (Inline-Karte).
- `AnalyseRow` (Zustimmungsablauf).
Es gibt keinen Teil des Altcodes, der `.overlay` ohne die drei Klassen `overlay`, `overlay-close`, `overlay-content` benutzt. Deshalb funktioniert die CSS-Zwischenlösung (2.3).
### 3.2 Status-Töne (`lib/ticketStatus.js`, ungetestet, neu mit Test)
| DB-Wert (gespeichert, englisch bleibt) | Gruppe | heute | neu (Prototyp) |
|---|---|---|---|
| `Open` | open | `.status-open` (grün) | grau (kein Ton) |
| `Occupied`, `Assigned`, `Added Info`, `In Kontakt` | working | blau | blau |
| `Awaiting`, `Halted` | waiting | amber | orange |
| `In Test` | review | `.status-assigned` (teal) | violett |
| `Closed`, `Zugesagt` | done | grau (`.status-closed`) | grün |
| `Cancelled`, `Aborted`, `Abgesagt` | cancelled | grau (`.status-closed`) | `plain` (nur Text, `--text-3`) |
- `done` und `cancelled` teilen sich heute `status-closed` und müssen getrennt werden.
- Implementiert wird eine neue Funktion `statusTone(status)` in `lib/ticketStatus.js`. Sie liefert `'' | 'blue' | 'orange' | 'violet' | 'green' | 'plain'`, `statusClass()` entfällt. Neuer Test `tests/unit/ticketStatus.test.mjs`.
- Das ist ein **sichtbarer Bedeutungswechsel** (Offen war grün, Erledigt grau). Er ist im Prototyp so freigegeben, wird aber in Abschnitt 8 erneut genannt.
- Weitere Töne-Vokabulare, die in Komponenten gemappt werden und deren lib-Tests dadurch unberührt bleiben:
- `previewStatus().color` (`green|yellow|red|grey`), Mapping in `PreviewBlock`: yellow→orange.
- `analyseStatus().color` (`green|red|grey`).
- `worksheetTone()`: `git-open`→`badge orange` "Zeit offen", `git-done`→`badge green` "Zeit eingetragen", `comment`→Zeile `.ws.comment`, `work`→kein Badge.
- `deadlineTone()` (legal).
- `formatDelta().dir` (Serverpuls).
- `ageLabel().stale`.
---
## 4. Icon-Regel
Regel: Icons nur dort, wo sie handeln, und auch dann als Zeichen statt Bibliothek. Konsequenz: **keine einzige Icon-Komponente bleibt**, und beide Bibliotheken fliegen raus.
Heute: 71 verschiedene Icons in 187 Verwendungen in 51 Dateien (17 `@tabler/icons-react` in `Navbar.jsx` und `ProjectContextPanel.jsx`, 53 `react-icons` (fa6/fa), 1 `react-icons/si` = `SiGitea`). Mit `LeadsPage` (25 Tags) entfallen davon nach Paket 0 bereits 25.
### A. Entfernen (dekorativ, der Text daneben genügt)
| Icon(s) | Stellen |
|---|---|
| 14 Menü-Icons (`IconTicket`, `IconCalendar`, `IconFolders`, `IconLayoutGrid`, `IconUsers`, `IconMessages`, `IconReceiptRefund`, `IconDiscount`, `IconMailCog`, `IconTargetArrow`, `IconReportMoney`, `IconScale`, `IconActivityHeartbeat`, `IconSettings`) | `Navbar.jsx` |
| `IconLayoutSidebarLeftCollapse/Expand` | `Navbar.jsx` (Funktion entfällt) |
| `IconFolders` | `ProjectContextPanel.jsx` |
| `FaPlus` (15) | `TicketsPage` x2, `TicketCard` (Arbeitsblatt hinzufügen), `TicketProjects`, `FixedCostsCard`, `InvoiceCreateModal`, `InvoiceTable`, `OffersView`, `AdminPage`, `AssignProjectPage` x2, `CustomersPage` x2, `ProjectsPage` (dort icon-only, siehe B) |
| `FaFloppyDisk` (4), `FaGear`, `FaSliders`, `FaAngleDown`, `FaCodeBranch`, `FaCircleInfo`, `FaFolder` x2, `FaLock`, `FaClone`, `FaKey`, `FaArrowUp` | `SettingsCard`, `AdminPage`, `CustomerDetailPage`, `FinancePage`, `TicketsPage`, `AssignedProjectCard`, `ProjectsPage` |
| `FaClock` x4, `FaUser` x2, `FaExchangeAlt`, `FaComment` x2, `FaHandsHelping`, `FaPen`, `FaCheck`, `FaChevronDown/Up` (pro Arbeitsblatt) | `WorksheetList.jsx` (Aufklapp-Mechanik entfällt) |
| `FaPlay`, `FaStop` | `TicketCard.jsx` (werden zu Text "Beginn"/"Fällig" in den Facts) |
| `FaTriangleExclamation` (7) | `AboCard` x2, `ForecastTable`, `StatusLine`, `TransactionTable` (die Wörter "gefährdet", "Hinweis" tragen die Bedeutung) |
| `FaArrowUpRightFromSquare` x2 | `CreditDetail`, `KundenansichtButton` |
| `FaBoxArchive` (3), `FaBoxOpen` (2), `FaTrash` | `CustomerDetailPage`, `ProjectsPage` (Archiv-Knöpfe als Text), `AdminPage` (siehe B) |
| `FaStar` x2 ("Wichtiger Kunde") | `CustomersPage`, `CustomerDetailPage` (wird `.badge` "Wichtig") |
| `FaArrowLeft` x5, `FaArrowRight` x2 | `AssignProjectPage`, `CustomerDetailPage` x2, `FinanceSettingsPage`, `CustomerChatPage` ("Zurück", "Weiter", Breadcrumb tragen den Sinn; mobile Chat-Rückkehr als Text "← Alle Unterhaltungen") |
| `FaRotateRight` x4, `FaRotate` x2 | "Erneut versuchen", "Jetzt abrufen", "Aktualisieren" bleiben als reiner Text |
| `Badge.dot` (5 Aufrufer), `EmptyState.icon`, `Kpi.icon` | `CustomersPage` x2, `CustomerDetailPage` x3; Slots ohne Aufrufer |
### B. Durch Text ersetzen (heute icon-only oder reiner Lader)
| Icon | Wo | Ersatz |
|---|---|---|
| `FaSpinner` (37) | 25 Dateien (Lader und Busy-Zustand in Knöpfen) | "Wird geladen …", "Speichert …", "Sendet …" |
| `FaEdit`, `FaTrash` | `AdminPage` Zeilenaktionen | `.link` "Bearbeiten", `.link-danger` "Löschen" |
| `FaTrashCan` | `InvoiceCreateModal` Positionszeile | `.link` "Entfernen" |
| `FaEye` | `PreviewLinkButton` (holt Einmal-Link, `onClick` bleibt) | `.link` "Vorschau öffnen" |
| `SiGitea` | `GiteaLinkButton` | `.link` "In Gitea öffnen" |
| `FaLinkSlash` | `AssignedProjectCard` | `.link-danger` "Lösen" |
| `FaUserPlus` | `EditorDropdown` (wird `<select>`) | Option "Nicht zugewiesen" |
| `FaChevronLeft/Right` | `PlanboardPage` | Zeichen ← und → neben "Heute" |
| `FaGlobe`, `FaArrowsRotate` x2, `FaPen`, `FaPlus` (icon-only) | `ProjectsPage` (hosten, abgleichen, neu deployen, bearbeiten, neu) | Textknöpfe/`.link` |
| `FaEye/FaEyeSlash/FaCopy/FaCheck` | `CopyableCredential` (handelnd, daher Entscheidung offen; Standard Text) | "Anzeigen/Verbergen", "Kopieren/Kopiert" |
| `FaCopy/FaCheck` | `CustomerFunktionen` (Site-Key) | "Kopieren/Kopiert" |
| Text-Glyphen `✓ Abo`, `+ Abo`, `↳ Unterstützung`, `→` (Status), `✓` (Berechnung), `✓ ! i` in Badges, `▲/▼` (Deltas), `▴/▾` (Preview-Toggle), Emoji `🔴🟠🟢` | `WorksheetList`, `CreateWorksheetModal`, `WorksheetTimesModal`, `CreditDetail`, `AppsTable`/`ServerpulsPage`, `AssignedProjectCard` | "Abo"/"+ Abo", "Unterstützung", "Status danach: X", Text-Badge, Vorzeichen der Zahl, "Vorschau anzeigen/ausblenden", Badge "Alles ok/Warnungen/Critical" |
### C. Behalten, aber als Zeichen (keine Komponente)
| Zweck | Umsetzung |
|---|---|
| Dialog schließen (7 `FaTimes` + 3 `FaXmark` in Dialogen/Bannern) | `<button class="dialog-close" aria-label="Schließen">×</button>` (muss ein echter Button sein für Tastatur und `useDialogFocus`) |
| Sortierpfeile, Auf-/Zuklappen | Zeichen ↑ ↓ (`AppsTable`), ▾ ▴ (`LiquidityHead`) |
| Chat-Widget (Anhang, Kamera, Mikrofon, Senden, Play, Download usw.) | Inline-SVGs aus dem **fremden** Portal-Code (`chat-composer.js`, 15 Stück), nicht änderbar; Anhang-Menü-Icons optional per CSS ausblenden |
### D. package.json danach
Beide Pakete können entfernt werden: `@tabler/icons-react` (^3.36.0) und `react-icons` (^4.12.0). Alle 50 Importdateien (nach LeadsPage-Löschung) verlieren ihre Importe. Zum Schluss `rg "react-icons|@tabler" src` = 0 Treffer, dann `npm uninstall` und die Lock-Datei prüfen.
- `uplot` bleibt (Serverpuls).
- Beim lokalen `npm uninstall` entfernt npm `node_modules/react-icons`, die 147 getrackten Dateien erscheinen als Löschung (siehe 7.6).
- Der Tabler-Import sitzt nur in 2 Dateien. Das Löschen aller `Fa*`-Imports ist der Aufwand (162 Icon-Tags in 50 Dateien).
---
## 5. Arbeitspakete
### Zusammenfassung
| Paket | Inhalt | Aufwand (PT) | Der Eigentümer sieht danach |
|---|---|---|---|
| **P0** | Vorbereitung, Totcode weg, Test-Harness | 0,5 | nichts |
| **P1** | Tokens, Shell, Primitives, Login, Kompat, Mini-Fixes | 3,5-4 | die ganze App ist weiß, Sidebar nur Text, Karten ohne Box, Login neu |
| **P2** | Tickets: Liste als Tabelle, eigene Ticket-Seite, Arbeitsblätter, Dialoge | 5-5,5 | Ticket-Hauptarbeitsplatz im neuen Design |
| **P3** | Planboard | 0,5 | Wochenraster ohne Karten |
| **P4** | Kundenliste und Kundenseite (9 Reiter), Funktionen/Abo/Protokoll | 2,5-3 | Kundenbereich im neuen Design |
| **P5** | Projekte, Zuordnen-Assistent, Services, Projekt-Bausteine | 3,5-4 | Projektbereich im neuen Design |
| **P6** | Chat, Gutschriften, Abo-Angebote, E-Mail-Speicher | 4 | Rest von Kunden-Unterseiten |
| **P7** | Finanzen | 4-4,5 | Finanzseiten ohne Karten |
| **P8** | Rechtliches und Admin | 2 | letzte Admin-Flächen |
| **P9** | Serverpuls | 1-1,5 | Dashboard hell, Charts hell |
| **P10** | Aufräumen (Icon-Pakete, Kompat, tote CSS) | 1 | Bundle kleiner, nichts Neues |
| | **Summe** | **27,5-30,5** | |
PT = Entwicklertage inklusive Selbsttest und Screenshot-Abgleich.
**Ablauf für jedes Paket:**
1. Arbeit im Worktree `dev-design`, Branch `feature/design-weiss`. Nach jedem Paket gibt es einen eigenen Commit-Stapel mit Tag (zum Beispiel `design-p2`).
2. Definition of Done:
- `npm test` grün (312 oder mehr),
- `npm run build` grün (Bundle-Größe notiert),
- Vorher-/Nachher-Screens (1440px und 390px),
- keine neuen Inline-Styles außer Laufzeitwerten,
- keine Icon-Importe im Bereich,
- deutsche Texte unverändert (Diff-Stichprobe),
- `git add` nur explizite Pfade.
3. Live gebracht wird vom Eigentümer mit `git -C <worktree> push origin feature/design-weiss:test` (Fast-Forward). Rückgängig mit Revert-Commit, ein Force-Push ist nicht nötig.
4. P7, P8 und P9 sind voneinander unabhängig und können parallel in Worktrees entstehen. Deployt wird nacheinander.
---
### P0: Vorbereitung und Totcode (0,5 PT, unsichtbar)
- **Dateien:**
- Worktree `dev-design` von `test`.
- `tp2` (1 Commit) vorher in `test` bringen, siehe 7.5.
- Löschen: `src/pages/LeadsPage.jsx` (614 Zeilen, 53 Inline-Styles, 25 Icon-Tags), `src/hooks/useLeads.js` (282 Zeilen).
- Bleiben: Route `/leads` mit Redirect, pausierter Menüeintrag, `lib/leads.js`.
- **Test-Harness** nach dem Rezept in `woms-ticketsystem.md` neu bauen (esbuild-Bundle, Playwright mit gemocktem Appwrite, unter `systemd-run` mit Speichergrenze) und "Vorher"-Screens für etwa 15 Schlüsselseiten erzeugen.
- **Test-Auswirkung:** keine (kein Test importiert `LeadsPage` oder `useLeads`).
- **Sichtbar:** nichts.
### P1: Fundament, Tokens, Shell, Primitives, Login (3,5-4 PT)
Danach ist die ganze App weiß und boxlos, auch wenn die Seiten innen noch alte Layouts haben.
- **Dateien und Änderungen:**
- `src/styles/global.css`:
- Tokens (2.1), Alias- und Kompat-Block (2.2 und 2.3).
- Löschen: Lato, Glows, Scrollbar und Selection, tote Regeln (66 Namen, rund 88 Regeln, 285 Zeilen), Navbar, Ticket-Row, Slider, Dropzone, Modern-Card, Footer.
- Neu anlegen: Basis, `.shell`, `.sidebar`, `.page`, `.mobile-bar`, `.scrim`, `.page-head`, `.crumb`, `h2.section`, `.btn*`, `.badge*`, `.tabs`, `.input/.select/.textarea/.check`, `.toolbar`, `.table*`, `.empty`, `.facts/.stats/.kv/.two-col/.prose`, `.urgent-toggle`, `.list-row`, `.seg`, `.overlay/.dialog*`, `.field`, `.toast`, `.msg*`, `.loading`, `.login*`, Autocomplete-Popover, `.datetime-pair`.
- Nicht übernehmen: `.proto*`, `html[data-accent]`, `.nav-gap`. `.ws*` kommt mit P2, `.week/.day/.appt` mit P3.
- `index.html`, neu `public/favicon.svg`.
- `App.jsx`: Shell-Markup, `menuOpen`, `ScrollToTop`, `.loading`-Block. Routen und Auth unverändert.
- `Navbar.jsx` plus neue `Logo.jsx`, Löschen von `Footer.jsx`, `LetterGlitch.jsx`, `lib/sidebar.js` und `tests/unit/sidebar.test.mjs`. `serverpuls.css` L70-74 (`.side-dot*`) wandert in die Sidebar-Regeln.
- `AdminRoute.jsx`: nur den Inline-Style gegen `.loading` tauschen, das Markup `<p>Wird geladen …</p>` bleibt wörtlich (Test, siehe 7.4).
- `LoginPage.jsx`:
- Prototyp-Markup (`.login`, `.login-box`, Logo 40px, `h1` "Anmelden", `.sub`, `.field`, `.input`, `.btn.btn-primary.btn-block` "Weiter").
- Code-Schritt (`h1` "Code eingeben", `.code-input`, `.login-links`: "Code erneut senden" · "Abbrechen") plus Unterschritt "Code senden" (fehlt im Prototyp, bleibt) und `.login-foot`.
- Labels deutsch ("E-Mail", "Passwort"), `autocomplete`-Attribute ergänzen.
- Logik (`login`, `verifyCode`, `sendCode`, `cancelMfa`, `NO_STAFF_ACCESS`, `safeReturnPath`) bleibt exakt.
- `components/ui/*`: `Badge`, `Button`, `Card` (zu `section`), `Tabs`, `PageHeader`, `EmptyState`, `Field`, `Kpi` (zu `Stat`), `StatusPill`, `AutocompleteInput`, `DateTimeInput`, `DurationSelect` (Klassenwechsel), neu `Dialog.jsx`, `index.js` ergänzen. `lib/ticketStatus.js`: `statusTone()` plus neuer Test.
- **Pflicht-Mini-Fixes, damit nichts unlesbar wird** (je Minuten bis eine Stunde):
- `chat.css` Variablen (2.3 Punkt 4).
- `AssignedProjectCard.jsx` L27 (Label `#cbd5e0`) und L82 (Hintergrund `rgba(30,41,59,.5)` mit blauem Rand) auf `--text-2` und `--bg-subtle`.
- `Chart.jsx` L28-36 (Achsen `#9aa7b4` → `#5e6678`, Grid `rgba(255,255,255,.06)` → `#ebeef1`, Ticks → `#d8dde3`, Serien-Default `#10b981` → `#111111`).
- Hell-auf-Dunkel-Textfarben in `admin-areas.css` (`#f87171`, `#fbbf24`), `serverpuls.css` (`#fca5a5`, `#fbbf24`), `AdminPage.css` (`#6ee7b7`, `#fca5a5`, `#34d399`) und `global.css` Finance/Legal (`#fecaca`, `#fbbf24`, `#f87171`) auf `--red-fg`/`--orange-fg`/`--green-fg`.
- `PreviewBlock`-Log (`rgba(0,0,0,.35)`) bleibt unschön, aber lesbar bis P5.
- **Test-Auswirkung:** `sidebar.test.mjs` entfällt (312 → 309, dann +neu `ticketStatus.test.mjs`). `adminRoute.test.mjs` bleibt grün, solange das `<p>` unverändert ist. Alle anderen Tests sind lib-Tests.
- **Der Eigentümer sieht:** weiße App, Sidebar nur als Textliste mit Zählern (ohne Einklappen, auf dem Handy ein Menü-Knopf), neue Statusfarben, schwarze Primärknöpfe, weiße Dialoge (zunächst als Zwischenlösung), neues Login. Listen und Karten innen sehen noch nach altem Layout aus (Ticketkarten, Hover-Dropdowns, Pillen-Raster), sind aber weiß und lesbar.
- **QA-Pflichtliste (390px und 1440px):** Login, Ticket-Liste mit aufgeklappter Karte, Arbeitsblätter, Kundenseite (alle 9 Reiter), Chat (Blase, Composer, Senden-Knopf), Projekte (Karten, 2 Dialoge inklusive `PreviewBlock`), Zuordnen, Services, Planboard, Finanzen (alle Blöcke, 2 Dialoge), Rechtliches, Serverpuls (Charts), Admin, Gutschriften/Angebote/Speicher.
### P2: Tickets: Liste, Ticket-Seite, Arbeitsblätter, Dialoge (5-5,5 PT, davon Route 2-2,5)
Empfehlung "eigene Seite" (siehe Abschnitt 6). Bei Variante "aufklappen" entfallen etwa 1,2 PT.
- **Dateien:**
- `pages/TicketsPage.jsx`: Tabs (Aktiv/Offen/In Arbeit/Wartet auf Kunde/Prüfung/Erledigt/Alle) plus `.toolbar` (Suche live, Art-Select, "Nur dringende") plus `.table` (Ticket | Status | Zuständig | Termin | Zeit | Erstellt) plus `.table-foot`. Die Liste bleibt beim Nachladen sichtbar (heute wird sie bei jedem Refetch durch einen Spinner ersetzt und entlädt alle Karten). Filterzustand in die URL.
- Neu `pages/TicketPage.jsx` (Route `/tickets/:woid/:tab?`) plus Hook `useWorkorder(woid)` (nutzt `Query.equal('woid',[String(woid)])`, wie die heutige Direktsuche). Loading, Fehler und "Ticket nicht gefunden". Header mit `.crumb`, `.eyebrow` ("woid · Art"), h1 = Betreff, `.sub` = Kunde als Link, `.actions` = `UrgentToggle` + Status-`<select>` + primärer "Arbeitsblatt hinzufügen". Dann `.facts` (Zuständig-Select, Termin mit Uhrzeit, Fällig, Einsatz, Angefragt von, Erstellt), `.tabs` Details | Arbeitsblätter(n).
- `TicketCard.jsx` entfällt, ihre Zustände und Hooks wandern in `TicketPage` (`useWorksheets(ticket.woid)` immer, `useCustomerAbo` nur wenn Reiter oder Dialog es braucht). `handleCreateWorksheet` (Status-Rückschreiben und Akquise-Automatik für `customerStatus` bei Zugesagt/Abgesagt) wird **vorher** in `lib`/Hook extrahiert und mit einem Test abgesichert, weil es heute ungetestete Geschäftslogik ist.
- `WorksheetList.jsx`: komplett neu als `.ws`-Zeilen (Raster 148px | 1fr | auto, Haarlinie) ohne Gradienten, ohne Aufklappen, ohne Staffel-Animation, ohne JS-Hover und ohne die 35 toten Bootstrap-Klassen. `WorksheetSummary` wird `.stats` (Arbeitszeit gesamt, davon Abo-Stunden (neu berechnet aus `countsToPlan`), Zeiten offen mit `.warn`). Reihenfolge bleibt "neueste zuerst". `formatTime` ist doppelt vorhanden und wird vereinheitlicht ("2:15 h").
- Dialoge auf `<Dialog>`: `CreateTicketModal`, `CreateWorksheetModal`, `WorksheetTimesModal`, `OwnSupportModal`, `SupportersEditor` (Zeile aus Select + Input + "= 30 min" + `.link` "Entfernen"; im 520-px-Dialog zweizeilig).
- `StatusDropdown` und `EditorDropdown` werden native `<select class="select">`. **Wichtig:** der aktuelle Wert muss immer als Option vorhanden sein, auch wenn er nicht in `TICKET_STATUSES` steht (Akquise `In Kontakt/Zugesagt/Abgesagt`, alte `Assigned/Added Info/Halted/Aborted`), sonst zeigt das Select stillschweigend "Offen".
- `UrgentToggle.jsx`: Schalter-Markup.
- `ProjectContextPanel.jsx` wird der Abschnitt "Weitere Tickets von <Kunde>" (immer sichtbar, "Keine weiteren Tickets.").
- `App.jsx`: neue Route; neue `lib/ticketRoutes.js` mit `ticketPath(woid, tab)`.
- Alle 7 Deep-Link-Stellen auf `ticketPath()`: `PlanboardPage` L80, `AssignProjectPage` L156 und L405, `ServicesPage` L72, `ProjectContextPanel` L70, `CustomerFunktionen` L104 (`LeadsPage` entfällt in P0). `TicketsPage` behält `?woid=N` als `<Navigate replace to="/tickets/N">`.
- Löschen: `.tcard*`, `.urgent-toggle`-Altregeln, `.dropdown*`, `.worksheet-summary`, `.chevron`, `.ticket-tab-content`.
- Der Projekt-Block (`TicketProjects`, `AssignedProjectCard`, `PreviewBlock`, Link-Buttons, `AnalyseRow`) bleibt hier unverändert (nur P1-Mini-Fixes) und kommt in P5.
- **Zusatzaufwand (optional, +0,5 PT, nur Leseabfragen):** Tab-Zähler (7 `Query.limit(1)`-Abfragen, Wert `total`), Spalte "Zeit" (eine gebündelte Arbeitsblatt-Abfrage pro Seite mit `worksheetMinutes`), Spalte "Zuständig" aus der Mitarbeiterliste statt `assignedName` (das nach Neuzuweisung veraltet).
- **Test-Auswirkung:** keine bestehenden Tests betroffen. Neu: Tests für `statusTone` (aus P1), die extrahierte Worksheet-Logik und die Filter-zu-URL-Helfer.
- **Sichtbar:** Ticketliste als schlichte Tabelle mit Tabs, Klick öffnet eine eigene Seite mit Facts, Reitern und flachen Arbeitsblatt-Zeilen. Alle Modals sind echte weiße Dialoge mit Esc und Klick außerhalb.
### P3: Planboard (0,5 PT)
- **Dateien:** `pages/PlanboardPage.jsx`.
- **Änderungen:**
- `.week` als CSS-Klasse (7 Spalten, Haarlinien, Wochenende `--bg-subtle`, heute = schwarze Datumspille, `.appt`-Kacheln als `<Link>`, Akquise `--appt-akquise`, unter 860px einspaltig).
- Kopf mit Untertitel "Kalenderwoche N · 5. Oktober bis 11. Oktober · n Termine" (`getISOWeek`, `de`-Locale), Aktionen ← / Heute / →.
- Die 12 Inline-Styles verschwinden.
- **Zu entscheiden:** stornierte Tickets ausblenden (Prototyp) oder wie heute zeigen.
- **Test-Auswirkung:** keine. Planboard-Logik (`groupAppointmentsByDay` usw.) bleibt.
- **Sichtbar:** Wochenansicht ohne Karten.
### P4: Kundenliste und Kundenseite (2,5-3 PT)
- **Dateien:**
- `pages/CustomersPage.jsx`: Tabs (Feste Kunden/Leads/Archiviert/Alle mit Zählern) + Suche + `.table` (Unternehmen mit `.cell-sub` Ansprechpartner | Kundennummer | Standort | E-Mail | Telefon | Offene Tickets), "Neuer Kunde" primär, "Neuer Lead" und die Admin-Beschränkung bleiben. Avatar-Kachel und Stern entfallen ("Wichtig" als Badge).
- `CreateCustomerForm` wird `<Dialog>`. **Kopplung:** `AssignProjectPage.jsx` L12/L277 nutzt die Komponente inline und muss an dieser einen Stelle in P4 mitgezogen werden.
- `pages/CustomerDetailPage.jsx`: Crumb ("Kunden / Name"), Eyebrow (Kundennummer · Stufe als Klartext), h1 nur der Name, 9 Reiter als `.tabs` mit horizontalem Scroll. Übersicht als `.two-col` mit `h2.section` + `dl.kv` (Stammdaten, Rechnungsdaten, Portal-Zugang). Tickets-Reiter als `.table` mit `tr.row`-Links, Projekte-Reiter bis P5 mit P1-Mini-Fixes.
- `CustomerFunktionen.jsx` (Raster-Pseudo-Tabelle → `.table`, Status als Badge-Ton, Aktionen als `.link`; hier landet der `tp2`-Commit, siehe 7.5), `AboStatusCard.jsx`, `CustomerViewLog.jsx`, `KundenansichtButton.jsx` (Standard-Variante `.btn` statt primary, damit pro Kopf nur 1 schwarzer Knopf bleibt).
- `admin-areas.css`: Grundstil neu (Haarlinien, `.area-kv` ≙ `.kv`, `.area-muted/-danger/-warn` → `--text-2/--red-fg/--orange-fg`, Tint-Kästen raus), die Raster der Gutschriften/Angebote/Speicher folgen in P6.
- `lib/customerTabs.js` bleibt unverändert.
- **Test-Auswirkung:** keine (`customerTabs.test.mjs` pinnt `id`/`label`).
- **Sichtbar:** Kundenbereich ohne Karten, mit Tabelle und `kv`-Listen.
- **Entscheidung:** Stammdaten bleiben wie heute immer editierbar (im `kv`-Layout), oder der Prototyp-Umschalter "Bearbeiten" wird übernommen (Standard: bleiben editierbar).
### P5: Projekte, Zuordnen-Assistent, Services, Projekt-Bausteine (3,5-4 PT)
- **Dateien:**
- `pages/ProjectsPage.jsx`: `PageHeader` + "Gitea abgleichen" (`.btn`) + "Neues Projekt" (primär), Tabs (Aktiv/Vorlagen/Archiviert), `.toolbar` mit Suche + Kunde-Select (+ Mitarbeiter-Select, siehe Abschnitt 8), eine `.table` (Projekt mit `.cell-sub` Repo | Kunde | Art | Adresse | Ticket als `.link` | Status-Badge | Letzter Push), Fuß "n Projekte". Klick öffnet den Dialog.
- **Alle heutigen Steuerelemente müssen erhalten bleiben** (Liste in der Projekt-Inventur): Suche, 6 Ansichten, Kunde- und Mitarbeiter-Filter, Kunde-Select je Projekt (inklusive `window.confirm`), Hosten/Aktualisieren/Preview/Gitea, "Weitere Treffer"-Links, `notice`-Zeile, Fehlerbanner.
- Neu-Dialog: Anzeigename, Subdomain, **Repo-Name** (im Prototyp nicht vorhanden), Kunde. Bearbeiten-Dialog: Name, Typ, Subdomain mit Ergebnis-Hinweis, Archivieren/Reaktivieren (`.link-danger`, `window.confirm` bleibt), "Speichern & synchronisieren".
- `PreviewBlock` steht **außerhalb** des `<form>` (sonst löst Enter im Hauptdialog Speichern aus). Der Dialog wird breiter (`.dialog-wide`) oder der Block ein eigener Abschnitt.
- `fetchAllProjects` zeigt "wird geladen" nur beim Erstladen (heute springt die Liste nach jeder Aktion).
- `AssignProjectPage.jsx`: `.crumb` + h1 "Projekt zuordnen" + `.sub`; die Header-Card entfällt, jeder Schritt = `h2.section` + Inhalt; Art-Wahl als `.seg`/`.btn`-Reihe, Ja/Nein als zwei `.btn`, Kunden- und Ticketliste als `.list-row` mit `--bg-hover`-Auswahl **(Auswahl nicht nur über das alte `var(--surface-2)`, sondern auch textlich)**; Zusammenfassung als `.kv`; Preview-Zeile mit `.check` und `.field`/`.hint`. Reine Logik (`nextStep/prevStep/canFinish/openTicketsFor`, R9-Regel, 409) bleibt.
- `ServicesPage.jsx`: `h2.section` je Art mit Zähler, Projektzeilen als `.list-row`, Feature-Badges (Verfügbar grün/In Entwicklung orange), Preview-Punkt → Badge (Online grün, Wird gebaut orange, Fehler rot, Aus plain).
- Projekt-Bausteine: `TicketProjects`, `AssignedProjectCard` (zugleich in `CustomerDetailPage`), `AnalyseRow` (Zustimmung wird `<Dialog>`, Wortlaut aus `lib/analyseView.js` **unverändert**), `PreviewBlock` (Statuspunkt → Badge, Fehlertext `--red-fg`, Log auf `--bg-subtle`, 27 Inline-Styles in 4-5 Klassen), `PreviewLinkButton`, `GiteaLinkButton`, `CopyableCredential` (auch von `CustomerDetailPage`, `AdminPage`, `CustomerFunktionen` genutzt, Signatur von `copyText()` unverändert).
- `lib/previewEnv.js`, `lib/previewView.js`: Vokabular bleibt, die Zuordnung auf Badge-Ton passiert in der Komponente (so bleiben `previewEnv.test.mjs` und `previewView.test.mjs` grün). `dotColor()` bleibt als Funktion bestehen, wird aber nicht mehr gerendert (kann in P10 samt Test entfallen).
- `btn-sm`-Altlast (7 Dateien) verschwindet.
- **Test-Auswirkung:** keine (Tests prüfen nur Logik). `servicesView.test.mjs` deepEqual-t `features` – nur ändern, wenn `servicesView.js` ein Feld bekommt (Standard: nicht).
- **Sichtbar:** Projektbereich als Tabelle, Zuordnen als ruhige Schrittseite, flache Projekt-Abschnitte im Ticket und beim Kunden.
### P6: Chat, Gutschriften, Abo-Angebote, E-Mail-Speicher (4 PT)
Im Prototyp sind diese vier Seiten nur Platzhalter, die Gestaltung wird aus den Bausteinen abgeleitet und am ersten fertigen Screenshot freigegeben.
- **Dateien:**
- Chat: `CustomerChatPage.jsx` (Liste als Haarlinien-Zeilen ohne Card, senkrechte Trennlinie `--line`, Ungelesen-Zähler als kleine Zahl oder schwarze Pille, `.input` für die Suche), `ChatThread.jsx` (nur Fehlerzeile ohne Icon, **`woms-chat`, `chat-messages` und die Container-Kinder bleiben**), `chat.css` komplett neu (siehe 7.2).
- Gutschriften: `CreditsPage`, `CreditsView`, `CreditList` (Filter als `.tabs`, `.table` mit klickbarer Zeile, 7 Spalten brauchen `hide-sm/hide-md`-Priorisierung), `CreditDetail` (zweispaltig: `.two-col` + `h2.section` + `.kv`, Berechnung als `.field-row`, Pruef-Badges ohne Glyphen; Knöpfe: Genehmigen `.btn-primary`, Rückfrage `.btn`, Ablehnen `.link-danger`; `busyRef`, `window.confirm`, Debounce und die Eingabe-IDs `calc-*`/`credit-*` bleiben).
- Abo-Angebote: `OffersPage`, `OffersView`, `OfferList` (Tabs/Select statt `.seg`, `.table`, Aktionen als `.link`), `OfferForm` (Dialog, Autocomplete-Clipping prüfen).
- E-Mail-Speicher: `StorageTasksPage` (`.table`, `data-storage-done` am Knopf erhalten).
- `admin-areas.css` fertig (Rest-Raster) bzw. löschen, sobald alle 7 Importeure umgestellt sind.
- **Test-Auswirkung:** keine (reine Lib-Tests).
- **Sichtbar:** Messenger hell, Gutschriften/Angebote/Speicher als Tabellen.
### P7: Finanzen (4-4,5 PT)
- **Dateien:**
- `global.css` L1155-1476 neu (rund 320 Zeilen, 26 Dunkel-Literale).
- `FinancePage` (`h2.section` je Block, **`INVOICES_ANCHOR` und `finanzen-buchungen` als `id` samt `scroll-margin-top` bleiben**, `.fin-stack gap` auf 0 setzen, sonst 36px + 24px Abstand), `FinanceSettingsPage`, `BankCallbackPage`.
- `LiquidityHead` (`.stats` mit `.v.neg`), `StatusLine` (unboxed), `TodoList` (Zeilen mit Badge, kein farbiger Streifen), `ForecastTable`, `TaxCard`, `AboCard`, `InvoiceTable` (Altersstreifen als `.stats`, Filter `.seg`, Status-Badges, Aktionen als Text), `TransactionTable`, `Pager` (`.table-foot`), `BankAccessCard`, `FixedCostsCard`, `SettingsCard`, `FormField` (Fehlerring statt Rahmenfarbe), `AddressWarning`, `CustomerInvoices`.
- Dialoge: `AssignDialog` (Zweischritt-Bestätigung unverändert, Vorschläge als `.check`-Zeilen statt Karten, `useDialogFocus`, Esc-Listener, `useScrollIntoView`), `InvoiceCreateModal` (breiter Dialog, Positionsraster behält die Spalten, Autocomplete-Popover, Enter sendet nie).
- Responsive: die Container-Query-Stapelung (@container 560/640/860) bleibt, `container-type` wandert vom `.fin-*-card` auf den Wrapper (`.fin-block`/`.fin-table-wrap`). Die Klassen-Hooks (`fin-forecast-date`, `fin-inv-*`, `fin-tx-*` und `data-label`) und die `role`-Attribute bleiben.
- Beträge: `euro()`/`euroWhole()`/`signedEuro()` (U+2212) und tabular-nums bleiben. Negative Salden `--red-fg`, die Vorzeichen bleiben der eigentliche Träger der Bedeutung.
- **Test-Auswirkung:** keine (finance-Tests sind Lib-Tests, deepEqual auf Töne bleibt, weil das Mapping in `Badge.jsx` sitzt).
- **Sichtbar:** Finanzen ohne Karten, Dialoge weiß.
### P8: Rechtliches und Admin (2 PT)
- **Dateien:**
- Rechtliches: `LegalPage` (Tabs), `LegalTextsTab` (Kartenraster → eine Spalte oder Tabelle, Versionen als Haarlinienzeilen ohne Kästen und gestrichelte Ränder, Aufklapp-Zeile wird echter `<button>`), `ProviderContractsTab` (`.table`, Dringlichkeit nur im Termin-Feld als Badge/Text, keine getönten Zeilen), `ContractForm` und `VersionForm` auf `<Dialog>` mit `useDialogFocus` (heute ohne Fokusfalle und Esc), `legalUi`, `global.css` L1122-1153.
- Admin: `AdminPage.jsx` (Header, Tabelle `Mitarbeiterkonten`, Formular als Dialog, Zeilenaktionen als Text), `AdminPage.css` komplett neu schreiben (rund 350 von 416 Zeilen entfallen). **Behalten und neu einfärben:** `.credential*`, `.sr-only`, `.spin` (werden auch von `CopyableCredential` in `CustomerDetailPage`, `CustomerFunktionen` benutzt; sie wandern nach `global.css`).
- **Test-Auswirkung:** keine (`legal.test.mjs` pinnt nur die Token `red|orange|normal|grey|missing`, die als Daten bleiben).
- **Sichtbar:** Rechtliches und Admin ohne Karten.
### P9: Serverpuls (1-1,5 PT)
- **Dateien:** `ServerpulsPage.jsx`, `serverpuls.css`, `AppsTable.jsx`, `Chart.jsx`, `Sparkline.jsx`.
- **Änderungen:**
- Kopf: Emoji → Text-Badge. Alerts als `.list-row` mit Stufen-Badge (kein Linksstreifen). Kacheln als `.stats` mit Sparkline darunter. Deltas als Textfarbe (rot = gestiegen, grün = gesunken). Panels als `h2.section`. Pills als `.seg`.
- Chart-Farben als Konstanten (Canvas kann kein `var()`), eine gemeinsame Konstante: Linie `#111111`, Füllung `rgba(17,17,17,.06)`, Spitze `#8a93a3` gestrichelt, Achsen `#5e6678`, Grid `#ebeef1` (nur horizontal), Ticks `#d8dde3`. Die Projekt-Detail-Charts werden alle `#111111`. Legende 13px `--text-2`.
- `data-testid` (19 Stück in Serverpuls plus `sp-nav-dot`) bleiben.
- **Test-Auswirkung:** keine (`serverpulsFormat.test.mjs` prüft `dir` und `stale`, nicht Klassen).
- **Sichtbar:** helles Dashboard.
### P10: Aufräumen (1 PT)
- **Dateien:**
- `package.json` plus Lock: `@tabler/icons-react` und `react-icons` entfernen (nach `rg` = 0 Treffer).
- Kompat-Block, tote Klassen und Aliase löschen (`.ui-btn*`, `.ui-card*`, `.badge-*`, `.overlay-content`, `.overlay-close`, `.form-control`/`.form-group`/`.form-label`, `.spinner`, `.btn-dark`, `.btn-teal`, `.text-grey`, `.dropdown*`, Alt-Token, `--surface-*` usw.).
- `src/assets/fonts` (falls nicht schon in P1), `previewEnv.dotColor` samt Test, `Kpi`-Reste.
- Kommentare in `navLinks.js` und `navLinks.test.mjs` ("Navbar ordnet Icons zu") anpassen.
- **Optional, als eigenes kleines Paket:** `git rm -r --cached node_modules` (8.890 Dateien). `.gitignore` führt es ohnehin, der Build installiert neu, `tp2` ist unkritisch (siehe 7.6).
- **Abschluss:** Bundle-Größe vorher/nachher, Screens aller Bereiche, `rg "style=\{"` dokumentieren (Ziel: nur Laufzeitwerte).
- **Sichtbar:** nichts Neues, kleineres Bundle.
---
## 6. Offene Entscheidung: Ticket als eigene Seite oder aufklappen
Zusammenfassung der Vergleichsanalyse des Ticket-Lesers (Aufwand und Risiko jeweils auf den visuellen Umbau draufgerechnet):
| Kriterium | Eigene Seite (`/tickets/:woid`) | Aufklappen (Karte bleibt) |
|---|---|---|
| Abweichung vom freigegebenen Prototyp | keine | groß (Prototyp hat Crumb, Eyebrow, h1, Facts, Tabs mit URL) |
| Zusatzaufwand Struktur | **+2 bis 2,5 PT** (Route+Seite+Hook 0,5; Karte → Seite, `handleCreateWorksheet` extrahieren 0,75; Filter in URL, Tabellenzeilen als Links 0,75; Deep-Links, Planboard, Navigation nach Neuanlage 0,25; QA 0,25-0,5) | **+0,75 bis 1 PT** (Zeile als Tabellenzeile mit Detail darunter, `<button aria-expanded>`, Auto-Aufklappen bei `?woid=`) |
| Deep-Link auf ein Ticket oder einen Reiter | erstklassig (`/tickets/123/arbeitsblaetter`) | `?woid=` zeigt eine einzelne eingeklappte Karte, kein Reiter-Link |
| Browser-Zurück und Lesezeichen | ja | nein |
| Lange Listen | Liste bleibt kurz | Seite wird lang |
| Performance | Liste mountet nicht mehr pro Zeile 3 Modals + `useEmployees` + `useWorksheets` + `useCustomerAbo` | bleibt pro Zeile |
| Filterzustand | muss in die URL (löst nebenbei das heutige Zurücksetzen beim Zurückkehren) | entfällt |
| Bekannter Fehler | Liste entlädt sich bei jedem Refetch → muss in beiden Varianten behoben werden | dort besonders schlimm: Refetch klappt alle offenen Karten zu und verliert deren Zustand |
| Risiko | mittel: sechs bis sieben Links, Filter-in-URL, Tickets ohne `woid` (Fallback `$id.slice(-5)`), Zurück-Verhalten | niedrig |
**Empfehlung: eigene Seite.** Sie entspricht dem freigegebenen Prototyp und, laut Projektnotiz, der Entscheidung des Eigentümers (siehe 0.4). Außerdem verschwindet der teure Pro-Zeile-Zustand, und Deep-Links werden einfach. Die Mehrkosten von rund 1,5 PT gegenüber "aufklappen" bestehen größtenteils aus der Filter-in-URL-Arbeit, die ohnehin einen heutigen Schmerzpunkt behebt.
Umsetzungsskizze (alles in P2, kein Datenmodell nötig):
1. Route `/tickets/:woid/:tab?` vor dem Fallback `*`. Die Sidebar markiert "Tickets" für Unterpfade bereits (`isActive` nutzt `startsWith(href + '/')`).
2. Datenabruf per `woid` (nicht `$id`): `useWorkorder(woid)` mit `Query.equal('woid',[String(woid)])`, Rückgabe wie `useWorkorders` inklusive `updateWorkorder`. Zustände: Laden, Fehler, "Ticket nicht gefunden". Fallback für Tickets ohne `woid`: Parameter als `$id` zulassen (Direktabruf).
3. Reiter `details`/`arbeitsblaetter` im Pfad (nicht im State), Modals einmal pro Seite.
4. Legacy `?woid=` bleibt per `<Navigate replace>`. Die sieben Link-Stellen wechseln auf `ticketPath()`.
5. Filter-, Such- und Limit-Zustand der Liste in `useSearchParams`; Breadcrumb "Tickets" führt mit demselben Suchstring zurück. Nach "Neues Ticket" Navigation auf `/tickets/<neue woid>` (`createWorkorder` liefert `data.woid`).
6. `TicketProjects` löst bei jedem Seitenaufruf `importNewReposThrottled()` aus (auf 60 s gedrosselt). Das ist heute beim Aufklappen der Fall und ändert sich nicht.
7. Fallback bei Rückzug der Entscheidung: P2 ohne Route (0,75-1 PT statt 2-2,5 PT), Zeile bleibt `<button aria-expanded>`, das Refetch-Entladen muss trotzdem behoben werden.
---
## 7. Risiken und Stolpersteine
### 7.1 Charts (Serverpuls, uPlot)
- Canvas-Farben kommen als JS-Optionen (`Chart.jsx` L28-36 und L45, `ServerpulsPage.jsx` L223-237). CSS-Variablen wirken dort nicht, es müssen Literale sein. Die Instanz wird nur bei geänderten Serien-Labels oder Höhe neu gebaut, die Farben müssen also statische Konstanten sein.
- Weiße Alpha-Grid- und Tick-Linien sind auf Weiß unsichtbar, `#9aa7b4` hat 2,45:1. Beides steht als Pflicht-Mini-Fix in P1.
- Alt-Inkonsistenz: Amber ist im Hauptchart "Spitze", im Detailchart "CPU"; Smaragd ist im Hauptchart "CPU", im Detailchart "RAM". Mit durchgängigem Schwarz verschwindet das Problem.
- `uplot/dist/uPlot.min.css` (Cursor `#607D8B`, Selektion `rgba(0,0,0,.07)`) passt schon auf Weiß.
### 7.2 Fremd gerenderter Chat-Composer
- Das Portal-CSS `chat-composer.css` wird erst beim ersten Öffnen eines Chats ans Ende von `<head>` gehängt (`lib/loadChatComposer.js`), also **nach** dem gebauten WOMS-CSS. Gleich spezifische WOMS-Regeln verlieren. Deshalb müssen alle WOMS-Chat-Regeln das Präfix `.woms-chat` behalten oder `!important` tragen.
- Der Fremdcode ist nur über `--chat-*`-Variablen auf `.woms-chat` und überstimmende Selektoren steuerbar. Hart verdrahtete Weiß- und Schwarz-Alphas (Icon-Knöpfe, Datei-Chips `rgba(0,0,0,.22)`, Tray, Audio-Leiste, Menü-Hover) sind auf Weiß unsichtbar oder nur halb lesbar. Sie müssen einzeln überschrieben werden: transparente Knöpfe mit Ring-Schatten, Chip `--bg-hover` ohne Rand, Menü weiß mit `--shadow-pop`, Senden-Knopf schwarz mit weißer Schrift, Fehlerfarben `--red-fg`.
- Lightbox und Kamera hängen am `<body>` und sehen nur die Portal-Defaults (Lila, dunkel). Standard: dunkel lassen (Medienbetrachter), da sie die `--chat-*`-Werte von `.woms-chat` nicht erreichen.
- Nicht änderbar aus WOMS: Texte ("Senden", "Aufnahme", "Bild", "Kamera", "Datei", "Dateien hier ablegen" als CSS-`content`), 15 Inline-SVGs, das Portal-Repo (teilt die Datei mit dem dunklen, lila Kundenportal). Eine Tokenisierung dort wäre ein Eingriff ins Kundenportal und nur nach Absprache sinnvoll.
- Vertrag zwischen WOMS und Portal: Klassen `woms-chat`, `chat-messages`, `chat-bubble`, `chat-meta`, `chat-dropzone`/`chat-drop-active`, `dataset.stick`. React besitzt nur die leeren Container. `ChatThread.jsx` darf dort keine Kinder oder Struktur ergänzen.
- Desktop-/Handy-Breakpoint des Fremdcodes ist 640 px (Platzhalter) und 880 px / coarse pointer (Senden-Symbol), unabhängig von den WOMS-Breakpoints. Der Chat-Wechsel Liste/Verlauf (`chat.css`, 720 px) bleibt davon unberührt.
- Tipp: `tests/unit/loadChatComposer.test.mjs` sichert die Lade-Reihenfolge. Nicht anfassen.
### 7.3 Latin-1
Nur `.env.development` und `.env.production`. Nicht editieren. Alle Quellen sind UTF-8, das Bearbeiten mit Edit/Write ist unproblematisch. Die einzige Binärdatei im Quellbaum ist `lato-regular.ttf` (wird gelöscht).
### 7.4 Tests
- 312 Tests, reine Logik, keine Komponententests. Ein stilles Brechen von Markup oder Layout bemerkt kein Test, die Absicherung ist ausschließlich der Screenshot-Abgleich.
- Design-gekoppelte Tests:
- `adminRoute.test.mjs` liest `AdminRoute.jsx` als Quelltext und verlangt `<p>Wird geladen …</p>` ohne `className`/`style` (und kein "Loading"). Das Markup bleibt wörtlich.
- `sidebar.test.mjs` (Breakpoint 720, `startsCollapsed`) wird mit `lib/sidebar.js` gelöscht.
- `previewEnv.test.mjs` prüft die Hex-Werte von `dotColor()`, `previewView.test.mjs` die Token `green|yellow|red|grey`. **Gegenmittel:** Vokabular und Hex unverändert lassen, nur in der Komponente mappen.
- Ton-Namen `muted|info|ok|warn|danger` sind deepEqual-gepinnt in `financeFormat`, `financeView`, `financeSettings`, `adminFormat`, `funktionenView`. Gegenmittel: Mapping nur in `Badge.jsx`.
- `legal.test.mjs` pinnt `deadlineTone` (`red|orange|normal|grey|missing`).
- `customerTabs.test.mjs` pinnt `Object.keys(entry)` auf `id` und `label`.
- `navLinks.test.mjs`: deepEqual auf `key/label/href/count`, ein zusätzliches Feld `hot` ist erlaubt.
- `servicesView.test.mjs` deepEqual-t `features [{label, statusLabel}]`.
- Lücken (neu mit Tests): `ticketStatus.js` (Status-Töne), `ticketForm.worksheetTone`, `planboard`, alle Hooks, alle Komponenten, die extrahierte Worksheet-Logik, die URL-/Filter-Helfer.
- `tests/unit/load.mjs` bündelt mit `esbuild`, das nur transitiv über `vite` da ist (0.21.5) und nicht in den `devDependencies` steht. Ein Vite-Major-Upgrade kann die Tests still brechen.
### 7.5 Paralleler Branch `feature/terminplaner-tp2`
- Divergiert (1 ahead, 3 behind). Der eine Commit `6f03712` berührt `CustomerFunktionen.jsx` (3 Zeilen), `funktionenView.js`, `funktionenView.test.mjs`.
- Empfehlung: **vor P4 in `test` mergen** (Push durch den Eigentümer) oder den Commit per Cherry-Pick in `dev-design` holen. Dann gibt es keinen Konflikt in der in P4 komplett neu gebauten Datei. Alternativ nach P4 per Rebase mit manuellem Auflösen der 3 Zeilen.
- Sollte TP2 später weitere WOMS-Oberfläche bauen: ab P1 mit den neuen Klassen und `Dialog`, nicht mit den Altklassen. Der WOMS-Anteil des Plans ist mir nicht im Detail bekannt (der Plan-Commit `a72380c` liegt im Portal-Repo).
### 7.6 Getracktes `node_modules`
- Siehe 0.6: für den Build wirkungslos, aber Stolperfalle für Commits (nur explizite Pfade) und für `npm uninstall` (147 gelöschte getrackte `react-icons`-Dateien erscheinen im `git status`).
- Neue Worktrees checken die 8.890 Dateien mit aus und sind danach sauber (geprüft in `dev-termine`). Ein `npm install` im Worktree ändert getrackte Dateien.
- Optionaler Cleanup `git rm -r --cached node_modules` ist unkritisch, auch wenn `tp2` das Verzeichnis unverändert trägt.
### 7.7 Deploy-Webhook und Auslieferung
- Siehe 0.1 und 0.2. Nur `test` deployt, `build/` ist Live-Checkout, kein Staging.
- Jedes Paket muss einzeln lauffähig und rückrollbar sein. Rollback per Revert-Commit.
- Container-Build hat 1 GB Node-Heap und macht pro Deploy `npm install` (1-3 min). Neue `public/`-Dateien werden von Vite nach `dist/` kopiert, `deploy.sh` kopiert `dist/*` nach `/out`, `public/favicon.svg` ist damit unkritisch.
- Server-Speicher knapp (Vite dev wird per OOM getötet). Nur der esbuild-Harness unter `systemd-run` ist belastbar.
### 7.8 Weitere Stolpersteine
- **Eine CSS-Bundle:** `App.jsx` importiert alle Seiten statisch, es gibt genau ein CSS. Ein entfernter oder umbenannter Klassenname bricht Seiten, die niemand öffnet, und fällt erst zur Laufzeit auf. Das ist die Begründung für den Kompat-Block in P1 und die QA-Pflichtliste.
- **Namenskollisionen** zwischen Prototyp und Bestand mit anderer Bedeutung: `.btn`, `.badge`, `.page`, `.field`, `.seg`, `.toolbar`, `.empty`, `.list-row`, `.urgent-toggle` (Schalter statt gestrichelter Pille), `.overlay` (Backdrop plus zentrierter `.dialog` statt schwarzer Vollbild), `.id`, `.muted`/`.faint`. `.table` ist unkritisch, die Bestandsregel `.table` ist tot.
- **79 `!important`** (`.btn-green`, `.btn-primary`, `.form-control`, `.card`, `.status-*`, `.priority-*`, `.dropdown .btn`) kämpfen gegen neue Regeln. Löschen, nicht überschichten.
- **Dokument-Scroll statt Scroll-Container:** `position: sticky` in `AdminPage.css` L186 bekommt einen neuen Bezug, `scrollIntoView` ist unkritisch (`useScrollIntoView`, `ChatThread` hat eine eigene Scrollbox). 100vh kommt nur in `LoginPage` und `global.css` L801 vor.
- **Breakpoints:** Sidebar 720 → 860, dazwischen halbfertige Layouts, wenn Seiten noch die alten Breakpoints nutzen (`@media 720`, `700`, `640`, `768`). Pro Paket die Seitenbreakpoints auf 860/1100 ziehen.
- **Dialoge:** `useDialogFocus` braucht `onKeyDown` am Overlay, einen inneren Dialog mit `ref` und `tabIndex={-1}` und fokussierbare Elemente **innerhalb** des Overlays (der Schließen-Button muss drin sein). Escape-Listener der Finanzdialoge: `InvoiceCreateModal` respektiert `e.defaultPrevented` für das Autocomplete. Native `required` und `novalidate`-Verhalten bleiben. Autocomplete-Popover (280px) im scrollenden `.dialog-body` kann abgeschnitten werden.
- **Link-Erkennbarkeit:** viele Links hängen an `--accent` (siehe 2.2) und werden schwarz. `a { color: inherit }` plus `.link` mit Unterstreichung ist der Ausweg. `a.list-row` und Tabellenlinks brauchen eine Hover-Reaktion.
- **Statusfarben-Semantik** (Offen grün → grau, Erledigt grau → grün, Prüfung teal → violett) ist für Mitarbeiter ein sichtbarer Bedeutungswechsel.
- **Zeilenklick vs. Controls:** `tr.row` darf Klicks auf `a`, `button`, `select`, `input` nicht verschlucken (der Prototyp ignoriert sie). Für Tastatur und Mittelklick ein echter Link in der ersten Zelle.
- **Abo-Stunden-Sichtbarkeitsregel** in `WorksheetList` (`onTogglePlan && !isComment && totalTime>0 && (aboHoursAllowed || countsToPlan)`) und `canEditTimes`, `canAddSupport`, `isGit`, `needsTimeEntry` bleiben beim Rewrite exakt.
- **Datenabdeckung der Prototyp-Spalten:** "Zeit", "Tab-Zähler", "Offene Tickets (Kunden)", "Letzter Push" haben keine fertigen Felder (siehe Abschnitt 8).
- **`tr`-Hover-Trick** des Prototyps (`box-shadow` links/rechts 12px) setzt eine `border-collapse: collapse`-Tabelle ohne `overflow:hidden` voraus. In Dialogen und Container-Wrappern prüfen.
- **StrictMode:** doppelte Effekt-Läufe in Dev sind nach Entfernen von `LetterGlitch` (rAF-Dauerlast, keine Reduced-Motion-Abfrage) kein Thema mehr.
---
## 8. Was bewusst NICHT geändert wird
Das Redesign ist rein visuell. Ausnahme ist allein die Ticket-Seite samt Route (Abschnitt 6).
- **Verhalten:**
- Alle Hooks (`src/hooks/*`) und alle Lib-Logik: `finance*`, `assign*`, `support`, `mfa`, `staffAccess`, `serverpuls*`, `projectList`, `analyse*`, `previewApi` usw.
- Auth- und 2FA-Ablauf, Polling (Sidebar 5 min, Serverpuls 60 s plus bei Sichtbarkeit, Chat 15 s und 30 s, Preview 5 s), Pflichtfelder und Validierungen, 15-Minuten-Schritte, Doppelklick-Sperren (`busyRef`, `lockRef`).
- Alle `window.confirm`/`alert`-Rückfragen bleiben (rund 12 Stellen).
- URL-Parameter `?tab=`, `?antrag=`, `?kunde=`, `?woid=` (letzteres als Redirect), alle `data-testid`.
- Navigation: `navLinks.js` (Reihenfolge, Labels, Hrefs, pausierte Leads) unverändert, Routen außer `/tickets/:woid`.
- Lead-Automatik bleibt pausiert.
- **Daten:**
- Appwrite-Schema, Collections, Attribute, Permissions, Indizes. Keine neuen Felder, keine Datenmigration.
- Gespeicherte Statuswerte (englisch, das Kundenportal leitet daraus die Fortschrittsleiste ab).
- Portal-Repo und `chat-composer.css/js`.
- `.env*` (Latin-1).
- Server-API.
- Sortierung der Ticketliste bleibt `$createdAt` absteigend. Der Prototyp-Fußtext "Sortiert nach Dringlichkeit, dann neueste" würde einen Index auf `priority` brauchen und wird deshalb im Fußtext angepasst, nicht in der Datenbank.
- **Texte:**
- Alle rechtlich relevanten Texte (AVV-/Zustimmungstext der Website-Analyse samt Textvorlage, Abo-/Gutschrift-/§19-Hinweise, `TaxCard`-Disclaimer), Mail- und Chat-Vorlagen und die Lib-Fehlermeldungen.
- Statuslabels und Tab-Überschriften, soweit sie schon mit dem Prototyp übereinstimmen (Offen, In Arbeit, Wartet auf Kunde, Prüfung, Erledigt, Storniert).
- Die ae/ue-Schreibung in Bestandstexten (zum Beispiel "oeffnen", "Menue") wird nicht flächig korrigiert.
- Ausnahmen, die dem **Prototyp-Wortlaut** folgen (freigegeben): Login ("Anmelden", "E-Mail", "Passwort", "Weiter", "Code eingeben", Untertitel "Ticketsystem für das WEBklar-Team"), "Abmelden", "Wird geladen …" statt "Loading...", "Menü".
- **Nicht im Umfang:** Dark-Mode-Umschalter, neue Funktionen, Terminplaner TP2, Server-/Compose-Änderungen.
### 8.1 Abweichungen Prototyp zu Bestand, die vor dem jeweiligen Paket entschieden werden müssen
Standardvorschlag in der letzten Spalte (wird übernommen, wenn der Eigentümer nichts anderes sagt).
| # | Paket | Punkt | Standard |
|---|---|---|---|
| 1 | P1/P2 | Statusfarben-Wechsel (Offen/Erledigt/Prüfung) | wie Prototyp |
| 2 | P2 | Einfachwahl-Tabs (Aktiv, Offen, …) statt Mehrfachauswahl plus "Anwenden"; "Aktiv" = bisheriger Standard (open+working+waiting+review) | wie Prototyp, Filter live |
| 3 | P2 | Tab-Zähler und Spalte "Zeit" (zusätzliche Lesezugriffe) | mitliefern (+0,5 PT), "Zuständig" aus Mitarbeiterliste |
| 4 | P2 | Status-Auswahl: Prototyp bietet bei Akquise nur [In Kontakt, Erledigt, Storniert], Bestand zeigt für alle Tickets die sechs generischen Werte | Bestand beibehalten, aktueller Wert immer als Option |
| 5 | P2 | "Neues Ticket": Prototyp mit Zuständig-Select und Einsatz-Segment, Bestand mit automatischer Zuweisung und ausgeblendetem Einsatz | Bestand beibehalten |
| 6 | P2 | Arbeitsblatt-Dialog: Hinweis "Der Kunde sieht diesen Text im Kundenportal" ist im Prototyp, im Bestand unklar | nur übernehmen, wenn nachweislich wahr |
| 7 | P2 | Reihenfolge der Arbeitsblätter (Prototyp älteste zuerst, Bestand neueste zuerst) | Bestand |
| 8 | P2/P3 | Stornierte Tickets im Planboard | wie Bestand (anzeigen); Prototyp blendet sie aus |
| 9 | P4 | Spalten "Telefon" und "Offene Tickets" in der Kundenliste | "Telefon" ja (Feld vorhanden), "Offene Tickets" nein (zusätzliche Abfrage) |
| 10 | P4 | Stammdaten: Lese/Bearbeiten-Umschalter (Prototyp) oder immer editierbar (Bestand) | immer editierbar |
| 11 | P5 | Projektansichten "Unzugeordnet", "Zugeordnet", "Alle inkl. Archiv" und Mitarbeiter-Filter haben im Prototyp keinen Platz | Tabs Aktiv/Vorlagen/Archiviert plus Selects (Kunde mit Option "Ohne Kunde", Mitarbeiter); "Alle" als vierter Tab |
| 12 | P5 | Spalte "Letzter Push" (kein Feld in `websiteProjects`), "Art" meint `typeOf()` (Hosting-Modus), nicht `project.kind` (Services-Art) | `$updatedAt` zeigen oder weglassen; Begriffe sauber trennen |
| 13 | P6/P7/P8/P9 | Chat, Gutschriften, Angebote, Speicher, Services, Finanzen, Rechtliches, Admin, Serverpuls sind im Prototyp nur Platzhalter | Gestaltung aus den Bausteinen, Abnahme am ersten fertigen Screenshot je Paket |
| 14 | P6 | Chat-Blasen: eigene Blase grau oder schwarz | fremd `--bg-hover`, eigene `--grey-bg` (Schwarz wäre zu viel Akzent); Lightbox/Kamera dunkel |
| 15 | P5 | Kopier-/Zeigen-Knöpfe in `CopyableCredential`: Icons oder Text | Text |
| 16 | P1 | Footer ("Webklar Ticket-Plattform · © Jahr") | entfällt; falls nötig, 12px in `.sidebar-foot` |
| 17 | P1 | `<title>` ("Webklar WOMS 2.0" oder "Ticketsystem · WEBklar") | unverändert lassen |
| 18 | P1 | Serverpuls-Punkt: ein Zustand (Prototyp) oder zwei (Bestand: medium und critical) | zwei (amber, rot) |
| 19 | P6 | Pseudo-Tabellen mit 5-7 Spalten (Gutschriften, Angebote): Prototyp blendet Spalten aus (`hide-sm/hide-md`) statt zu stapeln | Spaltenpriorität je Tabelle festlegen, Aktionen in letzte Spalte |
| 20 | P2 | `.btn`-Variante "danger" und `size="sm"` haben im Ziel keine Entsprechung | `.link-danger` bzw. Einheitsgröße (32px) |