Commit Graph
153 Commits
Author SHA1 Message Date
duffyduckandClaude Opus 4.8 0d42bd89e8 Bankkarte-/Ausweis-Details in Vertrag + neues Feld Kartennummer
Schema: BankCard.cardNumber (String?, optional) + Migration
20260813100000_bank_card_number (ADD COLUMN IF NOT EXISTS), auf Dev
angewandt + prisma generate; Prod via migrate deploy. Eingabefeld
"Kartennummer" im Bankkarten-Modal (Kundenakte).

Vertragsansicht (ContractDetail) und Vertrag bearbeiten (ContractForm)
zeigen jetzt bei Bankkarte zusaetzlich BIC/Bank/Kartennummer/Ablaufdatum
und bei Ausweis Behoerde/Ausstellung/Ablaufdatum sowie Geburtsort +
Geburtsdatum des Kunden - jeweils mit Copy-Button und nur wenn gesetzt.
Im Form je Select in eigenem div gewrappt (Grid-Alignment).

Verifiziert: tsc + vite build gruen, cardNumber Round-Trip (update->read),
Contract-Include liefert alle Felder inkl. customer.birthDate/birthPlace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 15:00:29 +02:00
duffyduckandClaude Opus 4.8 06d51cf680 PDF-Viewer-Modal fuer Bankkarte-/Ausweis-Dokument
Neue Komponente PdfViewerModal (Modal + iframe auf viewUrl(documentPath),
inkl. Neuer-Tab/Download-Links). Buttons:
- Vertragsansicht (ContractDetail): im Card-Header von Bankkarte/Ausweis
- Vertrag bearbeiten (ContractForm): neben dem Label Bankkarte/Ausweis

Button erscheint NUR wenn ein documentPath hinterlegt/gewaehlt ist. Nutzt den
bestehenden /api/files/download-Endpoint (Per-File-Ownership-Check) - kein
neuer Zugriffspfad. iframe (nicht embed/object wegen object-src 'none');
same-origin via CSP default-src 'self' erlaubt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 13:15:57 +02:00
duffyduckandClaude Opus 4.8 2d12221c41 Vertragsliste (Staff-Tabelle): Karteninhaber/Netz/Kuendigung nachgezogen
Der vorige Commit ergaenzte nur die Baum-Render-Pfade (Kundenakte + Portal).
Die Staff-Ansicht unter /contracts rendert aber eine eigene Tabelle aus der
flachen getAll-Liste - dort fehlten die neuen Felder noch. Jetzt zeigt auch
die Staff-Tabelle Karteninhaber + Netz hinter der Rufnummer und die
Kuendigungsbestaetigung in roter Schrift. Backend liefert die Felder via
getAllContracts bereits mit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 12:26:58 +02:00
duffyduckandClaude Opus 4.8 c5cf2c7416 Vertragslisten: Karteninhaber + Mobilfunknetz + Kuendigung (rot)
Beide Vertragslisten (Kundenakte-Baum + Hauptmenue /contracts) zeigen bei
Mobilfunkvertraegen mit Rufnummer jetzt zusaetzlich Karteninhaber
(SimCard.cardUser der angezeigten SIM, nur wenn gesetzt) und Netz
(mobileNetwork -> Telekom/Vodafone/Telefonica (o2)) inline. Vertraege mit
Kuendigungsbestaetigung (cancellationConfirmationDate) bekommen eine eigene
Zeile in roter, fetter Schrift.

Backend: getContractTreeForCustomer + getAllContracts selektieren nun
mobileNetwork + cardUser (Tree zusaetzlich cancellationConfirmationDate).
Shared-Helper getContractTypeInfo um cardUser/network + mobileNetworkLabel
erweitert. Beide Listen rendern ueber den Tree-Endpoint.

Verifiziert: tsc + vite build gruen, Tree-Service liefert die Felder,
Helper-Mapping getestet (Vodafone/Telekom/o2, leerer Karteninhaber ausgelassen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 12:18:24 +02:00
duffyduckandClaude Opus 4.8 d0619141bb Fix: Modal-Formulare nach Abbrechen nicht mehr mit alten Daten vorbefuellt
Die Modals in der Kundenakte (Bankkarte, Adresse, Ausweis, Zaehler,
Zaehlerstand) bleiben dauerhaft gemountet und wurden nur ueber isOpen
umgeschaltet. Der Reset-Effekt haengte an <entity>?.id - bei Neuanlage
immer undefined, also kein Reset beim erneuten Oeffnen: nach "Abbrechen"
standen die vorher getippten Daten noch drin.

Jetzt Reset beim Oeffnen (Guard if(!isOpen), Deps [isOpen, <entity>?.id]),
weiterhin kein Reset bei jedem Tastendruck. Ausserdem den fehlerhaften
useState(()=>{})-Init-Missbrauch im Bankkarten-Modal entfernt.
StressfreiEmail-/AdditionalForwards-Modal waren bereits korrekt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 14:31:03 +02:00
duffyduckandClaude Opus 4.8 91fbe12299 BLZ-Bankdaten: Laufzeit-Auto-Update ins Volume + Einstellungen-Seite
Statt npm-Rebuild-Wartung aktualisiert sich der Bankleitzahlen-Datensatz
jetzt zur Laufzeit. Ein Scheduler (taeglich 03:30 + Catch-up 90s) prueft
gemaess konfigurierbarem Intervall und laedt current.json/next.json von
npm/jsDelivr (Paket bankdata-germany) in das neue Bind-Mount-Volume
BANKDATA_DIR (./data/bankdata -> /app/bankdata).

Lookup bevorzugt den Volume-Datensatz vor den ins Image gebackenen Daten
(Fallback). Es wird kein Fremdcode ausgefuehrt - nur JSON gelesen und die
current+next-Delta-Logik nachgebaut. Validierung (>=1000 Eintraege, Format)
+ atomarer Write (tmp+rename) schuetzen den guten Stand vor Muell.

Datenschutz: Der Updater sendet keine Kundendaten, laedt nur eine
oeffentliche Datendatei; abschaltbar; bei Fehler/ohne Egress greift Builtin.

Neue Einstellungen-Seite /settings/bank-data zeigt Datenstand, Update-
Verfuegbarkeit und bietet "Jetzt aktualisieren" + Auto-Update-Schalter +
Intervall. Endpoints GET /api/settings/blz, POST /api/settings/blz/update-now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 13:15:07 +02:00
duffyduckandClaude Opus 4.8 73bd72bd9f Bankkarte: IBAN pruefen + BIC/Bank offline aus Bundesbank-BLZ ausfuellen
Neuer Button "BIC & Bank aus IBAN abrufen" im Bankkarten-Modal fuellt BIC +
Banknamen automatisch und validiert dabei die IBAN-Pruefziffer (mod-97).
Leeres IBAN-Feld -> OK-Messagebox statt Anfrage.

Datenschutzfreundlich/offline: kein Dritt-Dienst. Nachschlag im eigenen
Backend ueber die Bundesbank-Bankleitzahlendatei (bankdata-germany) +
ibantools fuer die Pruefziffer. Die IBAN verlaesst nie den Server; zurueck
kommen nur oeffentliche Bankverzeichnis-Daten.

Endpoint: POST /api/bank-cards/iban-lookup (nur eingeloggt).
Wartung: bankdata-germany/ibantools ~quartalsweise per npm update ziehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 12:32:53 +02:00
duffyduckandClaude Opus 4.8 aff3f7e84a Update-Banner: klebt oben fest (sticky), Sticky-Header rasten darunter ein
Der Hinweis "Neue Version verfuegbar" scrollte bisher weg. Jetzt sticky an
der Viewport-Oberkante; das Banner meldet seine gemessene Hoehe als CSS-Var
--app-banner-h, an der sich die Sticky-Header von ContractDetail/ContractForm
ausrichten (top-[var(--app-banner-h,0px)]), damit sie nicht dahinter
verschwinden.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 12:20:31 +02:00
duffyduckandClaude Opus 4.8 48e65be91c Hauptmenue: Gutschriften/Lieferscheine-Gesamtuebersicht (portal-scoped)
Neuer Menuepunkt 'Gutschriften' -> Seite /credit-notes mit Tabelle
aller Belege (Beleg-Nr, Art, Kunde, Vertrag, Betrag, Datum, PDF),
Suche + Pagination.

Neuer Endpoint GET /credit-notes (NICHT staff-only wie die uebrigen
Credit-Note-Endpoints): Staff sieht alle Belege aller Kunden, Portal-
Kunden nur eigene + vertretene (Vollmacht via hasAuthorization).
customerIds kommt aus dem JWT, nicht aus Query/Body -> nicht
manipulierbar. Fuer Portal wird receiptPath aus der Response entfernt
(Belege bleiben staff-only). Route requirePermission contracts:read.

Verifiziert: Staff -> alle Belege; Portal-scoped -> nur eigene, korrekt
zugeordnet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 10:23:53 +02:00
duffyduckandClaude Opus 4.8 5e606df49e Gutschrift: separater Lieferschein-Nummernkreis (GoBD, luecken-frei)
Pentester R142: Uebergang Geld->betragsloser Sachwert setzte die schon
vergebene Gutschriftsnummer auf null -> Luecke in der GS-Serie.

Loesung: betragsloser Sachwert = Lieferschein mit eigener
Lieferscheinnummer aus separatem Nummernkreis.

- Schema: CreditNote.deliveryNoteNumber (nullbar, unique) + neues Model
  DeliveryNoteNumberRange (Default-Praefix 'LS-') + Migration.
- deliveryNoteNumberRange.service (mirror, eigener Zaehler, FOR UPDATE).
- Nummern lazy pro Serie, NIE freigeben: Uebergaenge behalten die
  jeweils vergebene Nummer der anderen Serie reserviert -> kein
  Doppelverbrauch, keine Luecke. effectiveNumber() liefert je nach Typ
  die passende (LS/GS) fuer Anzeige/PDF/Audit.
- Endpunkte GET/PUT /credit-notes/delivery-note-number-range; Settings-
  Seite verwaltet jetzt beide Nummernkreise. PDF-Titel 'Sachwert-
  Uebergabe', Dateiname lieferschein-...
- Frontend: Typ + displayNumber in Liste/Modal.

Verifiziert: Sachwert 0 -> LS-Nr, GS-Zaehler unberuehrt; Geld -> GS-Nr;
Uebergaenge behalten beide Nummern (kein Neuverbrauch, keine Luecke).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 08:47:49 +02:00
duffyduckandClaude Opus 4.8 c29ffd7bea Vertragsformular: Link-Icon am Zaehler-Label zur Kundenakte (Strom/Gas)
Wie bei Bankkarte/Ausweis/Adresse: das 'Zaehler'-Label im Energie-
Vertragsformular bekommt ein LabelWithLink zum Zaehler-Tab der
Kundenakte (/customers/:id?tab=meters), oeffnet im neuen Tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 22:51:38 +02:00
duffyduckandClaude Opus 4.8 5f06ed9830 Fix Folgevertrag aus deaktiviertem Vertrag + Kundendaten-Modal erweitern
1) Folgevertrag-Bug: Backend legt den Folgevertrag korrekt mit
   previousContractId an. Der Verlust passierte im Frontend - das
   Vorgaenger-Dropdown holt Vertraege ueber getAll, das DEACTIVATED
   standardmaessig ausblendet. Beim Bearbeiten des Folgevertrags war der
   deaktivierte Vorgaenger nicht als Option da -> Verknuepfung ging beim
   Speichern verloren.
   Fix: getAllContracts + Controller + contractApi.getAll um
   includeDeactivated erweitert; Vorgaenger-Dropdown nutzt es und markiert
   deaktivierte Vertraege mit '· deaktiviert'. Verifiziert (Flag inkludiert
   deaktivierte; Folgevertrag setzt previousContractId).

2) Kundendaten-Modal: zeigt jetzt zusaetzlich Lieferadresse + (falls
   abweichend) Rechnungsadresse des geoeffneten Vertrags, die Stressfrei-
   Adresse des Vertrags einzeln und darunter alle weiteren Stressfrei-
   Adressen des Kunden. CustomerInfoModal nimmt optionale Vertragskontext-
   Props; ContractDetail + ContractForm uebergeben sie.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 22:49:41 +02:00
duffyduckandClaude Opus 4.8 8169c74d8e Vertrag: Auto-CANCELLED bei Kuendigungsbestaetigung + Cockpit-Filter
1) Auto-Status: Wird eine Kuendigungsbestaetigung hinzugefuegt (Datum
   ueber Formular und/oder Dokument-Upload), wird der Vertrag von ACTIVE
   auf CANCELLED gesetzt und das Vertragsende = Kuendigungsdatum.
   Zentrale Funktion maybeCancelOnCancellationConfirmation (idempotent,
   nur aus ACTIVE). Upload-Route ersetzt die alte Inline-Logik (setzt
   jetzt auch endDate); Update-Controller triggert nur bei neu/geaendertem
   Bestaetigungsdatum (manuelle Status-Korrekturen bleiben unangetastet).

2) Cockpit-Filter 'Kuendigungsbestaetigung': neue Liste
   cancellationConfirmations (Vertraege mit Bestaetigung in Status
   ACTIVE/DRAFT/CANCELLED) + Filter-Option im Cockpit-Dropdown. Eigene
   Liste, weil bereits CANCELLED-Vertraege mangels Issue sonst nicht in
   der Cockpit-Liste auftauchen.

Beides lokal verifiziert (Helper: ACTIVE->CANCELLED + endDate; Cockpit:
Vertrag erscheint in der Liste mit korrektem Status).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 15:13:05 +02:00
duffyduckandClaude Opus 4.8 2c705272ea Gutschriften Phase 3a: Firmenstammdaten (Absender) fuer PDF/ZUGFeRD
Neues CompanyProfile-Modell (Einzel-Zeile) + Migration (IF NOT EXISTS):
Firmenname, Anschrift, USt-IdNr, Steuernummer, Handelsregister,
Geschaeftsfuehrer, Kontakt, IBAN/BIC/Bank. Fliesst spaeter in
Gutschrift-PDF + ZUGFeRD-Verkaeuferdaten ein.

Backend: Service (getOrCreate/update mit Feld-Whitelist), Controller,
Routes GET/PUT /api/company-profile (settings:read/update), auditiert.

Frontend: Settings-Seite 'Firmenstammdaten (Absender)'
(/settings/company-profile) mit Firma/Anschrift, Steuer/Register,
Kontakt/Bank. Typ + companyProfileApi + Menue-Eintrag.

Phase 3b (PDF + ZUGFeRD-Erzeugung) folgt darauf auf.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-06 13:19:36 +02:00
duffyduckandClaude Opus 4.8 9e38cb32f0 Gutschriften Phase 2b: Vertrags-UI + Beleg-Upload + Nummernkreis-Seite
Frontend:
- CreditNotesSection im Vertragsdetail (eigene Section wie Rechnungen):
  Liste + Anlegen/Bearbeiten (Modal). Geld/Sachwert mit passenden
  Feldern, USt-Block mit Live-Vorschau Netto/USt/Brutto, Datum, Ort,
  Unterschrift, 'Ware erhalten'. Loeschen mit Bestaetigung.
- Ueberweisungsbeleg-Upload/-Anzeige/-Entfernen (nur Geld-Gutschriften).
- Nummernkreis-Verwaltung als Settings-Seite mit Live-Vorschau der
  naechsten Nummer.
- Frontend-Typen (CreditNote etc.) + creditNoteApi.

Backend:
- Ueberweisungsbeleg-Upload-Route /upload/credit-notes/:id/receipt
  (+ DELETE), Portal geblockt, Ownership ueber Vertrag.
- fileDownload: Ownership-Aufloesung fuer credit-note-receipts +
  credit-notes (Vertrag) ergaenzt.

Als Section statt Tab-Balken, weil die Vertragsansicht kartenbasiert
ist (kein bestehender Tab-Balken); spaeter umbaubar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-06 13:15:02 +02:00
duffyduckandClaude Opus 4.8 3580c51cb1 Gutschriften Phase 2a: Kleinunternehmer-Flag am Kunden + USt-Default
Kunden-Feld vatExempt (Kleinunternehmer/USt-befreit §19) + Migration
(ADD COLUMN IF NOT EXISTS). Checkbox im Kundenformular nur fuer
Firmenkunden.

Gutschrift-Vorbelegung vatRelevant wird aus dem Kunden abgeleitet:
Firmenkunde ohne USt-Befreiung -> USt-relevant an (Netto), sonst aus
(wie Privat). Jede Gutschrift speichert ihren eigenen Snapshot, ein
spaeterer Statuswechsel des Kunden aendert bestehende Gutschriften
nicht.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-06 10:10:13 +02:00
duffyduckandClaude Opus 4.8 3bbe262039 Vertrag kopieren: Legacy-phoneNumber/simCardNumber auch leeren (R135/136)
Pentester-Fund: die Legacy-Scalar-Felder mobileDetails.phoneNumber und
simCardNumber (kein sichtbares Formularfeld mehr) wurden beim Kopieren
noch uebernommen - anders als die SIM-Karten-Liste und anders als
'Rufnummern werden nicht uebernommen'. Zwei getrennte Vertraege
desselben Kunden haetten so unbemerkt dieselbe Rufnummer getragen.

Datenintegritaet, kein Security-Loch. Beide Felder jetzt im Kopier-
Leer-Block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 13:26:42 +02:00
duffyduckandClaude Opus 4.8 2aee1f1114 Vertrag kopieren: Portal-Zugangsdaten mitkopieren
Auf Wunsch: Portal-Benutzername + Stressfrei-Verknuepfung/'nicht
benoetigt'-Flag werden im Kopier-Modus behalten (bei gleichem Anbieter
oft identisch). Nur das Passwort bleibt leer, weil es verschluesselt
gespeichert und - wie beim Bearbeiten - nicht im Klartext ins Feld
geladen wird. Banner-Hinweis ergaenzt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 12:58:41 +02:00
duffyduckandClaude Opus 4.8 a2ed0e94f1 Vertrag kopieren: neuen eigenstaendigen Vertrag aus Vorlage anlegen
Kopieren-Button in der Vertragsansicht oeffnet das Neu-Formular unter
/contracts/new?copyFrom=<id> mit allen Daten des Quellvertrags
vorbefuellt. Beim Speichern entsteht ein frischer, unabhaengiger
Vertrag (kein previousContractId-Link, kein VVL).

Use-Case: neuer Mobilfunk-/o.ae. Vertrag ist meist fast identisch;
nur Preis, Laufzeit, Kunden-/Vertragsnummer, Anbieter/Tarif aendern
sich - der Rest bleibt gleich.

Im Kopier-Modus geleert (Integritaet/Eindeutigkeit): Status->DRAFT,
Vorgaenger-Link, alle Datumsfelder, Kunden-/Vertragsnummer beim
Anbieter + Plattform-Nummern, Portal-Zugang. SIM-Karten & Rufnummern
werden nicht uebernommen. Rest (Anbieter/Tarif/Preise/Detailfelder/
Notizen) bleibt als Vorlage.

Rein Frontend (ContractForm copy-Mode + Button in ContractDetail),
kein neuer Endpoint - nutzt bestehendes Create.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 12:44:49 +02:00
duffyduckandClaude Opus 4.8 5bbb7dfffe PdfDragButton komplett entfernt (Plattformgrenze)
Test-Ergebnis: Browser kann einer fremden Desktop-App keine echte
lokale Datei zum Anhaengen uebergeben. URL-Drag -> Thunderbird nur Link
(0 Bytes, Fehler beim Senden); Datei-Drop in den Text -> nur Dateiname
als Text; auf die Anhang-Leiste (Thunderbird/Linux) -> ebenfalls kein
echter Anhang; Webmail -> gar nicht moeglich.

Entscheidung: Feature raus. Download- und Anzeigen-Button decken den
Bedarf zuverlaessig ab. PdfDragButton geloescht, fileUrl()-Token-Param
zurueckgebaut. Pentest R131 damit gegenstandslos (kein Drag mehr).

Die separaten Copy-Buttons fuer IBAN + Ausweisnummer im Vertrags-
formular bleiben erhalten.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 14:51:52 +02:00
duffyduckandClaude Opus 4.8 a509bb7826 PdfDragButton: PDF per Drag-and-Drop aus Bankkarte/Ausweis ziehen
Neue wiederverwendbare Komponente PdfDragButton: ziehbares Element,
mit dem der hinterlegte Scan direkt aus dem Browser in ein Mail-Fenster
(Anhang) oder den Datei-Explorer gezogen werden kann (Chromium-
DownloadURL, Format <mime>:<name>:<absolute-url>).

Bewusste Plattform-Grenze: PDF per Strg+V als Datei einfuegen geht im
Browser nicht (Web-Clipboard darf keine OS-Datei-Zwischenablage
befuellen) -> Drag-and-Drop. DownloadURL nur Chrome/Edge, nicht
Firefox (dort Klick-Fallback: Datei im Tab oeffnen).

Eingebaut an: ContractForm (neben IBAN/Ausweisnummer-Copy), Vertrags-
ansicht (Bankkarte/Ausweis-Card), Kundenakte-Tabs Bankkarten+Ausweise.
Nur bei vorhandenem documentPath. Nutzt bestehende fileUrl()-Download-
URL (Token als Query, Per-File-Ownership-Check unveraendert) - kein
neuer Endpoint, keine neue Angriffsflaeche.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 11:50:01 +02:00
duffyduckandClaude Opus 4.8 f0480ab432 Vertragsformular: Copy-Buttons fuer Bankkarte (IBAN) + Ausweis
In Kundendaten verknuepfen je ein Kopieren-Button neben dem Label.
Kopiert nur den reinen Wert der aktuell gewaehlten Option: bei
Bankkarte die IBAN ohne Namen, bei Ausweis die Ausweisnummer ohne
(TYP). Button erscheint nur bei ausgewaehltem Eintrag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 11:24:09 +02:00
duffyduckandClaude Opus 4.7 2d8ee7f569 Kundenportal: Deaktivierte-Toggle in "Meine Verträge"-Header
Der Toggle "Deaktivierte anzeigen" saß im obersten Seiten-Header.
Verschoben in den Gruppen-Header "Meine Verträge" (rechtsbündig,
nur beim eigenen Kunden-Block), damit er direkt bei den Verträgen
sitzt statt losgelöst ganz oben.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 02:51:31 +02:00
duffyduckandClaude Opus 4.7 cda6d2814e Kundenakte: Tab "Geworben / angeworben" (Kundenempfehlungen)
Neuer Tab vor "Datenschutz", nur Mitarbeiter/Admin (nicht Portal),
ohne Consent-Pflicht. Zwei Abschnitte:
1. "<Kunde> wurde an Board geholt durch:" – max. 1 Werber
   (DB-Unique auf recruitedId).
2. "<Kunde> hat folgende Kunden an Board geholt:" – beliebig viele.

Jede Zeile: Kunde per Lupe-Such-Modal (breite Suche über Name/
Kundennr./Firma/E-Mail/Telefon) + Beziehungs-Dropdown. Löschen +
Externtab-Link zur Kundenakte pro Zeile.

Bidirektional aus EINEM Datensatz: "A geworben durch B" erscheint
automatisch bei B unter "hat geworben"; von beiden Akten
hinzufügbar/löschbar.

Backend: neues Model CustomerReferral (recruiter/recruited FKs,
recruitedId @unique, relationship) + Migration. Beziehungs-Whitelist
serverseitig; Self-Werbung + Doppel-Werber (409) abgefangen.
Portal-Token wird explizit geblockt (Defense-in-Depth, nicht nur
UI-Ausblendung). CREATE/DELETE auditiert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 17:50:15 +02:00
duffyduckandClaude Opus 4.7 fd8be1d5c4 Kundenportal: Toggle "Deaktivierte Verträge anzeigen"
Der Admin-/Mitarbeiter-Blick (CustomerDetail) hatte den Toggle schon;
jetzt auch in der Kundenportal-Vertragsübersicht (ContractList,
Portal-Zweig). Button nur bei isCustomerPortal, reicht
includeDeactivated an die getTreeForCustomer-Queries durch
(showDeactivated im Query-Key → frischer Fetch beim Umschalten).

Security: derselbe Endpoint hinter canAccessCustomer (R120) – der
Portal-Kunde bekommt nur eigene/vertretene Bäume, das Flag weitet
nur den Status-Filter innerhalb der erlaubten Daten. Keine neue
Exposition.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 16:48:24 +02:00
duffyduckandClaude Opus 4.7 8eb3790e7b Mobilfunk: Feld "Mobilfunknetz" unter Anbieter & Tarif
Neues Dropdown "Mobilfunknetz" in der Anbieter-&-Tarif-Karte, nur
sichtbar bei Vertragstyp Mobilfunk. Optionen: Bitte auswählen (leer),
Telekom, Vodafone, Telefónica.

Neues Feld MobileContractDetails.mobileNetwork (String nullable,
speichert TELEKOM/VODAFONE/TELEFONICA) + idempotente Migration
(ADD COLUMN IF NOT EXISTS). String statt Enum, damit weitere Netze
ohne Migration ergänzbar sind. Anzeige in der Vertragsansicht mit
lesbarem Netz-Namen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 13:07:56 +02:00
duffyduckandClaude Opus 4.7 bc5d639703 Dashboard: anstehende Geburtstage anzeigen (wie im Cockpit)
Die Geburtstags-Sektion gab es bisher nur im Vertrags-Cockpit.
Jetzt zusätzlich auf dem Dashboard für Mitarbeiter/Admins – gleiche
birthdayApi.getUpcoming(7, 30)-Query und identische Karten-
Darstellung (Heute/vergangen/kommend, Link zur Kundenakte). Kein
Backend-Change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 11:33:58 +02:00
duffyduckandClaude Opus 4.7 d059912c1e Kundenansicht: Toggle „Deaktivierte Verträge anzeigen"
Der Vertragsbaum beim Kunden blendete DEACTIVATED-Verträge
komplett aus. Da der aktuellste Vertrag die Baumwurzel ist und
Vorgänger als Children hängen, verschwand eine ganze Kette aus
der Ansicht, sobald die Wurzel deaktiviert wurde – so „verschwand"
ein Vertrag scheinbar, als ein aktiver Folgevertrag gelöscht und
der Vorgänger vorher deaktiviert worden war.

getContractTreeForCustomer bekommt ein optionales
includeDeactivated-Flag (Default false = bisheriges Verhalten),
durchgereicht per Query-Param includeDeactivated=true. Toggle-
Button (Eye/EyeOff) im Kunden-Vertragstab; showDeactivated ist
Teil des Query-Keys → frischer Fetch beim Umschalten. Deaktivierte
behalten ihr graues DEACTIVATED-Badge.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 21:45:58 +02:00
duffyduckandClaude Opus 4.7 d0611b740a Vertrag-Löschen: Kette bleibt intakt, Liste refresht
Zwei zusammenspielende Bugs:

1) Backend: deleteContract-Reihenfolge scheiterte am UNIQUE-
   Constraint auf Contract.previousContractId. Beim Middle-Delete
   (A → B → C, B löschen) hielt B im UPDATE-Moment noch selbst
   previousContractId=A – der Versuch, C ebenfalls auf A
   umzubiegen, warf MariaDB Duplicate-Entry, das UPDATE brach ab.

   Fix: Transaction, umgedrehte Reihenfolge – erst B löschen
   (DB-Cascade ON DELETE SET NULL räumt C.previousContractId ab,
   gibt A-Slot frei), dann C sauber auf A umbiegen.

2) Frontend: deleteMutation invalidierte weder Contract-Listen
   noch Kunden-Vertragsbaum und hatte keinen onError-Handler.
   Nach dem Delete wurde nach /contracts navigiert, dort zeigte
   der stale Cache noch den gelöschten Vertrag – Eindruck: „ganze
   Historie weg". Bei Bug 1 fehlgeschlagen brach das schweigend
   ab.

   Fix: queryClient.invalidateQueries für ['contracts'],
   ['contract-tree', customerId], ['customer', customerId] nach
   Erfolg. onError-Toast bei Fehler.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 20:52:02 +02:00
duffyduckandClaude Opus 4.7 dcdb3cb057 Vertrag: „Portaldaten nicht möglich / nicht benötigt"-Opt-out
Dritter Radio-Button in der Zugangsdaten-Card des Vertragsformulars.
Wenn gesetzt, unterdrückt das Cockpit die Warnung „Portal-Zugangs-
daten fehlen" für diesen Vertrag – für Anbieter ohne Portal oder
Kunden, die bewusst keine Zugangsdaten pflegen. Verstopft das
Cockpit sonst dauerhaft.

Neues Feld Contract.portalCredentialsNotRequired (Boolean, default
false) + idempotente Migration (ADD COLUMN IF NOT EXISTS). Bestand
bleibt auf false, Warnung greift wie bisher.

Beim Umschalten auf Opt-out werden portalUsername,
portalPasswordEncrypted und stressfreiEmailId server-seitig
explizit auf NULL gesetzt – Datenhygiene, damit keine verwaisten
Anmeldedaten in der DB stehen bleiben.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 14:45:32 +02:00
duffyduckandClaude Opus 4.7 aeecc3327f SIM-Karten: PIN/PUK-Fehler statt stummer Alert-Meldung
Der bisherige alert() maskierte die eigentliche Ursache (Vertrag
nicht gefunden / Kein Zugriff / Decrypt-Fehler). Ersetzt durch
Toast mit Backend-Message + console.error mit Stacktrace, plus
Info-Toast wenn die SIM in der DB gar keine PIN/PUK hinterlegt hat.

Endpoint (GET /contracts/simcard/:id/credentials) und Auditing
unverändert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 14:45:20 +02:00
duffyduckandClaude Opus 4.7 e743e6795b Vertrag-Zugangsdaten: Externtab-Link zu Kunden-Adressen
Icon neben "Portal Benutzername" öffnet die Stressfrei-/Corporate-
Adressverwaltung des Kunden in neuem Tab, ohne das Vertragsformular
zu verlassen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 13:54:35 +02:00
duffyduckandClaude Opus 4.7 70724e35dc Kündigungsdatum: Cursor bleibt beim Tippen im Feld
Bug: bei Eingabe der Jahreszahl im Kündigungsbestätigungs-Datum
sprang der Fokus raus und die Seite scrollte hoch. Ursache:
<input type=date> onChange feuert bei jedem Tastendruck. Sobald
"18.08.0002" ein gültiges Datum ergab, feuerte der Mutation-Aufruf
eine PUT-Request, die Server-Antwort re-renderte den Input und
der Cursor landete am Feldanfang – der Rest der Jahreszahl kam
im nächsten Feld an.

Fix: neue kleine AutosaveDateInput-Komponente – hält den Wert
während der Eingabe lokal, feuert onSave erst bei onBlur oder
Enter. Escape verwirft die Änderung. Kein Save bei unverändertem
Wert.

Angewendet auf beide betroffenen Stellen in ContractDetail
(Bestätigung erhalten am – Kündigungsschreiben + Options-Variante).

Andere Datumsfelder der App laufen entweder über react-hook-form
(batched, save auf Submit) oder halten den Wert lokal – die sind
vom Bug nicht betroffen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-11 15:32:25 +02:00
duffyduckandClaude Opus 4.7 96f8715e13 Bankkarte: Beschreibung in Vertrags-Ansicht + Select anzeigen
ContractDetail: Bankkarten-Card zeigt jetzt die description
kursiv unter Bankname (whitespace-pre-line für mehrzeilige
Notizen).

ContractForm: Bankkarten-Select hängt die Beschreibung kompakt
an das Label an (Zeilenumbrüche flach, auf 60 Zeichen gecapped)
– hilft beim Unterscheiden mehrerer Karten desselben Kunden.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-10 02:09:58 +02:00
duffyduckandClaude Opus 4.7 eeb89e09db Vertrag-Zugangsdaten: Externtab-Links neben Stressfrei-Select
Rechts neben dem Stressfrei-Adressen-Dropdown sitzen jetzt zwei
kleine Icon-Links (nur bei gewähltem Kunden sichtbar):
- ExternalLink → öffnet den Stressfrei-Tab des Kunden im neuen Tab
  (dort kann man die Adressen anlegen/pflegen)
- Mail → öffnet den E-Mail-Postfach-Tab des Kunden im neuen Tab

Damit muss man beim Vertragserstellen nicht mehr die Seite
wechseln, um schnell eine Stressfrei-Adresse anzulegen oder ins
Postfach zu springen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-09 13:34:55 +02:00
duffyduckandClaude Opus 4.7 e7f9aa65d4 Stressfrei-Email: Mailbox+Passwort per Default aktiv
Beim Anlegen einer neuen Stressfrei-E-Mail-Adresse: sobald die
Checkbox "Beim Provider anlegen" aktiv ist, wird jetzt automatisch
auch die zweite Checkbox "Echte Mailbox erstellen" mitgeschaltet.
Damit läuft `createEmail` mit `createMailbox: true` durch –
Backend generiert per generateSecurePassword ein Kennwort,
schickt es via -passwd an Plesk und speichert es verschlüsselt.

Vorher musste man beide Häkchen manuell setzen und übersah leicht
die zweite Checkbox → Adresse landete nur mit Weiterleitung ohne
IMAP/SMTP-Zugang bei Plesk.

Falls jemand nur eine reine Weiterleitung ohne Postfach will,
kann er die zweite Checkbox weiterhin abwählen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-09 12:34:24 +02:00
duffyduckandClaude Opus 4.7 616cb77c0d Bankkarte: optionales Beschreibung-Feld
Neues optionales Freitext-Feld `description` (TEXT NULL) auf
BankCard – z.B. "Geschäftskonto", "gemeinsames Konto mit Partner".
Migration mit IF NOT EXISTS.

Backend service create/update nimmt description entgegen.
Frontend:
- BankCard-Type um description ergänzt.
- BankCardModal: neue Textarea zwischen Ablaufdatum und Aktiv-
  Checkbox mit Placeholder-Hilfetext.
- Bank-Kartenübersicht zeigt die Beschreibung kursiv unter den
  bestehenden Feldern, falls gesetzt (whitespace-pre-line für
  mehrzeilige Notizen).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-09 12:22:17 +02:00
duffyduckandClaude Opus 4.7 57959cd782 Kunde: persönliche Du/Sie-Präferenz pro Mitarbeiter
Neue Tabelle UserCustomerSalutation (PK userId+customerId,
preference 'DU'|'SIE'). Fehlender Eintrag → Fallback auf
Customer.useInformalAddress (Kunden-Default).

Backend:
- customerService: get/set/clearSalutationPreference mit Fallback-
  Logik. Response enthält immer effektive Präferenz + `source`
  ('user' | 'customer-default'), damit die UI den Standard-Text
  anzeigen kann.
- customerController: 3 Endpunkte GET/PUT/DELETE
  /:customerId/salutation-preference. userId aus dem JWT, canAccessCustomer
  greift.

Frontend:
- customerApi: 3 neue Methoden.
- CustomerDetail: neues Feld "Anrede für mich" mit Du/Sie-Toggle
  und Zurücksetzen-Link. Wird nur Mitarbeitern angezeigt, nicht
  Portal-Usern.
- Bestehendes Feld "Anrede per" bekommt Hinweis "Standard für alle
  Mitarbeiter", damit die Semantik der beiden Felder klar ist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-06 00:34:51 +02:00
duffyduckandClaude Opus 4.7 d14db3de2f Vertragsauswahl: Kundennr./Zählernr./Telefonnr. in allen Pickern
Zentraler Helper contractLabel.ts liefert je nach Vertragstyp die
sinnvollen Zusatzinfos (nur wenn Daten vorhanden):
- Kdnr. <customer.customerNumber> (immer)
- Anbieter-Vtr. <contractNumberAtProvider>
- Zähler <meterNumber> (Strom/Gas)
- Mobil <main-SIM-Nummer> (Mobilfunk)
- Festnetz <main-phone> (Internet)

Backend contractApi.getAll include um energyDetails.meter und
internetDetails.phoneNumbers erweitert – vorher fehlten die im
Response.

Angewendet auf 5 Call-Sites:
- AssignToContractModal (Email-zu-Vertrag zuordnen)
- TaskList (Mitarbeiter- und Kundenportal-"Neue Aufgabe/Anfrage")
- Dashboard (Support-Anfrage-Modal)
- PdfTemplates (Test-Vorlage-Kontext-Auswahl)
- ContractForm (Vorgänger-Vertrag-Select)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-06 00:26:42 +02:00
duffyduckandClaude Opus 4.7 0b78323c7b Vertrag: neuer Status ONGOING (Läuft weiter auf unbestimmte Zeit)
Enum-Wert ONGOING neu, MariaDB-Migration (ans Ende gehängt, damit
keine Rows umgeschrieben werden). Prisma Client neu generiert.

Cockpit-Logik: ONGOING wird in die Query mit reingeholt (damit
DSGVO-/Consent-Warnungen weiter greifen), aber alle Fristen-
Blöcke pro Vertrag geskippt:
- Kündigungsfrist
- Vertragsende
- Zwischenrechnung-Frist

Kein Skip für Daten-Qualität (fehlende Adresse, Bank, Portal-
Zugang etc.) – die sind auch für unbefristete Verträge relevant.

Frontend: statusLabels/statusVariants/statusDescriptions in
ContractDetail, ContractList, CustomerDetail und
ContractDetailModal um ONGOING ergänzt. Status-Select im
ContractForm bekommt die neue Option zwischen ACTIVE und
CANCELLED.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-06 00:22:03 +02:00
duffyduckandClaude Opus 4.7 cc6fa9be47 Aufgaben: erledigte trotzdem bearbeiten + Nachträge anlegen
Drei Frontend-Guards raus, die verhindert haben, dass man an
erledigten Aufgaben noch etwas machen kann:

- TaskList: der Reply-/Subtask-Input war für erledigte Aufgaben
  komplett ausgeblendet. Jetzt sichtbar mit angepasstem Placeholder
  ("Nachtrag zu erledigter Aufgabe…" / "Antwort zu erledigter
  Anfrage…").
- ContractDetail: der Subtask-Input war für erledigte Aufgaben aus.
  Jetzt sichtbar – man muss die Aufgabe nicht mehr als offen
  markieren, nur um einen Nachtrag anzuhängen.
- ContractDetail: der Edit-Button war für erledigte Aufgaben aus.
  Jetzt immer verfügbar (Löschen war eh schon da).

Backend hatte keine Restriktion, es waren rein UI-Gate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-04 00:55:58 +02:00
duffyduckandClaude Opus 4.7 a3bdb3f467 Passwort-Set: echte Fehlermeldung anzeigen statt Fallback
Der axios-Response-Interceptor packt die Backend-Meldung in
new Error(message) um und wirft .response weg. Der Catch-
Handler beim Passwort-Setzen las aber err.response.data.error
(nach dem Interceptor undefined) und fiel deshalb immer auf
"Passwort konnte nicht gesetzt werden" zurück.

Ergebnis: der Admin sah nie den echten Grund – typischerweise
"Passwort erfüllt Mindestanforderungen nicht: mindestens 25
Zeichen" oder "Eigenes Passwort ist falsch".

Fix: err.message lesen. Das ist genau der vom Interceptor
gesetzte String.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 09:17:02 +02:00
duffyduckandClaude Opus 4.7 4d3382b5a4 Vertreter: Portal-Pflicht raus, Warn-Badge stattdessen
Suche und Add haben bisher nur Kunden mit portalEnabled=true zugelassen –
das machte das Feature quasi unnutzbar, weil die meisten Kunden kein
Portal aktiviert haben. Der Zugriff ist ohnehin erst dann effektiv,
wenn der Vertreter ein Portal-Konto bekommt.

- searchCustomersForRepresentative: portalEnabled-Filter raus, dafür
  Feld im select mitgeliefert.
- addRepresentative: Portal-Pflicht-Check raus.
- getRepresentedByList: portalEnabled im rep-Include, damit die UI
  auch für schon hinterlegte Vertreter das Badge zeigen kann.
- CustomerDetail: gelbes "Portal inaktiv"-Badge in Suchergebnissen
  und in der Vertreter-Liste. Hinweistext geändert.
- CustomerSummary-Type: portalEnabled? ergänzt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 08:53:02 +02:00
duffyduckandClaude Opus 4.7 b48cd72a2b Vertrag-UI: Sticky-Header + Sticky-Footer
ContractDetail: der obere Balken (Vertragsnummer, Kunde-Link,
Aktions-Buttons wie Folgevertrag/Bearbeiten/Löschen) bleibt beim
Scrollen sichtbar. -mx-8 px-8 überbrückt das Layout-Padding,
damit der Balken bündig sitzt.

ContractForm: Heading + Kunde-Zeile sticky oben, Speichern/
Abbrechen sticky unten. Damit muss man beim langen Formular nicht
mehr nach oben oder unten rollen.

ScrollToTopButton: von bottom-6 auf bottom-24 verschoben, sonst
sitzt der runde Nach-oben-Button beim Runterscrollen genau über
dem Speichern-Button im Sticky-Footer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-02 20:46:15 +02:00
duffyduckandClaude Opus 4.7 b23ebeefc3 Vertrag-UI: Kunde-Link + Externtab + Info-Modal überall einheitlich
ContractDetail (Vertragsansicht): neben dem Kunden-Link sitzt jetzt
zusätzlich ein ExternalLink-Icon, das die Kundenakte in einem neuen
Tab öffnet. Info-Icon (Schnellansicht-Modal) bleibt wie gehabt.

ContractForm (Neuer/Bearbeiten-Vertrag): bekommt unter dem Heading
dieselbe Kunden-Zeile wie ContractDetail – Customer-Name als Link,
ExternalLink-Icon für neuen Tab, Info-Icon für die
CustomerInfoModal-Schnellansicht. Nur sichtbar wenn schon ein
Kunde gewählt ist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-24 21:03:20 +02:00
duffyduckandClaude Opus 4.7 4ab0340473 Pentest R95: portalUsername (Manual-Modus) härten
R95.1 MEDIUM: foo\r\nBcc:evil@x.de → Header-Injection-Vektor
R95.3 LOW: <script>...</script>@x.de → silent stripHtml-Mutation
R95.4 LOW: >190 Zeichen → VARCHAR-Overflow → 500 statt 400

Fix: validatePortalUsername() in sanitize.ts mit Whitelist
^[A-Za-z0-9_\-/.@+ ]{0,100}$. Strukturell sind CRLF, Tab, alle
Control-Chars, Tags und Quotes raus → R95.1+R95.3 ohne extra
Check. Max 100 → ApiError(400) → R95.4. Raw-Input vor stripHtml
geprüft (R87-Pattern). Eingehängt in sanitizeContractBody.

R95.2 (Email-Format-Pflicht) bewusst NICHT übernommen:
portalUsername ist im Manual-Modus nicht zwingend eine Email
(Vodafone, 1&1, EWE und Stadtwerke nutzen Kundennummern oder
Pseudonyme als Portal-Login). Doku in SECURITY-HARDENING.md
§ Runde 95.

Frontend: maxLength={100} am Input als UX-Schicht.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 15:24:57 +02:00
duffyduckandClaude Opus 4.7 f1102a24b7 CopyButton: Portal-Benutzername + E-Mail-Postfach-Selector
ContractForm: neben dem "Portal Benutzername"-Label sitzt jetzt
ein CopyButton, der je nach Modus den manuellen Eingabewert oder
die ausgewählte Stressfrei-Adresse in die Zwischenablage kopiert.
Erscheint nur wenn der jeweilige Wert nicht leer ist.

EmailClientTab + ContractEmailsSection: rechts neben dem Account-
Selector liegt jetzt ein CopyButton, der die Postfach-Adresse des
aktuell gewählten Mailbox-Kontos kopiert. Funktioniert in beiden
UI-Varianten (Single-Account-Span und Multi-Account-Dropdown).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 15:05:25 +02:00
duffyduckandClaude Opus 4.7 8b10316683 Anbieter: Kontakt + Kündigung als Stammdaten
Sieben neue optionale Felder am Provider (contactEmail,
contactPhone, contactFax, contactAddress, cancellationEmail,
cancellationFax, cancellationAddress). Postadressen TEXT,
Rest VARCHAR(191). Migration mit IF NOT EXISTS.

Modal "Anbieter bearbeiten" bekommt neue Sektion "Kontakt &
Kündigung" mit zwei Untergruppen. Backend validiert Emails
gegen isValidEmail (Header-Injection-Schutz), Telefon/Fax
gegen sanitizePhoneField (kein CRLF), Postadressen via
sanitizeNotes mit 500-Cap. Factory-Defaults Export/Import
mitgezogen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 13:10:59 +02:00
duffyduckandClaude Opus 4.7 c8b86ca9a7 Pentest R86: Vertrags-Identifier max 100 + Charset-Whitelist
R86.1 LOW + R86.2 LOW: >999-Zeichen liefen in DB-Overflow (500
statt 400), Attribut-Injection (`foo" onerror=…` ohne
umschließenden Tag) überlebte stripHtml.

Fix: validateContractIdentifier() (max 100,
^[A-Za-z0-9_\-/. ]{0,100}$) in sanitize.ts, eingehängt in
sanitizeContractBody. Wirft ApiError(400, …). Literales Space
statt \s → kein CRLF/Tab → kein Header-Injection-Vektor in
CSV-/Mail-/PDF-Export. Greift auf alle fünf Identifier-Felder
(Provider + Sales-Platform). ContractForm-Inputs bekommen
maxLength={100} als UX-Schicht.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-19 14:14:00 +02:00
duffyduckandClaude Opus 4.7 0b7bb89ebc Vertrag: Auftragsnummer Vertriebsplattform vor Kundennummer
Contract.orderNumberAtSalesPlatform (VARCHAR(191) NULL) mit
Migration 20260619100000_contract_order_number_at_sales_platform
(IF NOT EXISTS). Form-Input, Detail-Zeile mit Copy-Button,
Audit-Mapping, Renewal-Copy und XSS-Strip-Allowlist analog zu
den bestehenden Sales-Platform-Feldern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-19 13:49:04 +02:00