backend/.env war seit "first commit" getrackt (mit echten Secrets:
JWT_SECRET, ENCRYPTION_KEY, DB-Password). Das Pattern .env war zwar
in .gitignore, wirkte aber nicht rückwirkend.
- git rm --cached backend/.env (Datei bleibt lokal)
- backend/.gitignore + frontend/.gitignore: explizite !.env.example
Whitelist zur Klarstellung
- Neue Root-.gitignore mit gemeinsamen Patterns (Env, OS, IDE, Logs)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vorher: nur "Zurück / Weiter". Jetzt:
[«] [‹] [1] [2] [3] [4] [5*] [6] [7] [8] [9] [10] [›] [»]
10 Seitenzahlen-Buttons, current centered (clamped an Anfang/Ende).
Zusätzlich Doppelpfeile für erste/letzte Seite. Kompakt + verständlich
auch bei 50+ Seiten.
Helper paginationWindow() rechnet das Fenster aus, sodass bei
totalPages <= 10 alle gezeigt werden, sonst current ungefähr mittig
mit Clamp an die Ränder.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Backend: DELETE /api/monitoring/events (settings:update). Optional
?olderThanDays=N – nur Events älter als N Tage löschen.
Hinterlässt selbst einen Audit-Eintrag "Log geleert: X Einträge"
mit User-E-Mail + IP, damit der Vorgang nachvollziehbar bleibt.
- Frontend: "Log leeren"-Button öffnet Bestätigungs-Modal mit
optionalem "älter als X Tage"-Filter. Roter Bestätigungs-Button.
- Frontend: PageSize-Selector (10/25/50/100/200) neben dem Header.
Wechsel setzt automatisch zurück auf Seite 1.
Live-verifiziert: Clear löscht 10 Events, schreibt 1 Audit-Event,
PageSize=5 wird in pagination respektiert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Loose Ends aus Runde 5/7 abgearbeitet.
🛡 DNS-Rebinding-Schutz in SSRF-Guard
- safeResolveHost() löst Hostname zu IPv4+IPv6 auf, prüft jede IP
gegen die Block-Liste, gibt {ip, servername} zurück.
- Caller (test-connection, test-mail-access) übergibt host=ip plus
servername=hostname an die Mail-Services. Damit kann ein zweiter
DNS-Lookup zur Connection-Zeit nicht plötzlich auf interne IPs
umlenken (rebound-Attack).
- ImapCredentials/SmtpCredentials um optionales servername-Feld
erweitert; Services nutzen es als TLS-SNI / Cert-Validation-Hint.
🔒 Per-File-Ownership-Check (DSGVO-Härtung)
- express.static('/api/uploads') ersetzt durch GET /api/files/download
mit Pfad→Resource→Owner-Mapping in fileDownload.service.ts.
- 12 subDir-Mappings (bank-cards, documents, contract-documents,
invoices, cancellation-*, authorizations, business-/commercial-/
privacy-, pdf-templates).
- canAccessCustomer / canAccessContract / Permission-Check je nach
Owner-Typ. Portal-User sieht jetzt nur eigene Dateien, selbst wenn
er fremde Filenames kennt.
- Backwards-Compat: /api/uploads/* bleibt als Shim erhalten, ruft
intern denselben Owner-Check.
- Frontend fileUrl() zeigt auf /api/files/download?path=...&token=...
Live-verifiziert:
- Eigene Datei: 200, random Pfad: 404, ../etc/passwd: 400, kein
Token: 401, Backwards-Compat-Shim: 200.
- DNS-Rebinding: nip.io-Hostname mit interner Target-IP wird via
DNS-Lookup geblockt; gmail.com (legitim) geht durch.
Bewusst nicht gemacht:
- Signierte URLs mit kurzlebigen Download-Tokens – v1.2-Item, da
invasiv für <a href>-Flows ohne JS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Live-Pentest gegen Dev-Server + 3 parallele Audit-Agents.
🚨 CRITICAL: /api/uploads/* war ohne Auth erreichbar
- express.static('/api/uploads', ...) → jeder konnte mit ratbarer URL
sensible PDFs (Kündigungsbestätigungen, Ausweise, Bankkarten,
Vollmachten) ziehen. Live-verifiziert: 23-KB-PDF eines echten Kunden
ohne Login geladen.
- Fix: authenticate-Middleware vor static-Handler (req.query.token
unterstützung war schon da, jetzt aktiv genutzt).
- Frontend: utils/fileUrl.ts hängt JWT als ?token=... an. 24 direkte
/api${...Path}-URLs in 5 Dateien per Skript migriert (CustomerDetail,
ContractDetail, InvoicesSection, PdfTemplates, GDPRDashboard).
🚨 HIGH: Login-Timing User-Enumeration
- bcrypt.compare wurde nur bei existierenden Usern ausgeführt → 110ms
vs 10ms Differenz, Email-Enumeration trivial messbar.
- Fix: Dummy-bcrypt-compare bei invalid user (Cost 12). Plus Lazy-
Rehash bei erfolgreichem Login: alte Cost-10-Hashes (z.B. admin aus
Installation) werden auf BCRYPT_COST upgraded, damit Dummy- und
Echt-Hash-Cost zusammenpassen.
- Live-verifiziert nach Admin-Rehash: 422ms (invalid) vs 423ms (valid)
– Side-Channel dicht.
🚨 HIGH: XSS via Privacy-Policy/Imprint-HTML
- 4 Frontend-Seiten renderten Backend-HTML ohne DOMPurify
(PortalPrivacy, ConsentPage, PortalWebsitePrivacy, PortalImprint).
Admin-eingegebene <script>-Tags wären bei jedem Portal-Kunden-
Besuch ausgeführt worden – auch auf der öffentlichen Consent-Seite.
- Fix: DOMPurify.sanitize mit strikter FORBID_TAGS/ATTR Config.
🛡 HIGH: IDOR-Härtung an Upload-/Document-Endpoints
- canAccessContract jetzt in: uploadContractDocument,
deleteContractDocument, handleContractDocumentUpload (Kündigungs-
Letter+Confirmation), handleContractDocumentDelete,
saveAttachmentAsContractDocument.
- Defense-in-Depth: aktuell durch requirePermission abgesichert,
schützt auch gegen künftige Staff-Scoping-Rollen.
Offen für v1.1:
- Per-File-Ownership-Check für /api/uploads (Kontroll-Lookup
welche Ressource zur Datei gehört)
- TipTap-Link-Tool javascript:-Protokoll blockieren
- Prisma-Error-Messages in Admin-Endpoints generisch sanitisieren
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Beim automatischen Status-Wechsel wird jetzt auch das passende Datum gesetzt,
damit Status und Datumsfeld konsistent sind (Cockpit-Warnung "Datum fehlt"
verschwindet sofort nach Upload).
Backend:
- Upload-Handler für Kündigungsbestätigung(s-Optionen) nimmt optional
`confirmationDate` aus multipart an, speichert als
cancellationConfirmationDate / cancellationConfirmationOptionsDate.
Fallback: heute (nur falls Feld noch leer war).
- maybeActivateOnDeliveryConfirmation nimmt optional deliveryDate, setzt
Contract.startDate falls leer. Fallback: heute.
Frontend:
- ContractDetail: neues kleines Modal beim Kündigungsbestätigungs-Upload
fragt das Bestätigungs-Datum ab (Default: heute oder bereits gesetzter
Wert). Der bestehende inline-Datums-Editor bleibt für spätere Korrekturen.
- ContractDocumentsSection: Datums-Input erscheint conditional im
Upload-Bereich, sobald Typ "Lieferbestätigung" gewählt ist.
- SaveAttachmentModal (E-Mail-Anhang → Vertragsdokument): gleicher
Datums-Input conditional für "Lieferbestätigung".
- API-Methoden uploadCancellationConfirmation / uploadDocument /
saveAttachmentAsContractDocument nehmen optional Datum entgegen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Die drei letzten wichtigen Features für ein produktionsreifes 1.0.0:
## 1. Passwort vergessen-Flow
Der klassische Selfservice-Reset per Email – sowohl für Mitarbeiter als auch
für Portal-Kunden. User können sich nicht mehr aussperren, Admin muss nicht
mehr manuell eingreifen.
- Neues Link "Passwort vergessen?" auf Login-Seite
- PasswordResetRequest: Email + Typ-Auswahl (Mitarbeiter / Portal)
- PasswordResetConfirm: Token-basierte Bestätigung + neues Passwort (min 6 Zeichen)
- Token ist 2 Stunden gültig, dann muss neu angefordert werden
- Token ist kryptografisch sicher (crypto.randomBytes(32))
- User-Enumeration-Schutz: Backend gibt immer 200 zurück, egal ob Email existiert
- Nach erfolgreichem Reset werden ALLE bestehenden Sessions gekickt
(tokenInvalidatedAt gesetzt) – falls jemand parallel eingeloggt war
DB:
- User.passwordResetToken + passwordResetExpiresAt
- Customer.portalPasswordResetToken + portalPasswordResetExpiresAt
## 2. Rate-Limiting gegen Brute-Force
Mit express-rate-limit:
- Login: 10 Versuche pro 15 Minuten pro IP. Erfolgreiche zählen nicht mit.
- Passwort-Reset-Request: 5 Versuche pro Stunde pro IP (Mail-Flut verhindern)
Sowohl Mitarbeiter-Login als auch Portal-Login geschützt.
## 3. Auto-Geburtstagsgrüße per Cron
Das autoBirthdayGreeting-Flag hatten wir schon, aber kein Scheduler der
ihn wirklich abschickt. Jetzt:
- Läuft täglich um 08:00 Uhr
- Findet Kunden mit heutigem Geburtstag + autoBirthdayGreeting=true
- Nur Email-Kanal (Messenger brauchen Browser-Klick)
- Catch-up 30s nach Server-Start: wenn Server am Geburtstag down war, wird
beim nächsten Boot nachgeholt
- lastBirthdayGreetingYear verhindert Doppelversand
Dependencies: node-cron, @types/node-cron, express-rate-limit
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Problem: Das 'Bezeichnung für Kunden-E-Mails'-Feld (UI-Label) war irreführend.
Ein User hat dort die Domain 'stressfrei-meyer.xyz' eingetragen statt ins
Domain-Feld – das eigentliche Domain-Feld blieb unverändert, und das Label
zeigte dann unpassend die Domain-Schreibweise.
Fix: Das Label-Feld ist in 99% aller Fälle nicht nötig, weil es automatisch
aus der Domain abgeleitet wird (stressfrei-wechseln.de → Stressfrei-Wechseln).
Der Edge-Case 'komplett anderer Anzeigetext als aus Domain ableitbar' kommt
selten vor und kann später bei Bedarf über direkten DB-Zugriff/Developer-Panel
gesetzt werden.
Das Schema-Feld bleibt erhalten (für zukünftige Erweiterungen), nur das
Formular-Feld ist weg. Stattdessen Hinweistext unter dem Domain-Feld:
'Wird auch für die Kunden-E-Mail-Adressen genutzt (z.B. name@...)'
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Problem: Nach dem Ändern der Provider-Domain blieb die alte Domain
(stressfrei-wechseln.de) im Adress-Hinzufügen-Dialog bestehen, weil der
Frontend-Hook useProviderSettings() einen 5-Minuten staleTime hat und
nicht invalidiert wurde.
Fix:
- In allen Provider-Mutations (create/update/delete) wird jetzt auch
'email-provider-public-settings' invalidiert → Domain & Label greifen
sofort in allen Komponenten
Zusätzlich Domain-Validierung eingebaut:
- Frontend: pattern am Input + Live-Fehlermeldung
Format: name.tld (mit Subdomains erlaubt, z.B. mail.meine-firma.de)
Input auto-lowercase + trim
- Backend: validateDomain() in createProviderConfig/updateProviderConfig
Wirft Error mit sprechender Meldung bei ungültigem Format
- Schützt vor Versehen im UI + direkten API-Aufrufen
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Alle hardcoded Referenzen auf 'stressfrei-wechseln.de' und 'Stressfrei-Wechseln'
durch dynamische Werte aus der EmailProviderConfig ersetzt. Notwendig für
Multi-Mandanten-Betrieb, wenn das CRM an Dritte vermietet wird.
Schema:
- Neues Feld EmailProviderConfig.customerEmailLabel (String?)
- Wenn leer, wird Label aus Domain abgeleitet ('stressfrei-wechseln.de' → 'Stressfrei-Wechseln')
Backend:
- Neuer Endpoint GET /api/email-providers/public-settings liefert { domain, customerEmailLabel }
- Neue Service-Funktionen: getProviderPublicSettings(), deriveLabelFromDomain()
- create/updateProviderConfig erweitert um customerEmailLabel
Frontend:
- Neuer Hook useProviderSettings() mit Auto-Caching
- Neues Eingabefeld 'Bezeichnung für Kunden-E-Mails' im Provider-Modal
- Dynamische Domain-Suffix im Adress-Hinzufügen-Dialog (@<domain>)
- Tab-Label 'Stressfrei-Wechseln' im Kunden-Detail → dynamisch
- 'Stressfrei-Wechseln Adresse' in ContractForm → dynamisch
- '(Stressfrei-Wechseln)' Badge in ContractDetail → dynamisch
- 'Stressfrei-Wechseln E-Mail' im Generate-Modal → dynamisch
- Leere-Zustand-Meldungen in Tab und E-Mail-Client → dynamisch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bisher blieb ein fehlgeschlagener IMAP-Sync oder E-Mail-Versand still – der User
sah nur im Browser-Devtools, dass etwas schief lief. Jetzt erscheint eine rote
Toast-Benachrichtigung (8 Sekunden) mit der konkreten Fehlermeldung des Servers,
z.B. 'Sync fehlgeschlagen: IMAP-Authentifizierung fehlgeschlagen: NO [AUTHENTICATIONFAILED]'.
EmailClientTab (Synchronisieren-Button):
- toast.success bei erfolgreichem Sync
- toast.error bei Fehler + bei Backend-Response mit success=false
ComposeEmailModal (Senden):
- toast.success bei erfolgreichem Versand
- toast.error bei SMTP-Fehler mit Server-Response (zusätzlich zum Inline-Fehler)
Außerdem im imapService.testImapConnection:
- Roh-Error wird jetzt geloggt (code, response, responseStatus, authenticationFailed)
- ImapFlow-spezifische Felder werden in die Fehlermeldung übernommen, sodass
z.B. '2 NO [AUTHENTICATIONFAILED] Authentication failed.' direkt sichtbar wird
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Das bestehende „Verbindung testen" prüft nur den API-Zugang (Plesk/cPanel),
nicht den eigentlichen IMAP/SMTP-Zugang der System-E-Mail. Das führte dazu,
dass Anhang-Downloads scheiterten obwohl der API-Test grün war.
Neuer Button im EmailProviders-Modal: „E-Mail-Zugang testen (IMAP + SMTP)"
- Testet IMAP-Empfang und SMTP-Versand separat
- Zeigt pro Protokoll Erfolg oder Fehlermeldung mit Server/Port/Verschlüsselung
- Nutzt die hinterlegte System-E-Mail-Adresse + Passwort
- Funktioniert auch vor dem ersten Speichern (mit Formulardaten)
Außerdem im Anhang-Download:
- Retry-Mechanismus bei transienten TLS/Netzwerk-Fehlern (3 Versuche)
- Socket-Timeout 30s gegen hängende Verbindungen
- Sprechende Fehlermeldungen (z.B. Hinweis auf selbstsigniertes Zertifikat)
- Debug-Logging mit Host/Port/User/Folder/UID
Backend:
- Neuer Endpoint POST /api/email-providers/test-mail-access
- fetchAttachment in imapService: Retry-Wrapper + fetchAttachmentInner
- Besseres Error-Handling in downloadAttachment (Cert-Hinweis, Auth, Timeout)
Frontend:
- emailProviderApi.testMailAccess()
- EmailProviders-Modal: neuer Button + zweispaltige Ergebnis-Anzeige für IMAP+SMTP
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ein neues System um Stammdaten-Kataloge zwischen Installationen zu teilen –
explizit ohne Kundendaten, Verträge oder Einstellungen.
**Was wird exportiert:**
- Anbieter + zugehörige Tarife
- Kündigungsfristen
- Vertragslaufzeiten
- Vertragskategorien
- PDF-Auftragsvorlagen (JSON + PDF-Dateien + Feldzuordnungen)
**Was NICHT:**
- Kundendaten, Verträge, Dokumente, Emails, SMTP-Einstellungen
→ dafür gibt es den Datenbank-Backup
**Neue Einstellungsseite /settings/factory-defaults:**
- Zeigt Anzahl pro Kategorie (Anbieter, Tarife, Fristen, …)
- "Exportieren"-Button lädt ZIP herunter (manifest.json + JSONs + PDFs)
- Import-Anleitung inline
**Import-Script:**
- `npm run seed:defaults` (tsx scripts/seed-factory-defaults.ts)
- Liest alle JSON-Dateien aus backend/factory-defaults/*/*.json
- Merged mehrere Dateien automatisch pro Kategorie (unique-key gewinnt zuletzt)
- Upsertet idempotent → kann mehrfach ausgeführt werden
- Kopiert PDF-Vorlagen aus factory-defaults/pdf-templates/ nach uploads/pdf-templates/
- Alte PDF-Dateien werden beim Re-Import entsorgt
Backend:
- services/factoryDefaults.service.ts: collectFactoryDefaults() + exportFactoryDefaults()
- controllers/factoryDefaults.controller.ts: preview + export
- routes/factoryDefaults.routes.ts: GET /api/factory-defaults/preview + /export
- scripts/seed-factory-defaults.ts: CLI-Import-Script
- .gitignore: factory-defaults/* außer .gitkeep und README.md
Frontend:
- pages/settings/FactoryDefaults.tsx: Übersicht + Export-Button
- Settings-Karte „Factory-Defaults" im System-Abschnitt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Das useInformalAddress-Feld war:
1. Im Frontend-Submit-Handler nicht in submitData enthalten (wurde bei jedem Update rausgefiltert)
2. Im Service-Type nicht definiert (TypeScript-mäßig unbekannt)
3. Beim Laden im Edit-Mode: Boolean aus DB matchte nicht das String-value des <select>
Fixes:
- Frontend: submitData enthält jetzt useInformalAddress (String oder Boolean → sauberes Boolean)
- Frontend: beim reset() wird Boolean zu 'true'/'false' konvertiert für <select>
- Backend: Service-Type erweitert um useInformalAddress, autoBirthdayGreeting, autoBirthdayChannel
- Backend: Audit-Feldlabels für die neuen Felder
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Der SaveAttachmentModal hat jetzt drei Modi (wenn E-Mail einem Vertrag zugeordnet ist):
1. Als Dokument – in feste Slots (Kündigungsschreiben etc.), unverändert
2. Als Vertragsdokument – NEU: flexible ContractDocument-Tabelle mit Typ-Dropdown
(Auftragsformular, Lieferbestätigung, Vertragsunterlagen, Vollmacht,
Widerrufsbelehrung, Preisblatt, Sonstiges) + optionalen Notizen
3. Als Rechnung – jetzt für ALLE Vertragstypen (vorher nur Strom/Gas)
Backend:
- Neuer Endpoint POST /api/emails/:id/attachments/:filename/save-as-contract-document
- saveAttachmentAsInvoice + saveEmailAsInvoice: ELECTRICITY/GAS-Einschränkung entfernt,
nutzt jetzt addInvoiceByContract als Fallback für Nicht-Energie-Verträge
Frontend:
- cachedEmailApi.saveAttachmentAsContractDocument hinzugefügt
- SaveAttachmentModal: neuer Mode 'contractDocument' mit Typ+Notizen
- Mode-Toggle zeigt jetzt alle drei Optionen wenn Vertrag zugeordnet
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Neuer Cake-Button neben dem Geburtsdatum in den Stammdaten öffnet ein Modal
mit drei Funktionen:
1. **Gruß-Marker zurücksetzen** (lastBirthdayGreetingYear → null)
- Für Debugging oder als Fallback, wenn der Kunde den Gruß erneut sehen soll
- Mit Bestätigungsdialog
2. **Geburtstagsgruß jetzt senden** (Email / WhatsApp / Telegram / Signal)
- Email: direkt via System-SMTP mit HTML-Template (Du/Sie-abhängig)
- WhatsApp/Telegram/Signal: öffnet vorbefülltes Fenster mit Gruß-Text
- Text beachtet Du/Sie-Verhältnis (pronomen, possessiv, etc.)
- Mit Bestätigungsdialog
3. **Automatisch senden** – neue Einstellung am Customer
- autoBirthdayGreeting (Boolean) + autoBirthdayChannel (String)
- Für späteren Cron-basierten Automatik-Versand vorbereitet
Backend:
- birthday.service.ts: resetBirthdayGreeting, buildBirthdayGreetingText, getBirthdayGreetingData
- birthday.controller.ts: resetBirthdayGreeting, sendBirthdayGreeting
- Routes: POST /birthdays/:customerId/reset + /send
- Audit-Log bei beiden Aktionen
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Schema:
- Customer.useInformalAddress: Boolean (Default: false = Sie)
- Auch bei Firmenkunden verfügbar (Chef kann man auch duzen)
Frontend:
- Neues Pflichtfeld "Anrede per" (Du/Sie) im Kunden-Formular
- Anzeige als Badge in CustomerDetail-Stammdaten
Geburtstagsgruß im Portal:
- Bei Du: "Herzlichen Glückwunsch, Max! Alles Gute zu deinem 42. Geburtstag!"
- Bei Sie: "Herzlichen Glückwunsch, Herr Müller! Alles Gute zu Ihrem 42. Geburtstag!"
- Konsistent auch bei nachträglichen Glückwünschen (hattest/hatten, bist/sind etc.)
- Backend liefert firstName, lastName, salutation und useInformalAddress
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bisher wurden die Felder nur bei Privatkunden angezeigt. Jetzt sind sie
unabhängig vom Kundentyp verfügbar, z.B. für Ansprechpartner bei Firmen.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- backend/dist, backend/node_modules aus Git-Tracking entfernt (waren bereits in .gitignore)
- frontend/dist, frontend/node_modules ebenfalls entfernt
- Neue frontend/.gitignore erstellt (fehlte komplett)
Die Dateien bleiben lokal erhalten und werden durch npm install + build regeneriert.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Admin (Vertrags-Cockpit):
- Neue Section "Geburtstage" zeigt Kunden mit Geburtstag
- Fenster: -7 bis +30 Tage um heute
- Farbcodierung: heute (pink), vergangen (amber), bevorstehend (grau)
- Anzeige: Name, Kundennummer, Geburtsdatum, Alter, "Heute!" / "In X Tagen" / "Vor X Tagen"
Portal (Kundenportal):
- Modal mit Geburtstagsgruß wenn Geburtstag heute oder in den letzten 7 Tagen war
- Unterscheidet zwischen aktuellem Geburtstag und nachträglichen Glückwünschen
- Schönes Gradient-Design mit Konfetti-Emojis
- Wird pro Jahr nur einmal angezeigt (Customer.lastBirthdayGreetingYear)
- Bestätigung speichert das aktuelle Jahr
Backend:
- Neues Feld Customer.lastBirthdayGreetingYear (Int?)
- Service birthday.service.ts mit Fenster-Logik + Alter-Berechnung
- Endpoints /api/birthdays/upcoming (Admin),
/api/birthdays/my-birthday (Portal GET + POST /acknowledge)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- PDF-Template-Editor in Einstellungen: Vorlagen hochladen, Formularfelder automatisch auslesen, CRM-Felder zuordnen
- PDF-Vorschau mit annotierten Feldnamen, seitenweise Sortierung der Felder
- Auftrag generieren aus Vertragsdaten (Button im Vertrags-Detail)
- Dynamische Rufnummern-Felder mit Vorwahl-Extraktion und konfigurierbarer Maximalanzahl
- Nicht zugeordnete Felder bleiben editierbar im generierten PDF
- Eigentümer-Felder mit Namens-Kombinationen (Firma+Name etc.) und Fallback auf Kundendaten
- Stressfrei-E-Mail als Feld-Option im Template-Editor
- Objekttyp, Lage und Lage des Anschlusses als neue Felder bei Festnetz-Verträgen (DSL, Glasfaser, Kabel)
- Bankverbindung-Fallback: wenn keine am Vertrag verknüpft, wird automatisch die neueste aktive des Kunden genommen
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>