Umsetzungspläne Aufträge und Rechnungen: Übersicht und 12 Strang-Pläne
Parallele Stränge L0–L7 (Portal), W0–W2 (WOMS), Z (Integration und Kontrolle); Vertragsfragen der Planrunde entschieden (Abschnitt „Nachträge nach der Planrunde“ in der Übersicht). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0bb463eb7d
commit
9d82fc579f
13 files changed
+25196
-3
No files matched your search
@@ -91,7 +91,7 @@ Die Strang-Pläne wurden parallel geschrieben; ihre „Vertragsfragen“ sind hi
|
||||
|
||||
**L2:** `baueRechnung` bekommt zusätzlich `createdBy`; `zeilentexte[schluessel]` ersetzt den ganzen Text vor dem letzten „ – “ (einschließlich Datum bzw. „KW …“); **Selbstheilung**: stößt eine Prüfung („bereits abgerechnet“, offener Entwurf zur AB) auf eine Projektrechnung, deren Stripe-Rechnung storniert oder gelöscht ist, wird sie `aktiv: false` und die Prüfung wiederholt; nur finalisierte Rechnungen zählen als „bereits berechnet“, ein Schluss-Entwurf blockiert weitere Anzahlungen; Schlussrechnung bis 40 Zeilen; Aufwand ohne `auftragId` nimmt die jüngste gültige Aufwands-AB nur bei eingeschaltetem Schalter (sonst 100 €); Buchungsfehler nach einem Stripe-Erfolg werden protokolliert, nicht geworfen; Schreibweisen „KW 05/2026“ (Schlüssel `KW-2026-05`), `stundenText` ohne Endnullen, Texte eines Tages mit „; “ verbunden.
|
||||
|
||||
**L3:** Senden: Sperren `SPERRE_NUMMER` + `sperreAuftrag(id)` → **innerhalb** der Sperre Eingabe und Kunde prüfen → Nummer → PDF (`AuftragError` aus `erzeugeAuftragPdf`, z. B. 409 „Entwurf unvollständig …“, wird durchgereicht; andere Fehler 500) → `speicherePdf` → Token → speichern (scheitert danach etwas: `entfernePdf`) → Sperren frei → Mail mit dem **gespeicherten** Dokument. „Erneut senden“ läuft über `/erinnern`: nach einem Mail-Fehler als normale K31, sonst als Erinnerung; `gesendetAn` = aktuelle Kunden-E-Mail. Neu: `ordneProjektZu(id, projektId)` + Route `POST …/auftraege/:id/projekt` (V5). Stripe-Metadaten wandern beim Ersetzen **nicht** mit (nur `projektRechnungen.auftragId`).
|
||||
**L3:** Senden: Sperren `SPERRE_NUMMER` + `sperreAuftrag(id)` → **innerhalb** der Sperre Eingabe und Kunde prüfen → Nummer → PDF (`AuftragError` aus `erzeugeAuftragPdf`, z. B. 409 „Entwurf unvollständig …“, wird durchgereicht; andere Fehler 500) → `speicherePdf` → Token → speichern (scheitert danach etwas: `entfernePdf`) → Sperren frei → Mail mit dem **gespeicherten** Dokument. „Erneut senden“ läuft über `/erinnern`: nach einem Mail-Fehler als normale K31, sonst als Erinnerung; `gesendetAn` = aktuelle Kunden-E-Mail. Neu: `ordneProjektZu(id, projektId)` + Route `POST …/auftraege/:id/projekt` (V5); hängt unter derselben `sperreAuftrag(id)` zuerst die Projektrechnungen der AB mit `projektId === ''` um (Stripe unverändert), zuletzt die AB. Stripe-Metadaten wandern beim Ersetzen **nicht** mit (nur `projektRechnungen.auftragId`).
|
||||
|
||||
**L4:** statische Figtree-TTFs aus `@expo-google-fonts/figtree` (OFL) unter `server/assets/fonts/`; reicht `privat` an `AB_TEXTE.termine` durch; leere Steuernummer → Zeile entfällt.
|
||||
|
||||
@@ -101,6 +101,10 @@ Die Strang-Pläne wurden parallel geschrieben; ihre „Vertragsfragen“ sind hi
|
||||
|
||||
**L7:** Norm der Frühbeginn-Erklärung § 356 Abs. 5 Nr. 2 und § 357a Abs. 2 BGB; § 649 BGB (Kostenanschlag) ist die richtige Norm für den Überschreitungssatz; Datenschutz-Abschnitt als **letzter** Commit des Strangs (wird erst beim AB-Livegang gemergt); alte AGB-PDF-Fassungen bleiben liegen; Karten in WOMS → Rechtliches laut Freigabe-Dokument (Karte ohne Datei entfällt).
|
||||
|
||||
**WOMS:** Bildschirmfotos nur mit eigenen Skripten im Scratchpad (das Prüfwerkzeug des Redesigns unter `/root/reports/ticketsystem-design-2026-10-04/werkzeug*` wird nicht geändert); der WOMS-Push erfolgt erst, wenn das Redesign auf `test` liegt, und vorher wird `feature/auftraege` auf den dann aktuellen `origin/test` gebracht.
|
||||
|
||||
**Z:** Node-20-Läufe mit `node:20-alpine`; Merge mit `--no-ff` (kein Rebase der Strang-Branches); Gegenprüfung (haiku) nur für Critical/Important, Minors gehen ungeprüft als Liste an den Nutzer; die Prüfbilder des Endstands werden nach `/root/reports/auftraege-rechnungen-2026-10-06/bilder/` kopiert (der Scratchpad überlebt keinen Neustart); ein Löschlauf für alte ABs nach Ablauf der Aufbewahrung ist nicht im Umfang (Minor-Liste).
|
||||
|
||||
**Livegang:** Das Setup-Skript läuft **vor** dem Push (Wegwerf-Container mit dem Worktree, siehe Plan 12) oder als Ausweichweg sofort danach per `docker exec`; es legt `projektRechnungen` in jedem Fall an. Der WOMS-Push setzt das Attribut `customers.kundenart` voraus (Setup vorher).
|
||||
|
||||
---
|
||||
@@ -129,7 +133,7 @@ Die Strang-Pläne wurden parallel geschrieben; ihre „Vertragsfragen“ sind hi
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. **Gleichzeitige Aufwandsrechnungen** mit denselben Arbeitsblättern (Doppelklick, zwei Admins): die zweite muss mit 409 „Arbeitsblatt bereits abgerechnet“ scheitern. Test in L2 (zwei parallele `createProjectInvoice` mit gleichen `arbeitsblattIds` unter Sperre `['auftraege-rechnung', kundenId]`).
|
||||
1. **Gleichzeitige Aufwandsrechnungen** mit denselben Arbeitsblättern (Doppelklick, zwei Admins): die zweite muss mit 409 „Arbeitsblatt bereits abgerechnet“ scheitern. Test in L2 (zwei parallele `createProjectInvoice` mit gleichen `arbeitsblattIds` unter Sperre `sperreRechnung(kundenId)` = `['auftraege-rechnung:<kundenId>']`).
|
||||
2. **Alte Token nach Erinnerung, neuer Fassung, Storno oder Widerruf**: der alte Link darf nie eine AB bestätigen; die Seite erklärt den Grund. Tests in L5 (Erinnerung → alter Token 404; ersetzt/storniert/widerrufen → 410 mit Meldung).
|
||||
3. **Schlussrechnung neben stornierten oder noch nicht gesendeten Rechnungen**: Stornos zählen nicht, ein offener Entwurf zur AB blockiert mit 409. Tests in L2.
|
||||
4. **Unvollständige Kundendaten beim AB-Senden** (keine Kundenart, keine Anschrift, keine E-Mail, Interessent statt Kunde): klare Meldung **vor** Nummernvergabe und PDF, keine Nummer verbraucht. Tests in L3.
|
||||
@@ -333,7 +337,7 @@ Einbindung (L0): in `server/index.js` **vor** `app.use('/api/admin/finanzen', ad
|
||||
### V6 Mails (L5)
|
||||
|
||||
Vorlagen in `server/mail-templates/` mit Kopfkommentar wie die vorhandenen (`renderMail`):
|
||||
- `K31 Auftragsbestaetigung.html` – Betreff „Ihre Auftragsbestätigung {{nummer}} – {{titel}}“; Platzhalter `name, nummer, titel, datum, betrag, link, gueltig_bis`; Blöcke `erinnerung, privat, festpreis, aufwand, teamkopie`. Anhänge: AB-PDF (`<nummer>.pdf`) und AGB-PDF (`public/rechtliches/agb-<AGB_FASSUNG>.pdf`, Dateiname `WEBklar-AGB-<AGB_FASSUNG>.pdf`). **Kein** BCC an das Team (`bccTeam: false`), weil die Mail den Bestätigungslink trägt; das Team bekommt eine Kopie mit Block `teamkopie` an `CHAT_NOTIFY_EMAILS` (statt Knopf: „Kopie für das Team – der Bestätigungslink steht nur in der Mail an den Kunden.“), auch bei Erinnerungen. Anhang-Fassung: `auftrag.agbFassung` (Rückfall `AGB_FASSUNG`); fehlt die Datei, wirft der Versand (→ `mailStatus: 'fehler'`).
|
||||
- `K31 Auftragsbestaetigung.html` – Betreff „Ihre Auftragsbestätigung {{nummer}} – {{titel}}“; Platzhalter `name, nummer, titel, datum, betrag, link, gueltig_bis`; Blöcke `erinnerung, privat, festpreis, aufwand, kundenmail, teamkopie` (`kundenmail` = Gegenblock zu `teamkopie`, weil die Vorlagen nur positive Blöcke kennen; die Teamkopie geht als eine Mail ohne Anhänge über `sendAdminNotice` nach der Kundenmail). Anhänge: AB-PDF (`<nummer>.pdf`) und AGB-PDF (`public/rechtliches/agb-<AGB_FASSUNG>.pdf`, Dateiname `WEBklar-AGB-<AGB_FASSUNG>.pdf`). **Kein** BCC an das Team (`bccTeam: false`), weil die Mail den Bestätigungslink trägt; das Team bekommt eine Kopie mit Block `teamkopie` an `CHAT_NOTIFY_EMAILS` (statt Knopf: „Kopie für das Team – der Bestätigungslink steht nur in der Mail an den Kunden.“), auch bei Erinnerungen. Anhang-Fassung: `auftrag.agbFassung` (Rückfall `AGB_FASSUNG`); fehlt die Datei, wirft der Versand (→ `mailStatus: 'fehler'`).
|
||||
- `K32 Auftrag bestaetigt.html` – Betreff „Ihre Bestätigung zu {{nummer}} ist eingegangen“; Platzhalter `name, nummer, titel, zeitpunkt, fruehester_beginn, fruehbeginn_text`; Blöcke `fruehbeginn` (gibt `FRUEHBEGINN_TEXT` wörtlich wieder; kein Satz „Ihr Widerrufsrecht bleibt unberührt“), `ohne_fruehbeginn`.
|
||||
- `T22 Auftrag bestaetigt.html` – Betreff „[Aufträge] Auftrag {{nummer}} bestätigt: {{kunde}}“; Platzhalter `kunde, nummer, titel, zeitpunkt, ueber, woms_link, fruehester_beginn`; Blöcke `privat_fruehbeginn, privat_ohne_fruehbeginn`. `woms_link = <womsBaseUrl>/customers/<kundenId>?tab=invoices`.
|
||||
- Link der K31: `<config.publicBaseUrl>/auftrag.html#t=<token>` (Fragment: landet nicht in Server- und Proxy-Protokollen; die Seite liest `#t=` und als Rückfall `?t=`).
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,973 @@
|
||||
# WOMS-Basis – Umsetzungsplan (Strang W0)
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Das WOMS bekommt die gemeinsame Grundlage für „Aufträge und Rechnungen“: alle neuen Portal-Aufrufe (V8) im API-Client, den Kundenreiter „Aufträge und Rechnungen“ mit dem Container `AuftraegeUndRechnungen` (Ladezustände, Dialogzustand, bestehende Rechnungsliste, Adresswarnung) und vier gültige Platzhalter, die W1 und W2 jeweils durch ihre echte Komponente ersetzen.
|
||||
|
||||
**Architecture:** Der API-Client bleibt dünn: Pfade und Methoden stehen im reinen Modul `src/lib/auftraegeEndpoints.js` (unter Node mit Fetch-Attrappe testbar, weil `financeApi.js` Appwrite lädt), und `financeRequest.js` lernt Blob-Antworten (`as: 'blob'`) für das PDF. Die Ansichtslogik des Reiters (Ladezustand, frische Kundenart, Meldungen) steht getestet in `src/lib/auftraegeAnsicht.js`; der Container verdrahtet sie mit `useFinanceResource`, den vier Platzhaltern und der bestehenden `InvoiceTable` und ersetzt `CustomerInvoices` im Reiter. Die Bausteine des weißen Designs werden nur benutzt, nie verändert.
|
||||
|
||||
**Tech Stack:** React 18, Vite 5, react-router-dom 6, reines CSS (keine neue Datei), Tests mit `node --test` und dem esbuild-Lader `tests/unit/load.mjs` (`node:test`, `assert/strict`), Appwrite Web SDK 13 (nur in `financeApi.js` und `teamApi.js`, nie in getesteten Modulen), Node 24 auf dem Host.
|
||||
|
||||
**Spec:** `/home/webklar/apps/kundenbereich.webklar.com/dev-auftraege/docs/superpowers/specs/2026-10-06-auftraege-und-rechnungen-design.md` (Abschnitt 1 „Oberfläche in WOMS“, dazu 4 „Projektkonto“ und 6 „Schnittstellen“).
|
||||
|
||||
**Übersicht/Verträge:** `/home/webklar/apps/kundenbereich.webklar.com/dev-auftraege/docs/superpowers/plans/2026-10-06-auftraege-00-uebersicht.md` (verbindlich: V5 Endpunkte, V8 WOMS-Schnittstellen und der Abschnitt „Nachträge nach der Planrunde“; dazu V3 Datenformen). Die „Vertragsfragen“ am Ende dieser Datei sind dort entschieden; weicht diese Datei von der Übersicht ab, gilt die Übersicht.
|
||||
|
||||
Jeder Implementierer liest die Spec (Abschnitt 1), die Übersicht (V5, V8) und diese Datei.
|
||||
|
||||
---
|
||||
|
||||
## Global Constraints
|
||||
|
||||
Alle Punkte der Übersicht gelten (Sprache, Git ohne Push und ohne `git add -A`, Ressourcen und `flock`, Sicherheit der Umgebung, `build/` nie anfassen, vorhandene Tests bleiben grün). Für W0 kommt hinzu:
|
||||
|
||||
- **Startbedingung (hart):** Der Strang beginnt erst, wenn die Redesign-Pakete P4 (Kunden), P6 (Kunden-Unterseiten) und P7 (Finanzen) in `feature/design-weiss` zusammengeführt und als `design-p4`, `design-p6`, `design-p7` getaggt sind. Task 1 prüft das. Fehlt etwas, gilt **STOPP**: nichts anlegen, dem Orchestrator melden (er fragt den Nutzer: warten oder auf `test` bauen und eine Mitnahme-Notiz in `/root/reports/ticketsystem-design-2026-10-04/strang/mitnehmen.md` hinterlegen).
|
||||
- **Anker statt Zeilennummern:** Dieser Plan nennt keine Zeilennummern. Maßgeblich sind Namen und Ankertexte im zusammengeführten Stand; Task 1 Step 5 prüft sie gegen den Stand beim Start.
|
||||
- **Stand von `test`:** `origin/test` ist dem Redesign voraus (Öffnungszeiten: `zeitenView.js`, zusätzliche Zeilen in `financeRequest.js` und `teamApi.js`). Änderungen an diesen Dateien hängen sich an Ankertexte an und lassen fremde Zeilen unberührt.
|
||||
- **Dateien:** W0 ändert nur die Dateien der Tabelle „Dateien“. Alle anderen Redesign-Dateien (`InvoiceTable.jsx`, `AddressWarning.jsx`, `InvoiceCreateModal.jsx`, `FinancePage.jsx`, `global.css`, …) bleiben unberührt; sie gehören W1 und W2.
|
||||
- **Regeln des Redesigns für neue Dateien:** nur Bausteine aus `src/components/ui` und vorhandene Klassen (`msg`, `loading`, `link`, `fin-notice`, `fin-load-error`); keine Icon-Importe (`react-icons`, `@tabler`), kein `style={{…}}`, keine Farbliterale, keine KOMPAT-Klassen (`ui-btn`, `form-control`, `form-group`, `form-label`, `ui-card`, `badge-…`, `overlay-content`, `overlay-close`, `spinner`, `kpi`), keine neue CSS-Datei. P10 räumt KOMPAT und die Icon-Pakete weg; Neues darf davon nichts brauchen. Kommentare und Texte deutsch wie in den Nachbardateien.
|
||||
- **Tests:** `node:test`, `assert/strict`, `loadModule` aus `tests/unit/load.mjs`. `financeApi.js`, `teamApi.js` und alle Komponenten laden Appwrite und sind unter Node nicht ladbar; ihre Logik steht in reinen `src/lib`-Modulen. Neue Tests liegen in neuen Dateien (Ausnahme: `customerTabs.test.mjs`).
|
||||
- **Befehle mit Speichergrenzen** (Server mit wenig Arbeitsspeicher). `WT` ist `/home/webklar/apps/ticket.webklar.com/dev-auftraege`, `L` ist `/tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad`. Jede Shell-Zeile steht für sich (Umgebungsvariablen bleiben nicht erhalten), deshalb stehen die Pfade ausgeschrieben in den Steps.
|
||||
- Einzeltest: `cd $WT && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/<datei>.test.mjs`
|
||||
- Volle Suite, `npm ci` und Build laufen **nur** hinter `flock $L/auftraege-schwer.lock`, vorher `free -m` (unter 1500 MB „available“: warten), und unter `systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0` (Arbeitsweise des Redesigns). Läuft das Redesign noch schwere Läufe, zusätzlich dessen Sperre `/root/reports/ticketsystem-design-2026-10-04/sperren/schwer.lock` (erst unsere, dann deren).
|
||||
- Build: `node node_modules/vite/bin/vite.js build --outDir $L/auftraege-w0-dist --emptyOutDir` mit `NODE_OPTIONS=--max-old-space-size=1200` (nie `npm run build`, nie nach `dist/` im Worktree).
|
||||
- Bash-Timeout 600000 ms; dauert ein Lauf länger, mit `run_in_background` starten. Nie `pkill -f`.
|
||||
- **Git:** Worktree `/home/webklar/apps/ticket.webklar.com/dev-auftraege`, Branch `feature/auftraege`. Im Repo `build` (Branch `test`) entsteht nur der Worktree-Eintrag aus Task 1, sonst wird dort nichts angefasst. Commits einzeln mit Dateinamen, deutsche Nachricht, Schlusszeile `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>` (zweites `-m`). Kein Push, nie `--force`.
|
||||
- **Suche:** In dieser Shell ist `grep` eine Funktion, die Dateien still auslässt. Die Steps benutzen `rg` (13.0.0, ohne Lookarounds); notfalls `command grep -a`.
|
||||
|
||||
## Dateien
|
||||
|
||||
| Datei | Aktion | Aufgabe | Task |
|
||||
|---|---|---|---|
|
||||
| `src/lib/financeRequest.js` | ändern | `request(path, { method, body, as })`: `as: 'blob'` liefert bei Erfolg den rohen Inhalt (PDF) | 2 |
|
||||
| `tests/unit/financeRequestBlob.test.mjs` | neu | Tests dazu (Fetch-Attrappe) | 2 |
|
||||
| `src/lib/auftraegeEndpoints.js` | neu | rein: `createAuftraegeEndpoints(request)` (14 Funktionen nach V5, darunter `ordneAuftragProjektZu`) und `abgerechnetPfad(ticketId)` | 3 |
|
||||
| `tests/unit/auftraegeEndpoints.test.mjs` | neu | Pfade, Methoden, Körper, Kodierung, PDF-Blob | 3 |
|
||||
| `src/lib/financeApi.js` | ändern | exportiert die 14 Funktionen (Verdrahtung mit Appwrite-JWT) | 3 |
|
||||
| `src/lib/teamApi.js` | ändern | `getAbgerechnet(ticketId)` | 3 |
|
||||
| `src/lib/customerTabs.js`, `tests/unit/customerTabs.test.mjs` | ändern | Beschriftung „Aufträge und Rechnungen“ | 4 |
|
||||
| `src/lib/auftraegeAnsicht.js` | neu | rein: Laden, Ladezustand, frische Kundenart, Meldungen | 5, 6 |
|
||||
| `tests/unit/auftraegeAnsicht.test.mjs` | neu | Tests dazu | 5, 6 |
|
||||
| `src/components/auftraege/ProjektkontoListe.jsx`, `RechnungDialog.jsx`, `AuftragListe.jsx`, `AuftragDialog.jsx` | neu | Platzhalter mit den Props aus V8 (samt `konten` und `onShowOverview`), rendern `null`; W1 bzw. W2 ersetzen die Dateien | 7 |
|
||||
| `src/components/auftraege/AuftraegeUndRechnungen.jsx` | neu | Container des Reiters; reicht `konten` und `onShowOverview` an Liste und Dialoge durch | 7 |
|
||||
| `src/pages/CustomerDetailPage.jsx` | ändern | Reiter `invoices` zeigt den Container | 8 |
|
||||
| `src/components/finance/CustomerInvoices.jsx` | löschen | ersetzt durch den Container (keine Nutzer mehr) | 8 |
|
||||
|
||||
## Review Focus
|
||||
|
||||
Fälle, die die Spec nicht ausdrücklich nennt und die im Betrieb am ehesten auftreten, wahrscheinlichster zuerst:
|
||||
|
||||
1. **Schalter `AUFTRAEGE_ENABLED` aus (Vorgabe beim Livegang):** `auftraegeAktiv` ist `false` oder fehlt. Erwartet: kein Abruf der Aufträge (er würde 404 liefern), kein Aufträge-Abschnitt, keine Fehlermeldung; Projekte und Rechnungen laufen wie sonst. Test: Task 5, „Schalter aus“ und „unvollständige Antworten“.
|
||||
2. **Projektkonten-Abruf scheitert** (Setup-Skript nicht gelaufen, Portal-Fehler 500, Portal ohne den Endpunkt): Erwartet: Der Abschnitt „Projekte“ zeigt die Meldung mit „Erneut versuchen“, die Rechnungsliste und ihr Knopf „Rechnung schreiben“ bleiben bedienbar, Aufträge werden nicht abgerufen. Test: Task 5, „Projektkonten scheitern“ und „vor der ersten Antwort“.
|
||||
3. **Aufträge-Abruf scheitert allein** (Schalter an, Collection fehlt): Erwartet: Projekte bleiben sichtbar, der Aufträge-Abschnitt zeigt seine eigene Meldung. Test: Task 5, „Aufträge scheitern allein“.
|
||||
4. **Neuladen scheitert, nachdem Daten da sind** (Portal antwortet nach dem Senden einer Rechnung kurz nicht): Erwartet: Die alten Zeilen bleiben stehen, darüber steht eine Warnung mit „Erneut versuchen“, nichts springt auf „Wird geladen …“. Test: Task 5, „Neuladen scheitert“.
|
||||
5. **Kundenart eben gespeichert, die Kundenseite hält noch den alten Datensatz:** Erwartet: Die Dialoge bekommen den frischen Wert des Portals, der AB-Dialog fragt nicht erneut. Test: Task 6, `kundeMitKundenart`.
|
||||
|
||||
Weitere Randfälle haben eigene Tests in den Tasks: PDF-Abruf mit Fehlerantwort statt Blob und Kennungen mit Sonderzeichen (Task 2 und 3).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Startbedingung prüfen und Worktree anlegen
|
||||
|
||||
**Files:** keine Codeänderung. Legt `/home/webklar/apps/ticket.webklar.com/dev-auftraege` (Branch `feature/auftraege`) an.
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Tags `design-p4`, `design-p6`, `design-p7` (und ggf. `design-p8` bis `design-p10`) im WOMS-Repo, erreichbar über `/home/webklar/apps/ticket.webklar.com/build`.
|
||||
- Produces: Worktree mit **echtem** `node_modules` (W1 und W2 verlinken es per `ln -s ../dev-auftraege/node_modules node_modules`), den Basis-Stand `B` (Tag-Name) und die Ausgangs-Testzahl `N0`. Beides gehört in die Abschlussmeldung (Task 9).
|
||||
|
||||
- [ ] **Step 1: Redesign-Tags und Basis bestimmen**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/build
|
||||
for t in design-p4 design-p6 design-p7; do git rev-parse -q --verify "refs/tags/$t^{commit}" >/dev/null && echo "da $t" || echo "FEHLT $t"; done
|
||||
for t in design-p10 design-p9 design-p8 design-p7; do git rev-parse -q --verify "refs/tags/$t^{commit}" >/dev/null && { echo "BASIS=$t"; break; }; done
|
||||
```
|
||||
Expected: dreimal `da …` und eine Zeile `BASIS=design-p…`. Das ist der höchste vorhandene Tag und damit `B`; nennt der Orchestrator ausdrücklich einen anderen Stand, gilt dieser. Fehlt einer der drei Tags: **STOPP**.
|
||||
|
||||
- [ ] **Step 2: Inhalt prüfen (Tags allein genügen nicht)**
|
||||
|
||||
Run (mit `B` aus Step 1):
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/build
|
||||
B=<B aus Step 1>
|
||||
git merge-base --is-ancestor design-p7 "$B" && echo "design-p7 liegt im Basis-Stand"
|
||||
git cat-file -e "$B:src/lib/customerList.js" && echo "P4 ok"
|
||||
git cat-file -e "$B:tests/unit/creditFormat.test.mjs" && echo "P6 ok"
|
||||
git cat-file -e "$B:src/lib/financeUi.js" && echo "P7 ok"
|
||||
git merge-base --is-ancestor origin/test "$B" && echo "Basis enthält origin/test" || echo "HINWEIS: origin/test ist weiter als die Basis (nur melden)"
|
||||
```
|
||||
Expected: vier Bestätigungen (`design-p7 …`, `P4 ok`, `P6 ok`, `P7 ok`); die letzte Zeile ist nur ein Hinweis. Fehlt eine der vier: **STOPP** mit Meldung „Startbedingung nicht erfüllt: <was>“ samt Ausgabe von `git tag -l 'design-p*' 'strang-p*'` und `git log --oneline -3 design/kunden design/finanzen`.
|
||||
|
||||
- [ ] **Step 3: Worktree anlegen**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com
|
||||
test ! -e dev-auftraege && ! git -C build rev-parse -q --verify refs/heads/feature/auftraege >/dev/null && git -C build worktree add ../dev-auftraege -b feature/auftraege <B> && git -C dev-auftraege log --oneline -1
|
||||
```
|
||||
Expected: Der Worktree entsteht, die letzte Zeile ist der Commit der Basis. Existiert Ordner oder Branch schon: **STOPP** (ein früherer Versuch; Orchestrator fragen).
|
||||
|
||||
- [ ] **Step 4: `node_modules` wie im Redesign (echtes `npm ci`, kein Symlink)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
free -m | head -2
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && flock /tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad/auftraege-schwer.lock systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 nice -n 10 npm ci --no-audit --no-fund && git status --short
|
||||
```
|
||||
Expected: Exit 0 und keine Zeile von `git status` (`node_modules` steht in `.gitignore`).
|
||||
|
||||
- [ ] **Step 5: Anker gegen den Basis-Stand prüfen**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
rg -n "CustomerInvoices" src/pages/CustomerDetailPage.jsx
|
||||
rg -n "label: 'Rechnungen'" src/lib/customerTabs.js
|
||||
rg -n "return async function request" src/lib/financeRequest.js
|
||||
rg -n "getCustomerAboStatus" src/lib/teamApi.js
|
||||
rg -n "export default function (CustomerInvoices|InvoiceTable|AddressWarning)" src/components/finance
|
||||
rg -n "export function (useFinanceResource|useScrollIntoView)" src/hooks
|
||||
rg -n "export function (customerInvoicesGate|invoiceDoneMessage)" src/lib/financeView.js
|
||||
rg -n "export function noticeProps" src/lib/financeUi.js
|
||||
rg -n "export \{ default as (Card|Button|EmptyState) " src/components/ui/index.js
|
||||
rg -n "^\.fin-load-error|^\.fin-notice" src/styles
|
||||
```
|
||||
Expected: jeder Befehl mindestens ein Treffer (`CustomerDetailPage.jsx` zwei: Import und Verwendung, `export function (useFinanceResource|useScrollIntoView)` zwei). Fehlt ein Anker, weil das Redesign etwas umbenannt hat: **STOPP** und melden. Einzige Ausnahme: fehlen `.fin-load-error` und `.fin-notice` im CSS, benutzt Task 7 nur `msg …` ohne diese Zusatzklassen.
|
||||
|
||||
- [ ] **Step 6: Ausgangslage der Tests**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
L=/tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad
|
||||
free -m | head -2
|
||||
flock $L/auftraege-schwer.lock env NODE_OPTIONS=--max-old-space-size=1024 timeout 900 nice -n 10 npm test > $L/auftraege-w0-test.log 2>&1; echo "exit $?"
|
||||
rg "^(ℹ|#) (tests|pass|fail) " $L/auftraege-w0-test.log
|
||||
```
|
||||
Expected: `exit 0`, `fail 0`. Die Zahl hinter `pass` ist `N0` (notieren; Soll am Ende: `N0 + 21`). Schlägt schon die Basis fehl: **STOPP** und melden (Basis kaputt).
|
||||
|
||||
Kein Commit.
|
||||
|
||||
### Task 2: `financeRequest` liest Dateien (Blob)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/financeRequest.js`
|
||||
- Test: `tests/unit/financeRequestBlob.test.mjs` (neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `createFinanceRequest({ baseUrl, createJWT, fetchImpl })` (besteht).
|
||||
- Produces: `request(path, { method = 'GET', body, as })`. Mit `as: 'blob'` liefert eine erfolgreiche Antwort (`res.ok`) `await res.blob()`. Fehlerantworten kommen unverändert als JSON-Meldung des Portals (Fehlertexte, Status, JWT-Wiederholung bei 401). Ohne `as` oder mit jedem anderen Wert bleibt alles wie bisher. Das nutzt `getAuftragPdf` (Task 3).
|
||||
|
||||
- [ ] **Step 1: Failing Test schreiben**
|
||||
|
||||
`tests/unit/financeRequestBlob.test.mjs`:
|
||||
```js
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { loadModule } from './load.mjs'
|
||||
|
||||
const { createFinanceRequest, PORTAL_DOWN_TEXT } = await loadModule('src/lib/financeRequest.js')
|
||||
|
||||
const BASE = 'https://portal.example.invalid/api/admin/finanzen'
|
||||
|
||||
/** Antwort wie fetch: { ok, status, json(), blob() }; ohne `json` ist der Körper kein JSON (json() wirft). */
|
||||
const reply = (status, { json, blob } = {}) => ({
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: async () => {
|
||||
if (json === undefined) throw new SyntaxError('Unexpected token % in JSON')
|
||||
return json
|
||||
},
|
||||
blob: async () => blob,
|
||||
})
|
||||
|
||||
function client({ responses = [], createJWT } = {}) {
|
||||
const calls = []
|
||||
const queue = [...responses]
|
||||
const api = createFinanceRequest({
|
||||
baseUrl: BASE,
|
||||
createJWT: createJWT || (async () => ({ jwt: 'jwt-1' })),
|
||||
fetchImpl: async (url, init) => {
|
||||
calls.push({ url, init })
|
||||
return queue.shift() || reply(200, { json: {} })
|
||||
},
|
||||
})
|
||||
return { api, calls }
|
||||
}
|
||||
|
||||
const fails = async (promise) => promise.then(() => assert.fail('hätte scheitern müssen'), (err) => err)
|
||||
const pdf = () => new Blob(['%PDF-1.7 Probe'], { type: 'application/pdf' })
|
||||
|
||||
test('as blob: Erfolg liefert den rohen Inhalt als Blob, mit Bearer-JWT, ohne JSON zu lesen', async () => {
|
||||
const { api, calls } = client({
|
||||
responses: [{ ...reply(200, { blob: pdf() }), json: async () => assert.fail('json() darf bei as blob nicht gelesen werden') }],
|
||||
})
|
||||
const result = await api('/auftraege/a1/pdf', { as: 'blob' })
|
||||
assert.ok(result instanceof Blob)
|
||||
assert.equal(result.type, 'application/pdf')
|
||||
assert.equal(await result.text(), '%PDF-1.7 Probe')
|
||||
assert.equal(calls[0].url, `${BASE}/auftraege/a1/pdf`)
|
||||
assert.equal(calls[0].init.method, 'GET')
|
||||
assert.equal(calls[0].init.headers.Authorization, 'Bearer jwt-1')
|
||||
})
|
||||
|
||||
test('as blob: Fehler kommen weiter als Meldung des Portals mit Status (kein kaputtes PDF)', async () => {
|
||||
const { api } = client({ responses: [reply(404, { json: { error: 'PDF nicht gefunden.' } })] })
|
||||
const err = await fails(api('/auftraege/a1/pdf', { as: 'blob' }))
|
||||
assert.equal(err.message, 'PDF nicht gefunden.')
|
||||
assert.equal(err.status, 404)
|
||||
// Proxy-Fehlerseite ohne JSON: dieselbe Meldung wie bei JSON-Anfragen
|
||||
const { api: api2 } = client({ responses: [reply(502)] })
|
||||
assert.equal((await fails(api2('/auftraege/a1/pdf', { as: 'blob' }))).message, PORTAL_DOWN_TEXT)
|
||||
})
|
||||
|
||||
test('as blob: bei 401 einmal mit frischem JWT wiederholt, dann kommt der Blob', async () => {
|
||||
let n = 0
|
||||
const datei = pdf()
|
||||
const { api, calls } = client({
|
||||
createJWT: async () => ({ jwt: `jwt-${++n}` }),
|
||||
responses: [reply(401, { json: { error: 'abgelaufen' } }), reply(200, { blob: datei })],
|
||||
})
|
||||
assert.equal(await api('/auftraege/a1/pdf', { as: 'blob' }), datei)
|
||||
assert.deepEqual(calls.map((c) => c.init.headers.Authorization), ['Bearer jwt-1', 'Bearer jwt-2'])
|
||||
})
|
||||
|
||||
test('ohne as bleibt es JSON (der Blob wird nicht gelesen), ein unbekannter Wert verhält sich ebenso', async () => {
|
||||
const { api } = client({
|
||||
responses: [{ ...reply(200, { json: { ok: 1 } }), blob: async () => assert.fail('blob() darf ohne as nicht gelesen werden') }],
|
||||
})
|
||||
assert.deepEqual(await api('/x'), { ok: 1 })
|
||||
const { api: api2 } = client({ responses: [reply(200, { json: { ok: 2 } })] })
|
||||
assert.deepEqual(await api2('/x', { as: 'text' }), { ok: 2 })
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test laufen lassen**
|
||||
|
||||
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/financeRequestBlob.test.mjs`
|
||||
Expected: FAIL in zwei Tests („Erfolg liefert den rohen Inhalt als Blob“ und „bei 401 …“); `request` ignoriert `as` und liefert `{}`. Die anderen beiden bestehen schon.
|
||||
|
||||
- [ ] **Step 3: `as: 'blob'` in `createFinanceRequest` einbauen**
|
||||
|
||||
In `src/lib/financeRequest.js`: Die innere Funktion bekommt die Signatur `request(path, { method = 'GET', body, as } = {})`. Direkt nach dem `fetchWithRetry`-Aufruf und vor dem Lesen des JSON kommt `if (as === 'blob' && res.ok) return res.blob()` (mit einem Kommentar: Dateien wie das PDF der Auftragsbestätigung kommen roh, Fehler weiter als JSON). Header, JWT-Cache, Wiederholung bei 401 und alle Fehlertexte bleiben, ebenso die fremden Zeilen (`code`, `fehler`) im Fehlerzweig. Den Kommentarblock über `createFinanceRequest` um einen Satz zu `as: 'blob'` ergänzen.
|
||||
|
||||
- [ ] **Step 4: Tests laufen lassen**
|
||||
|
||||
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/financeRequestBlob.test.mjs tests/unit/financeRequest.test.mjs`
|
||||
Expected: PASS, in der neuen Datei `pass 4`, `fail 0`; die bestehende Datei bleibt grün.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
git add src/lib/financeRequest.js tests/unit/financeRequestBlob.test.mjs
|
||||
git commit -m "Finanzen-Anfrage: Dateien lesen (as: 'blob'), z. B. das PDF einer Auftragsbestätigung" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 3: API-Schicht – Pfade rein, `financeApi.js` und `teamApi.js` verdrahtet
|
||||
|
||||
**Files:**
|
||||
- Create: `src/lib/auftraegeEndpoints.js`
|
||||
- Test: `tests/unit/auftraegeEndpoints.test.mjs` (neu)
|
||||
- Modify: `src/lib/financeApi.js`, `src/lib/teamApi.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `request(path, { method, body, as })` aus `createFinanceRequest` (Task 2).
|
||||
- Produces:
|
||||
- `createAuftraegeEndpoints(request) → { getProjektkonten, getProjektkonto, previewInvoice, listAuftraege, getAuftrag, createAuftrag, patchAuftrag, deleteAuftrag, sendAuftrag, erinnereAuftrag, neueFassungAuftrag, storniereAuftrag, ordneAuftragProjektZu, getAuftragPdf }` (14 Funktionen) und `abgerechnetPfad(ticketId) → string` (reine Datei ohne Importe).
|
||||
- Aus `src/lib/financeApi.js` exportiert (V8; Basis `/api/admin/finanzen`, Antwortformen nach V5):
|
||||
- `getProjektkonten(kundeId) → { auftraegeAktiv, kunde, konten, ohneProjekt, stripeOk }`
|
||||
- `getProjektkonto(projektId) → { konto, auftraege, rechnungen, offeneArbeitsblaetter, stripeOk }`
|
||||
- `previewInvoice(body) → { positionen, summeCents, leistungszeitraum, customFields }`
|
||||
- `listAuftraege(kundeId) → { auftraege }`
|
||||
- `getAuftrag(id)`, `createAuftrag(body)`, `patchAuftrag(id, body) → { auftrag }`; `deleteAuftrag(id) → { ok: true }`
|
||||
- `sendAuftrag(id)`, `erinnereAuftrag(id) → { auftrag, mailFehler }`; `neueFassungAuftrag(id)`, `storniereAuftrag(id) → { auftrag }`
|
||||
- `ordneAuftragProjektZu(id, projektId) → { auftrag }` (Nachtrag der Planrunde, V5 Zeile L3: ordnet einer AB ohne Projekt ein Projekt zu; 409, wenn sie schon eines hat oder `storniert`, `ersetzt` oder `widerrufen` ist; W2 braucht es für „Projekt zuordnen“)
|
||||
- `getAuftragPdf(id) → Promise<Blob>`
|
||||
- Aus `src/lib/teamApi.js`: `getAbgerechnet(ticketId) → { abgerechnet: { <worksheetId>: '<Rechnungsnummer>' | 'Entwurf' } }` (V5; für alle Mitarbeiter, ohne Beträge).
|
||||
- `createInvoice(body)` besteht und reicht den erweiterten Körper unverändert durch; nur der Kommentar wird nachgezogen.
|
||||
|
||||
- [ ] **Step 1: Failing Test schreiben**
|
||||
|
||||
`tests/unit/auftraegeEndpoints.test.mjs`:
|
||||
```js
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { loadModule } from './load.mjs'
|
||||
|
||||
const { createAuftraegeEndpoints, abgerechnetPfad } = await loadModule('src/lib/auftraegeEndpoints.js')
|
||||
const { createFinanceRequest } = await loadModule('src/lib/financeRequest.js')
|
||||
|
||||
const BASE = 'https://portal.example.invalid/api/admin/finanzen'
|
||||
|
||||
// Echte Anfrage-Schicht mit Fetch-Attrappe: geprüft wird, was das Portal erreicht (URL, Methode, Kopf, JSON-Körper)
|
||||
function setup(answer = () => ({ ok: true, status: 200, json: async () => ({ ok: true }) })) {
|
||||
const calls = []
|
||||
const request = createFinanceRequest({
|
||||
baseUrl: BASE,
|
||||
createJWT: async () => ({ jwt: 'jwt-1' }),
|
||||
fetchImpl: async (url, init) => {
|
||||
calls.push({ url, method: init.method, body: init.body, auth: init.headers.Authorization })
|
||||
return answer()
|
||||
},
|
||||
})
|
||||
return { api: createAuftraegeEndpoints(request), calls }
|
||||
}
|
||||
|
||||
// [Funktion, Argumente, Methode, Pfad ab BASE, Körper]
|
||||
const FAELLE = [
|
||||
['getProjektkonten', ['k1'], 'GET', '/projektkonten?kunde=k1', undefined],
|
||||
['getProjektkonto', ['p1'], 'GET', '/projektkonten/p1', undefined],
|
||||
['previewInvoice', [{ customerId: 'k1', art: 'aufwand', projektId: 'p1' }], 'POST', '/rechnungen/vorschau', { customerId: 'k1', art: 'aufwand', projektId: 'p1' }],
|
||||
['listAuftraege', ['k1'], 'GET', '/auftraege?kunde=k1', undefined],
|
||||
['getAuftrag', ['a1'], 'GET', '/auftraege/a1', undefined],
|
||||
['createAuftrag', [{ kundenId: 'k1', titel: 'Website' }], 'POST', '/auftraege', { kundenId: 'k1', titel: 'Website' }],
|
||||
['patchAuftrag', ['a1', { titel: 'Neu' }], 'PATCH', '/auftraege/a1', { titel: 'Neu' }],
|
||||
['deleteAuftrag', ['a1'], 'DELETE', '/auftraege/a1', undefined],
|
||||
['sendAuftrag', ['a1'], 'POST', '/auftraege/a1/senden', undefined],
|
||||
['erinnereAuftrag', ['a1'], 'POST', '/auftraege/a1/erinnern', undefined],
|
||||
['neueFassungAuftrag', ['a1'], 'POST', '/auftraege/a1/neue-fassung', undefined],
|
||||
['storniereAuftrag', ['a1'], 'POST', '/auftraege/a1/stornieren', undefined],
|
||||
['ordneAuftragProjektZu', ['a1', 'p1'], 'POST', '/auftraege/a1/projekt', { projektId: 'p1' }],
|
||||
]
|
||||
|
||||
test('alle 14 Funktionen sind da (V8 samt „Projekt zuordnen“ aus V5)', () => {
|
||||
const { api } = setup()
|
||||
assert.deepEqual(Object.keys(api).sort(), [
|
||||
'createAuftrag', 'deleteAuftrag', 'erinnereAuftrag', 'getAuftrag', 'getAuftragPdf', 'getProjektkonten', 'getProjektkonto',
|
||||
'listAuftraege', 'neueFassungAuftrag', 'ordneAuftragProjektZu', 'patchAuftrag', 'previewInvoice', 'sendAuftrag', 'storniereAuftrag',
|
||||
])
|
||||
})
|
||||
|
||||
test('Pfad, Methode, Kopf und Körper jeder Funktion (die Vorschau trägt keinen Schlüssel dazu)', async () => {
|
||||
for (const [name, args, method, path, body] of FAELLE) {
|
||||
const { api, calls } = setup()
|
||||
await api[name](...args)
|
||||
assert.equal(calls.length, 1, name)
|
||||
assert.equal(calls[0].url, `${BASE}${path}`, name)
|
||||
assert.equal(calls[0].method, method, name)
|
||||
assert.equal(calls[0].auth, 'Bearer jwt-1', name)
|
||||
assert.equal(calls[0].body, body === undefined ? undefined : JSON.stringify(body), name)
|
||||
}
|
||||
})
|
||||
|
||||
test('Kennungen werden URL-kodiert, auch im Pfad der abgerechneten Arbeitsblätter', async () => {
|
||||
const { api, calls } = setup()
|
||||
await api.getProjektkonten('k 1&x=2')
|
||||
await api.getAuftrag('a/b?c')
|
||||
await api.storniereAuftrag('a#1')
|
||||
await api.ordneAuftragProjektZu('a b', 'p/1')
|
||||
assert.deepEqual(calls.map((c) => c.url.slice(BASE.length)), [
|
||||
'/projektkonten?kunde=k%201%26x%3D2',
|
||||
'/auftraege/a%2Fb%3Fc',
|
||||
'/auftraege/a%231/stornieren',
|
||||
'/auftraege/a%20b/projekt',
|
||||
])
|
||||
assert.equal(calls[3].body, JSON.stringify({ projektId: 'p/1' })) // die Projektkennung steht im Körper, nicht im Pfad
|
||||
assert.equal(abgerechnetPfad('100131'), '/arbeitsblaetter/abgerechnet?ticketId=100131')
|
||||
assert.equal(abgerechnetPfad('t 1/2'), '/arbeitsblaetter/abgerechnet?ticketId=t%201%2F2')
|
||||
})
|
||||
|
||||
test('getAuftragPdf liefert das PDF als Blob; eine Fehlerantwort kommt als Meldung des Portals', async () => {
|
||||
const datei = new Blob(['%PDF-1.7'], { type: 'application/pdf' })
|
||||
const ok = setup(() => ({ ok: true, status: 200, json: async () => assert.fail('kein JSON lesen'), blob: async () => datei }))
|
||||
assert.equal(await ok.api.getAuftragPdf('a1'), datei)
|
||||
assert.equal(ok.calls[0].url, `${BASE}/auftraege/a1/pdf`)
|
||||
assert.equal(ok.calls[0].method, 'GET')
|
||||
const fehler = setup(() => ({ ok: false, status: 404, json: async () => ({ error: 'PDF nicht gefunden.' }), blob: async () => assert.fail('kein Blob lesen') }))
|
||||
await assert.rejects(fehler.api.getAuftragPdf('a1'), (e) => e.message === 'PDF nicht gefunden.' && e.status === 404)
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test laufen lassen**
|
||||
|
||||
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/auftraegeEndpoints.test.mjs`
|
||||
Expected: FAIL, `src/lib/auftraegeEndpoints.js` fehlt.
|
||||
|
||||
- [ ] **Step 3: `src/lib/auftraegeEndpoints.js` implementieren**
|
||||
|
||||
Reine Datei ohne Importe, deutscher Kopfkommentar im Stil von `financeRequest.js`. `createAuftraegeEndpoints(request)` gibt ein Objekt mit genau den 14 Funktionen zurück. Jede ruft `request` mit Pfad relativ zu `/api/admin/finanzen`; jede Kennung läuft durch `encodeURIComponent`; Aufrufe ohne Körper übergeben keinen Körper. Tabelle:
|
||||
|
||||
| Funktion | Methode | Pfad | Körper |
|
||||
|---|---|---|---|
|
||||
| `getProjektkonten(kundeId)` | GET | `/projektkonten?kunde=<kundeId>` | – |
|
||||
| `getProjektkonto(projektId)` | GET | `/projektkonten/<projektId>` | – |
|
||||
| `previewInvoice(body)` | POST | `/rechnungen/vorschau` | `body` unverändert |
|
||||
| `listAuftraege(kundeId)` | GET | `/auftraege?kunde=<kundeId>` | – |
|
||||
| `getAuftrag(id)` | GET | `/auftraege/<id>` | – |
|
||||
| `createAuftrag(body)` | POST | `/auftraege` | `body` |
|
||||
| `patchAuftrag(id, body)` | PATCH | `/auftraege/<id>` | `body` |
|
||||
| `deleteAuftrag(id)` | DELETE | `/auftraege/<id>` | – |
|
||||
| `sendAuftrag(id)` | POST | `/auftraege/<id>/senden` | – |
|
||||
| `erinnereAuftrag(id)` | POST | `/auftraege/<id>/erinnern` | – |
|
||||
| `neueFassungAuftrag(id)` | POST | `/auftraege/<id>/neue-fassung` | – |
|
||||
| `storniereAuftrag(id)` | POST | `/auftraege/<id>/stornieren` | – |
|
||||
| `ordneAuftragProjektZu(id, projektId)` | POST | `/auftraege/<id>/projekt` | `{ projektId }` |
|
||||
| `getAuftragPdf(id)` | GET | `/auftraege/<id>/pdf` | – (Option `as: 'blob'`) |
|
||||
|
||||
`abgerechnetPfad(ticketId)` liefert `/arbeitsblaetter/abgerechnet?ticketId=<kodiert>` (Basis `/api/team`, siehe Step 4).
|
||||
|
||||
- [ ] **Step 4: Verdrahtung in `financeApi.js` und `teamApi.js`**
|
||||
|
||||
- `src/lib/financeApi.js`: `import { createAuftraegeEndpoints } from './auftraegeEndpoints'` zu den Importen. Hinter dem Block „Rechnungen“ ein Abschnitt mit Kommentar „Projektkonten, Aufträge und Rechnungsvorschau (Pfade und Methoden: auftraegeEndpoints.js, getestet)“ und `export const { getProjektkonten, getProjektkonto, previewInvoice, listAuftraege, getAuftrag, createAuftrag, patchAuftrag, deleteAuftrag, sendAuftrag, erinnereAuftrag, neueFassungAuftrag, storniereAuftrag, ordneAuftragProjektZu, getAuftragPdf } = createAuftraegeEndpoints(request)` (die vorhandene Konstante `request` wird übergeben). Der Kommentar über `createInvoice` nennt den erweiterten Körper `{ customerId, art, projektId, auftragId, items, arbeitsblattIds, anzahlung, zeilentexte, leistungszeitraum, daysUntilDue, memo, senden }`; die Funktion selbst bleibt unverändert.
|
||||
- `src/lib/teamApi.js`: `import { abgerechnetPfad } from './auftraegeEndpoints'` zu den Importen; vor dem Block „Abo-Status“ ein Abschnitt „Abgerechnete Arbeitsblätter (alle Mitarbeiter, ohne Beträge)“ mit `export const getAbgerechnet = (ticketId) => request(abgerechnetPfad(ticketId))` und einem Kommentar zur Antwortform.
|
||||
|
||||
- [ ] **Step 5: Tests und Syntaxprüfung**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/auftraegeEndpoints.test.mjs
|
||||
node_modules/.bin/esbuild src/lib/financeApi.js src/lib/teamApi.js --outdir=/tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad/auftraege-w0-syntax --log-level=warning
|
||||
```
|
||||
Expected: `pass 4`, `fail 0`; esbuild ohne Ausgabe und Exit 0 (nur Syntax, die Dateien lassen sich unter Node nicht bündeln).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
git add src/lib/auftraegeEndpoints.js tests/unit/auftraegeEndpoints.test.mjs src/lib/financeApi.js src/lib/teamApi.js
|
||||
git commit -m "Finanzen-API: Projektkonten, Auftragsbestätigungen (samt Projekt zuordnen), Rechnungsvorschau und abgerechnete Arbeitsblätter (Pfade rein und getestet)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 4: Reiter-Beschriftung „Aufträge und Rechnungen“
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/customerTabs.js`, `tests/unit/customerTabs.test.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `customerTabs(isAdmin)` und `resolveCustomerTab(tab, isAdmin)` (bestehen).
|
||||
- Produces: `customerTabs(true)[2]` ist `{ id: 'invoices', label: 'Aufträge und Rechnungen' }`. Die `id` bleibt `invoices` (Mails des Portals und Aufgaben der Finanzen-Seite verlinken `?tab=invoices`), der Reiter bleibt nur für Admins.
|
||||
|
||||
- [ ] **Step 1: Test anpassen (zuerst)**
|
||||
|
||||
In `tests/unit/customerTabs.test.mjs` die Zeile `{ id: 'invoices', label: 'Rechnungen' },` zu `{ id: 'invoices', label: 'Aufträge und Rechnungen' },` ändern und den Titel des ersten Tests zu `'customerTabs: volle Reiterfolge der Spec, „Aufträge und Rechnungen“ nur für Admins'`. Alles andere bleibt (es pinnt unter anderem, dass `customerTabs(false)` keinen Reiter `invoices` enthält und `resolveCustomerTab('invoices', false)` auf `overview` fällt).
|
||||
|
||||
- [ ] **Step 2: Test laufen lassen**
|
||||
|
||||
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/customerTabs.test.mjs`
|
||||
Expected: FAIL (die Beschriftung lautet noch „Rechnungen“).
|
||||
|
||||
- [ ] **Step 3: Beschriftung ändern**
|
||||
|
||||
In `src/lib/customerTabs.js` das Label des Eintrags `invoices` auf `'Aufträge und Rechnungen'` setzen und die beiden Kommentare, die „Rechnungen“ als Reiternamen nennen (Kopfkommentar der Datei und Doc-Kommentar von `customerTabs`), auf „Aufträge und Rechnungen“ nachziehen. `adminOnly` und alle anderen Reiter bleiben.
|
||||
|
||||
- [ ] **Step 4: Test laufen lassen**
|
||||
|
||||
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/customerTabs.test.mjs`
|
||||
Expected: PASS, `fail 0`.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
git add src/lib/customerTabs.js tests/unit/customerTabs.test.mjs
|
||||
git commit -m "Kundenreiter „Rechnungen“ heißt jetzt „Aufträge und Rechnungen“ (id bleibt invoices)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 5: Ansichtslogik – Laden und Ladezustand
|
||||
|
||||
**Files:**
|
||||
- Create: `src/lib/auftraegeAnsicht.js`
|
||||
- Test: `tests/unit/auftraegeAnsicht.test.mjs` (neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nichts (reine Datei; die API-Funktionen kommen als Argument).
|
||||
- Produces, alle aus `src/lib/auftraegeAnsicht.js`:
|
||||
- `KONTEN_FEHLER = 'Die Projektübersicht konnte nicht geladen werden.'`, `AUFTRAEGE_FEHLER = 'Die Aufträge konnten nicht geladen werden.'`
|
||||
- `ladeAuftraegeAnsicht(kundeId, { getProjektkonten, listAuftraege }) → Promise<{ konten, auftraege, auftraegeFehler }>`: `konten` ist die Antwort von `getProjektkonten`, `auftraege` die Liste oder `null`, `auftraegeFehler` Text oder `''`.
|
||||
- `ansichtsZustand({ data, error } = {}) → AnsichtsZustand` (`data` und `error` kommen aus `useFinanceResource`):
|
||||
```
|
||||
{ projekteStatus: 'laden' | 'fehler' | 'bereit', fehler: string, nachladeFehler: string,
|
||||
auftraegeAktiv: boolean, auftraegeStatus: 'aus' | 'bereit' | 'fehler', auftraegeFehler: string,
|
||||
konten: Projektkonto[], ohneProjekt: { berechnetCents, bezahltCents, anzahl }, stripeOk: boolean,
|
||||
kunde: { id, name, kundenart } | null, auftraege: AuftragTeam[] }
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Failing Test schreiben**
|
||||
|
||||
`tests/unit/auftraegeAnsicht.test.mjs`:
|
||||
```js
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { loadModule } from './load.mjs'
|
||||
|
||||
const { KONTEN_FEHLER, AUFTRAEGE_FEHLER, ladeAuftraegeAnsicht, ansichtsZustand } = await loadModule('src/lib/auftraegeAnsicht.js')
|
||||
|
||||
// Antwort von getProjektkonten (V5) mit den Feldern, die die Ansicht braucht
|
||||
const konten = (extra = {}) => ({
|
||||
auftraegeAktiv: false,
|
||||
kunde: { id: 'k1', name: 'Anelia Wolf', kundenart: 'privat' },
|
||||
konten: [{ projektId: 'p1', name: 'Website' }],
|
||||
ohneProjekt: { berechnetCents: 5000, bezahltCents: 0, anzahl: 1 },
|
||||
stripeOk: true,
|
||||
...extra,
|
||||
})
|
||||
const OHNE_PROJEKT_LEER = { berechnetCents: 0, bezahltCents: 0, anzahl: 0 }
|
||||
|
||||
// Attrappe für getProjektkonten und listAuftraege: gibt Werte zurück, wirft Fehler und merkt sich die Aufrufe
|
||||
function fakeApi({ projektkonten, auftraege } = {}) {
|
||||
const calls = []
|
||||
const antwort = (wert) => {
|
||||
if (wert instanceof Error) throw wert
|
||||
return wert
|
||||
}
|
||||
return {
|
||||
calls,
|
||||
getProjektkonten: async (id) => { calls.push(['konten', id]); return antwort(projektkonten) },
|
||||
listAuftraege: async (id) => { calls.push(['auftraege', id]); return antwort(auftraege) },
|
||||
}
|
||||
}
|
||||
|
||||
test('Schalter aus: die Aufträge werden gar nicht erst abgerufen', async () => {
|
||||
const api = fakeApi({ projektkonten: konten({ auftraegeAktiv: false }), auftraege: new Error('404 darf nie ankommen') })
|
||||
const result = await ladeAuftraegeAnsicht('k1', api)
|
||||
assert.deepEqual(api.calls, [['konten', 'k1']])
|
||||
assert.equal(result.konten.auftraegeAktiv, false)
|
||||
assert.equal(result.auftraege, null)
|
||||
assert.equal(result.auftraegeFehler, '')
|
||||
// ein fehlendes Feld gilt wie „aus“, nur true schaltet an; eine Antwort ohne Objekt wird zu {}
|
||||
const fehlt = fakeApi({ projektkonten: { konten: [] } })
|
||||
assert.equal((await ladeAuftraegeAnsicht('k1', fehlt)).auftraege, null)
|
||||
assert.deepEqual(fehlt.calls, [['konten', 'k1']])
|
||||
const nichts = await ladeAuftraegeAnsicht('k1', fakeApi({ projektkonten: null }))
|
||||
assert.deepEqual(nichts, { konten: {}, auftraege: null, auftraegeFehler: '' })
|
||||
})
|
||||
|
||||
test('Schalter an: erst die Projektkonten, dann die Aufträge des Kunden', async () => {
|
||||
const liste = [{ id: 'a1', nummer: 'AB-2026-0001', status: 'versendet' }]
|
||||
const api = fakeApi({ projektkonten: konten({ auftraegeAktiv: true }), auftraege: { auftraege: liste } })
|
||||
const result = await ladeAuftraegeAnsicht('k1', api)
|
||||
assert.deepEqual(api.calls, [['konten', 'k1'], ['auftraege', 'k1']])
|
||||
assert.deepEqual(result.auftraege, liste)
|
||||
assert.equal(result.auftraegeFehler, '')
|
||||
// eine Antwort ohne Liste gibt eine leere Liste statt eines Absturzes
|
||||
const leer = await ladeAuftraegeAnsicht('k1', fakeApi({ projektkonten: konten({ auftraegeAktiv: true }), auftraege: {} }))
|
||||
assert.deepEqual(leer.auftraege, [])
|
||||
})
|
||||
|
||||
test('Aufträge scheitern allein: die Projektkonten bleiben, der Fehler steht eigens da', async () => {
|
||||
const api = fakeApi({
|
||||
projektkonten: konten({ auftraegeAktiv: true }),
|
||||
auftraege: Object.assign(new Error('Interner Fehler.'), { status: 500 }),
|
||||
})
|
||||
const result = await ladeAuftraegeAnsicht('k1', api)
|
||||
assert.equal(result.konten.konten.length, 1)
|
||||
assert.equal(result.auftraege, null)
|
||||
assert.equal(result.auftraegeFehler, 'Interner Fehler.')
|
||||
const ohneText = await ladeAuftraegeAnsicht('k1', fakeApi({ projektkonten: konten({ auftraegeAktiv: true }), auftraege: new Error('') }))
|
||||
assert.equal(ohneText.auftraegeFehler, AUFTRAEGE_FEHLER)
|
||||
assert.equal(AUFTRAEGE_FEHLER, 'Die Aufträge konnten nicht geladen werden.')
|
||||
})
|
||||
|
||||
test('Projektkonten scheitern: der Fehler geht weiter, die Aufträge werden nicht abgerufen', async () => {
|
||||
const fehler = Object.assign(new Error('Interner Fehler.'), { status: 500 })
|
||||
const api = fakeApi({ projektkonten: fehler, auftraege: { auftraege: [] } })
|
||||
await assert.rejects(ladeAuftraegeAnsicht('k1', api), (e) => e === fehler)
|
||||
assert.deepEqual(api.calls, [['konten', 'k1']])
|
||||
})
|
||||
|
||||
test('ansichtsZustand: vor der ersten Antwort „laden“, nach einem Fehler „fehler“ mit der Meldung', () => {
|
||||
const laden = ansichtsZustand({ data: null, error: '' })
|
||||
assert.equal(laden.projekteStatus, 'laden')
|
||||
assert.equal(laden.auftraegeStatus, 'aus') // der Schalter ist noch unbekannt: kein Abschnitt, kein Flackern
|
||||
assert.deepEqual(laden.konten, [])
|
||||
assert.equal(ansichtsZustand().projekteStatus, 'laden')
|
||||
const fehler = ansichtsZustand({ data: null, error: KONTEN_FEHLER })
|
||||
assert.equal(fehler.projekteStatus, 'fehler')
|
||||
assert.equal(fehler.fehler, KONTEN_FEHLER)
|
||||
assert.equal(fehler.auftraegeStatus, 'aus') // ohne Projektkonten kein Schalter, also auch keine Aufträge
|
||||
assert.equal(KONTEN_FEHLER, 'Die Projektübersicht konnte nicht geladen werden.')
|
||||
})
|
||||
|
||||
test('ansichtsZustand: Schalter aus – Projekte bereit, kein Aufträge-Abschnitt', () => {
|
||||
const z = ansichtsZustand({ data: { konten: konten({ auftraegeAktiv: false }), auftraege: null, auftraegeFehler: '' }, error: '' })
|
||||
assert.equal(z.projekteStatus, 'bereit')
|
||||
assert.equal(z.auftraegeAktiv, false)
|
||||
assert.equal(z.auftraegeStatus, 'aus')
|
||||
assert.equal(z.konten.length, 1)
|
||||
assert.deepEqual(z.ohneProjekt, { berechnetCents: 5000, bezahltCents: 0, anzahl: 1 })
|
||||
assert.equal(z.stripeOk, true)
|
||||
assert.deepEqual(z.kunde, { id: 'k1', name: 'Anelia Wolf', kundenart: 'privat' })
|
||||
assert.equal(z.nachladeFehler, '')
|
||||
})
|
||||
|
||||
test('ansichtsZustand: Schalter an – Aufträge bereit oder mit eigenem Fehler, die Projekte bleiben', () => {
|
||||
const liste = [{ id: 'a1' }]
|
||||
const bereit = ansichtsZustand({ data: { konten: konten({ auftraegeAktiv: true }), auftraege: liste, auftraegeFehler: '' }, error: '' })
|
||||
assert.equal(bereit.auftraegeAktiv, true)
|
||||
assert.equal(bereit.auftraegeStatus, 'bereit')
|
||||
assert.deepEqual(bereit.auftraege, liste)
|
||||
const kaputt = ansichtsZustand({
|
||||
data: { konten: konten({ auftraegeAktiv: true }), auftraege: null, auftraegeFehler: 'Interner Fehler.' },
|
||||
error: '',
|
||||
})
|
||||
assert.equal(kaputt.projekteStatus, 'bereit')
|
||||
assert.equal(kaputt.auftraegeStatus, 'fehler')
|
||||
assert.equal(kaputt.auftraegeFehler, 'Interner Fehler.')
|
||||
assert.deepEqual(kaputt.auftraege, [])
|
||||
})
|
||||
|
||||
test('ansichtsZustand: Neuladen scheitert – die alten Daten bleiben, die Meldung steht daneben', () => {
|
||||
const data = { konten: konten({ auftraegeAktiv: true }), auftraege: [{ id: 'a1' }], auftraegeFehler: '' }
|
||||
const z = ansichtsZustand({ data, error: 'Das Kundenportal ist gerade nicht erreichbar.' })
|
||||
assert.equal(z.projekteStatus, 'bereit')
|
||||
assert.equal(z.fehler, '')
|
||||
assert.equal(z.nachladeFehler, 'Das Kundenportal ist gerade nicht erreichbar.')
|
||||
assert.equal(z.konten.length, 1)
|
||||
assert.equal(z.auftraegeStatus, 'bereit')
|
||||
assert.equal(z.auftraege.length, 1)
|
||||
})
|
||||
|
||||
test('ansichtsZustand: unvollständige Antworten brechen die Ansicht nicht', () => {
|
||||
for (const data of [{}, { konten: null }, { konten: {} }, { konten: { konten: 'x', ohneProjekt: 5, kunde: 'k' } }]) {
|
||||
const z = ansichtsZustand({ data, error: '' })
|
||||
assert.equal(z.projekteStatus, 'bereit')
|
||||
assert.deepEqual(z.konten, [])
|
||||
assert.deepEqual(z.ohneProjekt, OHNE_PROJEKT_LEER)
|
||||
assert.equal(z.stripeOk, true)
|
||||
assert.equal(z.kunde, null)
|
||||
assert.equal(z.auftraegeAktiv, false)
|
||||
assert.equal(z.auftraegeStatus, 'aus')
|
||||
}
|
||||
assert.equal(ansichtsZustand({ data: { konten: konten({ stripeOk: false }) }, error: '' }).stripeOk, false)
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test laufen lassen**
|
||||
|
||||
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/auftraegeAnsicht.test.mjs`
|
||||
Expected: FAIL, `src/lib/auftraegeAnsicht.js` fehlt.
|
||||
|
||||
- [ ] **Step 3: `src/lib/auftraegeAnsicht.js` implementieren**
|
||||
|
||||
Reine Datei (kein React, kein Appwrite, noch keine Importe), deutscher Kopfkommentar „Ansichtslogik des Reiters Aufträge und Rechnungen“. Regeln:
|
||||
- Die zwei Fehlertexte werden exportiert (Wortlaut siehe Interfaces).
|
||||
- `ladeAuftraegeAnsicht`: ruft zuerst `getProjektkonten(kundeId)`; ein Fehler dort wird nicht abgefangen. Ist die Antwort kein Objekt, gilt `{}`. Nur wenn `konten.auftraegeAktiv === true`, folgt `listAuftraege(kundeId)`: Erfolg gibt `antwort.auftraege`, wenn das ein Array ist, sonst `[]`; ein Fehler gibt `auftraege: null` und `auftraegeFehler: err.message`, bei leerer Meldung `AUFTRAEGE_FEHLER`. Sonst `auftraege: null`, `auftraegeFehler: ''`. Rückgabe immer `{ konten, auftraege, auftraegeFehler }`.
|
||||
- `ansichtsZustand({ data, error } = {})`:
|
||||
- Ohne `data`: `projekteStatus` ist `'fehler'` (mit `fehler = error`), wenn `error` nicht leer ist, sonst `'laden'`; `auftraegeStatus` ist dann `'aus'`; `konten` `[]`, `ohneProjekt` eine Kopie von `{ berechnetCents: 0, bezahltCents: 0, anzahl: 0 }`, `stripeOk` true, `kunde` null, `auftraege` `[]`.
|
||||
- Mit `data`: `projekteStatus: 'bereit'`, `fehler: ''`, `nachladeFehler: error || ''`. Gelesen wird aus `data.konten` (nur wenn Objekt, sonst `{}`): `auftraegeAktiv = (data.konten.auftraegeAktiv === true)`, `konten` nur wenn Array, `ohneProjekt` nur wenn Objekt (sonst die Null-Kopie), `stripeOk = (data.konten.stripeOk !== false)`, `kunde` nur wenn Objekt (sonst `null`).
|
||||
- `auftraegeStatus`: `'aus'` ohne Schalter, `'fehler'` bei Schalter und nicht leerem `data.auftraegeFehler`, sonst `'bereit'`. `auftraegeFehler` nur im Fall `'fehler'`. `auftraege` ist `data.auftraege`, wenn Schalter an und Array, sonst `[]`.
|
||||
|
||||
- [ ] **Step 4: Test laufen lassen**
|
||||
|
||||
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/auftraegeAnsicht.test.mjs`
|
||||
Expected: PASS, `pass 9`, `fail 0`.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
git add src/lib/auftraegeAnsicht.js tests/unit/auftraegeAnsicht.test.mjs
|
||||
git commit -m "Aufträge und Rechnungen: Laden und Ladezustand des Reiters als reine Funktionen (mit Tests)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 6: Ansichtslogik – Kundenart und Meldungen
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/auftraegeAnsicht.js`, `tests/unit/auftraegeAnsicht.test.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `invoiceDoneMessage(actionKey, rechnung)` aus `src/lib/financeView.js` (liefert für `'create-send'` „Rechnung <Nr> wurde an <E-Mail> gesendet.“ und für `'create-draft'` „Der Entwurf wurde gespeichert.“).
|
||||
- Produces, aus `src/lib/auftraegeAnsicht.js`:
|
||||
- `kundeMitKundenart(customer, kundeInfo) → customer'`: Ist `kundeInfo.kundenart` eines von `'unternehmen'`, `'privat'`, kommt `{ ...customer, kundenart: <dieser Wert> }` zurück (das Dokument selbst bleibt unverändert). In allen anderen Fällen (`kundeInfo` fehlt, kein Objekt, Wert leer, unbekannt oder keine Zeichenkette) die **unveränderte Referenz** `customer`; `null` bleibt `null`.
|
||||
- `erstelltMeldung(rechnung, info) → { tone, text }`: wie `InvoiceTable.handleCreated`: mit `info.notice` (Zeichenkette) `{ tone: info.tone || 'ok', text: info.notice }`, sonst `{ tone: 'ok', text: invoiceDoneMessage(info.senden ? 'create-send' : 'create-draft', rechnung) }`. `info` darf fehlen oder `null` sein.
|
||||
- `auftragMeldung(auftrag) → { tone, text }` für `onSaved(auftrag)`.
|
||||
|
||||
- [ ] **Step 1: Failing Tests ergänzen**
|
||||
|
||||
In `tests/unit/auftraegeAnsicht.test.mjs` die `loadModule`-Zeile um `kundeMitKundenart, erstelltMeldung, auftragMeldung` erweitern und anhängen:
|
||||
```js
|
||||
test('kundeMitKundenart: ein gültiger Wert des Portals gewinnt gegen das veraltete Kundendokument', () => {
|
||||
const doc = { $id: 'k1', name: 'Anelia Wolf', kundenart: '' }
|
||||
assert.deepEqual(kundeMitKundenart(doc, { id: 'k1', kundenart: 'privat' }), { ...doc, kundenart: 'privat' })
|
||||
assert.equal(doc.kundenart, '') // das Dokument der Seite wird nicht verändert
|
||||
assert.equal(kundeMitKundenart({ ...doc, kundenart: 'unternehmen' }, { kundenart: 'privat' }).kundenart, 'privat')
|
||||
})
|
||||
|
||||
test('kundeMitKundenart: ohne gültigen Wert vom Portal bleibt das Dokument (dieselbe Referenz)', () => {
|
||||
const doc = { $id: 'k1', kundenart: 'unternehmen' }
|
||||
for (const info of [null, undefined, {}, { kundenart: '' }, { kundenart: 'firma' }, { kundenart: 5 }, 'privat']) {
|
||||
assert.equal(kundeMitKundenart(doc, info), doc)
|
||||
}
|
||||
assert.equal(kundeMitKundenart(null, { kundenart: 'privat' }), null)
|
||||
})
|
||||
|
||||
test('erstelltMeldung: gesendet, Entwurf und die Meldung nach festgeschriebener, nicht gesendeter Rechnung', () => {
|
||||
assert.deepEqual(
|
||||
erstelltMeldung({ number: 'WEBKLAR-0009', customerEmail: 'a@b.de' }, { senden: true }),
|
||||
{ tone: 'ok', text: 'Rechnung WEBKLAR-0009 wurde an a@b.de gesendet.' },
|
||||
)
|
||||
assert.deepEqual(erstelltMeldung({ id: 'in_1' }, { senden: false }), { tone: 'ok', text: 'Der Entwurf wurde gespeichert.' })
|
||||
assert.deepEqual(erstelltMeldung(null, undefined), { tone: 'ok', text: 'Der Entwurf wurde gespeichert.' })
|
||||
assert.deepEqual(erstelltMeldung(null, null), { tone: 'ok', text: 'Der Entwurf wurde gespeichert.' })
|
||||
const text = 'Rechnung WEBKLAR-0010 wurde festgeschrieben, aber nicht versendet. Bitte in der Liste erneut senden.'
|
||||
assert.deepEqual(erstelltMeldung(null, { senden: true, notice: text, tone: 'warn', finalizedFilter: 'offen' }), { tone: 'warn', text })
|
||||
assert.deepEqual(erstelltMeldung(null, { senden: true, notice: text }), { tone: 'ok', text })
|
||||
})
|
||||
|
||||
test('auftragMeldung: Entwurf, gesendet und gesendet ohne zugestellte Mail', () => {
|
||||
assert.deepEqual(auftragMeldung({ status: 'entwurf', nummer: '' }), { tone: 'ok', text: 'Der Entwurf wurde gespeichert.' })
|
||||
assert.deepEqual(
|
||||
auftragMeldung({ status: 'versendet', nummer: 'AB-2026-0001', gesendetAn: 'k@x.de', mailStatus: 'gesendet' }),
|
||||
{ tone: 'ok', text: 'Auftragsbestätigung AB-2026-0001 wurde an k@x.de gesendet.' },
|
||||
)
|
||||
assert.deepEqual(
|
||||
auftragMeldung({ status: 'versendet', nummer: 'AB-2026-0001', gesendetAn: 'k@x.de', mailStatus: 'fehler' }),
|
||||
{ tone: 'warn', text: 'Auftragsbestätigung AB-2026-0001 ist versendet, die Mail an den Kunden wurde aber nicht zugestellt. Bitte in der Liste erneut senden.' },
|
||||
)
|
||||
assert.deepEqual(auftragMeldung(null), { tone: 'ok', text: 'Die Auftragsbestätigung wurde gespeichert.' })
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test laufen lassen**
|
||||
|
||||
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/auftraegeAnsicht.test.mjs`
|
||||
Expected: FAIL in den vier neuen Tests (`kundeMitKundenart is not a function` und Verwandte); die neun aus Task 5 bestehen weiter.
|
||||
|
||||
- [ ] **Step 3: Die drei Funktionen implementieren**
|
||||
|
||||
In `src/lib/auftraegeAnsicht.js` `import { invoiceDoneMessage } from './financeView'` ergänzen. Die erlaubten Kundenarten sind zwei Konstanten `'unternehmen'` und `'privat'` (Wertebereich von `customers.kundenart` nach V1; bewusst lokal, `auftragView.js` gehört W2). `auftragMeldung` prüft der Reihe nach, mit `a = auftrag` (Objekt, sonst `{}`) und `name = a.nummer ? 'Auftragsbestätigung <nummer>' : 'Die Auftragsbestätigung'`:
|
||||
1. `a.status === 'entwurf'` → `{ tone: 'ok', text: 'Der Entwurf wurde gespeichert.' }`
|
||||
2. `a.mailStatus === 'fehler'` → `{ tone: 'warn', text: '<name> ist versendet, die Mail an den Kunden wurde aber nicht zugestellt. Bitte in der Liste erneut senden.' }`
|
||||
3. `a.nummer` → `{ tone: 'ok', text: '<name> wurde[ an <gesendetAn>] gesendet.' }` (der Teil „an …“ nur bei nicht leerem `gesendetAn`)
|
||||
4. sonst → `{ tone: 'ok', text: 'Die Auftragsbestätigung wurde gespeichert.' }`
|
||||
|
||||
- [ ] **Step 4: Test laufen lassen**
|
||||
|
||||
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/auftraegeAnsicht.test.mjs`
|
||||
Expected: PASS, `pass 13`, `fail 0`.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
git add src/lib/auftraegeAnsicht.js tests/unit/auftraegeAnsicht.test.mjs
|
||||
git commit -m "Aufträge und Rechnungen: frische Kundenart und Rückmeldungen nach Rechnung und Auftrag (mit Tests)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 7: Platzhalter und Container
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/auftraege/ProjektkontoListe.jsx`, `RechnungDialog.jsx`, `AuftragListe.jsx`, `AuftragDialog.jsx`, `AuftraegeUndRechnungen.jsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Card`, `Button`, `EmptyState` (`../ui`), `AddressWarning` und `InvoiceTable` (`../finance/…`, Default-Exporte), `useFinanceResource` und `useScrollIntoView` (`../../hooks/…`, benannte Exporte), `getProjektkonten` und `listAuftraege` (`../../lib/financeApi`, Task 3), `customerInvoicesGate` (`../../lib/financeView`), `noticeProps` (`../../lib/financeUi`), alles aus `../../lib/auftraegeAnsicht` (Task 5 und 6).
|
||||
- Produces (Default-Exporte, Dateiname = Komponentenname, Props nach V8):
|
||||
- `AuftraegeUndRechnungen({ customer, onShowOverview })`
|
||||
- `ProjektkontoListe({ konten, ohneProjekt, auftraegeAktiv, stripeOk, onRechnung, onAuftrag })`
|
||||
- `RechnungDialog({ open, onClose, onCreated, kunde, projektId, auftraegeAktiv, onShowOverview })`
|
||||
- `AuftragListe({ auftraege, konten, onChanged, onBearbeiten, onShowOverview })`
|
||||
- `AuftragDialog({ open, onClose, onSaved, kunde, projektId, auftrag, konten, onShowOverview })`
|
||||
|
||||
Die Props entsprechen V8 nach den Nachträgen der Planrunde: Der Container reicht `konten` und `onShowOverview` an `AuftragListe` und `AuftragDialog` durch und `onShowOverview` (optional) auch an `RechnungDialog`.
|
||||
|
||||
- [ ] **Step 1: Vier Platzhalter schreiben**
|
||||
|
||||
Jede Datei ist `export default function <Dateiname>({ … })` mit der Prop-Signatur aus den Interfaces (destrukturiert, auch wenn nichts davon benutzt wird; sie dokumentiert den Vertrag) und gibt `null` zurück (kein React-Import nötig), mit einem Kommentarkopf in genau diesem Sinn (Platzhalter von W0, Strang W1 bzw. W2 ersetzt die Datei vollständig, Dateiname, Default-Export und Props bleiben):
|
||||
- `ProjektkontoListe.jsx` (W1): `projektId` in `onRechnung(projektId)` und `onAuftrag(projektId)` ist die Kennung des Projekts, `null` die Zeile „Ohne Projekt“. Die fertige Komponente rendert ihren Abschnitt selbst: `<Card title="Projekte">` (ein `<section>`; den Abstand zu den Nachbarn liefert `.page section + section`).
|
||||
- `RechnungDialog.jsx` (W1): `kunde` ist das Kundendokument oder `null` (Kundenwahl im Dialog); `projektId`: `undefined` = keine Vorauswahl, `null` = „Ohne Projekt“, Text = dieses Projekt; `auftraegeAktiv` ist nur der Startwert (danach gilt `getProjektkonten`, das der Dialog selbst lädt); `onShowOverview` ist optional („Zur Übersicht“ in der Adresswarnung); `onCreated(rechnung, info)`: `info` unverändert weiterreichen wie im bisherigen `InvoiceCreateModal` (`{ senden }`, bei festgeschriebener, nicht versendeter Rechnung `{ senden: true, notice, tone: 'warn', finalizedFilter }`), danach ruft der Dialog `onClose()`. Der Dialog sitzt zusätzlich in `InvoiceTable.jsx` (dort ersetzt W1 das Modal), `FinancePage.jsx` bleibt unverändert.
|
||||
- `AuftragListe.jsx` (W2): `auftraege` sind `AuftragTeam[]` (V3); `konten` liefern den Projektnamen der Zeile und die Auswahl für „Projekt zuordnen“ (Aktion für ABs ohne Projekt, ruft `ordneAuftragProjektZu(id, projektId)` aus `financeApi.js`); `onShowOverview` ist der Weg „Zur Übersicht“ aus Meldungen. Die fertige Komponente rendert `<Card title="Aufträge">` selbst und ruft nach jeder Aktion `onChanged()`; „Entwurf bearbeiten“ ruft `onBearbeiten(auftrag)`, nach „Neue Fassung“ ruft sie `onBearbeiten(neuerEntwurf)`.
|
||||
- `AuftragDialog.jsx` (W2): `kunde` kommt mit aktueller `kundenart` (der Container gleicht sie über `kundeMitKundenart` ab); `projektId` ist das vorausgewählte Projekt oder `null` (freier Titel); `auftrag` der Entwurf zum Bearbeiten oder `null`; `konten` die Projektkonten für die Projektwahl; `onShowOverview` der Weg „Zur Übersicht“; nach Speichern oder Senden ruft der Dialog `onSaved(auftrag)` (`AuftragTeam`, mit `mailStatus`) und danach `onClose()`. PDF-Vorschau über `openInNewTab(async () => URL.createObjectURL(await getAuftragPdf(id)))`.
|
||||
|
||||
- [ ] **Step 2: Container `AuftraegeUndRechnungen.jsx` schreiben**
|
||||
|
||||
Aufbau und Regeln (ersetzt `CustomerInvoices`; deutscher Kopfkommentar zu Aufgabe und Reihenfolge):
|
||||
- **Außen** `export default function AuftraegeUndRechnungen({ customer, onShowOverview })`: Ist `customerInvoicesGate(customer).ok` falsch, kommt wie bisher `EmptyState` mit `gate.title`, `gate.hint` und (nur mit `onShowOverview`) `<Button variant="primary" onClick={onShowOverview}>Daten erfassen</Button>`; es werden keine Abrufe gestartet. Sonst rendert sie die innere Komponente `AuftraegeInhalt` (die Hooks stehen erst dort).
|
||||
- **Innen** `AuftraegeInhalt({ customer, onShowOverview })`:
|
||||
- `const res = useFinanceResource(() => ladeAuftraegeAnsicht(customer.$id, { getProjektkonten, listAuftraege }), KONTEN_FEHLER)`; `const ansicht = ansichtsZustand(res)`; `const reload = res.reload`.
|
||||
- Zustand: `meldung` (`{ tone, text }` oder `null`), `rechnungsDialog` (`{ projektId }` oder `null`), `auftragsDialog` (`{ projektId, auftrag }` oder `null`), `listeToken` (Zahl, Start 0), dazu `meldungRef` mit `useScrollIntoView(meldungRef, meldung)`.
|
||||
- `const kunde = kundeMitKundenart(customer, ansicht.kunde)` (geht an beide Dialoge).
|
||||
- Öffnen eines Dialogs setzt zuerst `meldung` auf `null`.
|
||||
- `RechnungDialog.onCreated(rechnung, info)`: Dialogzustand `null`, `setMeldung(erstelltMeldung(rechnung, info))`, `setListeToken((n) => n + 1)`, `reload()`. `AuftragDialog.onSaved(auftrag)`: Dialogzustand `null`, `setMeldung(auftragMeldung(auftrag))`, `reload()`. `onClose` setzt den jeweiligen Zustand auf `null` (der Dialog ruft `onCreated` und danach `onClose`; die Handler sind in jeder Reihenfolge und mehrfach unschädlich).
|
||||
- **Reihenfolge der Ausgabe** (ein Fragment; die Abschnitte sind `<section>`, alles andere steht davor, damit `.page section + section` überall den Abstand liefert):
|
||||
|
||||
| Nr. | Element | Bedingung | Umsetzung |
|
||||
|---|---|---|---|
|
||||
| a | Adresswarnung | immer | `<AddressWarning customer={customer} onShowOverview={onShowOverview} variant="page" />` |
|
||||
| b | Rückmeldung | `meldung` | `<p ref={meldungRef} {...noticeProps(meldung.tone, 'fin-notice')}>{meldung.text}</p>` |
|
||||
| c | Nachladefehler | `projekteStatus === 'bereit'` und `nachladeFehler` | `<p {...noticeProps('warn', 'fin-notice')}>Die Übersicht konnte nicht neu geladen werden: {nachladeFehler}{' '}<button type="button" className="link" onClick={reload}>Erneut versuchen</button></p>` |
|
||||
| d | Projekte | `laden` | `<Card title="Projekte"><div className="loading" role="status">Projekte werden geladen …</div></Card>` |
|
||||
| | | `fehler` | `<Card title="Projekte"><div className="fin-load-error"><p className="msg warn" role="alert">{fehler}</p><Button onClick={reload}>Erneut versuchen</Button></div></Card>` |
|
||||
| | | `bereit` | `<ProjektkontoListe konten ohneProjekt auftraegeAktiv stripeOk onRechnung onAuftrag />` mit den Werten aus `ansicht`; `onRechnung(projektId)` öffnet den Rechnungsdialog, `onAuftrag(projektId)` den Auftragsdialog mit `auftrag: null` |
|
||||
| e | Aufträge | `auftraegeStatus === 'bereit'` | `<AuftragListe auftraege={ansicht.auftraege} konten={ansicht.konten} onChanged={reload} onBearbeiten={(auftrag) => … Auftragsdialog mit projektId: auftrag.projektId \|\| null und dem Entwurf} onShowOverview={onShowOverview} />` |
|
||||
| | | `'fehler'` | `<Card title="Aufträge">` mit demselben `fin-load-error`-Block wie bei Projekten, Text `ansicht.auftraegeFehler` |
|
||||
| | | `'aus'` | nichts |
|
||||
| f | Rechnungen | immer | `<InvoiceTable customer={customer} onShowOverview={onShowOverview} refreshToken={listeToken} onChanged={reload} />` (nur diese bisherigen Props) |
|
||||
| g | Dialoge | Zustand gesetzt | `<RechnungDialog open … kunde={kunde} projektId={rechnungsDialog.projektId} auftraegeAktiv={ansicht.auftraegeAktiv} onShowOverview={onShowOverview} />` bzw. `<AuftragDialog open … kunde={kunde} projektId={…} auftrag={…} konten={ansicht.konten} onShowOverview={onShowOverview} />`, nur gemountet, solange der Zustand gesetzt ist (frischer Zustand bei jedem Öffnen) |
|
||||
|
||||
- Regeln dazu: Fehlt `.fin-load-error` oder `.fin-notice` im CSS (Task 1 Step 5), entfallen diese beiden Zusatzklassen. Die Dialoge stehen als Geschwister hinter den Abschnitten, **nie** innerhalb von `InvoiceTable` oder `.fin-body` (dessen `container-type` würde `position: fixed` verbiegen). `InvoiceTable` behält seinen eigenen Knopf „Rechnung schreiben“; der Container fasst ihn nicht an. Kein `style`, keine neue CSS-Datei, keine Icons.
|
||||
|
||||
- [ ] **Step 3: Syntaxprüfung**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
node_modules/.bin/esbuild src/components/auftraege/*.jsx --loader:.jsx=jsx --outdir=/tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad/auftraege-w0-syntax --log-level=warning
|
||||
```
|
||||
Expected: keine Ausgabe, Exit 0 (reine Syntaxprüfung; den Import-Graphen prüft der Build in Task 9).
|
||||
|
||||
- [ ] **Step 4: Struktur gegen die Tabelle prüfen (Review Focus 1 bis 3)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
rg -n "<InvoiceTable|<AddressWarning|<ProjektkontoListe|<AuftragListe|<RechnungDialog|<AuftragDialog|useFinanceResource\(|ansichtsZustand\(|kundeMitKundenart\(" src/components/auftraege/AuftraegeUndRechnungen.jsx
|
||||
rg -n "konten=\{ansicht\.konten\}|onShowOverview=\{onShowOverview\}" src/components/auftraege/AuftraegeUndRechnungen.jsx
|
||||
```
|
||||
Expected: im ersten Befehl mindestens ein Treffer je Muster. Im zweiten mindestens dreimal `konten={ansicht.konten}` (`ProjektkontoListe`, `AuftragListe`, `AuftragDialog`) und mindestens fünfmal `onShowOverview={onShowOverview}` (`AddressWarning`, `InvoiceTable`, `RechnungDialog`, `AuftragListe`, `AuftragDialog`). Beim Lesen der Datei gegenprüfen: `<InvoiceTable` und `<AddressWarning` stehen **nicht** in einem Ausdruck, der von `projekteStatus` oder `auftraegeStatus` abhängt (die Rechnungsliste bleibt bei jedem Ausfall der Projektkonten und Aufträge bedienbar); `<AuftragListe` steht nur im Fall `auftraegeStatus === 'bereit'`; es gibt keinen Aufruf von `listAuftraege` außerhalb von `ladeAuftraegeAnsicht`.
|
||||
|
||||
- [ ] **Step 5: Hygiene**
|
||||
|
||||
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && rg -n "react-icons|@tabler|style=\{\{|ui-btn|form-control|form-group|form-label|ui-card|badge-|overlay-content|overlay-close|spinner|kpi|#[0-9a-fA-F]{3,8}\b|rgba?\(|console\.log" src/components/auftraege src/lib/auftraegeAnsicht.js src/lib/auftraegeEndpoints.js`
|
||||
Expected: keine Treffer.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
git add src/components/auftraege/ProjektkontoListe.jsx src/components/auftraege/RechnungDialog.jsx src/components/auftraege/AuftragListe.jsx src/components/auftraege/AuftragDialog.jsx src/components/auftraege/AuftraegeUndRechnungen.jsx
|
||||
git commit -m "Aufträge und Rechnungen: Container des Kundenreiters und vier Platzhalter für W1 und W2" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 8: Einbindung im Kundenreiter, `CustomerInvoices` entfällt
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/CustomerDetailPage.jsx`
|
||||
- Delete: `src/components/finance/CustomerInvoices.jsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `AuftraegeUndRechnungen({ customer, onShowOverview })` (Task 7).
|
||||
- Produces: Der Reiter `invoices` der Kundenseite zeigt den Container. Die Adresswarnung (`AddressWarning variant="page"`) und die Sperre „Noch ein Lead“ usw. stecken jetzt im Container und bleiben sichtbar. `AddressWarning.jsx` selbst bleibt unverändert (Nutzer: der Container und der Rechnungsdialog); `CustomerInvoices.jsx` hat danach keinen Nutzer mehr und wird gelöscht.
|
||||
|
||||
- [ ] **Step 1: Seite umstellen**
|
||||
|
||||
In `src/pages/CustomerDetailPage.jsx`:
|
||||
- Den Import `import CustomerInvoices from '../components/finance/CustomerInvoices'` ersetzen durch `import AuftraegeUndRechnungen from '../components/auftraege/AuftraegeUndRechnungen'`.
|
||||
- Die Verwendung `{activeTab === 'invoices' && <CustomerInvoices customer={customer} onShowOverview={() => setTab('overview')} />}` ersetzen durch `{activeTab === 'invoices' && <AuftraegeUndRechnungen key={customer.$id} customer={customer} onShowOverview={() => setTab('overview')} />}` (der `key` sorgt dafür, dass ein anderer Kunde nie den Zustand eines früheren erbt).
|
||||
- Sonst bleibt die Datei unverändert (laut Übersicht ändert W0 nur Import und Reiterzeile; das Feld Kundenart in der Übersicht gehört W2).
|
||||
|
||||
- [ ] **Step 2: Alte Datei entfernen**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
rg -n "CustomerInvoices" src tests
|
||||
git rm src/components/finance/CustomerInvoices.jsx
|
||||
rg -n "CustomerInvoices" src tests
|
||||
```
|
||||
Expected: Vor dem Löschen Treffer nur in `CustomerInvoices.jsx` selbst (und ggf. in Kommentaren); danach keine Treffer. Bleibt ein Import oder eine Verwendung stehen, wird sie umgestellt, bevor gelöscht wird.
|
||||
|
||||
- [ ] **Step 3: Syntax und Anker**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
node_modules/.bin/esbuild src/pages/CustomerDetailPage.jsx --loader:.jsx=jsx --outdir=/tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad/auftraege-w0-syntax --log-level=warning
|
||||
rg -n "AuftraegeUndRechnungen" src/pages/CustomerDetailPage.jsx
|
||||
```
|
||||
Expected: esbuild ohne Ausgabe; zwei Treffer (Import und Verwendung).
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
git add src/pages/CustomerDetailPage.jsx
|
||||
git commit -m "Kundenseite: Reiter „Aufträge und Rechnungen“ zeigt den neuen Container, CustomerInvoices entfällt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
(Die mit `git rm` vorgemerkte Löschung gehört in denselben Commit.)
|
||||
|
||||
### Task 9: Selbstprüfung
|
||||
|
||||
**Files:** keine Änderung, außer Korrekturen aus den Prüfungen.
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: alle Ergebnisse der Tasks 1 bis 8.
|
||||
- Produces: die Abschlussmeldung an den Orchestrator (Step 6). Von diesem Stand legt der Orchestrator die Worktrees von W1 und W2 an.
|
||||
|
||||
- [ ] **Step 1: Volle Suite (hinter dem `flock`)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
L=/tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad
|
||||
free -m | head -2
|
||||
flock $L/auftraege-schwer.lock env NODE_OPTIONS=--max-old-space-size=1024 timeout 900 nice -n 10 npm test > $L/auftraege-w0-test.log 2>&1; echo "exit $?"
|
||||
rg "^(ℹ|#) (tests|pass|fail) " $L/auftraege-w0-test.log
|
||||
```
|
||||
Expected: `exit 0`, `fail 0`, `pass` = `N0 + 21` (4 Blob, 4 Endpunkte, 13 Ansichtslogik; `customerTabs` ändert nur eine Erwartung).
|
||||
|
||||
- [ ] **Step 2: Build (hinter dem `flock`, mit Speichergrenze)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
L=/tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad
|
||||
free -m | head -2
|
||||
flock $L/auftraege-schwer.lock systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 env NODE_OPTIONS=--max-old-space-size=1200 nice -n 10 node node_modules/vite/bin/vite.js build --outDir $L/auftraege-w0-dist --emptyOutDir > $L/auftraege-w0-build.log 2>&1; echo "exit $?"
|
||||
tail -8 $L/auftraege-w0-build.log
|
||||
```
|
||||
Expected: `exit 0`, die letzten Zeilen nennen die gebauten Dateien und „built in …“, keine Fehlermeldung zu fehlenden Exporten oder Importen. Schlägt der Build fehl, wird die Ursache in der Datei des zuständigen Tasks korrigiert (neuer Commit „Korrektur: …“), danach Step 1 und 2 wiederholt.
|
||||
|
||||
- [ ] **Step 3: Vertragsliste abhaken (V8)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
for n in getProjektkonten getProjektkonto previewInvoice listAuftraege getAuftrag createAuftrag patchAuftrag deleteAuftrag sendAuftrag erinnereAuftrag neueFassungAuftrag storniereAuftrag ordneAuftragProjektZu getAuftragPdf; do rg -q "\b$n\b" src/lib/financeApi.js && echo "ok $n" || echo "FEHLT $n"; done
|
||||
rg -n "export const createInvoice|export const getAbgerechnet" src/lib/financeApi.js src/lib/teamApi.js
|
||||
for f in AuftraegeUndRechnungen ProjektkontoListe RechnungDialog AuftragListe AuftragDialog; do rg -c "export default function $f\b" src/components/auftraege/$f.jsx; done
|
||||
rg -n "label: 'Aufträge und Rechnungen'" src/lib/customerTabs.js
|
||||
rg -n "AuftraegeUndRechnungen" src/pages/CustomerDetailPage.jsx
|
||||
```
|
||||
Expected: 14-mal `ok …`, `createInvoice` und `getAbgerechnet` gefunden, fünfmal `1`, die Beschriftung und die Einbindung gefunden. Abgehakt wird außerdem (aus den Tests):
|
||||
- [ ] `getAuftragPdf` liefert einen Blob (Task 3, Test „getAuftragPdf …“); `ordneAuftragProjektZu` sendet `POST …/auftraege/<id>/projekt` mit `{ projektId }` (Task 3, Test „Pfad, Methode, Kopf und Körper …“).
|
||||
- [ ] Der Container hat die Props `{ customer, onShowOverview }`, die Platzhalter die Props aus V8 samt `konten` und `onShowOverview`.
|
||||
- [ ] Der Container reicht `konten` und `onShowOverview` an `AuftragListe` und `AuftragDialog` durch, `onShowOverview` auch an `RechnungDialog` (Task 7, Step 4).
|
||||
- [ ] `AddressWarning variant="page"` steht im Container (Task 7, Zeile a) und `CustomerInvoices` ist weg (Task 8).
|
||||
- [ ] Der Reiter bleibt nur für Admins (Test `customerTabs`).
|
||||
|
||||
- [ ] **Step 4: Hygiene der neuen Dateien**
|
||||
|
||||
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && rg -n "react-icons|@tabler|style=\{\{|ui-btn|form-control|form-group|form-label|ui-card|badge-|overlay-content|overlay-close|spinner|kpi|#[0-9a-fA-F]{3,8}\b|rgba?\(|console\.log" src/components/auftraege src/lib/auftraegeAnsicht.js src/lib/auftraegeEndpoints.js`
|
||||
Expected: keine Treffer.
|
||||
|
||||
- [ ] **Step 5: Dateiliste und Git-Zustand**
|
||||
|
||||
Run (mit `B` aus Task 1):
|
||||
```bash
|
||||
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
|
||||
B=<B aus Task 1>
|
||||
git diff --name-status "$B"..HEAD
|
||||
git log --oneline "$B"..HEAD
|
||||
git status --short
|
||||
```
|
||||
Expected: genau diese 17 Pfade (Reihenfolge egal): neu `src/components/auftraege/{AuftraegeUndRechnungen,ProjektkontoListe,RechnungDialog,AuftragListe,AuftragDialog}.jsx`, `src/lib/auftraegeAnsicht.js`, `src/lib/auftraegeEndpoints.js`, `tests/unit/{auftraegeAnsicht,auftraegeEndpoints,financeRequestBlob}.test.mjs`; geändert `src/lib/{customerTabs,financeApi,financeRequest,teamApi}.js`, `src/pages/CustomerDetailPage.jsx`, `tests/unit/customerTabs.test.mjs`; gelöscht `src/components/finance/CustomerInvoices.jsx`. Sieben Commits (plus eventuelle „Korrektur“-Commits), `git status --short` leer. Nichts gepusht.
|
||||
|
||||
- [ ] **Step 6: Abschlussmeldung an den Orchestrator**
|
||||
|
||||
Die Antwort nennt: Basis `B` und `N0`, letzten Commit (`git rev-parse --short HEAD`), Testzahl und Build-Ergebnis, die 17 Pfade, Abweichungen von diesem Plan samt Grund, und den Hinweis: „`node_modules` im Worktree ist echt und kann von W1 und W2 verlinkt werden.“ Dazu der Hinweis, dass der Container V8 samt Nachträgen der Planrunde umsetzt (Kindkomponenten rendern ihre `Card` selbst; `projektId` `undefined` = keine Vorauswahl, `null` = „Ohne Projekt“; `info` von `onCreated` unverändert; `konten` und `onShowOverview` durchgereicht).
|
||||
|
||||
---
|
||||
|
||||
## Vertragsfragen
|
||||
|
||||
Alle Fragen aus der Erstfassung dieses Plans sind **entschieden (siehe Übersicht, Nachträge nach der Planrunde, V5 und V8)**; die Übersicht gilt. Die Liste hält fest, was entschieden wurde und wo dieser Plan es umsetzt.
|
||||
|
||||
1. **Wer öffnet „Rechnung schreiben“ im Kundenreiter?** Entschieden (siehe Übersicht, Nachträge): `RechnungDialog` sitzt in `InvoiceTable.jsx` (dort ersetzt W1 das Modal), `FinancePage.jsx` bleibt unverändert. Umsetzung: Der Container öffnet `RechnungDialog` nur für die Projektzeilen und nutzt `InvoiceTable` mit seinen bisherigen Props (`customer`, `onShowOverview`, `refreshToken`, `onChanged`); der Dialog lädt Projektliste und Schalter selbst über `getProjektkonten` (`auftraegeAktiv` ist nur der Startwert). Es bleibt dabei: Nach dem Anlegen über eine Projektzeile behält die Liste ihren Filter, die Meldung des Containers nennt Entwurf oder Rechnung (Task 6 und 7); ein Entwurf liegt dann unter „Entwürfe“.
|
||||
2. **`projektId` im `RechnungDialog`.** Entschieden (siehe Übersicht, Nachträge; V8): `undefined` = keine Vorauswahl (Kartenknopf in `InvoiceTable`), `null` = „Ohne Projekt“ (Zeile „Ohne Projekt“), Text = dieses Projekt. Umsetzung: Der Container reicht die Kennung aus `onRechnung(projektId)` unverändert durch (Task 7, Zeile g).
|
||||
3. **`onCreated(rechnung, info)`.** Entschieden (siehe Übersicht, Nachträge; V8): `info` unverändert weiterreichen (`senden`, `notice`, `tone`, `finalizedFilter`). Umsetzung: `erstelltMeldung` wertet `notice` und `tone` aus (Task 6).
|
||||
4. **Abschnittsüberschriften.** Entschieden (siehe Übersicht, Nachträge; V8): Die Kindkomponenten rendern ihre `Card` samt Titel selbst. Umsetzung: Kommentare der Platzhalter und Container (Zustände „laden“ und „fehler“ mit denselben Titeln, Task 7).
|
||||
5. **PDF öffnen (W2).** Entschieden (siehe Übersicht, Nachträge; V8): `openInNewTab(async () => URL.createObjectURL(await getAuftragPdf(id)))`. Umsetzung: `getAuftragPdf` liefert den Blob (Task 2 und 3); das Öffnen gehört W2 (Hinweis im Kommentar von `AuftragDialog.jsx`).
|
||||
6. **Testbefehl.** Entschieden (siehe Übersicht, Nachträge; Tech Stack): Die WOMS-Tests laufen mit `npm test` (`tests/unit/*.test.mjs`). Umsetzung: Task 1 Step 6 und Task 9 Step 1.
|
||||
7. **Form von `abgerechnet`.** Entschieden (siehe Übersicht, Nachträge; V5): `{ abgerechnet: { <worksheetId>: '<Rechnungsnummer>' | 'Entwurf' } }`, für ein unbekanntes Ticket `{ abgerechnet: {} }`. W0 folgt V5 (Task 3); Spec Abschnitt 6 nennt die kürzere Form. Das Abzeichen für `'Entwurf'` legt W1 fest.
|
||||
8. **Dateizuständigkeit WOMS.** Entschieden (siehe Übersicht, Nachträge, Absatz „WOMS“): W0 besitzt API-Schicht (`financeApi.js`, `financeRequest.js`, `teamApi.js`, `auftraegeEndpoints.js`), `auftraegeAnsicht.js`, `customerTabs.js`, den Container samt vier Platzhaltern, die Reiterzeile in `CustomerDetailPage.jsx` und die Löschung von `CustomerInvoices.jsx`; W1 die Projektliste, den Rechnungsdialog, `InvoiceTable.jsx`, `WorksheetList.jsx`, `WorksheetTimesModal.jsx` und die Löschung von `InvoiceCreateModal.jsx`; W2 die Auftragsliste, den Auftragsdialog, das Feld Kundenart in `CustomerDetailPage.jsx` und `SettingsCard.jsx`. W2 startet nach W0, also gibt es in `CustomerDetailPage.jsx` keinen Konflikt.
|
||||
9. **Basis und Push-Reihenfolge.** Entschieden (siehe Übersicht, Nachträge): `feature/auftraege` (WOMS) enthält alle Redesign-Commits bis zum Basis-Tag `B`. Der Nutzer pusht erst, wenn das Redesign auf `test` liegt; vorher wird der Zweig auf `origin/test` aktualisiert (Z2). Die Übergabe nennt `B`; Task 1 Step 2 meldet, wenn `origin/test` weiter ist als `B`.
|
||||
10. **Prüfwerkzeug des Redesigns.** Entschieden (siehe Übersicht, Nachträge): Das Redesign-Werkzeug wird nicht geändert; Bildschirmfotos entstehen mit eigenen Skripten im Scratchpad (W1, W2, Z2). W0 macht keine Bildschirmfotos und fasst das Werkzeug nicht an.
|
||||
11. **Kundenart, Schreibweg.** Entschieden (siehe Übersicht, Nachträge; V5): Die AB-Dialoge (W2) schicken `kundenart` mit `POST` und `PATCH …/auftraege`, das Portal setzt sie am Kunden, wenn dort leer; die Übersicht (W2) schreibt das Feld bei Änderung direkt nach Appwrite. W0 gleicht in beiden Fällen über `kunde.kundenart` aus der Antwort von `getProjektkonten` ab (`kundeMitKundenart`, Task 6 und 7).
|
||||
12. **`onShowOverview` im `RechnungDialog`.** Entschieden (siehe Übersicht, Nachträge; V8): optionale Prop des Dialogs. Umsetzung: Der Container reicht sie durch (Task 7, Zeile g).
|
||||
|
||||
Mit der Planrunde kamen zwei Festlegungen für W0 dazu, die dieser Plan umsetzt:
|
||||
|
||||
13. **`ordneAuftragProjektZu(id, projektId)`.** V5 (Zeile L3) hat die Route `POST …/auftraege/:id/projekt` mit `{ projektId }`; W2 braucht sie für „Projekt zuordnen“ (Spec: „später zuordenbar“). Umsetzung: Task 3 (Pfad in `auftraegeEndpoints.js`, Test, Export in `financeApi.js`).
|
||||
14. **`konten` und `onShowOverview` für `AuftragListe` und `AuftragDialog`.** V8 (Nachträge): Der Container reicht beides durch (Projektname und Auswahl „Projekt zuordnen“ in der Liste, „Zur Übersicht“ in Meldungen). Umsetzung: Task 7 (Props der Platzhalter, Zeilen e und g, Step 4).
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user