From c5cf2c7416c1ece0906399be1dae2a5a75f3b653 Mon Sep 17 00:00:00 2001
From: duffyduck
Date: Thu, 13 Aug 2026 12:18:24 +0200
Subject: [PATCH] Vertragslisten: Karteninhaber + Mobilfunknetz + Kuendigung
(rot)
Beide Vertragslisten (Kundenakte-Baum + Hauptmenue /contracts) zeigen bei
Mobilfunkvertraegen mit Rufnummer jetzt zusaetzlich Karteninhaber
(SimCard.cardUser der angezeigten SIM, nur wenn gesetzt) und Netz
(mobileNetwork -> Telekom/Vodafone/Telefonica (o2)) inline. Vertraege mit
Kuendigungsbestaetigung (cancellationConfirmationDate) bekommen eine eigene
Zeile in roter, fetter Schrift.
Backend: getContractTreeForCustomer + getAllContracts selektieren nun
mobileNetwork + cardUser (Tree zusaetzlich cancellationConfirmationDate).
Shared-Helper getContractTypeInfo um cardUser/network + mobileNetworkLabel
erweitert. Beide Listen rendern ueber den Tree-Endpoint.
Verifiziert: tsc + vite build gruen, Tree-Service liefert die Felder,
Helper-Mapping getestet (Vodafone/Telekom/o2, leerer Karteninhaber ausgelassen).
Co-Authored-By: Claude Opus 4.8
---
backend/src/services/contract.service.ts | 11 +++++--
docs/todo.md | 13 +++++++++
frontend/src/pages/contracts/ContractList.tsx | 11 +++++++
.../src/pages/customers/CustomerDetail.tsx | 11 +++++++
frontend/src/services/api.ts | 4 ++-
frontend/src/utils/contractInfo.ts | 29 +++++++++++++++++--
6 files changed, 72 insertions(+), 7 deletions(-)
diff --git a/backend/src/services/contract.service.ts b/backend/src/services/contract.service.ts
index 7ccd831b..056c9b3a 100644
--- a/backend/src/services/contract.service.ts
+++ b/backend/src/services/contract.service.ts
@@ -105,7 +105,8 @@ export async function getAllContracts(filters: ContractFilters) {
mobileDetails: {
select: {
phoneNumber: true,
- simCards: { select: { phoneNumber: true, isMain: true } },
+ mobileNetwork: true,
+ simCards: { select: { phoneNumber: true, isMain: true, cardUser: true } },
},
},
// 2026-07-04: für Vertragsauswahl-Labels brauchen wir überall
@@ -1223,6 +1224,7 @@ export interface ContractTreeNode {
status: ContractStatus;
startDate: Date | null;
endDate: Date | null;
+ cancellationConfirmationDate: Date | null;
providerName: string | null;
tariffName: string | null;
previousContractId: number | null;
@@ -1233,7 +1235,8 @@ export interface ContractTreeNode {
address?: { street: string; houseNumber: string; postalCode: string; city: string } | null;
mobileDetails?: {
phoneNumber: string | null;
- simCards: { phoneNumber: string | null; isMain: boolean }[];
+ mobileNetwork: string | null;
+ simCards: { phoneNumber: string | null; isMain: boolean; cardUser: string | null }[];
} | null;
carInsuranceDetails?: { licensePlate: string | null } | null;
};
@@ -1268,6 +1271,7 @@ export async function getContractTreeForCustomer(
status: true,
startDate: true,
endDate: true,
+ cancellationConfirmationDate: true,
providerName: true,
tariffName: true,
previousContractId: true,
@@ -1279,7 +1283,8 @@ export async function getContractTreeForCustomer(
mobileDetails: {
select: {
phoneNumber: true,
- simCards: { select: { phoneNumber: true, isMain: true } },
+ mobileNetwork: true,
+ simCards: { select: { phoneNumber: true, isMain: true, cardUser: true } },
},
},
carInsuranceDetails: { select: { licensePlate: true } },
diff --git a/docs/todo.md b/docs/todo.md
index 02da85de..d9907f68 100644
--- a/docs/todo.md
+++ b/docs/todo.md
@@ -97,6 +97,19 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt
+- [x] **📇 Vertragslisten: Karteninhaber + Mobilfunknetz + Kündigung (rot)** (2026-08-13)
+ - In **beiden** Vertragslisten (Kundenakte-Baum + Hauptmenü `/contracts`): bei
+ Mobilfunkverträgen mit Rufnummer zusätzlich **Karteninhaber** (`SimCard.cardUser`
+ der angezeigten SIM, nur wenn gesetzt) und **Netz** (`mobileNetwork` →
+ Telekom/Vodafone/Telefónica (o2)) inline hinter der Rufnummer.
+ - Verträge mit **Kündigungsbestätigung**: `cancellationConfirmationDate` als
+ eigene Zeile in **roter, fetter Schrift** („Kündigungsbestätigung zum …").
+ - Backend: `getContractTreeForCustomer` + `getAllContracts` liefern jetzt
+ `mobileNetwork` + `cardUser` (Tree zusätzlich `cancellationConfirmationDate`;
+ Flat-Liste hat Scalars ohnehin via include). Beide Listen rendern über den
+ Tree-Endpoint. Shared-Helper `getContractTypeInfo` um `cardUser`/`network`
+ erweitert (+ `mobileNetworkLabel`).
+
- [x] **🛡️ Globaler API-Rate-Limit-Backstop + BLZ-Guard-Härtung (Pentest R148)** (2026-08-12)
- **Backstop:** Neuer genereller Limiter auf ALLE `/api`-Requests
(`apiBackstopRateLimiter`, in `index.ts` vor den Routern). Vorher gab es
diff --git a/frontend/src/pages/contracts/ContractList.tsx b/frontend/src/pages/contracts/ContractList.tsx
index d6ecec70..cf3afcdc 100644
--- a/frontend/src/pages/contracts/ContractList.tsx
+++ b/frontend/src/pages/contracts/ContractList.tsx
@@ -318,6 +318,12 @@ export default function ContractList() {
return typeInfo ? (
{typeInfo.label}: {typeInfo.value}
+ {typeInfo.cardUser && (
+ <> · Karteninhaber: {typeInfo.cardUser}>
+ )}
+ {typeInfo.network && (
+ <> · Netz: {typeInfo.network}>
+ )}
) : null;
})()}
@@ -328,6 +334,11 @@ export default function ContractList() {
` | Ende: ${formatDate(contract.endDate)}`}
)}
+ {contract.cancellationConfirmationDate && (
+
+ Kündigungsbestätigung zum {formatDate(contract.cancellationConfirmationDate)}
+
+ )}
{/* Vorgänger rekursiv rendern - für Wurzel nur wenn aufgeklappt, für Vorgänger immer */}
diff --git a/frontend/src/pages/customers/CustomerDetail.tsx b/frontend/src/pages/customers/CustomerDetail.tsx
index 93e0a2d1..2b6287fa 100644
--- a/frontend/src/pages/customers/CustomerDetail.tsx
+++ b/frontend/src/pages/customers/CustomerDetail.tsx
@@ -1882,6 +1882,12 @@ function ContractsTab({
return typeInfo ? (
{typeInfo.label}: {typeInfo.value}
+ {typeInfo.cardUser && (
+ <> · Karteninhaber: {typeInfo.cardUser}>
+ )}
+ {typeInfo.network && (
+ <> · Netz: {typeInfo.network}>
+ )}
) : null;
})()}
@@ -1892,6 +1898,11 @@ function ContractsTab({
` | Ende: ${formatDate(contract.endDate)}`}
)}
+ {contract.cancellationConfirmationDate && (
+
+ Kündigungsbestätigung zum {formatDate(contract.cancellationConfirmationDate)}
+
+ )}
{/* Vorgänger rekursiv rendern - für Wurzel nur wenn aufgeklappt, für Vorgänger immer */}
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index 2ea71680..28a9924f 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -913,6 +913,7 @@ export interface ContractTreeNodeContract {
status: string;
startDate: string | null;
endDate: string | null;
+ cancellationConfirmationDate?: string | null;
providerName: string | null;
tariffName: string | null;
previousContractId: number | null;
@@ -923,7 +924,8 @@ export interface ContractTreeNodeContract {
address?: { street: string; houseNumber: string; postalCode: string; city: string } | null;
mobileDetails?: {
phoneNumber: string | null;
- simCards: { phoneNumber: string | null; isMain: boolean }[];
+ mobileNetwork?: string | null;
+ simCards: { phoneNumber: string | null; isMain: boolean; cardUser?: string | null }[];
} | null;
carInsuranceDetails?: { licensePlate: string | null } | null;
}
diff --git a/frontend/src/utils/contractInfo.ts b/frontend/src/utils/contractInfo.ts
index e38d2e91..1abaa653 100644
--- a/frontend/src/utils/contractInfo.ts
+++ b/frontend/src/utils/contractInfo.ts
@@ -3,7 +3,8 @@ interface ContractInfoData {
address?: { street: string; houseNumber: string; postalCode: string; city: string } | null;
mobileDetails?: {
phoneNumber: string | null;
- simCards: { phoneNumber: string | null; isMain: boolean }[];
+ mobileNetwork?: string | null;
+ simCards: { phoneNumber: string | null; isMain: boolean; cardUser?: string | null }[];
} | null;
carInsuranceDetails?: { licensePlate: string | null } | null;
}
@@ -11,6 +12,21 @@ interface ContractInfoData {
export interface ContractTypeInfo {
label: string;
value: string;
+ // Nur bei Mobilfunk zusätzlich befüllt:
+ cardUser?: string; // wem die Karte gehört (SimCard.cardUser)
+ network?: string; // Mobilfunknetz, menschenlesbar (Telekom/Vodafone/…)
+}
+
+// Physisches Mobilfunknetz → Anzeigename.
+const MOBILE_NETWORK_LABELS: Record = {
+ TELEKOM: 'Telekom',
+ VODAFONE: 'Vodafone',
+ TELEFONICA: 'Telefónica (o2)',
+};
+
+export function mobileNetworkLabel(net?: string | null): string | undefined {
+ if (!net) return undefined;
+ return MOBILE_NETWORK_LABELS[net] ?? net;
}
export function getContractTypeInfo(contract: ContractInfoData): ContractTypeInfo | null {
@@ -39,9 +55,16 @@ export function getContractTypeInfo(contract: ContractInfoData): ContractTypeInf
if (!md) return null;
const mainSim = md.simCards?.find((s) => s.isMain && s.phoneNumber);
const anySim = md.simCards?.find((s) => s.phoneNumber);
- const phone = mainSim?.phoneNumber || anySim?.phoneNumber || md.phoneNumber;
+ const sim = mainSim || anySim;
+ const phone = sim?.phoneNumber || md.phoneNumber;
if (!phone) return null;
- return { label: 'Rufnummer', value: phone };
+ return {
+ label: 'Rufnummer',
+ value: phone,
+ // Karteninhaber der angezeigten SIM (kann vom Vertragsinhaber abweichen).
+ cardUser: sim?.cardUser?.trim() || undefined,
+ network: mobileNetworkLabel(md.mobileNetwork),
+ };
}
if (type === 'CAR_INSURANCE') {