import { useEffect, useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Loader2 } from 'lucide-react'; import toast from 'react-hot-toast'; import Modal from '../ui/Modal'; import Button from '../ui/Button'; import { contractApi, EmailAttachment } from '../../services/api'; import { formatDate } from '../../utils/dateFormat'; import { bankCardAttachmentName, identityDocAttachmentName, serverFileToAttachment, totalAttachmentBytes, } from './composeAttachmentHelpers'; import type { Contract, Address, BankCard, IdentityDocument } from '../../types'; 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'; export default function InsertCustomerDataModal({ isOpen, onClose, contractId, senderEmail, currentBody, currentAttachments, onResult, }: Props) { const { data, isLoading } = useQuery({ queryKey: ['contract', contractId, 'for-insert-data'], queryFn: () => contractApi.getById(contractId), enabled: isOpen, }); const contract = data?.data; const customer = contract?.customer; const deliveryAddress = contract?.address; // Rechnungsadresse nur eigenständig zeigen, wenn sie sich tatsächlich // von der Lieferadresse unterscheidet – sonst doppelt im Text. const billingAddress = useMemo(() => { if (!contract?.billingAddress) return undefined; if (!deliveryAddress) return contract.billingAddress; return contract.billingAddress.id !== deliveryAddress.id ? contract.billingAddress : undefined; }, [contract?.billingAddress, deliveryAddress]); const bankCard = contract?.bankCard; const identityDocument = contract?.identityDocument; // 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>({ customer: true, deliveryAddress: true, billingAddress: false, contract: true, }); // 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('master'); const [busy, setBusy] = useState(false); // Bei jedem Öffnen sinnvoll vorbelegen (sonst bleiben "checked" stale // wenn das Modal mal mit anderen Daten wieder aufgeht). useEffect(() => { if (isOpen && contract) { setChecked({ customer: !!customer, deliveryAddress: !!deliveryAddress, billingAddress: false, // nur wenn vorhanden, aber default aus contract: true, }); 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]); const toggle = (key: SectionKey) => { setChecked((prev) => ({ ...prev, [key]: !prev[key] })); }; const handleClose = () => { if (busy) return; onClose(); }; const handleConfirm = async () => { if (!contract) return; setBusy(true); try { const blocks: string[] = []; if (checked.customer && customer) { const chosenEmail = emailChoice === 'master' ? customer.email || '' : emailChoice === 'sender' ? senderEmail : ''; blocks.push(formatCustomerBlock(customer, contract, chosenEmail)); } if (checked.deliveryAddress && deliveryAddress) { blocks.push(formatAddressBlock('Lieferadresse', deliveryAddress)); } if (checked.billingAddress && billingAddress) { blocks.push(formatAddressBlock('Rechnungsadresse', billingAddress)); } if (checked.contract) { blocks.push(formatContractBlock(contract)); } if (insertBankText && bankCard) { blocks.push(formatBankBlock(bankCard)); } if (insertIdentityText && identityDocument) { blocks.push(formatIdentityBlock(identityDocument)); } const textToInsert = blocks .filter((b) => b.trim().length > 0) .join('\n\n'); // Anhänge sammeln const newAttachments: EmailAttachment[] = []; let runningSize = totalAttachmentBytes(currentAttachments); const tryAttach = async ( documentPath: string | undefined, filename: string, ): Promise => { if (!documentPath) return false; try { const att = await serverFileToAttachment(documentPath, filename); const approxBytes = Math.ceil(att.content.length * 0.75); if (runningSize + approxBytes > MAX_TOTAL_SIZE) { toast.error(`"${filename}" gesprengt das 25-MB-Anhang-Limit.`); return false; } newAttachments.push(att); runningSize += approxBytes; return true; } catch (err: any) { toast.error(err?.message || `Fehler beim Anhängen von "${filename}"`); return false; } }; if (attachBankPdf && bankCard?.documentPath) { await tryAttach( bankCard.documentPath, bankCardAttachmentName(bankCard.iban), ); } if (attachIdentityPdf && identityDocument?.documentPath) { await tryAttach( identityDocument.documentPath, identityDocAttachmentName( identityDocument.type, identityDocument.documentNumber, ), ); } const separator = currentBody && !currentBody.endsWith('\n') ? '\n\n' : ''; const newBody = textToInsert ? currentBody + separator + textToInsert : currentBody; onResult(newBody, newAttachments); onClose(); } finally { setBusy(false); } }; const nothingSelected = !checked.customer && !checked.deliveryAddress && !checked.billingAddress && !checked.contract && !insertBankText && !attachBankPdf && !insertIdentityText && !attachIdentityPdf; return (
{isLoading || !contract ? (
Daten werden geladen…
) : (
{customer && ( toggle('customer')} preview={previewCustomer(customer, contract)} extra={ checked.customer && (
E-Mail im Text:
) } /> )} {deliveryAddress && ( toggle('deliveryAddress')} preview={previewAddress(deliveryAddress)} /> )} {billingAddress && ( toggle('billingAddress')} preview={previewAddress(billingAddress)} /> )} toggle('contract')} preview={previewContract(contract)} /> {bankCard && ( 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 && ( 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} /> )} {/* Falls weder Customer noch Address etc. da sind */} {!customer && !deliveryAddress && !bankCard && !identityDocument && (

Keine weiteren Daten am Kunden hinterlegt.

)}
)}
Text wird ans Ende der Nachricht angehängt.
); } // ==================== UI-Helper ==================== interface SectionRowProps { title: string; checked: boolean; onToggle: () => void; preview: string; 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 (
{title}
{preview}
); } function SectionRow({ title, checked, onToggle, preview, extra }: SectionRowProps) { return (
{extra}
); } // ==================== Text-Block-Formatierung ==================== function fullName( customer: { salutation?: string; firstName: string; lastName: string; companyName?: string }, contractType: string, ): string { if (contractType === 'BUSINESS' && customer.companyName) { return customer.companyName; } const parts: string[] = []; if (customer.salutation) parts.push(customer.salutation); parts.push(customer.firstName); parts.push(customer.lastName); return parts.filter(Boolean).join(' '); } // 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: Contract, email: string, ): string { const lines: string[] = ['Kundendaten:']; lines.push(fullName(customer, contract.type)); if (contract.customerNumberAtProvider) { lines.push(`Kundennummer beim Anbieter: ${contract.customerNumberAtProvider}`); } if (customer.birthDate) lines.push(`Geburtsdatum: ${formatDate(customer.birthDate)}`); 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'); } function previewCustomer(customer: NonNullable, contract: Contract): string { return [ fullName(customer, contract.type), contract.customerNumberAtProvider ? `Anbieter-Kdnr.: ${contract.customerNumberAtProvider}` : '', ] .filter(Boolean) .join(' · '); } function formatAddressBlock(label: string, addr: Address): string { const lines: string[] = [`${label}:`]; lines.push(`${addr.street} ${addr.houseNumber}`); lines.push(`${addr.postalCode} ${addr.city}`); if (addr.country && addr.country.toLowerCase() !== 'deutschland' && addr.country !== 'DE') { lines.push(addr.country); } return lines.join('\n'); } 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:']; 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}`); if (c.contractNumberAtProvider) lines.push(`Vertragsnummer beim Anbieter: ${c.contractNumberAtProvider}`); if (c.orderNumberAtSalesPlatform) lines.push(`Auftragsnummer Vertriebsplattform: ${c.orderNumberAtSalesPlatform}`); if (c.customerNumberAtSalesPlatform) lines.push(`Kundennummer Vertriebsplattform: ${c.customerNumberAtSalesPlatform}`); if (c.contractNumberAtSalesPlatform) lines.push(`Vertragsnummer Vertriebsplattform: ${c.contractNumberAtSalesPlatform}`); if (c.startDate) lines.push(`Vertragsbeginn: ${formatDate(c.startDate)}`); if (c.endDate) lines.push(`Vertragsende: ${formatDate(c.endDate)}`); return lines.join('\n'); } function previewContract(c: Contract): string { 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 last4 = lastFourIban(b.iban); if (!last4) return ''; return `Bankverbindung:\nIBAN endet auf: ${last4}`; } function previewBank(b: BankCard): string { const last4 = lastFourIban(b.iban); return last4 ? `IBAN …${last4}` : 'IBAN nicht hinterlegt'; } function identityTypeLabel(type: IdentityDocument['type']): string { switch (type) { case 'PASSPORT': return 'Reisepass'; case 'DRIVERS_LICENSE': return 'Führerschein'; case 'OTHER': return 'Ausweisdokument'; case 'ID_CARD': default: return 'Personalausweis'; } } // 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 { if (!d.documentNumber) return ''; return `${identityTypeLabel(d.type)}-Nummer: ${d.documentNumber}`; } function previewIdentity(d: IdentityDocument): string { return d.documentNumber ? `Nr. ${d.documentNumber}` : 'Keine Nummer hinterlegt'; }