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 {