23 KiB
Admin-Panel ins Ticketsystem – Etappe 3 (Gutschriften, Abo & Angebote, E-Mail-Speicher, Funktionen) Implementation Plan
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: Die restlichen Admin-Panel-Bereiche laufen im WOMS: Sammellisten Gutschriften, Abo-Angebote, E-Mail-Speicher unter Kunden; Kundenreiter „Abo & Angebote“, „Gutschriften“, „Funktionen“; Abo-Status für alle Mitarbeiter. Das Admin-Panel bleibt bis Etappe 4 nutzbar.
Architecture: Die vier bestehenden Admin-Router des Portals (takeoverAdminRouter, offersAdminRouter, storageTasksAdminRouter, funktionenAdminRouter) werden zusätzlich unter /api/team eingehängt (Logik unverändert, Sitzung aus dem JWT), ergänzt um Kundenfilter, Zähl-Routen ohne Stripe, Abo-Status und signierte Nachweis-Links. WOMS baut die Oberflächen in React nach; reine Logik (Texte, Rechnungen, Prüfungen) in testbaren src/lib/*-Modulen, portiert aus public/admin.js.
Tech Stack: Portal Express 4 (ESM, Node 20), node:test + test/support/* (Appwrite-Nachbau, Stripe-Attrappe, Mail-Senke). WOMS React 18, react-router 6, node:test + tests/unit/load.mjs, Playwright-Harness im Scratchpad.
Spec: docs/superpowers/specs/2026-10-01-admin-panel-ins-ticketsystem-design.md (Abschnitt 1 Sidebar/Kundenseite, Abschnitt 2 Gutschriften/Abo & Angebote/E-Mail-Speicher/Funktionen, Abschnitt 4 Tabelle /api/team, Etappe 3 in Abschnitt 5). Abo-Status-Route aus Etappe 1 (dort bewusst verschoben) gehört hierher.
Global Constraints
- Arbeitsorte: Portal
/home/webklar/apps/kundenbereich.webklar.com/dev-admin-umzug(Branchfeature/admin-umzug, Standa6ac451= origin/main), WOMS/home/webklar/apps/ticket.webklar.com/dev-admin-umzug(Branchfeature/admin-umzug, Stand813c6a7= origin/test). Vor Beginn jegit fetch+git merge --ff-only. - Nie
git add -A, nichts pushen; Livegang Portal zuerst, dann WOMS (Nutzer pusht). command grep -a. UI-Texte deutsch mit Umlauten. Funktionsgleich zum Admin-Panel: Texte, Bestätigungsfragen, Prüfungen und Abläufe wortgleich auspublic/admin.js(Zeilenangaben je Task); keine neuen Funktionen.- Server-Logik der vier Router bleibt unverändert; nur Einhängen, Filter, Zähl-Routen, Abo-Status, Signatur-Links kommen dazu.
- Reiterfolge Kundenseite:
Übersicht · Tickets · Rechnungen · Projekte · Chat · Abo & Angebote · Gutschriften · Funktionen · Protokoll(idsoverview, tickets, invoices, projects, chat, abo, gutschriften, funktionen, protokoll; Rechnungen nur Admins). - Sidebar-Unterpunkte unter Kunden:
Chat(Zahl ungelesen),Gutschriften(Zahl offen),Abo-Angebote,E-Mail-Speicher(Zahl offen) – Abfrage alle 60 s bei sichtbarem Tab. - Routen WOMS:
/customers/gutschriften(?antrag=<id>),/customers/angebote,/customers/email-speicher; Kundenseite?tab=gutschriften&antrag=<id>. - Signierte Nachweis-Links:
signFileLink({ kind: 'takeover', customerId: <requestId>, id: <fileId> }), 15 min. - Speicherbremse für Builds/Playwright (
systemd-run --scope … MemoryMax).
Review Focus
- Doppelklick oder zwei Mitarbeiter gleichzeitig auf „Genehmigen & gutschreiben“: Erwartung: genau eine Gutschrift in Stripe, die zweite Aktion bekommt die Server-Meldung „Über diesen Antrag wurde bereits entschieden.“ (Task 6: Knopf während der Anfrage gesperrt, 409 sichtbar).
- Kundenwechsel im Angebotsformular, während der Kontext noch lädt: Erwartung: Vorschau und „Angebot anlegen“ zeigen nie Daten des vorigen Kunden; Speichern erst, wenn der Kontext zum aktuellen Kunden da ist (Task 7, Playwright).
- Mitarbeiter ohne Admin-Label: Erwartung: Gutschriften, Angebote, E-Mail-Speicher, Funktionen und Abo-Status funktionieren vollständig (Task 1/3 Server-Tests mit
jwt-staff). - Nachweis länger als 15 min geöffnet: Erwartung: erneutes Öffnen der Antragsdetails liefert frische Links (Task 2: Links werden bei jedem
GET /:idneu signiert). - Handy (390 px): Listen und Detail ohne waagerechtes Scrollen bedienbar (Task 6–8, Playwright).
Portal
Task 1: Admin-Router im Team-Eingang, Kundenfilter, Zähl-Route E-Mail-Speicher
Files:
- Modify:
server/routes/team/index.js(nachrequireStaffJwt:router.use('/takeover-credits', takeoverAdminRouter),router.use('/offers', offersAdminRouter),router.use('/storage-tasks', storageTasksAdminRouter),router.use('/funktionen', funktionenAdminRouter)) - Modify:
server/routes/takeoverCredits.js:119-153(?customerId=filtertrequests;openCountunverändert über alle) - Modify:
server/routes/offers.js:23-39(?customerId=filtertoffers) - Modify:
server/routes/storageTasks.js(neuGET /open-countvor/:id-Routen:{ open }= offene Speicher-Aufgaben, ohnereconcileOpenSwitches) - Test:
test/teamAdminRoutes.test.js
Interfaces:
-
Produces (HTTP, gleiche Form wie unter
/api/portal-admin):/api/team/takeover-credits[?status=all][&customerId=],/calc,/open-count,/:id,/:id/decision,/:id/apply;/api/team/offers[?customerId=],/context/:customerId,POST /,/:id/withdraw,/:id/notify;/api/team/storage-tasks,/open-count,/:id/done;/api/team/funktionen/:customerId,POST /:customerId/news. -
Step 1: Failing tests (Aufbau wie
test/teamChat.test.js;seedFixtures, Mitarbeitere_staff2; Gutschrift-AntraegetakeoverCreditsmit Schema ausschemaKeys('takeoverCredits'); Speicher-Aufgaben alscontractEventstype: 'speicher', status: 'offen'wie intest/storageTasks.test.js)
test('alle vier Bereiche mit jwt-staff erreichbar (kein Admin-Label noetig)', async () => {
for (const p of ['/api/team/takeover-credits', '/api/team/offers', '/api/team/storage-tasks', '/api/team/funktionen/c1']) {
assert.equal((await rawGet(p, staff)).status, 200, p)
}
})
test('Kundenfilter: Gutschriften und Angebote nur des Kunden, openCount weiter ueber alle', async () => {
const credits = (await rawGet('/api/team/takeover-credits?status=all&customerId=c1', staff)).body
assert.ok(credits.requests.length > 0 && credits.requests.every((r) => r.customerId === 'c1'))
assert.equal(credits.openCount, /* offene Antraege aller Kunden */ 2)
const offers = (await rawGet('/api/team/offers?customerId=c2', staff)).body.offers
assert.ok(offers.every((o) => o.customerId === 'c2'))
})
test('E-Mail-Speicher open-count ohne Stripe-Abgleich', async () => {
const before = st.calls.length
assert.deepEqual((await rawGet('/api/team/storage-tasks/open-count', staff)).body, { open: 1 })
assert.equal(st.calls.length, before)
})
test('Mitarbeiter aus dem JWT: Rueckfrage traegt "Andrej <andrej@webklar.com>", Erledigt traegt die Mail, Funktionen-Notiz den Namen', async () => {
// POST /api/team/takeover-credits/<id>/decision { action: 'question', note: 'Bitte Rechnung schicken' } -> 200, Verlauf by 'Andrej <andrej@webklar.com>'
// POST /api/team/storage-tasks/<id>/done -> task.doneBy 'andrej@webklar.com'
// POST /api/team/funktionen/c1/news { projectId, action: 'activate', note: 'Pilot' } -> adminNote enthaelt 'Andrej: activate'
})
test('ohne JWT 401 fuer alle neuen Pfade', async () => {})
- Step 2: Run
node --test test/teamAdminRoutes.test.js→ FAIL (404 unter/api/team/...) - Step 3: Implementieren
- Step 4: Run
npm test→ PASS (bestehendestorageTasks.test.js,funktionenRoutes.test.jsbleiben grün) - Step 5: Commit —
git add server/routes/team/index.js server/routes/takeoverCredits.js server/routes/offers.js server/routes/storageTasks.js test/teamAdminRoutes.test.js· Nachricht „Team-Eingang: Gutschriften, Angebote, E-Mail-Speicher und Funktionen fuer alle Mitarbeiter, Kundenfilter, Zaehlroute“
Task 2: Signierte Links für Gutschrift-Nachweise
Files:
- Modify:
server/routes/takeoverCredits.js:176-182(Antwort vonGET /:id: jedesrequest.files[i]bekommturl=${config.publicBaseUrl}/api/team/takeover-credits/<id>/files/<fileId>?exp=…&sig=…) - Modify:
server/routes/team/files.js(GET /takeover-credits/:id/files/:fileIdmitsig→ wietakeoverAdminRouter.get('/:id/files/:fileId')übergetTakeoverRequest+readUpload; ungültig → 403{ error: 'Link abgelaufen. Bitte den Antrag neu öffnen.' }; ohnesig→next()) - Test:
test/teamAdminRoutes.test.js(ergänzen)
Interfaces:
-
Consumes:
signFileLink/verifyFileLink(server/services/signedLinks.js). -
Produces:
request.files[].urlinGET /api/team/takeover-credits/:id(und unter/api/portal-admin, dort unbenutzt). -
Step 1: Failing tests
test('Nachweis: signierter Link ohne JWT 200 mit Inhalt, fremder Antrag/abgelaufen 403, ohne sig 401', async (t) => {
// Antrag mit gespeicherter Datei (wie test/takeoverCredits.test.js bzw. services/takeoverCredits.js storeUpload) seeden
// GET /api/team/takeover-credits/<id> (staff) -> request.files[0].url passt auf /\/api\/team\/takeover-credits\/<id>\/files\/<fid>\?exp=\d+&sig=[0-9a-f]{64}$/
// fetch(url ohne Origin) -> 200, Content-Type application/pdf
})
- Step 2: Run → FAIL · Step 3: Implementieren · Step 4:
npm test→ PASS - Step 5: Commit — „Gutschriften: signierte Links fuer Nachweise im Ticketsystem“
Task 3: Abo-Status für alle Mitarbeiter
Files:
- Modify:
server/routes/team/customers.js(GET /:id/abo) - Test:
test/teamRoutes.test.js(ergänzen)
Interfaces:
-
Consumes:
getCurrentSubscription(customerId)(server/services/stripeBilling.js:178, nur Appwrite),PRODUCT_KEYS/ACTIVE_STATUSESwie inserver/routes/admin/customers.js:49-61. -
Produces:
GET /api/team/customers/:id/abo→{ productKey: 'wartung'|'hosting'|null, active: boolean, status: string|null, storageUnits: number, startedAt: string|null, serviceStart: string|null, currentPeriodEnd: string|null, cancelAt: string|null }(productKey/activeexakt wie die Admin-Route, damituseCustomerAboumsteigen kann). -
Step 1: Failing test
test('abo: Mitarbeiter ohne Admin-Label sieht den Abo-Status', async () => {
aw.seed('subscriptions', { $id: 'sub_t', customerId: 'c3', status: 'active', productKey: 'wartung', storageUnits: 1, startedAt: '2026-09-01T10:00:00.000Z', serviceStart: '2026-09-15T00:00:00.000Z', currentPeriodEnd: '2026-11-01T00:00:00.000Z' })
const res = await rawGet('/api/team/customers/c3/abo', auth)
assert.deepEqual(res.body, { productKey: 'wartung', active: true, status: 'active', storageUnits: 1, startedAt: '2026-09-01T10:00:00.000Z', serviceStart: '2026-09-15T00:00:00.000Z', currentPeriodEnd: '2026-11-01T00:00:00.000Z', cancelAt: null })
assert.deepEqual((await rawGet('/api/team/customers/c4/abo', auth)).body, { productKey: null, active: false, status: null, storageUnits: 0, startedAt: null, serviceStart: null, currentPeriodEnd: null, cancelAt: null })
})
- Step 2: Run → FAIL · Step 3: Implementieren · Step 4:
npm test→ PASS - Step 5: Commit — „Abo-Status eines Kunden fuer alle Mitarbeiter im Team-Eingang“
WOMS
Task 4: Team-API, Zähler und Format-Hilfen
Files:
- Modify:
src/lib/teamApi.js,src/lib/navLinks.js(+ Test),src/components/Navbar.jsx,src/hooks/useCustomerAbo.js(aufgetCustomerAboStatusumstellen, Rückgabe unverändert) - Create:
src/hooks/useSidebarCounts.js(ersetztuseChatUnread.js, Datei entfernen),src/lib/money.js,src/lib/creditFormat.js,src/lib/offerFormat.js,src/lib/storageFormat.js - Test:
tests/unit/navLinks.test.mjs(anpassen),tests/unit/creditFormat.test.mjs,tests/unit/offerFormat.test.mjs,tests/unit/storageFormat.test.mjs
Interfaces:
-
Produces in
teamApi.js:listCredits({ status, customerId }),getCreditOpenCount(),getCredit(id),calcCredit(body),decideCredit(id, body),applyCredit(id);listOffers({ customerId }),getOfferContext(customerId),createOffer(body),withdrawOffer(id),notifyOffer(id);listStorageTasks(),getStorageOpenCount(),markStorageDone(id);getFunktionen(customerId),setNewsFeature(customerId, { projectId, action, note });getCustomerAboStatus(customerId). -
Produces:
useSidebarCounts() -> { chat, credits, storage }(60 s, sichtbarer Tab, Fehler behalten Wert);buildNavLinks({ isAdmin, pulseLevel, counts })mit Kunden-Kindernchat(countcounts.chat),gutschriften(/customers/gutschriften,counts.credits),angebote(/customers/angebote, kein count),speicher(/customers/email-speicher, Label „E-Mail-Speicher“,counts.storage). -
Produces in
money.js:formatCents(cents) -> '12,34 €',parseEuroToCents(text) -> number|null. -
Produces in
creditFormat.js(auspublic/admin.js:535-906):CREDIT_STATUS(Pill-Text und Ton je Status, admin.jscreditStatusPill),historyLabel(type),calcLine(calc)(Rechnungszeitraum … → Vorschlag …),decisionError(action, note) -> string|null(Notiz ≥ 5 Zeichen bei question/reject, Texte wie admin.js:845-857),decisionConfirm(action, amountCents, customerName) -> string(admin.js:858-863),decisionMessage(action, result) -> string(admin.js:873-881),applyMessage(result),BILLING_MONTHS = [1, 3, 6, 12, 24, 36]. -
Produces in
offerFormat.js(auspublic/admin.js:984-1317):addMonthsYmd,offerDiscountEstimate,discountLine(offer)(admin.js:1034-1057),offerPreviewText,evaluateOfferForm(context, form) -> { errors: string[], running: boolean, saveLabel: string, previewLines: string[] }(Regeln admin.jsrenderOfferPreview1155-1205),offerConfirm(context, form, preview)(admin.js:1229),createdMessage(result). -
Produces in
storageFormat.js(auspublic/admin.js:908-982):storageLabel(units),summaryText({ allocatedGb, packageGb, warnGb, packageName }),taskText(task),orderedText(task),failedLine(event). -
Step 1: Failing tests (Erwartungswerte wortgleich aus admin.js; je Modul mindestens: alle Status-/Verlaufs-Texte, eine vollständige Rechenzeile, beide Validierungsfehler, alle drei Bestätigungsfragen, Erfolgsmeldung mit gebucht/nicht gebucht und Mail gesendet/fehlgeschlagen; Angebots-Vorschau mit/ohne laufendem Abo, offenes Angebot blockiert, Preis ≥ regulär blockiert, Rabattende geschätzt und bekannt; Speicher-Texte einrichten/periodenende/teilweise/gescheitert;
formatCents(123456) === '1.234,56 €',parseEuroToCents('12,5') === 1250,parseEuroToCents('') === null)
test('buildNavLinks: vier Unterpunkte unter Kunden mit Zahlen', () => {
const kunden = buildNavLinks({ isAdmin: false, pulseLevel: 'ok', counts: { chat: 3, credits: 1, storage: 2 } }).find((l) => l.key === 'customers')
assert.deepEqual(kunden.children.map((c) => [c.key, c.label, c.href, c.count]), [
['chat', 'Chat', '/customers/chat', 3],
['gutschriften', 'Gutschriften', '/customers/gutschriften', 1],
['angebote', 'Abo-Angebote', '/customers/angebote', undefined],
['speicher', 'E-Mail-Speicher', '/customers/email-speicher', 2],
])
})
- Step 2: Run
node --test tests/unit/*.test.mjs→ FAIL - Step 3: Implementieren (Icons in
Navbar.jsx: GutschriftenIconReceiptRefund, AngeboteIconDiscount, E-Mail-SpeicherIconMailCog– vorher innode_modules/@tabler/icons-react/dist/esm/icons/prüfen, sonst nächstliegendes vorhandenes Icon) - Step 4: Run
npm test→ PASS - Step 5: Commit — „Team-API, Sidebar-Zaehler und Format-Hilfen fuer Gutschriften, Angebote und E-Mail-Speicher“
Task 5: Abo-Status, Funktionen-Reiter und E-Mail-Speicher
Files:
- Create:
src/components/customer/AboStatusCard.jsx,src/components/customer/CustomerFunktionen.jsx,src/pages/StorageTasksPage.jsx - Modify:
src/App.jsx(Route/customers/email-speicher),src/lib/customerTabs.js(+ Test:abo,gutschriften,funktionenan ihren Plätzen;aborendert vorerst nurAboStatusCard, Angebote folgen in Task 7),src/pages/CustomerDetailPage.jsx
Interfaces:
-
Consumes: Task 4.
-
Produces:
AboStatusCard({ customerId }): Zeilen „Abo“ (Wartungs-Abo/Hosting-Abo/kein Abo, Status-Pille), „Leistungsbeginn“, „Nächste Abbuchung“ (nur aktiv), „Gekündigt zum“ (nurcancelAt), „Zusatzspeicher“ (storageLabel). -
Produces:
CustomerFunktionen({ customerId })wie admin.js:188-296 (Tabelle Projekt | News | Ticket | Aktionen, Pillen Aus/Bestellt/Einrichtung/Aktiv/Abgeschaltet, Notiz gekürzt auf 60 Zeichen mit Titel, „Ticket fehlt“, Knöpfe „Aktivieren (ohne Zahlung)“/„Abschalten“ mit Inline-Bestätigung samt Notizfeld (max. 120) und „Ja“/„Abbrechen“, Status „Gespeichert.“). -
Produces:
StorageTasksPagewie admin.js:908-982 (Zusammenfassung mit Warnfarbe abwarnGb, Tabelle Fällig | Kunde | Aufgabe in konsoleH | Bestellt | „Erledigt“, Rückfrage „Stripe-Menge von Hand korrigiert?“ nur beipartial, Liste „Gescheiterte Änderungen (letzte 30 Tage)“, Erklärtext auspublic/admin.html:299); Kundenname verlinkt auf/customers/<id>. -
Step 1:
tests/unit/customerTabs.test.mjsauf die volle Reiterfolge anpassen → Run → FAIL -
Step 2: Playwright-Checks schreiben (Harness wie Etappe 2): E-Mail-Speicher-Zusammenfassung mit Warnfarbe ab Schwelle, Aufgabentexte einrichten/periodenende, „Erledigt“ ruft
POST …/done(beipartialerst nach Rückfrage), gescheiterte Liste; Funktionen-Reiter: „Aktivieren (ohne Zahlung)“ → Inline-Bestätigung mit Notiz → Body{ projectId, action: 'activate', note }→ Zeile aktualisiert, „Gespeichert.“; Abo-Status-Karte für aktives Abo und „kein Abo“ → Run → FAIL -
Step 3: Implementieren; Run Harness → ok;
npm test→ PASS -
Step 4: Commit — „Kundenseite: Abo-Status und Funktionen; Seite E-Mail-Speicher“
Task 6: Gutschriften (Sammelliste, Detail, Kunden-Reiter)
Files:
- Create:
src/components/credits/CreditList.jsx,src/components/credits/CreditDetail.jsx,src/pages/CreditsPage.jsx - Modify:
src/App.jsx(Route/customers/gutschriften),src/pages/CustomerDetailPage.jsx(ReitergutschriftenmitcustomerId-Filter,&antrag=öffnet Detail)
Interfaces:
-
Consumes: Task 4 (
listCredits,getCredit,calcCredit,decideCredit,applyCredit,creditFormat),KundenansichtButton(statt „Als Kunde ansehen“). -
Produces:
CreditList({ customerId?, selectedId, onSelect })(Filter „Offen“/„Alle“, Spalten und Leertexte wie admin.js; Knopf „Prüfen“/„Ansehen“);CreditDetail({ id, onClose, onChanged })mit allen Abschnitten aus admin.js:684-796 (Nachweise als Links auffile.url, neuer Tab), Formular mit 4 Prüfhaken, Rechner (calcCredit300 ms entprellt, Gutschrift-Betrag folgt dem Vorschlag bis zur ersten eigenen Eingabe), Notiz (max. 4000), Knöpfe „Genehmigen & gutschreiben“ (erst bei 4 Haken und Betrag > 0) / „Rückfrage senden“ / „Ablehnen“ mitdecisionErrorundwindow.confirm(decisionConfirm(…)); während der Anfrage alle drei Knöpfe gesperrt; Ergebnis überdecisionMessage; entschiedene Anträge zeigen die Zusammenfassung und ggf. „Jetzt in Stripe gutschreiben“. -
Produces:
CreditsPageunter/customers/gutschriften: Liste links/oben, Detail daneben/darunter;?antrag=<id>öffnet direkt, Schließen entfernt den Parameter. -
Step 1: Playwright-Checks schreiben (Harness wie Etappe 2 im Scratchpad, Stripe-/Mail-Ergebnisse per gemockter Team-API): Liste „Offen“ zeigt Pillen „Zu prüfen“/„Rückfrage beim Kunden“; Detail per
?antrag=; Genehmigen gesperrt bis 4 Haken; Rechner-Zeile nach Eingabe; Rückfrage ohne Notiz → Fehlertext ohne Anfrage; Genehmigen → confirm-Text exakt, Body{ action: 'approve', amountCents, checks, … }, Meldung „Genehmigt. Das Guthaben ist in Stripe gebucht. Die E-Mail an den Kunden ist raus.“; Doppelklick → genau eine POST-Anfrage; 409 → Servertext sichtbar, Knöpfe wieder frei; Nachweis-Link zeigt auffile.url; Kunden-Reiter zeigt nur Anträge dieses Kunden; 390 px ohne waagerechtes Scrollen. -
Step 2: Run Harness → FAIL (Seiten fehlen)
-
Step 3: Implementieren
-
Step 4: Harness → alle ok;
npm test→ PASS -
Step 5: Commit — „Gutschriften im Ticketsystem: Sammelliste, Pruefung und Entscheidung, Kunden-Reiter“
Task 7: Abo & Angebote (Sammelliste, Formular, Kunden-Reiter)
Files:
- Create:
src/components/offers/OfferList.jsx,src/components/offers/OfferForm.jsx,src/pages/OffersPage.jsx - Modify:
src/App.jsx(Route/customers/angebote),src/pages/CustomerDetailPage.jsx(Reiterabo:AboStatusCard, darunterOfferList customerIdund „Neues Angebot“ mit fest gewähltem Kunden)
Interfaces:
-
Consumes: Task 4 (
listOffers,getOfferContext,createOffer,withdrawOffer,notifyOffer,offerFormat,customerPickLabel/findCustomerByLabelauschatFormat.js),useCustomers. -
Produces:
OfferList({ customerId?, reloadKey })(Filter „Offen & aktiv“/„Alle“, Spalten, Statuszelle mit Pillen, Stripe-Links, Mailstatus, Knöpfe „E-Mail senden“/„Erneut senden“ und „Zurückziehen“ mit Bestätigungen/Meldungen wie admin.js);OfferForm({ fixedCustomerId?, onCreated, onCancel }): Felder und Vorgaben wie admin.js:1256-1311; Kontext genau einmal je aufgelöster Kunden-ID laden, veraltete Antworten verwerfen, beim Wechsel sofort leeren; Vorschau/Speichern überevaluateOfferForm, Speichern erst mit Kontext des aktuellen Kunden;window.confirm(offerConfirm(…)); nach dem Anlegen MeldungcreatedMessage. -
Produces:
OffersPageunter/customers/angebotemit Knopf „Neues Angebot“. -
Step 1: Playwright-Checks schreiben: Liste „Offen & aktiv“ filtert, Rabattzeile; „Neues Angebot“: Kunde per eindeutiger Beschriftung, Kontext genau einmal geladen; Kundenwechsel mit verzögertem Kontext von A → Vorschau zeigt nie A, „Angebot anlegen“ gesperrt bis Kontext B da; offenes Angebot blockiert mit rotem Hinweis; laufendes Abo → Knopftext „Angebot anlegen und aufs laufende Abo anwenden“; confirm-Text exakt; Body
{ customerId, productKey, monthlyPriceCents, months, validUntil, note, internalNote, notifyCustomer }; Zurückziehen/Erneut senden mit Bestätigung; Kunden-Reiterabozeigt Abo-Status und nur eigene Angebote; 390 px ohne waagerechtes Scrollen. -
Step 2: Run → FAIL · Step 3: Implementieren · Step 4: Harness → ok;
npm test→ PASS -
Step 5: Commit — „Abo-Angebote im Ticketsystem: Sammelliste, neues Angebot, Kunden-Reiter Abo & Angebote“
Abschluss
Task 8: Gesamtprüfung und Übergabe
- Portal
npm testgrün; neue Tests unter Node 20 grün. WOMSnpm testgrün; Vite-Build erfolgreich; alle Harness-Checks (Etappe 2 und 3) grün. - Abschlussreview beider Branches (frischer Reviewer, sonnet).
- Übergabe mit Livegang: 1. Portal
git -C /home/webklar/apps/kundenbereich.webklar.com/dev-admin-umzug push origin feature/admin-umzug:main; 2. Live-Check (ich): neue Team-Pfade ohne JWT 401, Admin-Panel unverändert; 3. WOMSgit -C /home/webklar/apps/ticket.webklar.com/dev-admin-umzug push origin feature/admin-umzug:test; 4. Live-Check: Bundle enthält „Genehmigen & gutschreiben“, „Neues Angebot“, „Gescheiterte Änderungen“; 5. Nutzer-Test mit einem echten offenen Antrag/Angebot erst nach Rücksprache (echte Stripe-Buchungen!).