MaLo-ID (Marktlokation) gehoert zur (Liefer-)Adresse, nicht zum Vertrag.
Address bekommt maloIdElectricity + maloIdGas (getrennte Marktlokationen je
Sparte), pflegbar im AddressModal (nur Lieferadresse). Im Vertrag ist die
MaLo-ID jetzt ein Lesefeld, das je nach Vertragstyp die MaLo der gewaehlten
Lieferadresse zeigt; ContractDetail/-Modal ebenso.
Schema + Migration 20260814100000: 2 Spalten (idempotent) + Daten-Migration
(bestehende EnergyContractDetails.maloId -> jeweilige Lieferadresse,
ELECTRICITY->maloIdElectricity / GAS->maloIdGas). Migrationslogik verifiziert.
Dabei einen selbst verursachten Regressions-Bug gefixt: beim R156-Umbau waren
die 10 owner*-Adressfelder aus der Address-Whitelist gefallen -> Eigentuemer-
Sektion speicherte seit cb21a2c nicht mehr. Address-Whitelist jetzt via
Pick-Helper, programmatisch gegen alle DB-Spalten abgeglichen (owner* + MaLo
drin, id/customerId/Timestamps raus). BankCard/Document gegengeprueft: ok
(nur documentPath bewusst upload-only ausgeschlossen).
Verifiziert: tsc+build gruen; owner + maloId speichern wieder, Injection
(id/customerId) blockiert; Daten-Migration Strom->Strom / Gas->Gas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
Eine Gutschrift/ein Lieferschein braucht eine Empfaengeradresse aufs
Dokument. Rechnungsadresse hat Vorrang, sonst Lieferadresse. Ist keine
von beiden hinterlegt -> Anlegen blockiert.
- Frontend: Klick auf 'Gutschrift anlegen' prueft defaults.hasRecipient-
Address; wenn false -> Modal-OK-Meldung statt Formular.
- Backend Defense-in-Depth: createCreditNote wirft 400, wenn weder
billingAddressId noch addressId gesetzt. getCreditNoteDefaults liefert
hasRecipientAddress.
Verifiziert: ohne Adresse -> hasRecipientAddress false + create 400;
mit Adresse -> ok.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Beleg-Upload jetzt fuer beide Arten: bei Geld die Ueberweisungs-
bestaetigung, bei Sachwert das unterschriebene Dokument. ReceiptControls
in der Liste fuer Geld UND Sachwert (Label je nach Typ). Endpoint war
schon typ-agnostisch.
- PDF-Unterschriftsblock nur noch bei Sachwert - eine Ueberweisung wird
nicht unterschrieben (Beleg = hochgeladene Ueberweisungsbestaetigung).
Bei Geld entfaellt der Unterschrift/Ort-Block; im Formular sind Ort +
'Unterschrift am' bei Geld ausgeblendet.
Verifiziert: beide PDFs erzeugen sauber (je 1 Seite).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Ein betragsloser Sachwert ist eher ein Lieferschein als eine Gutschrift
-> er soll KEINE Gutschriftsnummer aus dem Nummernkreis verbrauchen.
- Schema: CreditNote.number nullbar (Migration MODIFY ... NULL, UNIQUE
bleibt - MySQL erlaubt mehrere NULLs).
- createCreditNote: betragsloser Sachwert -> number=null, assignNextNumber
wird NICHT aufgerufen (Zaehler unangetastet).
- updateCreditNote: Uebergaenge - wird betragslos -> Nummer entfernen;
bekommt nachtraeglich Betrag & hatte keine -> jetzt Nummer vergeben.
- PDF/Liste/Modal/Audit: Fallback 'Sachwert-Uebergabe'/'Beleg #id' wenn
keine Nummer; PDF-Titel 'Sachwert-Uebergabe', kein ZUGFeRD (schon vorher).
Verifiziert: Sachwert 0 -> number null + Zaehler bleibt; Geld -> Nummer
+ Zaehler +1; Sachwert nachtraeglich mit Betrag -> Nummer vergeben.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Eine Sachwert-Gutschrift darf betragslos sein (Betrag leer/0): dann
findet keine Rechnungsstellung statt - der Kunde hat den Gegenstand
einfach als Subvention erhalten.
- Backend: leeres amount -> 0; Sachwert erlaubt 0, Geld verlangt > 0
(400 sonst). vatRelevant bei 0 erzwungen-false.
- PDF: betragsloser Sachwert -> Titel 'Sachwert-Uebergabe', kein
Betrags-/USt-Block (Hinweis keine Rechnungsstellung), KEIN ZUGFeRD-
Embedding. Mit Betrag -> unveraendert ZUGFeRD.
- Frontend: Wert-Feld bei Sachwert optional; USt-Block ausgeblendet ohne
Betrag; Liste zeigt 'Sachwert ohne Betrag (keine Rechnung)'.
Verifiziert: Sachwert 0 -> kein factur-x.xml; Geld 0 -> 400;
Sachwert 200 -> ZUGFeRD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Bei Geld-Gutschriften kann die Auszahlung auf ein anderes Konto gehen
als das Vertrags-Abbuchkonto:
- CreditNote.payoutBankCardId + Migration (FK ON DELETE SET NULL, damit
Loeschen einer Bankkarte die Gutschrift nicht mitreisst).
- Formular: Dropdown mit allen Bankkonten des Kunden (Default =
Vertrags-Abbuchkonto). getCreditNoteDefaults liefert bankCards +
contractBankCardId.
- Server prueft, dass die gewaehlte Bankkarte dem Kunden des Vertrags
gehoert (kein Fremdkonto unterschieben).
- PDF: bei Ueberweisung 'Unsere Bankverbindung' (Absender) + darunter
'an Bankkonto: <Kunden-IBAN> (<Inhaber>)'. Section-Zeile zeigt das
Auszahlungskonto.
Lokal verifiziert (Anlage + PDF).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PDF-Erzeugung fuer Gutschriften mit Firmenstammdaten als Absender,
Empfaenger (Kunde+Adresse), Netto/USt/Brutto (bzw. ohne USt-Ausweis),
Sachwert/'Ware erhalten' bzw. Ueberweisungs-Bankverbindung,
Unterschriftsblock, Firmen-Fusszeile.
Endpoint POST /credit-notes/:id/pdf; 'PDF'-Button in CreditNotesSection
(erzeugen + im Tab oeffnen bzw. vorhandenes ansehen). PDF-Pfad wird bei
inhaltlicher Aenderung geleert -> Neu-Erzeugung. Download ueber
fileDownload (credit-notes subDir, Vertrags-Ownership).
Lokal verifiziert (valides %PDF, korrekte Betraege).
ZUGFeRD-XML-Embedding (PDF/A-3, EN 16931) folgt als Teil 2 mit
Validator-Pruefung.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Thunderbird & Co. hatten beim URL-basierten Drag nur einen LINK als
Anhang gespeichert (0 Bytes) und die Datei erst beim Senden nachgeladen
-> Fehler, weil kurzlebiger Token abgelaufen. Der User erwartet, dass
die Datei direkt angehaengt wird, nicht der Link.
Loesung: Datei wird vorab per Bearer-Auth (Axios-Instanz) als Blob
geladen und beim dragstart als echter Datei-Inhalt uebergeben:
dataTransfer.items.add(File) + DownloadURL mit lokaler blob:-URL.
-> Datei-INHALT wird uebertragen, kein Link.
Blob wird beim Mount vorgeladen (Ladezustand 'laedt ...'); der
Endpoint /files/download schreibt kein Audit-Log, daher kein Spam.
Security R131 damit vollstaendig erledigt: im Drag steckt weder eine
Server-URL noch ein Token -> ein Fehl-Drop kann gar nichts mehr leaken
(kein Access-, kein Download-Token). Token-Cache/getDownloadToken im
Drag-Pfad entfernt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Zusaetzliches Mozilla-Drag-Format (URL\nTitel), damit Thunderbird und
Firefox als Drop-Ziel zuverlaessiger auf den Drag reagieren. Weiterhin
mit dem 60s-Download-Token.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Das Weglassen von text/uri-list + text/plain (optionale R131-Haertung
#2) hatte den Drop ins Mail-Fenster gebrochen: web-basierte Ziele
verstehen DownloadURL (Nativ-Format) nicht und reagierten gar nicht.
Jetzt werden im Normalfall wieder alle drei Formate gesetzt - aber mit
dem kurzlebigen 60s-Download-Token (R131-Empfehlung #1, der eigentliche
Fix des Token-SCOPE). Ein Fehl-Drop zeigt damit hoechstens eine 60s
gueltige Downloads-only-URL statt des 15-Min-Voll-Access-Tokens.
Nur im seltenen Warm-up-Rennen wird ausschliesslich DownloadURL mit
Access-Token-Fallback gesetzt (kein Klartext-Leak).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Der R131-Fix hatte den Drag per preventDefault() abgebrochen, wenn der
60s-Download-Token beim Griff noch nicht vorgewaermt war -> im Mail-
Fenster kam gar keine Datei mehr an.
Jetzt wird der Drag nie abgebrochen: bevorzugt der vorgewaermte
Download-Token, im seltenen Rennen Fallback auf den Access-Token, aber
weiterhin NUR in DownloadURL (kein text/plain) -> kein Klartext-Leak.
DownloadURL ist ein Nativ-Format und wird bei Fehl-Drop in Web-Text-
felder nicht als lesbarer Text ausgegeben, daher bleibt der R131-Fix
(kein Token-Leak als Text) erhalten.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pentest R131 (LOW-MEDIUM): Der Drag-Button haengte den 15-Min-Access-
Token an die URL und schrieb ihn per text/plain + text/uri-list beim
Drag mit. Ein Fehl-Drop in ein Text-/Chat-/URL-Feld haette den vollen
Access-Token (alle Berechtigungen, 15 Min) als lesbaren Text geleakt.
Fix:
- fileUrl() akzeptiert jetzt optionalen expliziten Token.
- PdfDragButton nutzt den kurzlebigen 60s-Download-Token
(authApi.getDownloadToken(), type:download, nur ?token=) statt des
Access-Tokens. Modul-weiter Cache mit Dedup, auf mount + hover
vorgewaermt (dragstart ist synchron, kann nicht awaiten).
- Es wird NUR noch DownloadURL im DataTransfer gesetzt, keine
text/plain- oder text/uri-list-Repraesentation -> Fehl-Drop in ein
Textfeld erzeugt gar keinen sichtbaren Text.
- Klick-Vorschau nutzt ebenfalls bevorzugt den Download-Token.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kleines Info-Icon hinter dem Drag-Element mit Tooltip, dass das Ziehen
nur in Chrome/Edge funktioniert und man in Firefox stattdessen klicken
soll. Gilt automatisch an allen Einbaustellen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Tab-Leiste ist mit dem zusaetzlichen Geworben/angeworben-Tab zu breit
geworden; der letzte Tab lief aus dem Karten-Rahmen. nav auf flex-wrap
umgestellt (gap-x-6 gap-y-1), damit ueberzaehlige Tabs in eine zweite
Zeile umbrechen und innerhalb der Karte bleiben.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beziehungs-Dropdown erweitert um Schwiegertochter/Schwiegersohn,
Schwiegermutter/Schwiegervater, Oma/Opa, Uroma/Uropa (Whitelist
front- und backend synchron).
Bearbeiten-Stift pro Zeile (vor der Muelltonne): Beziehung und/oder
Gegen-Kunde aenderbar. Neuer PUT /:customerId/referrals/:referralId
mit Whitelist-Pruefung, Doppel-Werber-409 (Self-Ausschluss),
Portal-Block und UPDATE-Auditeintrag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Die Cache-Header waren schon optimal (index.html no-store, Assets
immutable) – aber eine bereits laufende SPA holt sich nach einem
Deploy keinen neuen Code, bis der Nutzer neu lädt. Das fängt kein
Cache-Header ab.
useVersionCheck holt die (server-seitig no-store) index.html
periodisch (5 min) + beim Zurückkehren zum Tab, extrahiert die Menge
der referenzierten Vite-Asset-Hashes als Signatur und vergleicht sie
mit dem Startstand. Ändert sie sich (= neuer Build deployt), zeigt
UpdateBanner oben im Layout einen dezenten Hinweis mit "Jetzt neu
laden".
Kein Backend-/Build-Change; im Dev-Modus No-op (Vite-Dev liefert
keine /assets/<hash>-Dateien → leere Signatur → kein Alarm).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Neuer Tab "Spam" (zwischen Gesendet und Papierkorb), zeigt den Junk-/
Spam-Ordner des gewählten Postfachs – damit fälschlich als Spam
einsortierte Mails auffindbar sind.
Backend:
- EmailFolder-Enum um SPAM erweitert (Migration, Wert angehängt →
kein Rewrite bestehender Zeilen).
- imapService.findJunkFolderPath: ermittelt den Junk-Ordner per
Special-Use-Flag \Junk + üblicher Namensliste (Junk/Spam/…).
- syncAllFoldersForAccount synct den Junk-Ordner zusätzlich als
dbFolder=SPAM (syncEmailsForAccount bekommt dbFolder-Option).
- getCachedEmails + getFolderCountsForAccount um SPAM erweitert.
- Papierkorb-Move/Restore für Spam-Mails nutzt den echten Junk-Pfad
als Quell-/Zielordner.
Frontend: Tab + Badge (ungelesen/gesamt), gleicher List-/Detail-Pfad
wie INBOX; Zuordnen-zu-Vertrag auch aus Spam möglich.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug 1 (Header gestaucht): kompakteres Vertical-Spacing im
Header (px-4 pt-3 pb-2 statt p-4, space-y-2 statt -3),
Subject auf max 2 Zeilen begrenzt mit vollem Titel im
tooltip. min-h-0 auf der Flex-Container, damit der Body
tatsächlich flex-1 claimt.
Bug 2 (Link unsichtbar): Corporate-CTAs stylen ihre Buttons
gerne mit inline style="color:white;background:green"; der
zugehörige <style>-Block wird von DOMPurify entfernt, das
inline-style am <a> überlebt aber – Ergebnis: weißer Link
auf weißem CRM-Hintergrund, unsichtbar. Fix: von <a>-Tags
werden style und class gestrippt, dann greift Browser-
Default (blau/violett + unterstrichen) wieder.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
Flexbox-Items haben default min-width: auto. Ohne overflow-auto
(das ich in 6479598 rausgenommen habe, damit Sticky-Header
funktionieren) wächst der Wrapper mit jedem breiten Kind – lange
E-Mail-Adressen, breite Tabellen, PDF-URL-Anhänge – horizontal
über die Viewport-Breite hinaus.
min-w-0 cappt die Flex-Basis auf die verfügbare Breite, ohne
einen Scroll-Kontext zu schaffen (im Gegensatz zu overflow-auto),
also bleiben die Sticky-Header weiter wirksam.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Der Content-Wrapper hatte overflow-auto – das machte ihn zum
Scroll-Ancestor jedes Sticky-Elements, obwohl er wegen der
flexbox-min-h-auto-Regel nie selbst scrollte. Sticky-Header
in ContractDetail und ContractForm blieben deshalb nicht oben
kleben, weil der Scroll-Kontext, an den sie gebunden waren,
gar nicht scrollte.
Fix: overflow-auto entfernt. Jetzt scrollt wieder das Window,
Sticky sitzt an der Viewport-Kante. EmailClientTab arbeitet
mit calc(100vh - 240px) und ist von der Änderung nicht
betroffen.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Neben jeder Dokument-Zeile sitzt jetzt ein "Vorschau"-Link mit
ExternalLink-Icon, der die PDF in einem neuen Tab öffnet (via
viewUrl mit Token-Auth, inline-disposition). Klick darauf
schaltet bewusst NICHT die Checkbox um – die Auswahl bleibt,
nur das Dokument geht in einem zweiten Tab auf.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User-Bug: bei vielen E-Mails wuchs die Liste links unbegrenzt nach
unten, sodass die ganze Seite gescrollt werden musste.
- ContractEmailsSection: flex-Container von minHeight:400 auf
feste 600px Höhe gestellt. Die linke Liste hatte schon
overflow-y-auto – jetzt greift's auch.
- EmailClientTab: h-full auf calc(100vh - 240px) (mit
minHeight:500) bounded. h-full hat im Tab-Container vorher
nichts gebracht, weil der Parent selbst keine feste Höhe
hatte.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Modal ist für Mails AN den Anbieter gedacht – interne CRM-Nummern
interessieren dort niemanden.
- formatCustomerBlock: customer.customerNumber (intern) raus,
stattdessen contract.customerNumberAtProvider rein.
- formatContractBlock: interne contractNumber raus, restliche
Anbieter-/Vertriebsplattform-Nummern bleiben.
- Previews ziehen ebenfalls auf customerNumberAtProvider /
contractNumberAtProvider um, mit Hinweis-Text wenn keine
Anbieter-Nummer hinterlegt ist.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bank- und Ausweis-Section haben jetzt jeweils zwei unabhängige
Checkboxen statt der bisherigen Section-Checkbox + Sub-Attach:
- Bank: "Letzte 4 IBAN-Stellen einfügen" + "Bankkarte als PDF
anhängen". Text-Variante zeigt nur "IBAN endet auf: XXXX" – keine
volle IBAN/BIC/Bank-Liste mehr (Mail-Hygiene).
- Ausweis: "{Typ}-Nummer einfügen" + "{Typ} als PDF anhängen".
Text-Variante zeigt nur die Nummer, keine Behörde/Daten.
Alle drei Kombinationen "nur Text", "nur PDF" und "beides" sind
damit möglich, "keins von beidem" entspricht der Section-aus.
Schalter sind disabled wenn der jeweilige Wert (IBAN /
documentNumber / documentPath) nicht vorhanden ist.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
In der "Anrede & Name"-Section neue Radio-Wahl, sobald die
Section aktiv ist:
- Stammdaten-E-Mail (customer.email) – default wenn vorhanden
- Absender-Adresse (Postfach von dem gesendet wird)
- Keine E-Mail einfügen
Wird in den Customer-Block-Builder durchgereicht und ersetzt die
fix verdrahtete customer.email-Zeile. Wenn die Stammdaten-Mail
fehlt, ist der Radio "Stammdaten" disabled und der Default
springt auf "Absender".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Zwei neue Buttons im Compose-Modal (nur sichtbar bei Vertrag-
Kontext):
- Vertragsdokumente: listet alle am Vertrag gespeicherten
ContractDocuments gruppiert nach documentType. Auswahl →
Token-Download via fileUrl → base64 → Anhang.
- Kundendaten einfügen: zeigt Sections nur wenn Daten vorhanden
(Customer, Lieferadresse, ggf. Rechnungsadresse, Vertrag, Bank,
Ausweis). Bei Bank/Ausweis zusätzlich Sub-Checkbox "als PDF
anhängen" wenn documentPath vorhanden. Text-Blöcke ans Body-
Ende, PDFs in attachments[]. 25-MB-Limit beidseitig geprüft.
Helpers in composeAttachmentHelpers.ts:
- serverFileToAttachment(path, filename) für Token-URL→Blob→base64
- totalAttachmentBytes mit ~33% base64-Overhead
- sprechende Dateinamen via bankCardAttachmentName /
identityDocAttachmentName
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Bug: Im Vertrags-Tab (Gesendet/Gelöscht) und im Kunden-Haupt-
Postfach (Gelöscht) wurden Mails aus ALLEN Postfächern angezeigt,
unabhängig vom ausgewählten Postfach. Im Vertrag fehlte zusätzlich
der Vertrags-Filter im Papierkorb.
Backend:
- getEmailsForContract akzeptiert accountId → stressfreiEmailId
- getTrashEmails (controller + service) nimmt {accountId, contractId}
- getFolderCountsForContract bekommt optional stressfreiEmailId und
zusätzlich trash/trashUnread im Result
Frontend:
- API-Client (getForContract/getTrash/getContractFolderCounts) nimmt
Filter entgegen
- ContractEmailsSection reicht selectedAccountId in alle drei Queries
+ queryKey durch. Trash-Badge kommt jetzt aus contract-scoped
Counts statt account-globalem stressfreiEmailApi
- EmailClientTab reicht selectedAccountId in die Trash-Query durch
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stage: 2 Handy-JPGs → 23 MB PDF. Smartphone-Fotos haben
4000-6000 px Kante, das macht auch ohne Re-Encode 5-10 MB pro
Bild → PDF wird riesig.
Beim Hinzufügen werden Bilder jetzt auf max 2400 px lange Kante
runterskaliert (~290 DPI auf A4 = Druckqualität) und als JPEG mit
Quality 0.92 (Lightroom-Default) persistiert. Vorschau, Rotation/
Flip und PDF-Embed laufen alle auf dem skalierten Bild.
Erwartete Größe: 2 Handy-Fotos ≈ 1-2 MB PDF.
Klick auf die Vertragsnummer öffnet weiterhin im selben Tab
(via React-Router Link). Neues Icon daneben öffnet den Vertrag in
einem neuen Browser-Tab – analog zum Pro-Tab-Link in CustomerDetail.
Tabs-Komponente bekommt optionalen tabHrefBuilder(tabId)-Prop.
Wenn gesetzt, erscheint neben jedem Tab-Label ein kleines
ExternalLink-Icon, das den Tab via ?tab=<id> in einem neuen
Browser-Tab öffnet.
CustomerDetail übergibt den Builder. URL-Param wird eh schon
für den Tab-Sync genutzt – Anhängen reicht.
Click-stopPropagation verhindert, dass der Klick auf das Icon
gleichzeitig den Tab-Wechsel auslöst.
Nach DOMPurify-Sanitize alle <a>-Elemente auf target="_blank" +
rel="noopener noreferrer" setzen. Letzteres verhindert
window.opener-Tab-Hijacking. Sanitize + DOM-Walk in useMemo, läuft
nur bei Wechsel der Email neu.
1. AddressModal: Straße-Feld ließ sich nicht editieren. setFormData
wurde im Render-Body aufgerufen, wenn formData.street !==
address.street → Reset bei jedem Tastendruck. In useEffect mit
[address?.id]-Dependency umgezogen.
2. Multer-Limit von 10 MB auf 25 MB in upload.routes.ts,
gdpr.routes.ts, contract.routes.ts. Zwei Handy-Fotos zu PDF
kratzten am alten Limit. FileUpload-Hinweistext angepasst.
Stage-Bug: 2 Handy-JPGs à 2 MB → PDF >10 MB → Multer 413. Ursache:
Canvas-Re-Encode mit JPEG-Quality 1.0 blies jedes Bild auf 8-15 MB
auf (Quality 100 % ≠ "identisch zum Original", sondern "möglichst
viele Bits pro Pixel" – ein schon JPEG-komprimiertes Smartphone-
Foto wird so künstlich 4-8× größer).
Fix 1: Wenn Rotation/Flip unverändert (Standardfall), Original-
DataURL 1:1 in die PDF einbetten – kein Canvas-Roundtrip, keine
Quality-Aufblähung. 2-MB-JPEG bleibt 2 MB. Format-Detection per
data:image/png-Prefix (PNG vs JPEG).
Fix 2: Bei Transformation toDataURL('image/jpeg', 0.95) statt 1.0.
Visuell identisch für Foto-Inhalte, 50-70 % kleiner.
Kombiniert: 2 untransformierte Handy-Fotos ≈ 4 MB PDF (vorher
16-30 MB), 2 gedrehte ≈ 5-8 MB.