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 <noreply@anthropic.com>
This commit is contained in:
@@ -105,7 +105,8 @@ export async function getAllContracts(filters: ContractFilters) {
|
|||||||
mobileDetails: {
|
mobileDetails: {
|
||||||
select: {
|
select: {
|
||||||
phoneNumber: true,
|
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
|
// 2026-07-04: für Vertragsauswahl-Labels brauchen wir überall
|
||||||
@@ -1223,6 +1224,7 @@ export interface ContractTreeNode {
|
|||||||
status: ContractStatus;
|
status: ContractStatus;
|
||||||
startDate: Date | null;
|
startDate: Date | null;
|
||||||
endDate: Date | null;
|
endDate: Date | null;
|
||||||
|
cancellationConfirmationDate: Date | null;
|
||||||
providerName: string | null;
|
providerName: string | null;
|
||||||
tariffName: string | null;
|
tariffName: string | null;
|
||||||
previousContractId: number | null;
|
previousContractId: number | null;
|
||||||
@@ -1233,7 +1235,8 @@ export interface ContractTreeNode {
|
|||||||
address?: { street: string; houseNumber: string; postalCode: string; city: string } | null;
|
address?: { street: string; houseNumber: string; postalCode: string; city: string } | null;
|
||||||
mobileDetails?: {
|
mobileDetails?: {
|
||||||
phoneNumber: string | null;
|
phoneNumber: string | null;
|
||||||
simCards: { phoneNumber: string | null; isMain: boolean }[];
|
mobileNetwork: string | null;
|
||||||
|
simCards: { phoneNumber: string | null; isMain: boolean; cardUser: string | null }[];
|
||||||
} | null;
|
} | null;
|
||||||
carInsuranceDetails?: { licensePlate: string | null } | null;
|
carInsuranceDetails?: { licensePlate: string | null } | null;
|
||||||
};
|
};
|
||||||
@@ -1268,6 +1271,7 @@ export async function getContractTreeForCustomer(
|
|||||||
status: true,
|
status: true,
|
||||||
startDate: true,
|
startDate: true,
|
||||||
endDate: true,
|
endDate: true,
|
||||||
|
cancellationConfirmationDate: true,
|
||||||
providerName: true,
|
providerName: true,
|
||||||
tariffName: true,
|
tariffName: true,
|
||||||
previousContractId: true,
|
previousContractId: true,
|
||||||
@@ -1279,7 +1283,8 @@ export async function getContractTreeForCustomer(
|
|||||||
mobileDetails: {
|
mobileDetails: {
|
||||||
select: {
|
select: {
|
||||||
phoneNumber: true,
|
phoneNumber: true,
|
||||||
simCards: { select: { phoneNumber: true, isMain: true } },
|
mobileNetwork: true,
|
||||||
|
simCards: { select: { phoneNumber: true, isMain: true, cardUser: true } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
carInsuranceDetails: { select: { licensePlate: true } },
|
carInsuranceDetails: { select: { licensePlate: true } },
|
||||||
|
|||||||
@@ -97,6 +97,19 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ 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)
|
- [x] **🛡️ Globaler API-Rate-Limit-Backstop + BLZ-Guard-Härtung (Pentest R148)** (2026-08-12)
|
||||||
- **Backstop:** Neuer genereller Limiter auf ALLE `/api`-Requests
|
- **Backstop:** Neuer genereller Limiter auf ALLE `/api`-Requests
|
||||||
(`apiBackstopRateLimiter`, in `index.ts` vor den Routern). Vorher gab es
|
(`apiBackstopRateLimiter`, in `index.ts` vor den Routern). Vorher gab es
|
||||||
|
|||||||
@@ -318,6 +318,12 @@ export default function ContractList() {
|
|||||||
return typeInfo ? (
|
return typeInfo ? (
|
||||||
<p className={`text-sm text-gray-600 ${isPredecessor ? 'ml-6' : ''}`}>
|
<p className={`text-sm text-gray-600 ${isPredecessor ? 'ml-6' : ''}`}>
|
||||||
<span className="font-medium text-gray-700">{typeInfo.label}:</span> {typeInfo.value}
|
<span className="font-medium text-gray-700">{typeInfo.label}:</span> {typeInfo.value}
|
||||||
|
{typeInfo.cardUser && (
|
||||||
|
<> · <span className="font-medium text-gray-700">Karteninhaber:</span> {typeInfo.cardUser}</>
|
||||||
|
)}
|
||||||
|
{typeInfo.network && (
|
||||||
|
<> · <span className="font-medium text-gray-700">Netz:</span> {typeInfo.network}</>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
) : null;
|
) : null;
|
||||||
})()}
|
})()}
|
||||||
@@ -328,6 +334,11 @@ export default function ContractList() {
|
|||||||
` | Ende: ${formatDate(contract.endDate)}`}
|
` | Ende: ${formatDate(contract.endDate)}`}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{contract.cancellationConfirmationDate && (
|
||||||
|
<p className={`text-sm font-semibold text-red-600 ${isPredecessor ? 'ml-6' : ''}`}>
|
||||||
|
Kündigungsbestätigung zum {formatDate(contract.cancellationConfirmationDate)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Vorgänger rekursiv rendern - für Wurzel nur wenn aufgeklappt, für Vorgänger immer */}
|
{/* Vorgänger rekursiv rendern - für Wurzel nur wenn aufgeklappt, für Vorgänger immer */}
|
||||||
|
|||||||
@@ -1882,6 +1882,12 @@ function ContractsTab({
|
|||||||
return typeInfo ? (
|
return typeInfo ? (
|
||||||
<p className={`text-sm text-gray-600 ${isPredecessor ? 'ml-6' : ''}`}>
|
<p className={`text-sm text-gray-600 ${isPredecessor ? 'ml-6' : ''}`}>
|
||||||
<span className="font-medium text-gray-700">{typeInfo.label}:</span> {typeInfo.value}
|
<span className="font-medium text-gray-700">{typeInfo.label}:</span> {typeInfo.value}
|
||||||
|
{typeInfo.cardUser && (
|
||||||
|
<> · <span className="font-medium text-gray-700">Karteninhaber:</span> {typeInfo.cardUser}</>
|
||||||
|
)}
|
||||||
|
{typeInfo.network && (
|
||||||
|
<> · <span className="font-medium text-gray-700">Netz:</span> {typeInfo.network}</>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
) : null;
|
) : null;
|
||||||
})()}
|
})()}
|
||||||
@@ -1892,6 +1898,11 @@ function ContractsTab({
|
|||||||
` | Ende: ${formatDate(contract.endDate)}`}
|
` | Ende: ${formatDate(contract.endDate)}`}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{contract.cancellationConfirmationDate && (
|
||||||
|
<p className={`text-sm font-semibold text-red-600 ${isPredecessor ? 'ml-6' : ''}`}>
|
||||||
|
Kündigungsbestätigung zum {formatDate(contract.cancellationConfirmationDate)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Vorgänger rekursiv rendern - für Wurzel nur wenn aufgeklappt, für Vorgänger immer */}
|
{/* Vorgänger rekursiv rendern - für Wurzel nur wenn aufgeklappt, für Vorgänger immer */}
|
||||||
|
|||||||
@@ -913,6 +913,7 @@ export interface ContractTreeNodeContract {
|
|||||||
status: string;
|
status: string;
|
||||||
startDate: string | null;
|
startDate: string | null;
|
||||||
endDate: string | null;
|
endDate: string | null;
|
||||||
|
cancellationConfirmationDate?: string | null;
|
||||||
providerName: string | null;
|
providerName: string | null;
|
||||||
tariffName: string | null;
|
tariffName: string | null;
|
||||||
previousContractId: number | null;
|
previousContractId: number | null;
|
||||||
@@ -923,7 +924,8 @@ export interface ContractTreeNodeContract {
|
|||||||
address?: { street: string; houseNumber: string; postalCode: string; city: string } | null;
|
address?: { street: string; houseNumber: string; postalCode: string; city: string } | null;
|
||||||
mobileDetails?: {
|
mobileDetails?: {
|
||||||
phoneNumber: string | null;
|
phoneNumber: string | null;
|
||||||
simCards: { phoneNumber: string | null; isMain: boolean }[];
|
mobileNetwork?: string | null;
|
||||||
|
simCards: { phoneNumber: string | null; isMain: boolean; cardUser?: string | null }[];
|
||||||
} | null;
|
} | null;
|
||||||
carInsuranceDetails?: { licensePlate: string | null } | null;
|
carInsuranceDetails?: { licensePlate: string | null } | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ interface ContractInfoData {
|
|||||||
address?: { street: string; houseNumber: string; postalCode: string; city: string } | null;
|
address?: { street: string; houseNumber: string; postalCode: string; city: string } | null;
|
||||||
mobileDetails?: {
|
mobileDetails?: {
|
||||||
phoneNumber: string | null;
|
phoneNumber: string | null;
|
||||||
simCards: { phoneNumber: string | null; isMain: boolean }[];
|
mobileNetwork?: string | null;
|
||||||
|
simCards: { phoneNumber: string | null; isMain: boolean; cardUser?: string | null }[];
|
||||||
} | null;
|
} | null;
|
||||||
carInsuranceDetails?: { licensePlate: string | null } | null;
|
carInsuranceDetails?: { licensePlate: string | null } | null;
|
||||||
}
|
}
|
||||||
@@ -11,6 +12,21 @@ interface ContractInfoData {
|
|||||||
export interface ContractTypeInfo {
|
export interface ContractTypeInfo {
|
||||||
label: string;
|
label: string;
|
||||||
value: 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<string, string> = {
|
||||||
|
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 {
|
export function getContractTypeInfo(contract: ContractInfoData): ContractTypeInfo | null {
|
||||||
@@ -39,9 +55,16 @@ export function getContractTypeInfo(contract: ContractInfoData): ContractTypeInf
|
|||||||
if (!md) return null;
|
if (!md) return null;
|
||||||
const mainSim = md.simCards?.find((s) => s.isMain && s.phoneNumber);
|
const mainSim = md.simCards?.find((s) => s.isMain && s.phoneNumber);
|
||||||
const anySim = md.simCards?.find((s) => 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;
|
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') {
|
if (type === 'CAR_INSURANCE') {
|
||||||
|
|||||||
Reference in New Issue
Block a user