import { useState, useMemo, useEffect, useRef } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Link, useSearchParams } from 'react-router-dom'; import { pushHistory } from '../../utils/navigation'; import { contractApi, meterApi, birthdayApi } from '../../services/api'; import Card from '../../components/ui/Card'; import Badge from '../../components/ui/Badge'; import Select from '../../components/ui/Select'; import Button from '../../components/ui/Button'; import Input from '../../components/ui/Input'; import { AlertCircle, AlertTriangle, CheckCircle, Clock, Eye, Calendar, Key, FileText, ClipboardList, ChevronDown, ChevronRight, Zap, Wifi, Smartphone, Tv, Car, Flame, BellOff, RotateCcw, Receipt, ShieldAlert, ShieldX, CreditCard, Gauge, ExternalLink, CheckCircle2, Cake, } from 'lucide-react'; import { formatDate } from '../../utils/dateFormat'; import type { CockpitContract, CockpitUrgencyLevel, ContractType } from '../../types'; const typeIcons: Record = { ELECTRICITY: Zap, GAS: Flame, DSL: Wifi, CABLE: Wifi, FIBER: Wifi, MOBILE: Smartphone, TV: Tv, CAR_INSURANCE: Car, }; const typeLabels: Record = { ELECTRICITY: 'Strom', GAS: 'Gas', DSL: 'DSL', CABLE: 'Kabel', FIBER: 'Glasfaser', MOBILE: 'Mobilfunk', TV: 'TV', CAR_INSURANCE: 'KFZ', }; const urgencyColors: Record = { critical: 'bg-red-100 border-red-300 text-red-800', warning: 'bg-yellow-100 border-yellow-300 text-yellow-800', ok: 'bg-green-100 border-green-300 text-green-800', none: 'bg-gray-100 border-gray-300 text-gray-800', }; const urgencyBadgeVariants: Record = { critical: 'danger', warning: 'warning', ok: 'success', none: 'default', }; const issueTypeIcons: Record = { cancellation_deadline: Calendar, contract_ending: Clock, missing_cancellation_letter: FileText, missing_cancellation_confirmation: FileText, missing_portal_credentials: Key, missing_customer_number: FileText, missing_provider: FileText, missing_address: FileText, missing_bank: FileText, missing_meter: Zap, missing_sim: Smartphone, open_tasks: ClipboardList, pending_status: Clock, draft_status: FileText, review_due: RotateCcw, missing_invoice: Receipt, missing_identity_document: CreditCard, identity_document_expired: CreditCard, identity_document_expiring: CreditCard, missing_consents: ShieldAlert, consent_withdrawn: ShieldX, }; const categoryLabels: Record = { cancellationDeadlines: 'Kündigungsfristen', contractEnding: 'Vertragsenden', missingCredentials: 'Fehlende Zugangsdaten', missingData: 'Fehlende Daten', openTasks: 'Offene Aufgaben', pendingContracts: 'Wartende Verträge', missingInvoices: 'Fehlende Rechnungen', reviewDue: 'Erneute Prüfung fällig', missingConsents: 'Fehlende Einwilligungen', }; type FilterType = 'all' | 'critical' | 'warning' | 'ok' | 'deadlines' | 'credentials' | 'data' | 'tasks' | 'review' | 'invoices' | 'consents'; export default function ContractCockpit() { const [searchParams, setSearchParams] = useSearchParams(); const [expandedContracts, setExpandedContracts] = useState>(new Set()); // Filter aus URL-Parameter initialisieren const urlFilter = searchParams.get('filter') as FilterType | null; const [filter, setFilter] = useState(urlFilter || 'all'); // URL-Parameter bei Filter-Änderung aktualisieren useEffect(() => { if (filter === 'all') { searchParams.delete('filter'); } else { searchParams.set('filter', filter); } setSearchParams(searchParams, { replace: true }); }, [filter, searchParams, setSearchParams]); const { data: cockpitData, isLoading, error } = useQuery({ queryKey: ['contract-cockpit'], queryFn: () => contractApi.getCockpit(), staleTime: 0, }); // Geburtstage (kommende + vergangene der letzten 7 Tage) const { data: birthdaysData } = useQuery({ queryKey: ['upcoming-birthdays'], queryFn: () => birthdayApi.getUpcoming(7, 30), staleTime: 5 * 60_000, }); const queryClient = useQueryClient(); const [snoozeContractId, setSnoozeContractId] = useState(null); const [customDate, setCustomDate] = useState(''); const snoozeDropdownRef = useRef(null); // Close snooze dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (snoozeDropdownRef.current && !snoozeDropdownRef.current.contains(event.target as Node)) { setSnoozeContractId(null); setCustomDate(''); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const snoozeMutation = useMutation({ mutationFn: ({ contractId, data }: { contractId: number; data: { months?: number; nextReviewDate?: string } }) => contractApi.snooze(contractId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contract-cockpit'] }); setSnoozeContractId(null); setCustomDate(''); }, }); const handleSnooze = (contractId: number, months?: number) => { if (months) { snoozeMutation.mutate({ contractId, data: { months } }); } else if (customDate) { snoozeMutation.mutate({ contractId, data: { nextReviewDate: customDate } }); } }; const handleUnsnooze = (contractId: number) => { // Snooze aufheben: Leeres Objekt senden → nextReviewDate wird auf null gesetzt snoozeMutation.mutate({ contractId, data: {} }); }; const toggleExpanded = (contractId: number) => { setExpandedContracts(prev => { const next = new Set(prev); if (next.has(contractId)) { next.delete(contractId); } else { next.add(contractId); } return next; }); }; // Filter contracts const filteredContracts = useMemo(() => { if (!cockpitData?.data?.contracts) return []; const contracts = cockpitData.data.contracts; switch (filter) { case 'critical': return contracts.filter(c => c.highestUrgency === 'critical'); case 'warning': return contracts.filter(c => c.highestUrgency === 'warning'); case 'ok': return contracts.filter(c => c.highestUrgency === 'ok'); case 'deadlines': return contracts.filter(c => c.issues.some(i => ['cancellation_deadline', 'contract_ending'].includes(i.type)) ); case 'credentials': return contracts.filter(c => c.issues.some(i => i.type.includes('credentials')) ); case 'data': return contracts.filter(c => c.issues.some(i => i.type.startsWith('missing_') && !i.type.includes('credentials')) ); case 'tasks': return contracts.filter(c => c.issues.some(i => ['open_tasks', 'pending_status', 'draft_status'].includes(i.type)) ); case 'review': return contracts.filter(c => c.issues.some(i => i.type === 'review_due') ); case 'invoices': return contracts.filter(c => c.issues.some(i => i.type.includes('invoice')) ); case 'consents': return contracts.filter(c => c.issues.some(i => ['missing_consents', 'consent_withdrawn'].includes(i.type)) ); default: return contracts; } }, [cockpitData?.data?.contracts, filter]); if (isLoading) { return (
Laden...
); } if (error || !cockpitData?.data) { return (

Fehler beim Laden des Cockpits

); } const { summary, thresholds } = cockpitData.data; const renderContract = (contract: CockpitContract) => { const isExpanded = expandedContracts.has(contract.id); const TypeIcon = typeIcons[contract.type] || FileText; return (
{/* Contract Header */}
toggleExpanded(contract.id)} > {/* Expand Icon */}
{isExpanded ? ( ) : ( )}
{/* Type Icon */} {/* Contract Info */}
e.stopPropagation()} > {contract.contractNumber} {contract.issues.length} {contract.highestUrgency === 'ok' ? (contract.issues.length === 1 ? 'Hinweis' : 'Hinweise') : (contract.issues.length === 1 ? 'Problem' : 'Probleme')} {typeLabels[contract.type]}
e.stopPropagation()} > {contract.customer.customerNumber} - {contract.customer.name} {(contract.provider?.name || contract.providerName) && ( | {contract.provider?.name || contract.providerName} {(contract.tariff?.name || contract.tariffName) && ` - ${contract.tariff?.name || contract.tariffName}`} )}
{/* Actions */}
{/* Snooze Button */}
{/* Snooze Dropdown */} {snoozeContractId === contract.id && (
e.stopPropagation()} >
Zurückstellen
setCustomDate(e.target.value)} className="flex-1 text-sm" min={new Date().toISOString().split('T')[0]} />
{/* Snooze aufheben - zeige nur wenn review_due Issue existiert */} {contract.issues.some(i => i.type === 'review_due') && (
)}
)}
e.stopPropagation()} title="Zum Vertrag" >
{/* Expanded: Issues */} {isExpanded && (
{contract.issues.map((issue, idx) => { const IssueIcon = issueTypeIcons[issue.type] || AlertCircle; const UrgencyIcon = issue.urgency === 'critical' ? AlertCircle : issue.urgency === 'warning' ? AlertTriangle : issue.urgency === 'ok' ? CheckCircle : Clock; return (
{issue.label} {issue.details && ( {issue.details} )}
); })}
)}
); }; return (

Vertrags-Cockpit

Fristenschwellen anpassen
{/* Summary Cards */}

{summary.criticalCount}

Kritisch (<{thresholds.criticalDays} Tage)

{summary.warningCount}

Warnung (<{thresholds.warningDays} Tage)

{summary.okCount}

OK (<{thresholds.okDays} Tage)

{summary.totalContracts}

Verträge mit Handlungsbedarf

{/* Category Summary */}
{Object.entries(summary.byCategory).map(([key, count]) => ( count > 0 && (
{categoryLabels[key] || key}: {count}
) ))}
{/* Ausweis-Warnungen (vertragsunabhängig) */} {cockpitData.data.documentAlerts && cockpitData.data.documentAlerts.length > 0 && (

Ablaufende Ausweise

{cockpitData.data.documentAlerts.length}
{cockpitData.data.documentAlerts.map((alert) => (
{alert.customer.name} ({alert.customer.customerNumber})

{alert.type === 'ID_CARD' ? 'Personalausweis' : alert.type === 'PASSPORT' ? 'Reisepass' : alert.type === 'DRIVERS_LICENSE' ? 'Führerschein' : 'Ausweis'}{' '} {alert.documentNumber}

{alert.daysUntilExpiry < 0 ? ( Seit {Math.abs(alert.daysUntilExpiry)} Tagen abgelaufen ) : ( Noch {alert.daysUntilExpiry} Tage )}

{formatDate(alert.expiryDate)}

))}
)} {/* Gemeldete Zählerstände */} {cockpitData.data.reportedReadings && cockpitData.data.reportedReadings.length > 0 && (

Gemeldete Zählerstände

{cockpitData.data.reportedReadings.length}

Von Kunden gemeldete Zählerstände – bitte an den jeweiligen Anbieter übertragen.

{cockpitData.data.reportedReadings.map((reading) => (
{reading.meter.type === 'ELECTRICITY' ? ( ) : ( )}
{reading.customer.name} ({reading.customer.customerNumber}) {reading.contract && ( {reading.contract.contractNumber} )}

Zähler {reading.meter.meterNumber} – {reading.value} {reading.unit} am{' '} {formatDate(reading.readingDate)} {reading.notes && ` – ${reading.notes}`}

{reading.providerPortal && ( {reading.providerPortal.providerName} )}
))}
)} {/* Geburtstage */} {birthdaysData?.data && birthdaysData.data.length > 0 && (

Geburtstage

{birthdaysData.data.length}

Kunden mit Geburtstag in den nächsten 30 Tagen oder den letzten 7 Tagen.

{birthdaysData.data.map((b) => (
{b.name} ({b.customerNumber})

{formatDate(b.birthDate)} – wird {b.age} Jahre

{b.isToday ? ( 🎉 Heute! ) : b.isPast ? ( Vor {Math.abs(b.daysUntil)} Tag{Math.abs(b.daysUntil) === 1 ? '' : 'en'} ) : ( In {b.daysUntil} Tag{b.daysUntil === 1 ? '' : 'en'} )}
))}
)} {/* Filter */}
Filter: