import { useState, useEffect, useMemo } from 'react'; import { useQuery, useMutation, useQueryClient, useQueries } from '@tanstack/react-query'; import { Link, useSearchParams, useNavigate } from 'react-router-dom'; import { pushHistory } from '../../utils/navigation'; import { contractApi, ContractTreeNode } from '../../services/api'; import { useAuth } from '../../context/AuthContext'; import Card from '../../components/ui/Card'; import Button from '../../components/ui/Button'; import Input from '../../components/ui/Input'; import Select from '../../components/ui/Select'; import Badge from '../../components/ui/Badge'; import CopyButton from '../../components/ui/CopyButton'; import { Plus, Search, Eye, Edit, Trash2, User, Users, ChevronDown, ChevronRight, Info, X, ShieldAlert } from 'lucide-react'; import { gdprApi } from '../../services/api'; import { formatDate } from '../../utils/dateFormat'; import { getContractTypeInfo } from '../../utils/contractInfo'; import type { Contract, ContractType, ContractStatus } from '../../types'; const typeLabels: Record = { ELECTRICITY: 'Strom', GAS: 'Gas', DSL: 'DSL', CABLE: 'Kabelinternet', FIBER: 'Glasfaser', MOBILE: 'Mobilfunk', TV: 'TV', CAR_INSURANCE: 'KFZ-Versicherung', }; const statusLabels: Record = { DRAFT: 'Entwurf', PENDING: 'Ausstehend', ACTIVE: 'Aktiv', CANCELLED: 'Gekündigt', EXPIRED: 'Abgelaufen', DEACTIVATED: 'Deaktiviert', }; const statusVariants: Record = { ACTIVE: 'success', PENDING: 'warning', CANCELLED: 'danger', EXPIRED: 'danger', DRAFT: 'default', DEACTIVATED: 'default', }; // Status-Erklärungen für Info-Modal 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' }, ]; function StatusInfoModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) { if (!isOpen) return null; return (

Vertragsstatus-Übersicht

{statusDescriptions.map(({ status, label, description, color }) => (
{label} {description}
))}
); } export default function ContractList() { const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); // Filter-Werte aus URL-Parametern lesen const [search, setSearch] = useState(searchParams.get('search') || ''); const [type, setType] = useState(searchParams.get('type') || ''); const [status, setStatus] = useState(searchParams.get('status') || ''); const [page, setPage] = useState(parseInt(searchParams.get('page') || '1', 10)); // State für aufgeklappte Verträge (Baumstruktur) const [expandedContracts, setExpandedContracts] = useState>(new Set()); // Status-Info Modal const [showStatusInfo, setShowStatusInfo] = useState(false); const { hasPermission, isCustomer, isCustomerPortal, user } = useAuth(); const queryClient = useQueryClient(); // URL-Parameter aktualisieren wenn sich Filter ändern useEffect(() => { const params = new URLSearchParams(); if (search) params.set('search', search); if (type) params.set('type', type); if (status) params.set('status', status); if (page > 1) params.set('page', page.toString()); setSearchParams(params, { replace: true }); }, [search, type, status, page, setSearchParams]); const deleteMutation = useMutation({ mutationFn: contractApi.delete, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contracts'] }); }, }); const { data, isLoading } = useQuery({ queryKey: ['contracts', search, type, status, page, isCustomer ? user?.customerId : null], queryFn: () => contractApi.getAll({ search: search || undefined, type: type || undefined, status: status || undefined, page, limit: 20, customerId: isCustomer ? user?.customerId : undefined, }), }); // Alle Kunden-IDs ermitteln für Baumstruktur-Queries (Kundenportal) const allCustomerIds = useMemo(() => { if (!isCustomerPortal || !data?.data) return []; const ids = new Set(); // Eigene customerId hinzufügen if (user?.customerId) ids.add(user.customerId); // Freigegebene Kunden hinzufügen data.data.forEach(c => ids.add(c.customerId)); return [...ids]; }, [data?.data, isCustomerPortal, user?.customerId]); // Vollmacht-Status für vertretene Kunden (Portal) const { data: authStatusData } = useQuery({ queryKey: ['my-authorization-status'], queryFn: () => gdprApi.getMyAuthorizationStatus(), enabled: isCustomerPortal, }); const unauthorizedCustomers = useMemo(() => { if (!isCustomerPortal || !authStatusData?.data || !user?.representedCustomers) return []; return authStatusData.data .filter((s) => !s.hasAuthorization) .map((s) => { const cust = user.representedCustomers?.find((c) => c.id === s.customerId); return { customerId: s.customerId, customerName: cust ? `${cust.firstName} ${cust.lastName}` : `Kunde ${s.customerId}`, }; }); }, [authStatusData?.data, isCustomerPortal, user?.representedCustomers]); // Baumstruktur für alle Kunden laden (Kundenportal) const treeQueries = useQueries({ queries: allCustomerIds.map(customerId => ({ queryKey: ['contract-tree', customerId], queryFn: () => contractApi.getTreeForCustomer(customerId), enabled: isCustomerPortal, })), }); // Map von customerId zu Baum erstellen const contractTrees = useMemo(() => { const map = new Map(); allCustomerIds.forEach((customerId, index) => { const queryResult = treeQueries[index]; if (queryResult?.data?.data) { map.set(customerId, queryResult.data.data); } }); return map; }, [allCustomerIds, treeQueries]); // Für Kundenportal: Verträge nach Kunde gruppieren mit Baumstruktur const groupedContracts = useMemo(() => { if (!isCustomerPortal || !data?.data) return null; const groups: Record = {}; for (const contract of data.data) { const customerId = contract.customerId; if (!groups[customerId]) { const customerName = contract.customer ? (contract.customer.companyName || `${contract.customer.firstName} ${contract.customer.lastName}`) : `Kunde ${customerId}`; groups[customerId] = { customerId, customerName, isOwn: customerId === user?.customerId, contracts: [], tree: contractTrees.get(customerId) || [], }; } groups[customerId].contracts.push(contract); } // Eigene Verträge zuerst, dann alphabetisch nach Name return Object.values(groups).sort((a, b) => { if (a.isOwn && !b.isOwn) return -1; if (!a.isOwn && b.isOwn) return 1; return a.customerName.localeCompare(b.customerName); }); }, [data?.data, isCustomerPortal, user?.customerId, contractTrees]); // Toggle für Aufklappen/Zuklappen (Baumstruktur) 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 (Kundenportal) const renderPredecessors = (predecessors: ContractTreeNode[], depth: number): React.ReactNode => { return predecessors.map(node => (
{renderContractNode(node, depth)}
)); }; // Einzelnen Vertragsknoten rendern (Kundenportal) 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 as ContractType] || contract.type} {statusLabels[contract.status as ContractStatus] || contract.status} {isPredecessor && ( (Vorgänger) )}
{(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)}
)}
); }; return (

Verträge

{hasPermission('contracts:create') && !isCustomer && ( )}
setSearch(e.target.value)} />
setStatus(e.target.value)} options={Object.entries(statusLabels).map(([value, label]) => ({ value, label }))} className="w-48" />
{isLoading ? (
Laden...
) : data?.data && data.data.length > 0 ? ( <> {/* Kundenportal: Gruppierte Ansicht mit Baumstruktur */} {isCustomerPortal && groupedContracts ? (
{groupedContracts.map((group) => ( {/* Gruppen-Header */}
{group.isOwn ? ( <>

Meine Verträge

{group.contracts.length} ) : ( <>

Verträge von {group.customerName}

{group.contracts.length} )}
{/* Baumstruktur */} {group.tree.length > 0 ? (
{group.tree.map(node => renderContractNode(node, 0))}
) : (

Keine Verträge vorhanden.

)}
))} {/* Kunden ohne Vollmacht */} {unauthorizedCustomers.map((uc) => (

Verträge von {uc.customerName}

Einwilligung / Vollmacht fehlt. {uc.customerName} muss Ihnen zuerst eine Vollmacht erteilen.

))}
) : ( /* Standard-Ansicht für Mitarbeiter */
{!isCustomer && ( )} {data.data.map((contract) => { const typeInfo = getContractTypeInfo(contract as any); return ( {!isCustomer && ( )} ); })}
Vertragsnr.KundeTyp Anbieter / Tarif Status Beginn Aktionen
{contract.contractNumber} {typeInfo && (
{typeInfo.label}: {typeInfo.value}
)}
{contract.customer && ( {contract.customer.companyName || `${contract.customer.firstName} ${contract.customer.lastName}`} )} {typeLabels[contract.type]} {contract.providerName || '-'} {contract.tariffName && ( / {contract.tariffName} )} {statusLabels[contract.status]} {contract.startDate ? formatDate(contract.startDate) : '-'}
{hasPermission('contracts:update') && !isCustomer && ( )} {hasPermission('contracts:delete') && !isCustomer && ( )}
{data.pagination && data.pagination.totalPages > 1 && (

Seite {data.pagination.page} von {data.pagination.totalPages} ( {data.pagination.total} Einträge)

)}
)} ) : (
Keine Verträge gefunden.
)} {/* Status-Info Modal */} setShowStatusInfo(false)} />
); }