From b4b0dbb0048f1cd6cd3b57a40cf4257fe6ba0daa Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 30 May 2026 14:48:22 +0200 Subject: [PATCH] =?UTF-8?q?Kundenakte=20=E2=86=92=20Z=C3=A4hler:=20Aufklap?= =?UTF-8?q?p-Liste=20der=20zugeordneten=20Vertr=C3=A4ge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pro Zähler wird jetzt ein "Verträge (N)" Aufklapp-Bereich angezeigt, der alle Verträge auflistet, die diesen Zähler nutzen – sowohl als aktueller Hauptzähler (energyDetails.meterId) als auch über die Folgezähler-Kette (ContractMeter). Dedupliziert auf contractId. Jeder Eintrag ist Link auf den Vertrag im neuen Tab, mit Vertragsnummer, Anbieter und Status-Badge. Folgezähler-Ketten- Einträge werden mit "(über Folgezähler-Kette)" markiert. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/src/services/customer.service.ts | 18 +++++ .../src/pages/customers/CustomerDetail.tsx | 72 +++++++++++++++++++ frontend/src/types/index.ts | 32 +++++++++ 3 files changed, 122 insertions(+) diff --git a/backend/src/services/customer.service.ts b/backend/src/services/customer.service.ts index 918f02d5..45ec1d82 100644 --- a/backend/src/services/customer.service.ts +++ b/backend/src/services/customer.service.ts @@ -87,6 +87,24 @@ export async function getCustomerById(id: number) { readings: { orderBy: { readingDate: 'desc' }, }, + // Verträge, die diesen Zähler aktuell als Hauptzähler nutzen + // (energyDetails.meterId === meter.id) + energyDetails: { + include: { + contract: { select: { id: true, contractNumber: true, status: true, type: true, providerName: true } }, + }, + }, + // Verträge, in denen der Zähler in der ContractMeter-Kette steht + // (Vorgänger oder Nachfolger über Zählerwechsel) + contractMeters: { + include: { + energyContractDetails: { + include: { + contract: { select: { id: true, contractNumber: true, status: true, type: true, providerName: true } }, + }, + }, + }, + }, }, }, stressfreiEmails: { orderBy: { isActive: 'desc' } }, diff --git a/frontend/src/pages/customers/CustomerDetail.tsx b/frontend/src/pages/customers/CustomerDetail.tsx index 85524f4a..50f56505 100644 --- a/frontend/src/pages/customers/CustomerDetail.tsx +++ b/frontend/src/pages/customers/CustomerDetail.tsx @@ -1267,6 +1267,7 @@ function MetersTab({ const hasDeliveryAddress = addresses.some((a) => a.type === 'DELIVERY_RESIDENCE'); const [showReadingModal, setShowReadingModal] = useState<{ meterId: number; meterType: 'ELECTRICITY' | 'GAS'; tariffModel?: string } | null>(null); const [expandedMeter, setExpandedMeter] = useState(null); + const [expandedContractsForMeter, setExpandedContractsForMeter] = useState>(new Set()); const [editingReading, setEditingReading] = useState<{ meterId: number; meterType: 'ELECTRICITY' | 'GAS'; tariffModel?: string; reading: any } | null>(null); const queryClient = useQueryClient(); @@ -1441,6 +1442,77 @@ function MetersTab({

)} + {(() => { + // Zugeordnete Verträge zusammenstellen: zum einen über + // energyDetails (aktueller Hauptzähler), zum anderen über + // contractMeters (Folgezähler-Kette). Dedupliziert auf + // contractId, da ein Zähler über beide Wege auftauchen kann. + const seen = new Set(); + const linkedContracts: Array<{ id: number; contractNumber: string; status: string; type: string; providerName?: string; via: 'main' | 'chain' }> = []; + for (const ed of meter.energyDetails || []) { + if (ed.contract && !seen.has(ed.contract.id)) { + seen.add(ed.contract.id); + linkedContracts.push({ ...ed.contract, via: 'main' }); + } + } + for (const cm of meter.contractMeters || []) { + const ct = cm.energyContractDetails?.contract; + if (ct && !seen.has(ct.id)) { + seen.add(ct.id); + linkedContracts.push({ ...ct, via: 'chain' }); + } + } + if (linkedContracts.length === 0) return null; + const isContractsExpanded = expandedContractsForMeter.has(meter.id); + return ( +
+ + {isContractsExpanded && ( +
+ {linkedContracts.map((ct) => ( + + + {ct.contractNumber} + {ct.providerName && ( + – {ct.providerName} + )} + {ct.status} + {ct.via === 'chain' && ( + (über Folgezähler-Kette) + )} + + ))} +
+ )} +
+ ); + })()} + {sortedReadings.length > 0 && (
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index e0f45c48..67045355 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -221,6 +221,38 @@ export interface Meter { addressId?: number | null; address?: Address; readings?: MeterReading[]; + // Verträge, die diesen Zähler aktuell als Hauptzähler nutzen + energyDetails?: Array<{ + id: number; + contractId: number; + contract?: { + id: number; + contractNumber: string; + status: ContractStatus; + type: string; + providerName?: string; + }; + }>; + // Verträge, in denen der Zähler in der ContractMeter-Kette steht + // (Vorgänger oder Nachfolger über Zählerwechsel) + contractMeters?: Array<{ + id: number; + energyContractDetailsId: number; + position: number; + installedAt?: string; + removedAt?: string; + energyContractDetails?: { + id: number; + contractId: number; + contract?: { + id: number; + contractNumber: string; + status: ContractStatus; + type: string; + providerName?: string; + }; + }; + }>; } export interface ContractMeter {