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>
73 KiB
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-weisszusammengeführt und alsdesign-p4,design-p6,design-p7getaggt sind. Task 1 prüft das. Fehlt etwas, gilt STOPP: nichts anlegen, dem Orchestrator melden (er fragt den Nutzer: warten oder auftestbauen und eine Mitnahme-Notiz in/root/reports/ticketsystem-design-2026-10-04/strang/mitnehmen.mdhinterlegen). - 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/testist dem Redesign voraus (Öffnungszeiten:zeitenView.js, zusätzliche Zeilen infinanceRequest.jsundteamApi.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/uiund vorhandene Klassen (msg,loading,link,fin-notice,fin-load-error); keine Icon-Importe (react-icons,@tabler), keinstyle={{…}}, 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,loadModuleaustests/unit/load.mjs.financeApi.js,teamApi.jsund alle Komponenten laden Appwrite und sind unter Node nicht ladbar; ihre Logik steht in reinensrc/lib-Modulen. Neue Tests liegen in neuen Dateien (Ausnahme:customerTabs.test.mjs). - Befehle mit Speichergrenzen (Server mit wenig Arbeitsspeicher).
WTist/home/webklar/apps/ticket.webklar.com/dev-auftraege,List/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 ciund Build laufen nur hinterflock $L/auftraege-schwer.lock, vorherfree -m(unter 1500 MB „available“: warten), und untersystemd-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 --emptyOutDirmitNODE_OPTIONS=--max-old-space-size=1200(nienpm run build, nie nachdist/im Worktree). - Bash-Timeout 600000 ms; dauert ein Lauf länger, mit
run_in_backgroundstarten. Niepkill -f.
- Einzeltest:
- Git: Worktree
/home/webklar/apps/ticket.webklar.com/dev-auftraege, Branchfeature/auftraege. Im Repobuild(Branchtest) entsteht nur der Worktree-Eintrag aus Task 1, sonst wird dort nichts angefasst. Commits einzeln mit Dateinamen, deutsche Nachricht, SchlusszeileCo-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>(zweites-m). Kein Push, nie--force. - Suche: In dieser Shell ist
grepeine Funktion, die Dateien still auslässt. Die Steps benutzenrg(13.0.0, ohne Lookarounds); notfallscommand 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:
- Schalter
AUFTRAEGE_ENABLEDaus (Vorgabe beim Livegang):auftraegeAktivistfalseoder 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“. - 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“.
- 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“.
- 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“.
- 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-p8bisdesign-p10) im WOMS-Repo, erreichbar über/home/webklar/apps/ticket.webklar.com/build. -
Produces: Worktree mit echtem
node_modules(W1 und W2 verlinken es perln -s ../dev-auftraege/node_modules node_modules), den Basis-StandB(Tag-Name) und die Ausgangs-TestzahlN0. Beides gehört in die Abschlussmeldung (Task 9). -
Step 1: Redesign-Tags und Basis bestimmen
Run:
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):
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: “ samt Ausgabe von git tag -l 'design-p*' 'strang-p*' und git log --oneline -3 design/kunden design/finanzen.
- Step 3: Worktree anlegen
Run:
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_moduleswie im Redesign (echtesnpm ci, kein Symlink)
Run:
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:
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:
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 }). Mitas: '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). Ohneasoder mit jedem anderen Wert bleibt alles wie bisher. Das nutztgetAuftragPdf(Task 3). -
Step 1: Failing Test schreiben
tests/unit/financeRequestBlob.test.mjs:
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'increateFinanceRequesteinbauen
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
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 })auscreateFinanceRequest(Task 2). -
Produces:
createAuftraegeEndpoints(request) → { getProjektkonten, getProjektkonto, previewInvoice, listAuftraege, getAuftrag, createAuftrag, patchAuftrag, deleteAuftrag, sendAuftrag, erinnereAuftrag, neueFassungAuftrag, storniereAuftrag, ordneAuftragProjektZu, getAuftragPdf }(14 Funktionen) undabgerechnetPfad(ticketId) → string(reine Datei ohne Importe).- Aus
src/lib/financeApi.jsexportiert (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 oderstorniert,ersetztoderwiderrufenist; 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:
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.jsimplementieren
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.jsundteamApi.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)“ undexport const { getProjektkonten, getProjektkonto, previewInvoice, listAuftraege, getAuftrag, createAuftrag, patchAuftrag, deleteAuftrag, sendAuftrag, erinnereAuftrag, neueFassungAuftrag, storniereAuftrag, ordneAuftragProjektZu, getAuftragPdf } = createAuftraegeEndpoints(request)(die vorhandene Konstanterequestwird übergeben). Der Kommentar übercreateInvoicenennt 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)“ mitexport const getAbgerechnet = (ticketId) => request(abgerechnetPfad(ticketId))und einem Kommentar zur Antwortform. -
Step 5: Tests und Syntaxprüfung
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
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
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)undresolveCustomerTab(tab, isAdmin)(bestehen). -
Produces:
customerTabs(true)[2]ist{ id: 'invoices', label: 'Aufträge und Rechnungen' }. Dieidbleibtinvoices(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
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 }>:kontenist die Antwort vongetProjektkonten,auftraegedie Liste odernull,auftraegeFehlerText oder''.ansichtsZustand({ data, error } = {}) → AnsichtsZustand(dataunderrorkommen aususeFinanceResource):{ 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:
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.jsimplementieren
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 zuerstgetProjektkonten(kundeId); ein Fehler dort wird nicht abgefangen. Ist die Antwort kein Objekt, gilt{}. Nur wennkonten.auftraegeAktiv === true, folgtlistAuftraege(kundeId): Erfolg gibtantwort.auftraege, wenn das ein Array ist, sonst[]; ein Fehler gibtauftraege: nullundauftraegeFehler: err.message, bei leerer MeldungAUFTRAEGE_FEHLER. Sonstauftraege: null,auftraegeFehler: ''. Rückgabe immer{ konten, auftraege, auftraegeFehler }. -
ansichtsZustand({ data, error } = {}):- Ohne
data:projekteStatusist'fehler'(mitfehler = error), wennerrornicht leer ist, sonst'laden';auftraegeStatusist dann'aus';konten[],ohneProjekteine Kopie von{ berechnetCents: 0, bezahltCents: 0, anzahl: 0 },stripeOktrue,kundenull,auftraege[]. - Mit
data:projekteStatus: 'bereit',fehler: '',nachladeFehler: error || ''. Gelesen wird ausdata.konten(nur wenn Objekt, sonst{}):auftraegeAktiv = (data.konten.auftraegeAktiv === true),kontennur wenn Array,ohneProjektnur wenn Objekt (sonst die Null-Kopie),stripeOk = (data.konten.stripeOk !== false),kundenur wenn Objekt (sonstnull). auftraegeStatus:'aus'ohne Schalter,'fehler'bei Schalter und nicht leeremdata.auftraegeFehler, sonst'bereit'.auftraegeFehlernur im Fall'fehler'.auftraegeistdata.auftraege, wenn Schalter an und Array, sonst[].
- Ohne
-
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
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)aussrc/lib/financeView.js(liefert für'create-send'„Rechnung wurde an gesendet.“ und für'create-draft'„Der Entwurf wurde gespeichert.“). -
Produces, aus
src/lib/auftraegeAnsicht.js:kundeMitKundenart(customer, kundeInfo) → customer': IstkundeInfo.kundenarteines von'unternehmen','privat', kommt{ ...customer, kundenart: <dieser Wert> }zurück (das Dokument selbst bleibt unverändert). In allen anderen Fällen (kundeInfofehlt, kein Objekt, Wert leer, unbekannt oder keine Zeichenkette) die unveränderte Referenzcustomer;nullbleibtnull.erstelltMeldung(rechnung, info) → { tone, text }: wieInvoiceTable.handleCreated: mitinfo.notice(Zeichenkette){ tone: info.tone || 'ok', text: info.notice }, sonst{ tone: 'ok', text: invoiceDoneMessage(info.senden ? 'create-send' : 'create-draft', rechnung) }.infodarf fehlen odernullsein.auftragMeldung(auftrag) → { tone, text }füronSaved(auftrag).
-
Step 1: Failing Tests ergänzen
In tests/unit/auftraegeAnsicht.test.mjs die loadModule-Zeile um kundeMitKundenart, erstelltMeldung, auftragMeldung erweitern und anhängen:
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':
a.status === 'entwurf'→{ tone: 'ok', text: 'Der Entwurf wurde gespeichert.' }a.mailStatus === 'fehler'→{ tone: 'warn', text: '<name> ist versendet, die Mail an den Kunden wurde aber nicht zugestellt. Bitte in der Liste erneut senden.' }a.nummer→{ tone: 'ok', text: '<name> wurde[ an <gesendetAn>] gesendet.' }(der Teil „an …“ nur bei nicht leeremgesendetAn)- 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
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),AddressWarningundInvoiceTable(../finance/…, Default-Exporte),useFinanceResourceunduseScrollIntoView(../../hooks/…, benannte Exporte),getProjektkontenundlistAuftraege(../../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
kontenundonShowOverviewanAuftragListeundAuftragDialogdurch undonShowOverview(optional) auch anRechnungDialog. -
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):projektIdinonRechnung(projektId)undonAuftrag(projektId)ist die Kennung des Projekts,nulldie 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):kundeist das Kundendokument odernull(Kundenwahl im Dialog);projektId:undefined= keine Vorauswahl,null= „Ohne Projekt“, Text = dieses Projekt;auftraegeAktivist nur der Startwert (danach giltgetProjektkonten, das der Dialog selbst lädt);onShowOverviewist optional („Zur Übersicht“ in der Adresswarnung);onCreated(rechnung, info):infounverändert weiterreichen wie im bisherigenInvoiceCreateModal({ senden }, bei festgeschriebener, nicht versendeter Rechnung{ senden: true, notice, tone: 'warn', finalizedFilter }), danach ruft der DialogonClose(). Der Dialog sitzt zusätzlich inInvoiceTable.jsx(dort ersetzt W1 das Modal),FinancePage.jsxbleibt unverändert. -
AuftragListe.jsx(W2):auftraegesindAuftragTeam[](V3);kontenliefern den Projektnamen der Zeile und die Auswahl für „Projekt zuordnen“ (Aktion für ABs ohne Projekt, ruftordneAuftragProjektZu(id, projektId)ausfinanceApi.js);onShowOverviewist der Weg „Zur Übersicht“ aus Meldungen. Die fertige Komponente rendert<Card title="Aufträge">selbst und ruft nach jeder AktiononChanged(); „Entwurf bearbeiten“ ruftonBearbeiten(auftrag), nach „Neue Fassung“ ruft sieonBearbeiten(neuerEntwurf). -
AuftragDialog.jsx(W2):kundekommt mit aktuellerkundenart(der Container gleicht sie überkundeMitKundenartab);projektIdist das vorausgewählte Projekt odernull(freier Titel);auftragder Entwurf zum Bearbeiten odernull;kontendie Projektkonten für die Projektwahl;onShowOverviewder Weg „Zur Übersicht“; nach Speichern oder Senden ruft der DialogonSaved(auftrag)(AuftragTeam, mitmailStatus) und danachonClose(). PDF-Vorschau überopenInNewTab(async () => URL.createObjectURL(await getAuftragPdf(id))). -
Step 2: Container
AuftraegeUndRechnungen.jsxschreiben
Aufbau und Regeln (ersetzt CustomerInvoices; deutscher Kopfkommentar zu Aufgabe und Reihenfolge):
- Außen
export default function AuftraegeUndRechnungen({ customer, onShowOverview }): IstcustomerInvoicesGate(customer).okfalsch, kommt wie bisherEmptyStatemitgate.title,gate.hintund (nur mitonShowOverview)<Button variant="primary" onClick={onShowOverview}>Daten erfassen</Button>; es werden keine Abrufe gestartet. Sonst rendert sie die innere KomponenteAuftraegeInhalt(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 }odernull),rechnungsDialog({ projektId }odernull),auftragsDialog({ projektId, auftrag }odernull),listeToken(Zahl, Start 0), dazumeldungRefmituseScrollIntoView(meldungRef, meldung). const kunde = kundeMitKundenart(customer, ansicht.kunde)(geht an beide Dialoge).- Öffnen eines Dialogs setzt zuerst
meldungaufnull. RechnungDialog.onCreated(rechnung, info): Dialogzustandnull,setMeldung(erstelltMeldung(rechnung, info)),setListeToken((n) => n + 1),reload().AuftragDialog.onSaved(auftrag): Dialogzustandnull,setMeldung(auftragMeldung(auftrag)),reload().onClosesetzt den jeweiligen Zustand aufnull(der Dialog ruftonCreatedund danachonClose; 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-erroroder.fin-noticeim CSS (Task 1 Step 5), entfallen diese beiden Zusatzklassen. Die Dialoge stehen als Geschwister hinter den Abschnitten, nie innerhalb vonInvoiceTableoder.fin-body(dessencontainer-typewürdeposition: fixedverbiegen).InvoiceTablebehält seinen eigenen Knopf „Rechnung schreiben“; der Container fasst ihn nicht an. Keinstyle, keine neue CSS-Datei, keine Icons. -
Step 3: Syntaxprüfung
Run:
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:
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
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
invoicesder Kundenseite zeigt den Container. Die Adresswarnung (AddressWarning variant="page") und die Sperre „Noch ein Lead“ usw. stecken jetzt im Container und bleiben sichtbar.AddressWarning.jsxselbst bleibt unverändert (Nutzer: der Container und der Rechnungsdialog);CustomerInvoices.jsxhat 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 durchimport 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')} />}(derkeysorgt 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:
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:
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
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:
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:
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:
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):
-
getAuftragPdfliefert einen Blob (Task 3, Test „getAuftragPdf …“);ordneAuftragProjektZusendetPOST …/auftraege/<id>/projektmit{ projektId }(Task 3, Test „Pfad, Methode, Kopf und Körper …“). -
Der Container hat die Props
{ customer, onShowOverview }, die Platzhalter die Props aus V8 samtkontenundonShowOverview. -
Der Container reicht
kontenundonShowOverviewanAuftragListeundAuftragDialogdurch,onShowOverviewauch anRechnungDialog(Task 7, Step 4). -
AddressWarning variant="page"steht im Container (Task 7, Zeile a) undCustomerInvoicesist 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):
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.
- Wer öffnet „Rechnung schreiben“ im Kundenreiter? Entschieden (siehe Übersicht, Nachträge):
RechnungDialogsitzt inInvoiceTable.jsx(dort ersetzt W1 das Modal),FinancePage.jsxbleibt unverändert. Umsetzung: Der Container öffnetRechnungDialognur für die Projektzeilen und nutztInvoiceTablemit seinen bisherigen Props (customer,onShowOverview,refreshToken,onChanged); der Dialog lädt Projektliste und Schalter selbst übergetProjektkonten(auftraegeAktivist 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“. projektIdimRechnungDialog. Entschieden (siehe Übersicht, Nachträge; V8):undefined= keine Vorauswahl (Kartenknopf inInvoiceTable),null= „Ohne Projekt“ (Zeile „Ohne Projekt“), Text = dieses Projekt. Umsetzung: Der Container reicht die Kennung ausonRechnung(projektId)unverändert durch (Task 7, Zeile g).onCreated(rechnung, info). Entschieden (siehe Übersicht, Nachträge; V8):infounverändert weiterreichen (senden,notice,tone,finalizedFilter). Umsetzung:erstelltMeldungwertetnoticeundtoneaus (Task 6).- Abschnittsüberschriften. Entschieden (siehe Übersicht, Nachträge; V8): Die Kindkomponenten rendern ihre
Cardsamt Titel selbst. Umsetzung: Kommentare der Platzhalter und Container (Zustände „laden“ und „fehler“ mit denselben Titeln, Task 7). - PDF öffnen (W2). Entschieden (siehe Übersicht, Nachträge; V8):
openInNewTab(async () => URL.createObjectURL(await getAuftragPdf(id))). Umsetzung:getAuftragPdfliefert den Blob (Task 2 und 3); das Öffnen gehört W2 (Hinweis im Kommentar vonAuftragDialog.jsx). - 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. - 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. - 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 inCustomerDetailPage.jsxund die Löschung vonCustomerInvoices.jsx; W1 die Projektliste, den Rechnungsdialog,InvoiceTable.jsx,WorksheetList.jsx,WorksheetTimesModal.jsxund die Löschung vonInvoiceCreateModal.jsx; W2 die Auftragsliste, den Auftragsdialog, das Feld Kundenart inCustomerDetailPage.jsxundSettingsCard.jsx. W2 startet nach W0, also gibt es inCustomerDetailPage.jsxkeinen Konflikt. - Basis und Push-Reihenfolge. Entschieden (siehe Übersicht, Nachträge):
feature/auftraege(WOMS) enthält alle Redesign-Commits bis zum Basis-TagB. Der Nutzer pusht erst, wenn das Redesign auftestliegt; vorher wird der Zweig auforigin/testaktualisiert (Z2). Die Übergabe nenntB; Task 1 Step 2 meldet, wennorigin/testweiter ist alsB. - 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.
- Kundenart, Schreibweg. Entschieden (siehe Übersicht, Nachträge; V5): Die AB-Dialoge (W2) schicken
kundenartmitPOSTundPATCH …/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 überkunde.kundenartaus der Antwort vongetProjektkontenab (kundeMitKundenart, Task 6 und 7). onShowOverviewimRechnungDialog. 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:
ordneAuftragProjektZu(id, projektId). V5 (Zeile L3) hat die RoutePOST …/auftraege/:id/projektmit{ projektId }; W2 braucht sie für „Projekt zuordnen“ (Spec: „später zuordenbar“). Umsetzung: Task 3 (Pfad inauftraegeEndpoints.js, Test, Export infinanceApi.js).kontenundonShowOverviewfürAuftragListeundAuftragDialog. 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).