a4895374b9
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>
614 lines
22 KiB
TypeScript
614 lines
22 KiB
TypeScript
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<Record<SectionKey, boolean>>({
|
||
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<EmailChoice>('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<boolean> => {
|
||
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 (
|
||
<Modal
|
||
isOpen={isOpen}
|
||
onClose={handleClose}
|
||
title="Kundendaten einfügen"
|
||
size="lg"
|
||
>
|
||
<div className="space-y-4">
|
||
{isLoading || !contract ? (
|
||
<div className="flex items-center justify-center py-8 text-gray-500">
|
||
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
|
||
Daten werden geladen…
|
||
</div>
|
||
) : (
|
||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||
{customer && (
|
||
<SectionRow
|
||
title="Anrede & Name"
|
||
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 && (
|
||
<SectionRow
|
||
title="Lieferadresse"
|
||
checked={checked.deliveryAddress}
|
||
onToggle={() => toggle('deliveryAddress')}
|
||
preview={previewAddress(deliveryAddress)}
|
||
/>
|
||
)}
|
||
{billingAddress && (
|
||
<SectionRow
|
||
title="Rechnungsadresse"
|
||
checked={checked.billingAddress}
|
||
onToggle={() => toggle('billingAddress')}
|
||
preview={previewAddress(billingAddress)}
|
||
/>
|
||
)}
|
||
<SectionRow
|
||
title="Vertragsdaten"
|
||
checked={checked.contract}
|
||
onToggle={() => toggle('contract')}
|
||
preview={previewContract(contract)}
|
||
/>
|
||
{bankCard && (
|
||
<DualChoiceRow
|
||
title="Bankverbindung"
|
||
preview={previewBank(bankCard)}
|
||
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 && (
|
||
<DualChoiceRow
|
||
title={identityTypeLabel(identityDocument.type)}
|
||
preview={previewIdentity(identityDocument)}
|
||
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}
|
||
/>
|
||
)}
|
||
|
||
{/* Falls weder Customer noch Address etc. da sind */}
|
||
{!customer && !deliveryAddress && !bankCard && !identityDocument && (
|
||
<p className="text-sm text-gray-500 text-center py-4">
|
||
Keine weiteren Daten am Kunden hinterlegt.
|
||
</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex justify-between items-center pt-4 border-t border-gray-200">
|
||
<span className="text-xs text-gray-500">
|
||
Text wird ans Ende der Nachricht angehängt.
|
||
</span>
|
||
<div className="flex gap-3">
|
||
<Button variant="secondary" onClick={handleClose} disabled={busy}>
|
||
Abbrechen
|
||
</Button>
|
||
<Button
|
||
onClick={handleConfirm}
|
||
disabled={busy || isLoading || nothingSelected}
|
||
>
|
||
{busy ? (
|
||
<>
|
||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||
Einfügen…
|
||
</>
|
||
) : (
|
||
'Einfügen'
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
);
|
||
}
|
||
|
||
// ==================== 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 (
|
||
<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">
|
||
<label className="flex items-start gap-2 cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={checked}
|
||
onChange={onToggle}
|
||
className="mt-1 rounded"
|
||
/>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="text-sm font-medium text-gray-700">{title}</div>
|
||
<pre className="text-xs text-gray-500 mt-1 whitespace-pre-wrap font-sans">
|
||
{preview}
|
||
</pre>
|
||
</div>
|
||
</label>
|
||
{extra}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ==================== 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['customer']>,
|
||
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['customer']>, 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';
|
||
}
|