@@ -18,25 +18,34 @@ interface Props {
isOpen : boolean ;
onClose : ( ) = > void ;
contractId : number ;
/**
* E-Mail-Adresse des Postfachs, von dem die Mail abgeschickt wird.
* Wird in der "Anrede & Name"-Section als Alternative zur Stammdaten-
* E-Mail angeboten – User-Wunsch 2026-06-21: bei Kundendaten wählen,
* ob die Customer-Email oder die Stressfrei-Wechseln-Absender-Adresse
* eingefügt wird.
*/
senderEmail : string ;
currentBody : string ;
currentAttachments : EmailAttachment [ ] ;
onResult : ( newBody : string , addedAttachments : EmailAttachment [ ] ) = > void ;
}
type EmailChoice = 'master' | 'sender' | 'none' ;
const MAX_TOTAL_SIZE = 25 * 1024 * 1024 ;
type SectionKey =
| 'customer'
| 'deliveryAddress'
| 'billingAddress'
| 'contract'
| 'iban'
| 'identity' ;
| 'contract' ;
export default function InsertCustomerDataModal ( {
isOpen ,
onClose ,
contractId ,
senderEmail ,
currentBody ,
currentAttachments ,
onResult ,
@@ -61,18 +70,28 @@ export default function InsertCustomerDataModal({
const bankCard = contract ? . bankCard ;
const identityDocument = contract ? . identityDocument ;
// Sections die default-an sind: Anrede + Vertragsdaten. Anhang-Checkboxen
// bleiben default-aus (User-Intent).
// Sections die default-an sind: Anrede + Vertragsdaten. Anhang-/Text-
// Schalter für Bank + Ausweis bleiben default-aus (User-Intent: bewusst
// entscheiden, was vertraulich verschickt wird).
const [ checked , setChecked ] = useState < Record < SectionKey , boolean > > ( {
customer : true ,
deliveryAddress : true ,
billingAddress : false ,
contract : true ,
iban : false ,
identity : false ,
} ) ;
const [ attachBankCard , setAttachBankCard ] = useState ( false ) ;
const [ attachIdentity , setAttachIdentity ] = useState ( false ) ;
// Bank: zwei unabhängige Schalter. Text fügt nur die letzten 4 IBAN-
// Stellen ein (kein vollständiger IBAN-Versand per Mail = Default-Hygiene).
const [ insertBankText , setInsertBankText ] = useState ( false ) ;
const [ attachBankPdf , setAttachBankPdf ] = useState ( false ) ;
// Ausweis: Text-Schalter fügt nur die Ausweisnummer ein, kein Geburtsdatum
// / keine Ausstellungsdaten – falls der Empfänger nur die Nummer braucht.
const [ insertIdentityText , setInsertIdentityText ] = useState ( false ) ;
const [ attachIdentityPdf , setAttachIdentityPdf ] = useState ( false ) ;
// Welche E-Mail-Adresse in der Customer-Section steht:
// - 'master' = Stammdaten-E-Mail (customer.email)
// - 'sender' = Postfach-Adresse, von der die Mail abgeht (Stressfrei)
// - 'none' = E-Mail-Zeile weglassen
const [ emailChoice , setEmailChoice ] = useState < EmailChoice > ( 'master' ) ;
const [ busy , setBusy ] = useState ( false ) ;
// Bei jedem Öffnen sinnvoll vorbelegen (sonst bleiben "checked" stale
@@ -84,11 +103,13 @@ export default function InsertCustomerDataModal({
deliveryAddress : ! ! deliveryAddress ,
billingAddress : false , // nur wenn vorhanden, aber default aus
contract : true ,
iban : false ,
identity : false ,
} ) ;
setAttachBankCard ( false ) ;
setAttachIdentity ( false ) ;
setInsertBankText ( false ) ;
setAttachBankPdf ( false ) ;
setInsertIdentityText ( false ) ;
setAttachIdentityPdf ( false ) ;
// Default: Stammdaten-E-Mail wenn vorhanden, sonst Absender-Adresse.
setEmailChoice ( customer ? . email ? 'master' : 'sender' ) ;
}
} , [ isOpen , contract , customer , deliveryAddress ] ) ;
@@ -108,7 +129,13 @@ export default function InsertCustomerDataModal({
const blocks : string [ ] = [ ] ;
if ( checked . customer && customer ) {
blocks . push ( formatCustomerBlock ( customer , contract ) ) ;
const chosenEmail =
emailChoice === 'master'
? customer . email || ''
: emailChoice === 'sender'
? senderEmail
: '' ;
blocks . push ( formatCustomerBlock ( customer , contract , chosenEmail ) ) ;
}
if ( checked . deliveryAddress && deliveryAddress ) {
blocks . push ( formatAddressBlock ( 'Lieferadresse' , deliveryAddress ) ) ;
@@ -119,10 +146,10 @@ export default function InsertCustomerDataModal({
if ( checked . contract ) {
blocks . push ( formatContractBlock ( contract ) ) ;
}
if ( checked . iban && bankCard ) {
if ( insertBankText && bankCard ) {
blocks . push ( formatBankBlock ( bankCard ) ) ;
}
if ( checked . identity && identityDocument ) {
if ( insertIdentityText && identityDocument ) {
blocks . push ( formatIdentityBlock ( identityDocument ) ) ;
}
@@ -155,13 +182,13 @@ export default function InsertCustomerDataModal({
}
} ;
if ( attachBankCard && bankCard ? . documentPath ) {
if ( attachBankPdf && bankCard ? . documentPath ) {
await tryAttach (
bankCard . documentPath ,
bankCardAttachmentName ( bankCard . iban ) ,
) ;
}
if ( attachIdentity && identityDocument ? . documentPath ) {
if ( attachIdentityPdf && identityDocument ? . documentPath ) {
await tryAttach (
identityDocument . documentPath ,
identityDocAttachmentName (
@@ -188,10 +215,10 @@ export default function InsertCustomerDataModal({
! checked . deliveryAddress &&
! checked . billingAddress &&
! checked . contract &&
! checked . iban &&
! checked . identity &&
! attachBankCard &&
! attachIdentity ;
! insertBankText &&
! attachBankPdf &&
! insertIdentityText &&
! attachIdentityPdf ;
return (
< Modal
@@ -214,6 +241,56 @@ export default function InsertCustomerDataModal({
checked = { checked . customer }
onToggle = { ( ) = > toggle ( 'customer' ) }
preview = { previewCustomer ( customer , contract ) }
extra = {
checked . customer && (
< div className = "mt-2 ml-6 space-y-1" >
< div className = "text-xs font-medium text-gray-600" >
E - Mail im Text :
< / div >
< label className = "flex items-center gap-2 text-xs text-gray-700 cursor-pointer" >
< input
type = "radio"
name = "emailChoice"
checked = { emailChoice === 'master' }
onChange = { ( ) = > setEmailChoice ( 'master' ) }
disabled = { ! customer . email }
className = "text-blue-600"
/ >
< span >
Stammdaten - E - Mail
{ customer . email ? (
< span className = "text-gray-400" > ( { customer . email } ) < / span >
) : (
< span className = "text-gray-400" > ( nicht hinterlegt ) < / span >
) }
< / span >
< / label >
< label className = "flex items-center gap-2 text-xs text-gray-700 cursor-pointer" >
< input
type = "radio"
name = "emailChoice"
checked = { emailChoice === 'sender' }
onChange = { ( ) = > setEmailChoice ( 'sender' ) }
className = "text-blue-600"
/ >
< span >
Absender - Adresse
< span className = "text-gray-400" > ( { senderEmail } ) < / span >
< / span >
< / label >
< label className = "flex items-center gap-2 text-xs text-gray-700 cursor-pointer" >
< input
type = "radio"
name = "emailChoice"
checked = { emailChoice === 'none' }
onChange = { ( ) = > setEmailChoice ( 'none' ) }
className = "text-blue-600"
/ >
< span > Keine E - Mail einfügen < / span >
< / label >
< / div >
)
}
/ >
) }
{ deliveryAddress && (
@@ -239,47 +316,31 @@ export default function InsertCustomerDataModal({
preview = { previewContract ( contract ) }
/ >
{ bankCard && (
< SectionRow
< DualChoiceRow
title = "Bankverbindung"
checked = { checked . iban }
onToggle = { ( ) = > toggle ( 'iban' ) }
preview = { previewBank ( bankCard ) }
extra = {
bankCard . documentPath && (
< label className = "flex items-center gap-2 text-xs text-gray-600 mt-1 ml-6 cursor-pointer" >
< input
type = "checkbox"
checked = { attachBankCard }
onChange = { ( e ) = > setAttachBankCard ( e . target . checked ) }
className = "rounded"
/ >
< span > Bankkarte als PDF anhängen < / span >
< / label >
)
}
textChecked = { insertBankText }
onToggleText = { ( ) = > setInsertBankText ( ( v ) = > ! v ) }
textLabel = "Letzte 4 IBAN-Stellen einfügen"
textDisabled = { ! lastFourIban ( bankCard . iban ) }
pdfChecked = { attachBankPdf }
onTogglePdf = { ( ) = > setAttachBankPdf ( ( v ) = > ! v ) }
pdfLabel = "Bankkarte als PDF anhängen"
pdfDisabled = { ! bankCard . documentPath }
/ >
) }
{ identityDocument && (
< SectionRow
< DualChoiceRow
title = { identityTypeLabel ( identityDocument . type ) }
checked = { checked . identity }
onToggle = { ( ) = > toggle ( 'identity' ) }
preview = { previewIdentity ( identityDocument ) }
extra = {
identityDocument . documentPath && (
< label className = "flex items-center gap-2 text-xs text-gray-600 mt-1 ml-6 cursor-pointer" >
< input
type = "checkbox"
checked = { attachIdentity }
onChange = { ( e ) = > setAttachIdentity ( e . target . checked ) }
className = "rounded"
/ >
< span >
{ identityTypeLabel ( identityDocument . type ) } als PDF anhängen
< / span >
< / label >
)
}
textChecked = { insertIdentityText }
onToggleText = { ( ) = > setInsertIdentityText ( ( v ) = > ! v ) }
textLabel = { ` ${ identityTypeLabel ( identityDocument . type ) } -Nummer einfügen ` }
textDisabled = { ! identityDocument . documentNumber }
pdfChecked = { attachIdentityPdf }
onTogglePdf = { ( ) = > setAttachIdentityPdf ( ( v ) = > ! v ) }
pdfLabel = { ` ${ identityTypeLabel ( identityDocument . type ) } als PDF anhängen ` }
pdfDisabled = { ! identityDocument . documentPath }
/ >
) }
@@ -330,6 +391,69 @@ interface SectionRowProps {
extra? : React.ReactNode ;
}
interface DualChoiceRowProps {
title : string ;
preview : string ;
textChecked : boolean ;
onToggleText : ( ) = > void ;
textLabel : string ;
textDisabled? : boolean ;
pdfChecked : boolean ;
onTogglePdf : ( ) = > void ;
pdfLabel : string ;
pdfDisabled? : boolean ;
}
/**
* Sections, die unabhängig Text und PDF anbieten (Bank, Ausweis).
* Keine primäre Checkbox – beide Schalter wirken einzeln, deshalb
* kein "alle-ein/alle-aus" auf Section-Ebene nötig.
*/
function DualChoiceRow ( {
title ,
preview ,
textChecked ,
onToggleText ,
textLabel ,
textDisabled ,
pdfChecked ,
onTogglePdf ,
pdfLabel ,
pdfDisabled ,
} : DualChoiceRowProps ) {
return (
< div className = "border border-gray-200 rounded-lg p-3" >
< div className = "text-sm font-medium text-gray-700" > { title } < / div >
< div className = "text-xs text-gray-500 mt-1" > { preview } < / div >
< div className = "mt-2 space-y-1" >
< label className = { ` flex items-center gap-2 text-xs cursor-pointer ${ textDisabled ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700' } ` } >
< input
type = "checkbox"
checked = { textChecked }
onChange = { onToggleText }
disabled = { textDisabled }
className = "rounded"
/ >
< span > { textLabel } < / span >
< / label >
< label className = { ` flex items-center gap-2 text-xs cursor-pointer ${ pdfDisabled ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700' } ` } >
< input
type = "checkbox"
checked = { pdfChecked }
onChange = { onTogglePdf }
disabled = { pdfDisabled }
className = "rounded"
/ >
< span >
{ pdfLabel }
{ pdfDisabled && < span className = "ml-1" > ( keine PDF hinterlegt ) < / span > }
< / span >
< / label >
< / div >
< / div >
) ;
}
function SectionRow ( { title , checked , onToggle , preview , extra } : SectionRowProps ) {
return (
< div className = "border border-gray-200 rounded-lg p-3" >
@@ -368,12 +492,23 @@ function fullName(
return parts . filter ( Boolean ) . join ( ' ' ) ;
}
function formatCustomerBlock ( customer : NonNullable < Contract [ 'customer' ] > , contract : Contract ) : string {
// User-Wunsch 2026-06-21: das Modal ist für Mails AN den Anbieter gedacht.
// Die interne CRM-Kundennummer / -Vertragsnummer interessiert dort
// niemanden – relevant ist nur, was der Anbieter selbst vergeben hat
// (`customerNumberAtProvider`, `contractNumberAtProvider`). Wir blenden
// die internen Nummern komplett aus.
function formatCustomerBlock (
customer : NonNullable < Contract [ 'customer' ] > ,
contract : Contract ,
email : string ,
) : string {
const lines : string [ ] = [ 'Kundendaten:' ] ;
lines . push ( fullName ( customer , contract . type ) ) ;
if ( customer . customerNumber ) lines . push ( ` Kundennummer: ${ customer . customerNumber } ` ) ;
if ( contract . customerNumberAtProvider ) {
lines . push ( ` Kundennummer beim Anbieter: ${ contract . customerNumberAtProvider } ` ) ;
}
if ( customer . birthDate ) lines . push ( ` Geburtsdatum: ${ formatDate ( customer . birthDate ) } ` ) ;
if ( customer . email ) lines . push ( ` E-Mail: ${ customer . email } ` ) ;
if ( email ) lines . push ( ` E-Mail: ${ email } ` ) ;
if ( customer . phone ) lines . push ( ` Telefon: ${ customer . phone } ` ) ;
if ( customer . mobile ) lines . push ( ` Mobil: ${ customer . mobile } ` ) ;
return lines . join ( '\n' ) ;
@@ -382,7 +517,9 @@ function formatCustomerBlock(customer: NonNullable<Contract['customer']>, contra
function previewCustomer ( customer : NonNullable < Contract [ 'customer' ] > , contract : Contract ) : string {
return [
fullName ( customer , contract . type ) ,
customer . customerNumber ? ` Kundennummer: ${ customer . customerNumber } ` : '' ,
contract . customerNumberAtProvider
? ` Anbieter-Kdnr.: ${ contract . customerNumberAtProvider } `
: '' ,
]
. filter ( Boolean )
. join ( ' · ' ) ;
@@ -402,9 +539,12 @@ function previewAddress(addr: Address): string {
return ` ${ addr . street } ${ addr . houseNumber } , ${ addr . postalCode } ${ addr . city } ` ;
}
// Interne `contractNumber` raus (User-Wunsch 2026-06-21): für eine Mail
// an den Provider zählt nur die Vertragsnummer, die der Provider selbst
// vergeben hat. Vertriebsplattform-Nummern bleiben drin – die nutzt der
// CRM-Mitarbeiter teilweise auch für die Plattform-Korrespondenz.
function formatContractBlock ( c : Contract ) : string {
const lines : string [ ] = [ 'Vertragsdaten:' ] ;
lines . push ( ` Vertragsnummer: ${ c . contractNumber } ` ) ;
if ( c . provider ? . name ) lines . push ( ` Anbieter: ${ c . provider . name } ` ) ;
if ( c . tariff ? . name ) lines . push ( ` Tarif: ${ c . tariff . name } ` ) ;
if ( c . customerNumberAtProvider ) lines . push ( ` Kundennummer beim Anbieter: ${ c . customerNumberAtProvider } ` ) ;
@@ -418,23 +558,36 @@ function formatContractBlock(c: Contract): string {
}
function previewContract ( c : Contract ) : string {
const parts : string [ ] = [ c . contractNumber ] ;
const parts : string [ ] = [ ] ;
if ( c . contractNumberAtProvider ) {
parts . push ( ` Anbieter-Vtr.: ${ c . contractNumberAtProvider } ` ) ;
} else if ( c . provider ? . name ) {
parts . push ( '(keine Anbieter-Vertragsnummer hinterlegt)' ) ;
}
if ( c . provider ? . name ) parts . push ( c . provider . name ) ;
if ( c . tariff ? . name ) parts . push ( c . tariff . name ) ;
return parts . join ( ' · ' ) ;
}
// User-Wunsch 2026-06-21: nur die letzten 4 IBAN-Stellen einfügen, nicht
// die komplette IBAN/BIC/Bank-Liste. Vollständige Kontonummern per Mail
// versenden ist sowieso heikel – der Empfänger kann sich mit den letzten
// 4 Stellen für Identifikationszwecke ausweisen, ohne dass die ganze
// IBAN im Mail-Verlauf hängenbleibt.
function lastFourIban ( iban : string | undefined | null ) : string {
if ( ! iban ) return '' ;
return iban . replace ( /\s+/g , '' ) . slice ( - 4 ) ;
}
function formatBankBlock ( b : BankCard ) : string {
const lines : string [ ] = [ 'Bankverbindung:' ] ;
if ( b . accountHolder ) lines . push ( ` Kontoinhaber: ${ b . accountHolder } ` ) ;
lines . push ( ` IBAN : ${ b . iban } ` ) ;
if ( b . bic ) lines . push ( ` BIC: ${ b . bic } ` ) ;
if ( b . bankName ) lines . push ( ` Bank: ${ b . bankName } ` ) ;
return lines . join ( '\n' ) ;
const last4 = lastFourIban ( b . iban ) ;
if ( ! last4 ) return '' ;
return ` Bankverbindung: \ nIBAN endet auf : ${ last4 } ` ;
}
function previewBank ( b : BankCard ) : string {
return ` IBAN: ${ b . iban } ${ b . bankName ? ` · ${ b . bankName } ` : '' } ` ;
const last4 = lastFourIban ( b . iban ) ;
return last4 ? ` IBAN … ${ last4 } ` : 'IBAN nicht hinterlegt' ;
}
function identityTypeLabel ( type : IdentityDocument [ 'type' ] ) : string {
@@ -447,18 +600,14 @@ function identityTypeLabel(type: IdentityDocument['type']): string {
}
}
// User-Wunsch 2026-06-21: nur die Ausweisnummer einfügen, keine
// Behörde / Daten – wenn der Empfänger mehr Details braucht, soll er
// die beigefügte PDF benutzen.
function formatIdentityBlock ( d : IdentityDocument ) : string {
const lines : string [ ] = [ ` ${ identityTypeLabel ( d . type ) } : ` ] ;
if ( d . documentNumber ) lines . push ( ` Nummer: ${ d . documentNumber } ` ) ;
if ( d . issuingAuthority ) lines . push ( ` Ausstellende Behörde: ${ d . issuingAuthority } ` ) ;
if ( d . issueDate ) lines . push ( ` Ausstellungsdatum: ${ formatDate ( d . issueDate ) } ` ) ;
if ( d . expiryDate ) lines . push ( ` Gültig bis: ${ formatDate ( d . expiryDate ) } ` ) ;
return lines . join ( '\n' ) ;
if ( ! d . documentNumber ) return '' ;
return ` ${ identityTypeLabel ( d . type ) } - Nummer: ${ d . documentNumber } ` ;
}
function previewIdentity ( d : IdentityDocument ) : string {
const parts : string [ ] = [ ] ;
if ( d . documentNumber ) parts . push ( ` Nr. ${ d . documentNumber } ` ) ;
if ( d . expiryDate ) parts . push ( ` gültig bis ${ formatDate ( d . expiryDate ) } ` ) ;
return parts . join ( ' · ' ) ;
return d . documentNumber ? ` Nr. ${ d . documentNumber } ` : 'Keine Nummer hinterlegt' ;
}