import { useState, useEffect } from 'react'; import { useParams, Link, useNavigate, useSearchParams, useLocation } from 'react-router-dom'; import { pushHistory, popHistory } from '../../utils/navigation'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { customerApi, addressApi, bankCardApi, documentApi, meterApi, uploadApi, contractApi, stressfreiEmailApi, emailProviderApi, gdprApi, StressfreiEmail, ContractTreeNode } from '../../services/api'; import { EmailClientTab } from '../../components/email'; import { useAuth } from '../../context/AuthContext'; import Card from '../../components/ui/Card'; import Button from '../../components/ui/Button'; import Badge from '../../components/ui/Badge'; import Tabs from '../../components/ui/Tabs'; import Modal from '../../components/ui/Modal'; import Input from '../../components/ui/Input'; import Select from '../../components/ui/Select'; import FileUpload from '../../components/ui/FileUpload'; import { Edit, Plus, Trash2, MapPin, CreditCard, FileText, Gauge, Eye, EyeOff, Download, Globe, UserPlus, X, Search, Mail, Copy, Check, ChevronDown, ChevronRight, Info, Shield, ShieldCheck, ShieldX, ShieldAlert, Lock, ArrowLeft, Cake } from 'lucide-react'; import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton'; import BirthdayManagementModal from '../../components/BirthdayManagementModal'; import { formatDate } from '../../utils/dateFormat'; import { getContractTypeInfo } from '../../utils/contractInfo'; import { useProviderSettings } from '../../hooks/useProviderSettings'; import type { Address, BankCard, IdentityDocument, Meter, Customer, CustomerRepresentative, CustomerSummary, CustomerConsent, ConsentType, ConsentStatus, RepresentativeAuthorization } from '../../types'; export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?: number } = {}) { const { id } = useParams(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { hasPermission, isCustomerPortal } = useAuth(); const location = useLocation(); const back = popHistory(location.state, isCustomerPortal ? '/' : '/customers'); const [searchParams, setSearchParams] = useSearchParams(); const customerId = portalCustomerId || parseInt(id!); const defaultTab = searchParams.get('tab') || 'addresses'; const [activeTab, setActiveTab] = useState(defaultTab); const { customerEmailLabel } = useProviderSettings(); // Tab-Wechsel in URL synchronisieren (für Browser-History) const handleTabChange = (tabId: string) => { setActiveTab(tabId); setSearchParams({ tab: tabId }, { replace: true }); }; const [showAddressModal, setShowAddressModal] = useState(false); const [showBankCardModal, setShowBankCardModal] = useState(false); const [showDocumentModal, setShowDocumentModal] = useState(false); const [showMeterModal, setShowMeterModal] = useState(false); const [showStressfreiEmailModal, setShowStressfreiEmailModal] = useState(false); const [showBirthdayModal, setShowBirthdayModal] = useState(false); const [showInactive, setShowInactive] = useState(false); const [editingBankCard, setEditingBankCard] = useState(null); const [editingDocument, setEditingDocument] = useState(null); const [editingAddress, setEditingAddress] = useState
(null); const [editingMeter, setEditingMeter] = useState(null); const [editingStressfreiEmail, setEditingStressfreiEmail] = useState(null); const { data: customer, isLoading } = useQuery({ queryKey: ['customer', customerId], queryFn: () => customerApi.getById(customerId), }); // Consent-Status prüfen const { data: consentStatusData } = useQuery({ queryKey: ['consent-status', customerId], queryFn: () => gdprApi.checkConsentStatus(customerId), enabled: !!customer?.data, }); const hasConsentApproval = consentStatusData?.data?.hasConsent ?? false; const deleteMutation = useMutation({ mutationFn: () => customerApi.delete(customerId), onSuccess: () => { navigate('/customers'); }, }); if (isLoading) { return
Laden...
; } if (!customer?.data) { return
Kunde nicht gefunden
; } const c = customer.data; // Gesperrter Inhalt für Tabs ohne Einwilligung const blockedContent = ( handleTabChange('consents')} /> ); const tabs = [ { id: 'addresses', label: 'Adressen', content: ( setShowAddressModal(true)} onEdit={(addr) => setEditingAddress(addr)} /> ), }, { id: 'bankcards', label: 'Bankkarten', content: hasConsentApproval ? ( setShowInactive(!showInactive)} onAdd={() => setShowBankCardModal(true)} onEdit={(card) => setEditingBankCard(card)} /> ) : blockedContent, }, { id: 'documents', label: 'Ausweise', content: hasConsentApproval ? ( setShowInactive(!showInactive)} onAdd={() => setShowDocumentModal(true)} onEdit={(doc) => setEditingDocument(doc)} /> ) : blockedContent, }, { id: 'meters', label: 'Zähler', content: hasConsentApproval ? ( setShowInactive(!showInactive)} onAdd={() => setShowMeterModal(true)} onEdit={(meter) => setEditingMeter(meter)} /> ) : blockedContent, }, ...(!isCustomerPortal ? [{ id: 'stressfrei', label: customerEmailLabel, content: ( setShowInactive(!showInactive)} onAdd={() => setShowStressfreiEmailModal(true)} onEdit={(email) => setEditingStressfreiEmail(email)} /> ), }] : []), { id: 'emails', label: 'E-Mail-Postfach', content: hasConsentApproval ? ( ) : blockedContent, }, { id: 'contracts', label: 'Verträge', content: hasConsentApproval ? ( ) : blockedContent, }, ...(hasPermission('customers:update') ? [{ id: 'portal', label: 'Portal', content: ( ), }] : []), ...(hasPermission('customers:read') && !isCustomerPortal ? [{ id: 'consents', label: 'Einwilligungen / Datenschutz', content: ( { queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); queryClient.invalidateQueries({ queryKey: ['consent-status', customerId] }); }} /> ), }] : []), ]; return (

{c.type === 'BUSINESS' && c.companyName ? c.companyName : `${c.firstName} ${c.lastName}`}

{c.customerNumber}

{hasPermission('customers:update') && ( )} {hasPermission('customers:delete') && ( )}
Typ
{c.type === 'BUSINESS' ? 'Geschäftskunde' : 'Privatkunde'}
{c.salutation && (
Anrede
{c.salutation}
)}
Anrede per
{c.useInformalAddress ? 'Du (informell)' : 'Sie (formell)'}
Vorname
{c.firstName}
Nachname
{c.lastName}
{c.companyName && (
Firma
{c.companyName}
)} {c.foundingDate && (
Gründungsdatum
{new Date(c.foundingDate).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}
)} {c.birthDate && (
Geburtsdatum
{new Date(c.birthDate).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} {!isCustomerPortal && hasPermission('customers:update') && ( )}
)} {c.birthPlace && (
Geburtsort
{c.birthPlace}
)}
{c.email && ( )} {c.phone && (
Telefon
{c.phone}
)} {c.mobile && ( )}
{c.type === 'BUSINESS' && ( queryClient.invalidateQueries({ queryKey: ['customer', customerId] })} /> )} {c.notes && (

{c.notes}

)} setShowAddressModal(false)} customerId={customerId} /> setEditingAddress(null)} customerId={customerId} address={editingAddress} /> setShowBankCardModal(false)} customerId={customerId} /> setEditingBankCard(null)} customerId={customerId} bankCard={editingBankCard} /> setShowDocumentModal(false)} customerId={customerId} /> setEditingDocument(null)} customerId={customerId} document={editingDocument} /> setShowMeterModal(false)} customerId={customerId} /> setEditingMeter(null)} customerId={customerId} meter={editingMeter} /> setShowStressfreiEmailModal(false)} customerId={customerId} customerEmail={customer?.data?.email} /> setEditingStressfreiEmail(null)} customerId={customerId} email={editingStressfreiEmail} customerEmail={customer?.data?.email} /> {showBirthdayModal && c && ( setShowBirthdayModal(false)} /> )}
); } // Business Data Card with Document Uploads function BusinessDataCard({ customer, canEdit, onUpdate, }: { customer: Customer; canEdit: boolean; onUpdate: () => void; }) { const handleBusinessRegUpload = async (file: File) => { try { await uploadApi.uploadBusinessRegistration(customer.id, file); onUpdate(); } catch (error) { console.error('Upload fehlgeschlagen:', error); alert('Upload fehlgeschlagen'); } }; const handleBusinessRegDelete = async () => { if (!confirm('Gewerbeanmeldung wirklich löschen?')) return; try { await uploadApi.deleteBusinessRegistration(customer.id); onUpdate(); } catch (error) { console.error('Löschen fehlgeschlagen:', error); alert('Löschen fehlgeschlagen'); } }; const handleCommercialRegUpload = async (file: File) => { try { await uploadApi.uploadCommercialRegister(customer.id, file); onUpdate(); } catch (error) { console.error('Upload fehlgeschlagen:', error); alert('Upload fehlgeschlagen'); } }; const handleCommercialRegDelete = async () => { if (!confirm('Handelsregisterauszug wirklich löschen?')) return; try { await uploadApi.deleteCommercialRegister(customer.id); onUpdate(); } catch (error) { console.error('Löschen fehlgeschlagen:', error); alert('Löschen fehlgeschlagen'); } }; const hasData = customer.taxNumber || customer.commercialRegisterNumber || customer.businessRegistrationPath || customer.commercialRegisterPath; if (!hasData && !canEdit) return null; return (
{customer.taxNumber && (
Steuernummer
{customer.taxNumber}
)} {customer.commercialRegisterNumber && (
Handelsregisternummer
{customer.commercialRegisterNumber}
)}
{/* Dokumente */}
{/* Gewerbeanmeldung */}

Gewerbeanmeldung

{customer.businessRegistrationPath ? (
Anzeigen Download {canEdit && ( <> )}
) : canEdit ? ( ) : (

Nicht vorhanden

)}
{/* Handelsregisterauszug */}

Handelsregisterauszug

{customer.commercialRegisterPath ? (
Anzeigen Download {canEdit && ( <> )}
) : canEdit ? ( ) : (

Nicht vorhanden

)}
); } // Tab Components function AddressesTab({ customerId, addresses, canEdit, onAdd, onEdit, }: { customerId: number; addresses: Address[]; canEdit: boolean; onAdd: () => void; onEdit: (address: Address) => void; }) { const queryClient = useQueryClient(); const deleteMutation = useMutation({ mutationFn: addressApi.delete, onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }), }); return (
{canEdit && (
)} {addresses.length > 0 ? (
{addresses.map((addr) => (
{addr.type === 'BILLING' ? 'Rechnung' : 'Liefer-/Meldeadresse'} {addr.isDefault && Standard}
{canEdit && (
)}

{addr.street} {addr.houseNumber}

{addr.postalCode} {addr.city}

{addr.country}

{(addr.ownerFirstName || addr.ownerLastName || addr.ownerCompany) && (
Eigentümer: {addr.ownerCompany && {addr.ownerCompany} – } {addr.ownerFirstName} {addr.ownerLastName} {addr.ownerPhone && · {addr.ownerPhone}}
)}
))}
) : (

Keine Adressen vorhanden.

)}
); } function BankCardsTab({ customerId, bankCards, canEdit, showInactive, onToggleInactive, onAdd, onEdit, }: { customerId: number; bankCards: BankCard[]; canEdit: boolean; showInactive: boolean; onToggleInactive: () => void; onAdd: () => void; onEdit: (card: BankCard) => void; }) { const queryClient = useQueryClient(); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: Partial }) => bankCardApi.update(id, data), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }), }); const deleteMutation = useMutation({ mutationFn: bankCardApi.delete, onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }), }); const handleDocumentUpload = async (cardId: number, file: File) => { try { await uploadApi.uploadBankCardDocument(cardId, file); queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); } catch (error) { console.error('Upload fehlgeschlagen:', error); alert('Upload fehlgeschlagen'); } }; const handleDocumentDelete = async (cardId: number) => { if (!confirm('Dokument wirklich löschen?')) return; try { await uploadApi.deleteBankCardDocument(cardId); queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); } catch (error) { console.error('Löschen fehlgeschlagen:', error); alert('Löschen fehlgeschlagen'); } }; const filtered = showInactive ? bankCards : bankCards.filter((c) => c.isActive); return (
{canEdit && ( )}
{filtered.length > 0 ? (
{filtered.map((card) => (
{!card.isActive && Inaktiv} {card.expiryDate && new Date(card.expiryDate) < new Date() && ( Abgelaufen )}
{canEdit && (
{card.isActive ? ( ) : ( )}
)}

{card.accountHolder}

{card.iban}

{card.bic && (

BIC: {card.bic}

)} {card.bankName && (

{card.bankName}

)} {card.expiryDate && (

Gültig bis: {formatDate(card.expiryDate)}

)} {/* Dokument-Upload Bereich */}
{card.documentPath ? (
Anzeigen Download {canEdit && ( <> handleDocumentUpload(card.id, file)} existingFile={card.documentPath} accept=".pdf" label="Ersetzen" disabled={!card.isActive} /> )}
) : ( canEdit && card.isActive && ( handleDocumentUpload(card.id, file)} accept=".pdf" label="PDF hochladen" /> ) )}
))}
) : (

Keine Bankkarten vorhanden.

)}
); } function DocumentsTab({ customerId, documents, canEdit, showInactive, onToggleInactive, onAdd, onEdit, }: { customerId: number; documents: IdentityDocument[]; canEdit: boolean; showInactive: boolean; onToggleInactive: () => void; onAdd: () => void; onEdit: (doc: IdentityDocument) => void; }) { const queryClient = useQueryClient(); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: Partial }) => documentApi.update(id, data), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }), }); const deleteMutation = useMutation({ mutationFn: documentApi.delete, onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }), }); const handleDocumentUpload = async (docId: number, file: File) => { try { await uploadApi.uploadIdentityDocument(docId, file); queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); } catch (error) { console.error('Upload fehlgeschlagen:', error); alert('Upload fehlgeschlagen'); } }; const handleDocumentDelete = async (docId: number) => { if (!confirm('Dokument wirklich löschen?')) return; try { await uploadApi.deleteIdentityDocument(docId); queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); } catch (error) { console.error('Löschen fehlgeschlagen:', error); alert('Löschen fehlgeschlagen'); } }; const filtered = showInactive ? documents : documents.filter((d) => d.isActive); const docTypeLabels: Record = { ID_CARD: 'Personalausweis', PASSPORT: 'Reisepass', DRIVERS_LICENSE: 'Führerschein', OTHER: 'Sonstiges', }; return (
{canEdit && ( )}
{filtered.length > 0 ? (
{filtered.map((doc) => (
{docTypeLabels[doc.type]} {!doc.isActive && Inaktiv} {doc.expiryDate && new Date(doc.expiryDate) < new Date() && ( Abgelaufen )}
{canEdit && (
{doc.isActive ? ( ) : ( )}
)}

{doc.documentNumber}

{doc.issuingAuthority && (

Ausgestellt von: {doc.issuingAuthority}

)} {doc.expiryDate && (

Gültig bis: {new Date(doc.expiryDate).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}

)} {/* Führerschein-spezifische Anzeige */} {doc.type === 'DRIVERS_LICENSE' && doc.licenseClasses && (

Klassen: {doc.licenseClasses}

)} {doc.type === 'DRIVERS_LICENSE' && doc.licenseIssueDate && (

Klasse B seit: {new Date(doc.licenseIssueDate).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}

)} {/* Dokument-Upload Bereich */}
{doc.documentPath ? (
Anzeigen Download {canEdit && ( <> handleDocumentUpload(doc.id, file)} existingFile={doc.documentPath} accept=".pdf" label="Ersetzen" disabled={!doc.isActive} /> )}
) : ( canEdit && doc.isActive && ( handleDocumentUpload(doc.id, file)} accept=".pdf" label="PDF hochladen" /> ) )}
))}
) : (

Keine Ausweise vorhanden.

)}
); } function MetersTab({ customerId, meters, canEdit, showInactive, onToggleInactive, onAdd, onEdit, }: { customerId: number; meters: Meter[]; canEdit: boolean; showInactive: boolean; onToggleInactive: () => void; onAdd: () => void; onEdit: (meter: Meter) => void; }) { const [showReadingModal, setShowReadingModal] = useState<{ meterId: number; meterType: 'ELECTRICITY' | 'GAS'; tariffModel?: string } | null>(null); const [expandedMeter, setExpandedMeter] = useState(null); const [editingReading, setEditingReading] = useState<{ meterId: number; meterType: 'ELECTRICITY' | 'GAS'; tariffModel?: string; reading: any } | null>(null); const queryClient = useQueryClient(); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: Partial }) => meterApi.update(id, data), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }), }); const [deleteError, setDeleteError] = useState(null); const deleteMutation = useMutation({ mutationFn: meterApi.delete, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); setDeleteError(null); }, onError: (err) => { setDeleteError(err instanceof Error ? err.message : 'Fehler beim Löschen'); }, }); const deleteReadingMutation = useMutation({ mutationFn: ({ meterId, readingId }: { meterId: number; readingId: number }) => meterApi.deleteReading(meterId, readingId), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }), }); const filtered = showInactive ? meters : meters.filter((m) => m.isActive); // Sort readings by date (newest first) const getSortedReadings = (readings: any[] | undefined) => { if (!readings) return []; return [...readings].sort((a, b) => new Date(b.readingDate).getTime() - new Date(a.readingDate).getTime() ); }; return (
{canEdit && ( )}
{filtered.length > 0 ? (
{filtered.map((meter) => { const sortedReadings = getSortedReadings(meter.readings); const isExpanded = expandedMeter === meter.id; return (
{meter.type === 'ELECTRICITY' ? 'Strom' : 'Gas'} {meter.tariffModel === 'DUAL' && ( HT/NT )} {!meter.isActive && Inaktiv}
{canEdit && (
{meter.isActive && ( )} {meter.isActive ? ( ) : ( )}
)}

{meter.meterNumber}

{meter.location && (

Standort: {meter.location}

)} {sortedReadings.length > 0 && (

Zählerstände:

{sortedReadings.length > 3 && ( )}
{(isExpanded ? sortedReadings : sortedReadings.slice(0, 3)).map((reading) => (
{formatDate(reading.readingDate)}
{reading.valueNt !== undefined && reading.valueNt !== null ? ( <>HT: {reading.value.toLocaleString('de-DE')} / NT: {reading.valueNt.toLocaleString('de-DE')} {reading.unit} ) : ( <>{reading.value.toLocaleString('de-DE')} {reading.unit} )} {canEdit && (
)}
))}
)}
); })}
) : (

Keine Zähler vorhanden.

)} {showReadingModal && ( setShowReadingModal(null)} meterId={showReadingModal.meterId} meterType={showReadingModal.meterType} tariffModel={showReadingModal.tariffModel as any} customerId={customerId} /> )} {editingReading && ( setEditingReading(null)} meterId={editingReading.meterId} meterType={editingReading.meterType} tariffModel={editingReading.tariffModel as any} customerId={customerId} reading={editingReading.reading} /> )} {/* Fehler-Modal beim Löschen (z.B. Zähler noch an Vertrag) */} {deleteError && ( setDeleteError(null)} title="Zähler kann nicht gelöscht werden">

Der Zähler ist noch folgenden Verträgen zugeordnet und kann daher nicht gelöscht werden:

{deleteError.match(/[A-Z]+-[A-Z0-9]+/g)?.map((contractNumber) => ( setDeleteError(null)} className="flex items-center gap-2 p-3 bg-gray-50 border rounded-lg text-blue-600 hover:bg-blue-50 hover:border-blue-300 transition-colors" > {contractNumber} )) ?? (

{deleteError}

)}

Bitte entfernen Sie den Zähler zuerst aus den oben genannten Verträgen.

)}
); } function ContractsTab({ customerId, }: { customerId: number; }) { const { hasPermission } = useAuth(); const navigate = useNavigate(); const queryClient = useQueryClient(); const [expandedContracts, setExpandedContracts] = useState>(new Set()); const [showStatusInfo, setShowStatusInfo] = useState(false); // Lade Vertragsbaum statt flacher Liste const { data: treeData, isLoading } = useQuery({ queryKey: ['contract-tree', customerId], queryFn: () => contractApi.getTreeForCustomer(customerId), }); const contractTree = treeData?.data || []; const deleteMutation = useMutation({ mutationFn: contractApi.delete, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); queryClient.invalidateQueries({ queryKey: ['customers'] }); queryClient.invalidateQueries({ queryKey: ['contracts'] }); queryClient.invalidateQueries({ queryKey: ['contract-tree', customerId] }); }, onError: (error: any) => { alert(error?.message || 'Fehler beim Löschen des Vertrags'); }, }); const typeLabels: Record = { ELECTRICITY: 'Strom', GAS: 'Gas', DSL: 'DSL', FIBER: 'Glasfaser', MOBILE: 'Mobilfunk', TV: 'TV', CAR_INSURANCE: 'KFZ-Versicherung', }; const statusVariants: Record = { ACTIVE: 'success', PENDING: 'warning', CANCELLED: 'danger', EXPIRED: 'danger', DRAFT: 'default', DEACTIVATED: 'default', }; const statusDescriptions = [ { status: 'DRAFT', label: 'Entwurf', description: 'Vertrag wird noch vorbereitet', color: 'text-gray-600' }, { status: 'PENDING', label: 'Ausstehend', description: 'Wartet auf Aktivierung', color: 'text-yellow-600' }, { status: 'ACTIVE', label: 'Aktiv', description: 'Vertrag läuft normal', color: 'text-green-600' }, { status: 'EXPIRED', label: 'Abgelaufen', description: 'Laufzeit vorbei, läuft aber ohne Kündigung weiter', color: 'text-orange-600' }, { status: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' }, { status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' }, ]; const toggleExpand = (contractId: number) => { setExpandedContracts(prev => { const next = new Set(prev); if (next.has(contractId)) { next.delete(contractId); } else { next.add(contractId); } return next; }); }; // Rekursive Rendering-Funktion für Vorgänger const renderPredecessors = (predecessors: ContractTreeNode[], depth: number): React.ReactNode => { return predecessors.map(node => (
{renderContractNode(node, depth)}
)); }; // Einzelnen Vertragsknoten rendern const renderContractNode = (node: ContractTreeNode, depth: number = 0): React.ReactNode => { const { contract, predecessors, hasHistory } = node; const isExpanded = expandedContracts.has(contract.id); const isPredecessor = depth > 0; return (
{/* Aufklapp-Button nur bei Wurzelknoten mit Historie */} {!isPredecessor && hasHistory ? ( ) : !isPredecessor ? (
// Platzhalter für Ausrichtung ) : null} {contract.contractNumber} {typeLabels[contract.type] || contract.type} {contract.status} {depth === 0 && !isPredecessor && ( )} {isPredecessor && ( (Vorgänger) )}
{hasPermission('contracts:update') && ( )} {hasPermission('contracts:delete') && ( )}
{(contract.providerName || contract.provider?.name) && (

{contract.providerName || contract.provider?.name} {(contract.tariffName || contract.tariff?.name) && ` - ${contract.tariffName || contract.tariff?.name}`}

)} {(() => { const typeInfo = getContractTypeInfo(contract as any); return typeInfo ? (

{typeInfo.label}: {typeInfo.value}

) : null; })()} {contract.startDate && (

Beginn: {formatDate(contract.startDate)} {contract.endDate && ` | Ende: ${formatDate(contract.endDate)}`}

)}
{/* Vorgänger rekursiv rendern - für Wurzel nur wenn aufgeklappt, für Vorgänger immer */} {((depth === 0 && isExpanded) || depth > 0) && predecessors.length > 0 && (
{renderPredecessors(predecessors, depth + 1)}
)}
); }; if (isLoading) { return (
); } return (
{hasPermission('contracts:create') && (
)} {contractTree.length > 0 ? (
{contractTree.map(node => renderContractNode(node, 0))}
) : (

Keine Verträge vorhanden.

)} {/* Status Info Modal */} {showStatusInfo && (
setShowStatusInfo(false)} />

Vertragsstatus-Übersicht

{statusDescriptions.map(({ status, label, description, color }) => (
{label} {description}
))}
)}
); } // Gespeichertes Passwort anzeigen function StoredPasswordDisplay({ customerId }: { customerId: number }) { const [showStoredPassword, setShowStoredPassword] = useState(false); const [storedPassword, setStoredPassword] = useState(null); const [isLoading, setIsLoading] = useState(false); const handleShowPassword = async () => { if (showStoredPassword) { setShowStoredPassword(false); return; } setIsLoading(true); try { const result = await customerApi.getPortalPassword(customerId); setStoredPassword(result.data?.password || null); setShowStoredPassword(true); } catch (error) { console.error('Fehler beim Laden des Passworts:', error); alert('Fehler beim Laden des Passworts'); } finally { setIsLoading(false); } }; return (

Passwort ist gesetzt

{showStoredPassword && storedPassword && ( {storedPassword} )} {showStoredPassword && !storedPassword && ( (Passwort nicht verfügbar) )}
); } // Portal Tab Component function PortalTab({ customerId, canEdit, }: { customerId: number; canEdit: boolean; }) { const queryClient = useQueryClient(); const [showPassword, setShowPassword] = useState(false); const [newPassword, setNewPassword] = useState(''); const [searchTerm, setSearchTerm] = useState(''); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); // Lade Portal-Einstellungen const { data: portalData, isLoading: portalLoading } = useQuery({ queryKey: ['customer-portal', customerId], queryFn: () => customerApi.getPortalSettings(customerId), }); // Lade Vertreter-Liste const { data: representativesData, isLoading: repLoading } = useQuery({ queryKey: ['customer-representatives', customerId], queryFn: () => customerApi.getRepresentatives(customerId), }); // Portal-Einstellungen aktualisieren const updatePortalMutation = useMutation({ mutationFn: (data: { portalEnabled?: boolean; portalEmail?: string | null }) => customerApi.updatePortalSettings(customerId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer-portal', customerId] }); }, }); // Passwort setzen const setPasswordMutation = useMutation({ mutationFn: (password: string) => customerApi.setPortalPassword(customerId, password), onSuccess: () => { setNewPassword(''); queryClient.invalidateQueries({ queryKey: ['customer-portal', customerId] }); alert('Passwort wurde gesetzt'); }, onError: (error: Error) => { alert(error.message); }, }); // Vertreter hinzufügen const addRepMutation = useMutation({ mutationFn: (representativeId: number) => customerApi.addRepresentative(customerId, representativeId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer-representatives', customerId] }); setSearchTerm(''); setSearchResults([]); }, onError: (error: Error) => { alert(error.message); }, }); // Vertreter entfernen const removeRepMutation = useMutation({ mutationFn: (representativeId: number) => customerApi.removeRepresentative(customerId, representativeId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer-representatives', customerId] }); }, }); // Vertreter-Suche const handleSearch = async () => { if (searchTerm.length < 2) return; setIsSearching(true); try { const result = await customerApi.searchForRepresentative(customerId, searchTerm); setSearchResults(result.data || []); } catch (error) { console.error('Suche fehlgeschlagen:', error); } finally { setIsSearching(false); } }; if (portalLoading || repLoading) { return
Laden...
; } const portal = portalData?.data; const representatives = representativesData?.data || []; return (
{/* Portal-Einstellungen */}

Portal-Zugang

{/* Portal aktiviert */} {/* Portal E-Mail */}
updatePortalMutation.mutate({ portalEmail: e.target.value || null })} placeholder="portal@example.com" disabled={!canEdit || !portal?.portalEnabled} className="flex-1" />

Diese E-Mail wird für den Login ins Kundenportal verwendet.

{/* Passwort setzen */} {portal?.portalEnabled && (
setNewPassword(e.target.value)} placeholder="Mindestens 6 Zeichen" disabled={!canEdit} />
{portal?.hasPassword && ( )}
)} {/* Letzte Anmeldung */} {portal?.portalLastLogin && (

Letzte Anmeldung: {new Date(portal.portalLastLogin).toLocaleString('de-DE')}

)}
{/* Vertreter-Verwaltung */}

Vertreter (können Verträge einsehen)

Hier können Sie anderen Kunden erlauben, die Verträge dieses Kunden einzusehen. Beispiel: Der Sohn kann die Verträge seiner Mutter einsehen.

{/* Vertreter-Suche */} {canEdit && (
setSearchTerm(e.target.value)} placeholder="Kunden suchen (Name, Kundennummer)..." onKeyDown={(e) => e.key === 'Enter' && handleSearch()} className="flex-1" />

Nur Kunden mit aktiviertem Portal können als Vertreter hinzugefügt werden.

{/* Suchergebnisse */} {searchResults.length > 0 && (
{searchResults.map((customer) => (

{customer.companyName || `${customer.firstName} ${customer.lastName}`}

{customer.customerNumber}

))}
)}
)} {/* Aktuelle Vertreter */} {representatives.length > 0 ? (
{representatives.map((rep: CustomerRepresentative) => (

{rep.representative?.companyName || `${rep.representative?.firstName} ${rep.representative?.lastName}`}

{rep.representative?.customerNumber}

{canEdit && ( )}
))}
) : (

Keine Vertreter konfiguriert.

)}
); } // Modal Components function AddressModal({ isOpen, onClose, customerId, address, }: { isOpen: boolean; onClose: () => void; customerId: number; address?: Address | null; }) { const queryClient = useQueryClient(); const isEditing = !!address; const getInitialFormData = () => ({ type: address?.type || 'DELIVERY_RESIDENCE' as const, street: address?.street || '', houseNumber: address?.houseNumber || '', postalCode: address?.postalCode || '', city: address?.city || '', country: address?.country || 'Deutschland', isDefault: address?.isDefault || false, ownerCompany: address?.ownerCompany || '', ownerFirstName: address?.ownerFirstName || '', ownerLastName: address?.ownerLastName || '', ownerStreet: address?.ownerStreet || '', ownerHouseNumber: address?.ownerHouseNumber || '', ownerPostalCode: address?.ownerPostalCode || '', ownerCity: address?.ownerCity || '', ownerPhone: address?.ownerPhone || '', ownerMobile: address?.ownerMobile || '', ownerEmail: address?.ownerEmail || '', }); const [formData, setFormData] = useState(getInitialFormData); const createMutation = useMutation({ mutationFn: (data: typeof formData) => addressApi.create(customerId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); onClose(); setFormData(getInitialFormData()); }, }); const updateMutation = useMutation({ mutationFn: (data: typeof formData) => addressApi.update(address!.id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); onClose(); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (isEditing) { updateMutation.mutate(formData); } else { createMutation.mutate(formData); } }; const isPending = createMutation.isPending || updateMutation.isPending; // Update form when address prop changes if (isEditing && formData.street !== address.street) { setFormData(getInitialFormData()); } return (
setFormData({ ...formData, street: e.target.value })} required />
setFormData({ ...formData, houseNumber: e.target.value })} required />
setFormData({ ...formData, postalCode: e.target.value })} required />
setFormData({ ...formData, city: e.target.value })} required />
setFormData({ ...formData, country: e.target.value })} /> {/* Eigentümer (optional, nur bei Liefer-/Meldeadresse) */} {formData.type === 'DELIVERY_RESIDENCE' && (

Eigentümer

Nur ausfüllen wenn der Kunde nicht selbst Eigentümer ist (z.B. Mietwohnung).

setFormData({ ...formData, ownerCompany: e.target.value })} placeholder="z.B. Wohnungsbaugesellschaft" />
setFormData({ ...formData, ownerFirstName: e.target.value })} /> setFormData({ ...formData, ownerLastName: e.target.value })} />
setFormData({ ...formData, ownerStreet: e.target.value })} />
setFormData({ ...formData, ownerHouseNumber: e.target.value })} />
setFormData({ ...formData, ownerPostalCode: e.target.value })} />
setFormData({ ...formData, ownerCity: e.target.value })} />
setFormData({ ...formData, ownerPhone: e.target.value })} /> setFormData({ ...formData, ownerMobile: e.target.value })} /> setFormData({ ...formData, ownerEmail: e.target.value })} type="email" />
)}
); } function BankCardModal({ isOpen, onClose, customerId, bankCard, }: { isOpen: boolean; onClose: () => void; customerId: number; bankCard?: BankCard | null; }) { const queryClient = useQueryClient(); const isEditing = !!bankCard; const getInitialFormData = () => ({ accountHolder: bankCard?.accountHolder || '', iban: bankCard?.iban || '', bic: bankCard?.bic || '', bankName: bankCard?.bankName || '', expiryDate: bankCard?.expiryDate ? new Date(bankCard.expiryDate).toISOString().split('T')[0] : '', isActive: bankCard?.isActive ?? true, }); const [formData, setFormData] = useState(getInitialFormData); // Reset form when bankCard changes useState(() => { setFormData(getInitialFormData()); }); const createMutation = useMutation({ mutationFn: (data: any) => bankCardApi.create(customerId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); onClose(); setFormData({ accountHolder: '', iban: '', bic: '', bankName: '', expiryDate: '', isActive: true }); }, }); const updateMutation = useMutation({ mutationFn: (data: any) => bankCardApi.update(bankCard!.id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); onClose(); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const data = { ...formData, expiryDate: formData.expiryDate ? new Date(formData.expiryDate) : undefined, }; if (isEditing) { updateMutation.mutate(data); } else { createMutation.mutate(data); } }; const isPending = createMutation.isPending || updateMutation.isPending; // Update form when bankCard prop changes if (isEditing && formData.iban !== bankCard.iban) { setFormData(getInitialFormData()); } return (
setFormData({ ...formData, accountHolder: e.target.value })} required /> setFormData({ ...formData, iban: e.target.value })} required /> setFormData({ ...formData, bic: e.target.value })} /> setFormData({ ...formData, bankName: e.target.value })} /> setFormData({ ...formData, expiryDate: e.target.value })} onClear={() => setFormData({ ...formData, expiryDate: '' })} /> {isEditing && ( )} {!isEditing && (

Dokument-Upload ist nach dem Speichern in der Übersicht möglich.

)}
); } function DocumentModal({ isOpen, onClose, customerId, document, }: { isOpen: boolean; onClose: () => void; customerId: number; document?: IdentityDocument | null; }) { const queryClient = useQueryClient(); const isEditing = !!document; const getInitialFormData = () => ({ type: document?.type || 'ID_CARD' as const, documentNumber: document?.documentNumber || '', issuingAuthority: document?.issuingAuthority || '', issueDate: document?.issueDate ? new Date(document.issueDate).toISOString().split('T')[0] : '', expiryDate: document?.expiryDate ? new Date(document.expiryDate).toISOString().split('T')[0] : '', isActive: document?.isActive ?? true, licenseClasses: document?.licenseClasses || '', licenseIssueDate: document?.licenseIssueDate ? new Date(document.licenseIssueDate).toISOString().split('T')[0] : '', }); const [formData, setFormData] = useState(getInitialFormData); const createMutation = useMutation({ mutationFn: (data: any) => documentApi.create(customerId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); onClose(); setFormData({ type: 'ID_CARD', documentNumber: '', issuingAuthority: '', issueDate: '', expiryDate: '', isActive: true, licenseClasses: '', licenseIssueDate: '', }); }, }); const updateMutation = useMutation({ mutationFn: (data: any) => documentApi.update(document!.id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); onClose(); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const data: any = { ...formData, issueDate: formData.issueDate ? new Date(formData.issueDate) : undefined, expiryDate: formData.expiryDate ? new Date(formData.expiryDate) : undefined, }; // Führerschein-spezifische Felder nur bei Führerschein senden if (formData.type === 'DRIVERS_LICENSE') { data.licenseClasses = formData.licenseClasses || undefined; data.licenseIssueDate = formData.licenseIssueDate ? new Date(formData.licenseIssueDate) : undefined; } else { delete data.licenseClasses; delete data.licenseIssueDate; } if (isEditing) { updateMutation.mutate(data); } else { createMutation.mutate(data); } }; const isPending = createMutation.isPending || updateMutation.isPending; // Update form when document prop changes if (isEditing && formData.documentNumber !== document.documentNumber) { setFormData(getInitialFormData()); } return (
setFormData({ ...formData, documentNumber: e.target.value })} required /> setFormData({ ...formData, issuingAuthority: e.target.value })} />
setFormData({ ...formData, issueDate: e.target.value })} onClear={() => setFormData({ ...formData, issueDate: '' })} /> setFormData({ ...formData, expiryDate: e.target.value })} onClear={() => setFormData({ ...formData, expiryDate: '' })} />
{formData.type === 'DRIVERS_LICENSE' && ( <> setFormData({ ...formData, licenseClasses: e.target.value })} placeholder="z.B. B, BE, AM, L" /> setFormData({ ...formData, licenseIssueDate: e.target.value })} onClear={() => setFormData({ ...formData, licenseIssueDate: '' })} /> )} {isEditing && ( )} {!isEditing && (

Dokument-Upload ist nach dem Speichern in der Übersicht möglich.

)}
); } function MeterModal({ isOpen, onClose, customerId, meter, }: { isOpen: boolean; onClose: () => void; customerId: number; meter?: Meter | null; }) { const queryClient = useQueryClient(); const isEditing = !!meter; const getInitialFormData = () => ({ meterNumber: meter?.meterNumber || '', type: meter?.type || 'ELECTRICITY' as const, tariffModel: meter?.tariffModel || 'SINGLE' as const, location: meter?.location || '', isActive: meter?.isActive ?? true, }); const [formData, setFormData] = useState(getInitialFormData); const createMutation = useMutation({ mutationFn: (data: any) => meterApi.create(customerId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); onClose(); setFormData({ meterNumber: '', type: 'ELECTRICITY', tariffModel: 'SINGLE', location: '', isActive: true }); }, }); const updateMutation = useMutation({ mutationFn: (data: any) => meterApi.update(meter!.id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); onClose(); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (isEditing) { updateMutation.mutate(formData); } else { createMutation.mutate(formData); } }; const isPending = createMutation.isPending || updateMutation.isPending; // Update form when meter prop changes if (isEditing && formData.meterNumber !== meter.meterNumber) { setFormData(getInitialFormData()); } return (
setFormData({ ...formData, meterNumber: e.target.value })} required /> setFormData({ ...formData, tariffModel: e.target.value as any })} options={[ { value: 'SINGLE', label: 'Eintarifzähler (Standard)' }, { value: 'DUAL', label: 'Zweitarifzähler (HT/NT)' }, ]} /> )} setFormData({ ...formData, location: e.target.value })} placeholder="z.B. Keller, Wohnung" /> {isEditing && ( )}
); } function MeterReadingModal({ isOpen, onClose, meterId, meterType, tariffModel, customerId, reading, }: { isOpen: boolean; onClose: () => void; meterId: number; meterType: 'ELECTRICITY' | 'GAS'; tariffModel?: 'SINGLE' | 'DUAL'; customerId: number; reading?: { id: number; readingDate: string; value: number; valueNt?: number; unit: string; notes?: string } | null; }) { const queryClient = useQueryClient(); const isEditing = !!reading; const defaultUnit = meterType === 'ELECTRICITY' ? 'kWh' : 'm³'; const isDualTariff = tariffModel === 'DUAL'; const getInitialFormData = () => ({ readingDate: reading?.readingDate ? new Date(reading.readingDate).toISOString().split('T')[0] : new Date().toISOString().split('T')[0], value: reading?.value?.toString() || '', valueNt: reading?.valueNt?.toString() || '', notes: reading?.notes || '', }); const [formData, setFormData] = useState(getInitialFormData); const [error, setError] = useState(null); const createMutation = useMutation({ mutationFn: (data: any) => meterApi.addReading(meterId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); setError(null); onClose(); }, onError: (err) => { setError(err instanceof Error ? err.message : 'Fehler beim Speichern'); }, }); const updateMutation = useMutation({ mutationFn: (data: any) => meterApi.updateReading(meterId, reading!.id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); setError(null); onClose(); }, onError: (err) => { setError(err instanceof Error ? err.message : 'Fehler beim Speichern'); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const data: Record = { readingDate: new Date(formData.readingDate), value: parseFloat(formData.value), unit: defaultUnit, notes: formData.notes || undefined, }; if (isDualTariff && formData.valueNt) { data.valueNt = parseFloat(formData.valueNt); } if (isEditing) { updateMutation.mutate(data); } else { createMutation.mutate(data); } }; const isPending = createMutation.isPending || updateMutation.isPending; // Update form when reading prop changes if (isEditing && formData.value !== reading.value.toString()) { setFormData(getInitialFormData()); } return (
setFormData({ ...formData, readingDate: e.target.value })} required />
setFormData({ ...formData, value: e.target.value })} required />
{isDualTariff && (
setFormData({ ...formData, valueNt: e.target.value })} required />
)} {!isDualTariff && (
{defaultUnit}
)}
setFormData({ ...formData, notes: e.target.value })} placeholder="Optionale Notizen..." /> {error && (
{error}
)}
); } // ==================== KUNDEN-E-MAIL TAB ==================== function StressfreiEmailsTab({ customerId, emails, canEdit, showInactive, onToggleInactive, onAdd, onEdit, }: { customerId: number; emails: StressfreiEmail[]; canEdit: boolean; showInactive: boolean; onToggleInactive: () => void; onAdd: () => void; onEdit: (email: StressfreiEmail) => void; }) { const queryClient = useQueryClient(); const { customerEmailLabel } = useProviderSettings(); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: Partial }) => stressfreiEmailApi.update(id, data), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }), }); const deleteMutation = useMutation({ mutationFn: stressfreiEmailApi.delete, onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }), }); const filtered = showInactive ? emails : emails.filter((e) => e.isActive); return (
{canEdit && ( )}

Hinweis: Hier werden E-Mail-Weiterleitungsadressen verwaltet, die für die Registrierung bei Anbietern verwendet werden. E-Mails an diese Adressen werden sowohl an den Kunden als auch an Sie weitergeleitet.

{filtered.length > 0 ? (
{filtered.map((emailItem) => (
{emailItem.email} {!emailItem.isActive && Inaktiv}
{emailItem.notes && (
{emailItem.notes}
)}
{canEdit && (
{emailItem.isActive ? ( ) : ( )}
)}
))}
) : (

Keine {customerEmailLabel} Adressen vorhanden.

)}
); } // ==================== CREDENTIALS DISPLAY ==================== function CredentialsDisplay({ credentials, onHide, onResetPassword, isResettingPassword, }: { credentials: { email: string; password: string; imap: { server: string; port: number; encryption: string } | null; smtp: { server: string; port: number; encryption: string } | null; }; onHide: () => void; onResetPassword: () => void; isResettingPassword: boolean; }) { const [copiedField, setCopiedField] = useState(null); const copyToClipboard = async (text: string, fieldName: string) => { try { await navigator.clipboard.writeText(text); setCopiedField(fieldName); setTimeout(() => setCopiedField(null), 2000); } catch { // Fallback für ältere Browser const textArea = document.createElement('textarea'); textArea.value = text; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); setCopiedField(fieldName); setTimeout(() => setCopiedField(null), 2000); } }; const CopyButton = ({ text, fieldName }: { text: string; fieldName: string }) => ( ); const imapString = credentials.imap ? `${credentials.imap.server}:${credentials.imap.port}` : ''; const smtpString = credentials.smtp ? `${credentials.smtp.server}:${credentials.smtp.port}` : ''; return (
Zugangsdaten
{/* Benutzername & Passwort */}
{credentials.email}
{credentials.password}
{/* Server-Einstellungen */}
{credentials.imap && (
{imapString}
{credentials.imap.encryption}
)} {credentials.smtp && (
{smtpString}
{credentials.smtp.encryption}
)}
); } // ==================== STRESSFREI-EMAIL MODAL ==================== function StressfreiEmailModal({ isOpen, onClose, customerId, email, customerEmail, }: { isOpen: boolean; onClose: () => void; customerId: number; email?: StressfreiEmail | null; customerEmail?: string; }) { const [localPart, setLocalPart] = useState(''); const [notes, setNotes] = useState(''); const [provisionAtProvider, setProvisionAtProvider] = useState(false); const [createMailbox, setCreateMailbox] = useState(false); const [provisionError, setProvisionError] = useState(null); const [providerStatus, setProviderStatus] = useState<'idle' | 'checking' | 'exists' | 'not_exists' | 'error'>('idle'); const [isProvisioning, setIsProvisioning] = useState(false); // Domain dynamisch vom Provider (mit Fallback) const { domain: providerDomain } = useProviderSettings(); const domainSuffix = `@${providerDomain || 'stressfrei-wechseln.de'}`; const [isEnablingMailbox, setIsEnablingMailbox] = useState(false); const [mailboxEnabled, setMailboxEnabled] = useState(false); const [showCredentials, setShowCredentials] = useState(false); const [credentials, setCredentials] = useState<{ email: string; password: string; imap: { server: string; port: number; encryption: string } | null; smtp: { server: string; port: number; encryption: string } | null; } | null>(null); const [isLoadingCredentials, setIsLoadingCredentials] = useState(false); const [isResettingPassword, setIsResettingPassword] = useState(false); const queryClient = useQueryClient(); const isEditing = !!email; // Prüfe ob ein Provider konfiguriert ist const { data: configsData } = useQuery({ queryKey: ['email-provider-configs'], queryFn: () => emailProviderApi.getConfigs(), enabled: isOpen, // Immer laden wenn Modal offen }); const hasProvider = (configsData?.data || []).some(c => c.isActive && c.isDefault); // Helper: Extrahiert den lokalen Teil einer E-Mail-Adresse (vor dem @) const extractLocalPart = (fullEmail: string) => { if (!fullEmail) return ''; const atIndex = fullEmail.indexOf('@'); return atIndex > 0 ? fullEmail.substring(0, atIndex) : fullEmail; }; // Prüft ob E-Mail beim Provider existiert const checkProviderStatus = async (emailLocalPart: string) => { if (!hasProvider || !emailLocalPart) return; setProviderStatus('checking'); try { const result = await emailProviderApi.checkEmailExists(emailLocalPart); setProviderStatus(result.data?.exists ? 'exists' : 'not_exists'); } catch { setProviderStatus('error'); } }; // E-Mail nachträglich beim Provider anlegen const handleProvisionNow = async () => { if (!customerEmail || !localPart) return; setIsProvisioning(true); setProvisionError(null); try { const result = await emailProviderApi.provisionEmail(localPart, customerEmail); if (result.data?.success) { setProviderStatus('exists'); } else { setProvisionError(result.data?.error || 'Provisionierung fehlgeschlagen'); } } catch (error) { setProvisionError(error instanceof Error ? error.message : 'Fehler bei der Provisionierung'); } finally { setIsProvisioning(false); } }; // Mailbox nachträglich aktivieren const handleEnableMailbox = async () => { if (!email) return; setIsEnablingMailbox(true); setProvisionError(null); try { const result = await stressfreiEmailApi.enableMailbox(email.id); if (result.success) { setMailboxEnabled(true); queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); queryClient.invalidateQueries({ queryKey: ['mailbox-accounts', customerId] }); } else { setProvisionError(result.error || 'Mailbox-Aktivierung fehlgeschlagen'); } } catch (error) { setProvisionError(error instanceof Error ? error.message : 'Fehler bei der Mailbox-Aktivierung'); } finally { setIsEnablingMailbox(false); } }; // Mailbox-Status mit Provider synchronisieren const syncMailboxStatusFromProvider = async () => { if (!email) return; try { const result = await stressfreiEmailApi.syncMailboxStatus(email.id); if (result.success && result.data) { setMailboxEnabled(result.data.hasMailbox); if (result.data.wasUpdated) { // DB wurde aktualisiert, Query invalidieren queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); } } } catch (error) { console.error('Fehler beim Synchronisieren des Mailbox-Status:', error); } }; // Mailbox-Zugangsdaten laden const loadCredentials = async () => { if (!email) return; setIsLoadingCredentials(true); try { const result = await stressfreiEmailApi.getMailboxCredentials(email.id); if (result.success && result.data) { setCredentials(result.data); setShowCredentials(true); } } catch (error) { console.error('Fehler beim Laden der Zugangsdaten:', error); } finally { setIsLoadingCredentials(false); } }; // Passwort zurücksetzen const handleResetPassword = async () => { if (!email) return; if (!confirm('Neues Passwort generieren? Das alte Passwort wird ungültig.')) return; setIsResettingPassword(true); try { const result = await stressfreiEmailApi.resetPassword(email.id); if (result.success && result.data) { // Credentials mit neuem Passwort aktualisieren if (credentials) { setCredentials({ ...credentials, password: result.data.password }); } alert('Passwort wurde erfolgreich zurückgesetzt.'); } else { alert(result.error || 'Fehler beim Zurücksetzen des Passworts'); } } catch (error) { console.error('Fehler beim Zurücksetzen des Passworts:', error); alert(error instanceof Error ? error.message : 'Fehler beim Zurücksetzen des Passworts'); } finally { setIsResettingPassword(false); } }; // Reset form when modal opens or email changes useEffect(() => { if (isOpen) { if (email) { const emailLocalPart = extractLocalPart(email.email); setLocalPart(emailLocalPart); setNotes(email.notes || ''); setProviderStatus('idle'); setMailboxEnabled(email.hasMailbox || false); // Status beim Provider prüfen wenn Provider vorhanden if (hasProvider) { checkProviderStatus(emailLocalPart); // Mailbox-Status synchronisieren syncMailboxStatusFromProvider(); } } else { setLocalPart(''); setNotes(''); setProvisionAtProvider(false); setCreateMailbox(false); setProviderStatus('idle'); setMailboxEnabled(false); } setProvisionError(null); // Zugangsdaten zurücksetzen setShowCredentials(false); setCredentials(null); } }, [isOpen, email, hasProvider]); const createMutation = useMutation({ mutationFn: async (data: { email: string; notes?: string; provision?: boolean; createMailbox?: boolean }) => { // Verwendet die neue API-Funktion, die Provisioning und Mailbox-Erstellung unterstützt return stressfreiEmailApi.create(customerId, { email: data.email, notes: data.notes, provisionAtProvider: data.provision, createMailbox: data.createMailbox, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); queryClient.invalidateQueries({ queryKey: ['mailbox-accounts', customerId] }); setLocalPart(''); setNotes(''); setProvisionAtProvider(false); setCreateMailbox(false); onClose(); }, onError: (error) => { setProvisionError(error instanceof Error ? error.message : 'Fehler bei der Provisionierung'); }, }); const updateMutation = useMutation({ mutationFn: (data: Partial) => stressfreiEmailApi.update(email!.id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); onClose(); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setProvisionError(null); const fullEmail = localPart + domainSuffix; if (isEditing) { updateMutation.mutate({ email: fullEmail, notes: notes || undefined, }); } else { createMutation.mutate({ email: fullEmail, notes: notes || undefined, provision: provisionAtProvider, createMailbox: provisionAtProvider && createMailbox, }); } }; const isPending = createMutation.isPending || updateMutation.isPending; return (
setLocalPart(e.target.value.toLowerCase().replace(/[^a-z0-9._-]/g, ''))} placeholder="kunde-freenet" required className="block w-full px-3 py-2 border border-gray-300 rounded-l-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" /> {domainSuffix}

Vollständige Adresse: {localPart || '...'}{domainSuffix}