diff --git a/frontend/src/pages/contracts/ContractList.tsx b/frontend/src/pages/contracts/ContractList.tsx index 493a30b7..b3978632 100644 --- a/frontend/src/pages/contracts/ContractList.tsx +++ b/frontend/src/pages/contracts/ContractList.tsx @@ -1,14 +1,15 @@ import { useState, useEffect, useMemo } from 'react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQuery, useMutation, useQueryClient, useQueries } from '@tanstack/react-query'; import { Link, useSearchParams, useNavigate } from 'react-router-dom'; -import { contractApi } from '../../services/api'; +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 { Plus, Search, Eye, Edit, Trash2, User, Users } from 'lucide-react'; +import CopyButton from '../../components/ui/CopyButton'; +import { Plus, Search, Eye, Edit, Trash2, User, Users, ChevronDown, ChevronRight } from 'lucide-react'; import type { Contract, ContractType, ContractStatus } from '../../types'; const typeLabels: Record = { @@ -50,6 +51,9 @@ export default function ContractList() { 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()); + const { hasPermission, isCustomer, isCustomerPortal, user } = useAuth(); const queryClient = useQueryClient(); @@ -83,11 +87,49 @@ export default function ContractList() { }), }); - // Für Kundenportal: Verträge nach Kunde gruppieren + // 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]); + + // 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 = {}; + const groups: Record = {}; for (const contract of data.data) { const customerId = contract.customerId; @@ -96,9 +138,11 @@ export default function ContractList() { ? (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); @@ -110,7 +154,114 @@ export default function ContractList() { if (!a.isOwn && b.isOwn) return 1; return a.customerName.localeCompare(b.customerName); }); - }, [data?.data, isCustomerPortal, user?.customerId]); + }, [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}`} + +

+ )} + {contract.startDate && ( +

+ Beginn: {new Date(contract.startDate).toLocaleDateString('de-DE')} + {contract.endDate && + ` | Ende: ${new Date(contract.endDate).toLocaleDateString('de-DE')}`} +

+ )} +
+ + {/* 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 (
@@ -159,7 +310,7 @@ export default function ContractList() { ) : data?.data && data.data.length > 0 ? ( <> - {/* Kundenportal: Gruppierte Ansicht */} + {/* Kundenportal: Gruppierte Ansicht mit Baumstruktur */} {isCustomerPortal && groupedContracts ? (
{groupedContracts.map((group) => ( @@ -183,58 +334,14 @@ export default function ContractList() { )}
- {/* Vertrags-Tabelle */} -
- - - - - - - - - - - - - {group.contracts.map((contract) => ( - - - - - - - - - ))} - -
Vertragsnr.TypAnbieter / TarifStatusBeginnAktionen
{contract.contractNumber} - {typeLabels[contract.type]} - - {contract.providerName || '-'} - {contract.tariffName && ( - / {contract.tariffName} - )} - - - {statusLabels[contract.status]} - - - {contract.startDate - ? new Date(contract.startDate).toLocaleDateString('de-DE') - : '-'} - - -
-
+ {/* Baumstruktur */} + {group.tree.length > 0 ? ( +
+ {group.tree.map(node => renderContractNode(node, 0))} +
+ ) : ( +

Keine Verträge vorhanden.

+ )} ))}