Commit Graph
82 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 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 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 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
duffyduck fcc3b04725 Vertrag: Kunden-/Vertragsnummer bei Vertriebsplattform
Viele Vertriebsplattformen vergeben eigene Nummern, die nicht mit
denen des Endanbieters identisch sind. Zwei neue optionale Felder
unter "Anbieter & Tarif".

- Schema: Contract.customerNumberAtSalesPlatform +
  contractNumberAtSalesPlatform, Migration mit IF NOT EXISTS.
- ContractForm: zwei neue Inputs direkt unter den entsprechenden
  Provider-Feldern.
- ContractDetail: eigene Zeilen mit CopyButton.
- Audit-Log-Mapping + Renewal-Copy + XSS-Strip-Whitelist mitgezogen.
- Bonus: contractNumberAtProvider war im Renewal-Copy und Audit-
  Label-Mapping fehlend – mitkorrigiert.
2026-06-03 18:13:17 +02:00
duffyduck 5508d59652 SIM-Karten: Checkbox "eSIM" zwischen Hauptkarte und Multisim
Hardware-Plastikkarte vs. eSIM-Profil ist eigene Eigenschaft – eSIM
kann sowohl Hauptkarte als auch Multisim sein, deshalb dritter
Toggle statt entweder/oder.

- Schema: SimCard.isEsim Boolean default false, Migration mit
  IF NOT EXISTS.
- Backend: vier SimCard-Schreibpfade in contract.service.ts (Create,
  Update, Follow-Up, Renewal).
- UI: dritte Checkbox in ContractForm zwischen Hauptkarte und
  Multisim. ContractDetail zeigt blauen eSIM-Badge.
2026-06-03 16:13:24 +02:00
duffyduck 30f528596c JPGs → PDF: neuer Button überall bei PDF-Upload
- Neue Komponente JpgToPdfModal (jsPDF clientseitig, kein Backend-Roundtrip).
- Bilder hinzufügen per Klick, Drag&Drop oder Strg+V (Clipboard).
- Reihenfolge per Drag&Drop sortierbar; pro Bild 90°/180°-Drehung +
  horizontal/vertikal-Spiegelung.
- Jedes Bild = eine A4-Seite, Orientation automatisch nach Bild,
  JPEG-Qualität 100%.
- FileUpload-Komponente zeigt den Sekundär-Button automatisch, sobald
  accept PDF einschließt (Datenschutz, Vollmacht, Bankkarten, Ausweise,
  Gewerbeanmeldung, Handelsregister, Kündigungsschreiben/-bestätigung
  + jeweilige Optionen).
- Direktinputs ebenfalls erweitert: Vertragsdokumente (ContractDetail),
  Vollmacht-Tab (CustomerDetail), Rechnungen (InvoicesSection).
- PdfTemplates bewusst ausgenommen – braucht AcroForm-Felder.
2026-06-03 12:27:37 +02:00
duffyduckandClaude Opus 4.7 61daff8df9 Rufnummern: Vorwahl als eigenes Feld – verlässliche PDF-Befüllung
Bisher steht in PhoneNumber.phoneNumber die kombinierte Nummer
("04264 836975"). Die Wechselauftrag-PDFs splittten heuristisch
auf Vorwahl/Anschluss, was bei Sonderformaten daneben ging.

Schema: PhoneNumber.areaCode String? (optional, Bestandsdaten
werden beim nächsten Edit nachgepflegt). Migration
20260601200000_phone_area_code mit IF NOT EXISTS.

ContractForm: aus "Rufnummer" werden zwei Felder – "Vorwahl" und
"Rufnummer". Beim Speichern sendet das Frontend areaCode separat
UND die kombinierte phoneNumber (für Listen/Suchen weiter
unverändert). Beim Edit-Load wird areaCode bevorzugt; falls leer,
splittet die UI heuristisch und prefillt beides – User kann
korrigieren und beim Speichern wird der saubere Wert persistiert.

PDF-Template-Service: phoneAreaCode[N] und phoneLocal[N]
verwenden jetzt primär den gespeicherten areaCode aus der DB
(verlässlich), Heuristik nur als Fallback für Altbestand. Die
Template-Variablen-Liste war bereits korrekt definiert, jetzt
ist die Datenquelle solide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 14:10:25 +02:00
duffyduckandClaude Opus 4.7 d0d2715baa Pentest 46.1 HIGH + Info-Konsolidierung: zentrale URL-Validierung
46.1 HIGH (Stored XSS via provider.portalUrl): PUT /api/providers/:id
nahm `javascript:alert(...)` als portalUrl ohne Validierung an, das
Portal rendert es als <a href={portalUrl}> → Klick im Kunden-Browser
löste XSS aus.

Fix: neuer zentraler Helper backend/utils/url.validateHttpUrl
- erlaubt nur http(s)-Schemas (sperrt javascript:, data:, file:,
  vbscript:, blob: usw.)
- erfordert absoluten URL mit Host
- per Default keine privaten/Loopback-Hosts (über
  isPrivateOrBlockedHost), weil der Wert Endkunden gezeigt wird
- Trailing-Slash wird gestrippt

Eingebaut in:
- provider.service createProvider + updateProvider (HIGH-Fix)
- appSetting.service validateSettingValue für portalLoginUrl
  (Refactor der bestehenden ad-hoc Validierung → konsolidiert)

Defense-in-depth Frontend: frontend/utils/url.safeHttpUrl liefert
URLs nur zurück wenn http(s), sonst undefined. Eingesetzt in
ContractDetail bei Portal-Link-Rendering und Auto-Login, damit
Alt-Daten in der DB (vor diesem Fix angelegt) nicht klickbar
bleiben.

INFO-Konsolidierung: damit ist die Schema-/Host-Validierung
einheitlich an einer Stelle. Sanitize-Layer (stripHtml in
sanitize.ts) bleibt für reine Text-Felder zuständig.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 11:48:14 +02:00
duffyduckandClaude Opus 4.7 4acfd9de1c SIM-Karten: Feld "Kartennutzer" für Firmen-/Familienverträge
Bei Firmenverträgen (Vertragsinhaber = Firma, Nutzer = Mitarbeiter)
und Familienverträgen (Inhaber = Eltern, Nutzer = Kind) brauchten
wir ein Feld, das den tatsächlichen Nutzer der SIM-Karte erfasst.

Backend: SimCard.cardUser (String?, optional), Migration
20260601100000_sim_card_user mit IF NOT EXISTS. Im Service durch
Create + Update propagiert.

Frontend: Input "Kartennutzer" pro SIM-Karte in ContractForm
(eigene Zeile oberhalb der technischen Felder Rufnummer/SIM-Nr/
PIN/PUK). In ContractDetail wird der Nutzer als "Nutzer: <Name>"
neben den Hauptkarte/Multisim-Badges angezeigt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 08:10:16 +02:00
duffyduckandClaude Opus 4.7 4fb700cf57 Vertrags-Forms: Mini-Links zu Stammdaten in neuem Tab
ContractEmailsSection (Vertragsansicht): Zusätzlich zu "Postfach
öffnen" gibt es jetzt "Stressfrei wechseln Adressen" → Tab in der
Kundenakte.

ContractForm (Bearbeiten): Kleine ExternalLink-Icons neben den
Select-Labels:
- Lieferadresse + Rechnungsadresse → Kundenakte/Adressen
- Bankkarte → Kundenakte/Bankkarten
- Ausweis → Kundenakte/Ausweise
- Anbieter + Tarif → Settings/Anbieter & Tarife
- Vertriebsplattform → Settings/Vertriebsplattformen

Select-Komponente nimmt jetzt ReactNode als label (statt nur string),
um JSX-Labels mit eingebettetem Link zu erlauben. Rückwärts-
kompatibel zu allen bestehenden String-Aufrufen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 07:47:40 +02:00
duffyduckandClaude Opus 4.7 5269092d2a Fix: "Wurde sondergekündigt?"-Label nicht über volle Spaltenbreite klickbar
Label-Klasse war flex -> Block-Layout, das die ganze col-span-2-Zeile
einnimmt. Klicks rechts neben dem Text triggern dann ebenfalls die
Checkbox. Fix: inline-flex – die Label-Box passt sich an den Inhalt
(Checkbox + Text) an.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 12:49:37 +02:00
duffyduckandClaude Opus 4.7 a20e331f83 Strom/Gas-Details: "Zähler verwalten"-Link neben Card-Titel
Zusätzlich zum bestehenden Link im Folgezähler-Form bekommt auch
der Card-Header der Strom/Gas-Details einen Link "Zähler verwalten",
der die Zähler-Übersicht des Kunden in einem neuen Tab öffnet –
damit der Link immer sichtbar ist, nicht nur wenn die Folgezähler-
Form aufgeklappt ist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 15:11:01 +02:00
duffyduckandClaude Opus 4.7 2ee06630b9 Folgezähler-Forms: Checkbox "Alten Zähler deaktivieren" (default an)
Beide Folgezähler-Forms (Kundenakte MeterModal + Vertragsansicht
SuccessorMeterForm) bekommen eine Checkbox, die standardmäßig
angehakt ist. Beim Speichern wird der Vorgänger automatisch
auf isActive=false gesetzt – ein-klick-fähiger Zählerwechsel.

Backend: createMeter mit successorOf und addSuccessorMeter
akzeptieren deactivatePredecessor (Default true).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 14:39:05 +02:00
duffyduckandClaude Opus 4.7 3a9cece929 Vertragshistorie: Vertragsnummern als Link in neuem Tab
Erwähnte Vertragsnummern (Pattern PREFIX-RANDOM) in Title und
Description werden gegen previousContract + followUpContract des
aktuellen Vertrags aufgelöst und als Link mit target="_blank"
gerendert. Nicht aufgelöste Nummern bleiben als Text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 14:33:11 +02:00
duffyduckandClaude Opus 4.7 e527aebb84 Vorvertrag-Verbrauch als Schätzwert im Folgevertrag
ContractForm (Strom/Gas): Wenn ein previousContractId gesetzt ist,
wird der Vorvertrag samt Readings nachgeladen, der Verbrauch
clientseitig berechnet und als "Vorvertrag: X kWh [Übernehmen]"
unter dem Jahresverbrauch-Feld angezeigt. Bei Gas auch unter
"Jahresverbrauch (kWh)".

ContractDetail (Strom/Gas): Wenn annualConsumption leer ist und
ein berechenbarer Vorvertrag existiert, wird "~X kWh, geschätzt
aus Vorvertrag" in der Jahresverbrauch-Zelle angezeigt – damit
der Wert beim Lesen schon als Anhaltspunkt da steht.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 14:28:55 +02:00
duffyduckandClaude Opus 4.7 13213846f4 Folgezähler-Form: Link "Zähler verwalten" → Kundenakte (neuer Tab)
Wenn im Vertrag kein passender Zähler im Dropdown auftaucht, kann
der User mit einem Klick die Zähler-Übersicht des Kunden in einem
neuen Tab öffnen, um dort einen neuen Zähler anzulegen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 14:15:40 +02:00
duffyduckandClaude Opus 4.7 61ce35821d Endstand alter Zähler fließt in Verbrauchsberechnung ein
Bisher wurde "Letzter Stand alter Zähler" zwar in
ContractMeter.finalReading gespeichert, aber nirgends ausgewertet.

Neuer Helper recordPredecessorFinalReading legt am Wechseldatum
einen regulären MeterReading-Eintrag für den Vorgänger an
(idempotent, mit Validierung gegen vorhandene Stände). Aufgerufen
aus addSuccessorMeter (Vertragsansicht) und createMeter mit
successorOf (Kundenakte).

Folge: Der Endstand erscheint in der Zählerstände-Liste des alten
Zählers und fließt automatisch über calculateMultiMeterConsumption
in den Verbrauch (Zeitraum bis removedAt ist inklusive).

UI-Hinweise in beiden Folgezähler-Forms erklären den Effekt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 14:14:03 +02:00
duffyduckandClaude Opus 4.7 34e106f253 Fix: Folgezähler-Button auch bei Single-Meter-Verträgen anzeigen
Bei Folgeverträgen / Bestandsverträgen ohne ContractMeter-Eintrag
war der "Folgezähler hinzufügen"-Button unsichtbar, weil er nur
im Multi-Meter-Zweig gerendert wurde.

Zusätzlich im addSuccessorMeter-Backend: bei Single-Meter-Verträgen
wird der bisherige energyDetails.meterId jetzt als ContractMeter
position 0 backfillt und als removed markiert, damit die Kette
lückenlos ist und der alte Zähler im Vertrag dokumentiert bleibt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 14:03:01 +02:00
duffyduckandClaude Opus 4.7 6f378d750c Vertragsansicht: Standort + Inaktiv-Badge beim Zähler anzeigen
In der Zähler-Zelle der Strom/Gas-Details wird jetzt zusätzlich
"Inaktiv" (rot) neben der Zählernummer angezeigt und der
Standort als kleine Zeile darunter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 13:32:12 +02:00
duffyduckandClaude Opus 4.7 c099b41796 Zähler → Lieferadresse-Pflichtfeld + Vertragsfilter
- Meter.addressId (FK → Address, ON DELETE SET NULL) + Migration
  20260530100000_meter_address mit IF NOT EXISTS
- Service erzwingt beim Create: Lieferadresse vorhanden + zum
  Kunden gehörig + Typ DELIVERY_RESIDENCE
- MeterModal: Pflicht-Dropdown "Lieferadresse"; Save disabled
  ohne Adresse; Hinweis-Banner. Bestandszähler ohne Adresse zeigen
  "nicht zugeordnet – bitte über Bearbeiten nachpflegen"
- ContractForm: Zähler-Dropdown filtert auf Vertrags-Lieferadresse;
  deaktivierte Zähler bleiben sichtbar mit "(deaktiviert)"; bei
  Auswahl Toast-Warnung wegen möglichem Altvertrag

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 13:18:24 +02:00
duffyduckandClaude Opus 4.7 0bd2f9be7e fix: "Anzeigen"-Buttons öffnen Datei wieder im Browser-Tab
Folge-Symptom des Pen-30.13-Fixes: alle file-downloads liefen mit
Content-Disposition: attachment – das ist gegen Stored-XSS richtig,
hat aber die "Anzeigen"-Buttons (Bankkarten / Ausweise /
Verträge / etc.) kaputtgemacht, weil der Browser jetzt
herunterlud statt im Tab zu öffnen.

Magic-Byte-basierter Whitelist-Pfad eingebaut: optional ?disposition=
inline am Download-Endpoint, ABER nur wenn die ersten Bytes der
Datei das Magic eines safe Typs zeigen (PDF, PNG, JPEG, GIF, WebP).
Bei Mismatch fällt's auf attachment zurück – Stored-XSS bleibt
weiterhin unmöglich, falls jemand HTML als .pdf hochlädt.

Frontend: neuer viewUrl(path)-Alias = fileUrl(path, {inline: true}).
Alle Stellen mit `<a href={fileUrl(...)} target="_blank">` oder
`window.open(fileUrl(...), '_blank')` (13 Stellen über CustomerDetail,
ContractDetail, PdfTemplates, GDPRDashboard, InvoicesSection)
nutzen jetzt viewUrl. Download-Stellen bleiben fileUrl
(= attachment, byte-genaues File-Save).

Live-verifiziert auf dev:
- ohne Param: attachment (default, Stored-XSS-Schutz)
- ?disposition=inline + echte PDF: inline + application/pdf
- ?disposition=inline + HTML als .pdf: attachment (Magic-Mismatch
  → Browser lädt herunter statt zu rendern)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 09:19:04 +02:00
duffyduckandClaude Opus 4.7 ee4ca9df07 Zugangsdaten-Card: Portal-Link des Anbieters anzeigen
Im Vertragsdetail unter "Zugangsdaten" zwischen Benutzername und
Passwort jetzt eine zusätzliche Zeile "Portal-Link" mit klickbarem
Link zum Anbieter-Portal (öffnet in neuem Tab, mit Copy-Button).
Greift auf das bestehende c.provider.portalUrl-Feld zurück (wird
auch schon für den Auto-Login-Button verwendet).

Schema und Host werden im Anzeigetext gestrippt, die volle URL
bleibt im href und im title-Attribut sichtbar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 11:27:02 +02:00