Compare commits
4 Commits
0d024b94c2
...
a20e331f83
| Author | SHA1 | Date | |
|---|---|---|---|
| a20e331f83 | |||
| 43aaf697a1 | |||
| b0e45c0ea0 | |||
| 95b7261227 |
@@ -59,6 +59,39 @@ const PORTAL_HIDDEN_CONTRACT_FIELDS = [
|
||||
'nextReviewDate', // Snooze-Workflow ist internes Cockpit-Feature
|
||||
] as const;
|
||||
|
||||
// User-eingabe String-Felder am Contract, die in der UI dargestellt werden.
|
||||
// Werden beim Read über stripHtml geschickt, damit Alt-Daten mit rohen
|
||||
// XSS-Payloads (vor Einführung von sanitizeContractBody) nicht mehr als
|
||||
// `<script>alert(...)</script>` in der Liste auftauchen. Neue Daten sind
|
||||
// schon beim Write gestrippt, aber doppelt hält besser.
|
||||
const CONTRACT_DISPLAY_STRING_FIELDS = [
|
||||
'providerName',
|
||||
'tariffName',
|
||||
'customerNumberAtProvider',
|
||||
'contractNumberAtProvider',
|
||||
'portalUsername',
|
||||
'previousProviderName',
|
||||
'previousCustomerNumber',
|
||||
'previousContractNumber',
|
||||
'notes',
|
||||
] as const;
|
||||
|
||||
// User-eingabe String-Felder am Customer für dieselbe Read-Time-Defensive.
|
||||
const CUSTOMER_DISPLAY_STRING_FIELDS = [
|
||||
'firstName',
|
||||
'lastName',
|
||||
'companyName',
|
||||
'salutation',
|
||||
'email',
|
||||
'phone',
|
||||
'mobile',
|
||||
'portalEmail',
|
||||
'portalUsername',
|
||||
'taxNumber',
|
||||
'commercialRegisterNumber',
|
||||
'notes',
|
||||
] as const;
|
||||
|
||||
const SENSITIVE_USER_FIELDS = [
|
||||
'password',
|
||||
'passwordResetToken',
|
||||
@@ -79,6 +112,11 @@ export function sanitizeCustomer<T extends Record<string, unknown>>(customer: T
|
||||
for (const field of SENSITIVE_CUSTOMER_FIELDS) {
|
||||
delete copy[field];
|
||||
}
|
||||
for (const field of CUSTOMER_DISPLAY_STRING_FIELDS) {
|
||||
if (typeof copy[field] === 'string') {
|
||||
copy[field] = stripHtml(copy[field]);
|
||||
}
|
||||
}
|
||||
if (Array.isArray(copy.contracts)) {
|
||||
copy.contracts = (copy.contracts as Record<string, unknown>[]).map((c) => sanitizeContract(c));
|
||||
}
|
||||
@@ -126,6 +164,25 @@ export function sanitizeContract<T extends Record<string, unknown>>(contract: T
|
||||
for (const field of SENSITIVE_CONTRACT_FIELDS) {
|
||||
delete copy[field];
|
||||
}
|
||||
for (const field of CONTRACT_DISPLAY_STRING_FIELDS) {
|
||||
if (typeof copy[field] === 'string') {
|
||||
copy[field] = stripHtml(copy[field]);
|
||||
}
|
||||
}
|
||||
// Nested: previousProviderName liegt im energyDetails-Sub-Objekt
|
||||
if (copy.energyDetails && typeof copy.energyDetails === 'object') {
|
||||
const ed = copy.energyDetails as Record<string, unknown>;
|
||||
if (typeof ed.previousProviderName === 'string') {
|
||||
ed.previousProviderName = stripHtml(ed.previousProviderName);
|
||||
}
|
||||
if (typeof ed.previousCustomerNumber === 'string') {
|
||||
ed.previousCustomerNumber = stripHtml(ed.previousCustomerNumber);
|
||||
}
|
||||
}
|
||||
// Nested: previousContract wird rekursiv auch sanitisiert
|
||||
if (copy.previousContract && typeof copy.previousContract === 'object') {
|
||||
copy.previousContract = sanitizeContract(copy.previousContract as Record<string, unknown>);
|
||||
}
|
||||
if (copy.customer && typeof copy.customer === 'object') {
|
||||
copy.customer = sanitizeCustomer(copy.customer as Record<string, unknown>);
|
||||
}
|
||||
|
||||
@@ -2591,7 +2591,24 @@ export default function ContractDetail() {
|
||||
|
||||
{/* Type-specific details */}
|
||||
{c.energyDetails && (
|
||||
<Card className="mb-6" title={c.type === 'ELECTRICITY' ? 'Strom-Details' : 'Gas-Details'}>
|
||||
<Card
|
||||
className="mb-6"
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<span>{c.type === 'ELECTRICITY' ? 'Strom-Details' : 'Gas-Details'}</span>
|
||||
<Link
|
||||
to={`/customers/${c.customerId}?tab=meters`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800 hover:underline font-normal"
|
||||
title="Zähler-Übersicht des Kunden in neuem Tab öffnen"
|
||||
>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
Zähler verwalten
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<dl className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{c.energyDetails.meter && (
|
||||
<div>
|
||||
|
||||
@@ -1304,9 +1304,15 @@ function MetersTab({
|
||||
(m.energyDetails?.some((ed) => ed.contract) ?? false)
|
||||
|| (m.contractMeters?.some((cm) => cm.energyContractDetails?.contract) ?? false);
|
||||
|
||||
// Aktiv-Filter (Default: nur aktive) und "Nur Zähler ohne Verträge"-Filter
|
||||
// wirken unabhängig: erst Aktiv, dann optional auf orphans einschränken.
|
||||
// - beide aus: alle aktiven Zähler
|
||||
// - nur "Inaktive": alle Zähler (aktiv + inaktiv)
|
||||
// - nur "ohne Verträge": aktive Zähler OHNE Vertrag
|
||||
// - beide an: alle Zähler ohne Vertrag (aktiv + inaktiv)
|
||||
const filtered = meters
|
||||
.filter((m) => showInactive ? true : m.isActive)
|
||||
.filter((m) => showWithoutContracts ? true : hasAnyContract(m));
|
||||
.filter((m) => showWithoutContracts ? !hasAnyContract(m) : true);
|
||||
|
||||
// Sort readings by date (newest first)
|
||||
const getSortedReadings = (readings: any[] | undefined) => {
|
||||
|
||||
@@ -262,13 +262,24 @@ export function calculateMultiMeterConsumption(
|
||||
let firstStart: MeterReading | undefined;
|
||||
let lastEnd: MeterReading | undefined;
|
||||
|
||||
const contractStartMs = new Date(startDate).getTime();
|
||||
const contractEndMs = new Date(endDate).getTime();
|
||||
|
||||
for (const cm of contractMeters) {
|
||||
const readings = cm.meter?.readings || [];
|
||||
if (readings.length === 0) continue;
|
||||
|
||||
// Zeitraum für diesen Zähler bestimmen
|
||||
const meterStart = cm.installedAt || startDate;
|
||||
const meterEnd = cm.removedAt || endDate;
|
||||
// Zeitraum für diesen Zähler bestimmen, GE-CLAMPED auf die Vertragslaufzeit.
|
||||
// Ohne Clamp würden Folgezähler, die nach Vertragsende installiert wurden
|
||||
// (typisch bei Vorgängerverträgen einer Folgevertrags-Kette), zukünftige
|
||||
// Zählerstände in den Verbrauch dieses Vertrags einrechnen.
|
||||
const installedMs = cm.installedAt ? new Date(cm.installedAt).getTime() : contractStartMs;
|
||||
const removedMs = cm.removedAt ? new Date(cm.removedAt).getTime() : contractEndMs;
|
||||
const meterStartMs = Math.max(installedMs, contractStartMs);
|
||||
const meterEndMs = Math.min(removedMs, contractEndMs);
|
||||
if (meterStartMs > meterEndMs) continue; // Zähler liegt komplett außerhalb der Laufzeit
|
||||
const meterStart = new Date(meterStartMs).toISOString();
|
||||
const meterEnd = new Date(meterEndMs).toISOString();
|
||||
|
||||
const result = calculateConsumption(readings, meterStart, meterEnd, contractType);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user