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') {