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
|
'nextReviewDate', // Snooze-Workflow ist internes Cockpit-Feature
|
||||||
] as const;
|
] 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 = [
|
const SENSITIVE_USER_FIELDS = [
|
||||||
'password',
|
'password',
|
||||||
'passwordResetToken',
|
'passwordResetToken',
|
||||||
@@ -79,6 +112,11 @@ export function sanitizeCustomer<T extends Record<string, unknown>>(customer: T
|
|||||||
for (const field of SENSITIVE_CUSTOMER_FIELDS) {
|
for (const field of SENSITIVE_CUSTOMER_FIELDS) {
|
||||||
delete copy[field];
|
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)) {
|
if (Array.isArray(copy.contracts)) {
|
||||||
copy.contracts = (copy.contracts as Record<string, unknown>[]).map((c) => sanitizeContract(c));
|
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) {
|
for (const field of SENSITIVE_CONTRACT_FIELDS) {
|
||||||
delete copy[field];
|
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') {
|
if (copy.customer && typeof copy.customer === 'object') {
|
||||||
copy.customer = sanitizeCustomer(copy.customer as Record<string, unknown>);
|
copy.customer = sanitizeCustomer(copy.customer as Record<string, unknown>);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2591,7 +2591,24 @@ export default function ContractDetail() {
|
|||||||
|
|
||||||
{/* Type-specific details */}
|
{/* Type-specific details */}
|
||||||
{c.energyDetails && (
|
{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">
|
<dl className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{c.energyDetails.meter && (
|
{c.energyDetails.meter && (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1304,9 +1304,15 @@ function MetersTab({
|
|||||||
(m.energyDetails?.some((ed) => ed.contract) ?? false)
|
(m.energyDetails?.some((ed) => ed.contract) ?? false)
|
||||||
|| (m.contractMeters?.some((cm) => cm.energyContractDetails?.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
|
const filtered = meters
|
||||||
.filter((m) => showInactive ? true : m.isActive)
|
.filter((m) => showInactive ? true : m.isActive)
|
||||||
.filter((m) => showWithoutContracts ? true : hasAnyContract(m));
|
.filter((m) => showWithoutContracts ? !hasAnyContract(m) : true);
|
||||||
|
|
||||||
// Sort readings by date (newest first)
|
// Sort readings by date (newest first)
|
||||||
const getSortedReadings = (readings: any[] | undefined) => {
|
const getSortedReadings = (readings: any[] | undefined) => {
|
||||||
|
|||||||
@@ -262,13 +262,24 @@ export function calculateMultiMeterConsumption(
|
|||||||
let firstStart: MeterReading | undefined;
|
let firstStart: MeterReading | undefined;
|
||||||
let lastEnd: MeterReading | undefined;
|
let lastEnd: MeterReading | undefined;
|
||||||
|
|
||||||
|
const contractStartMs = new Date(startDate).getTime();
|
||||||
|
const contractEndMs = new Date(endDate).getTime();
|
||||||
|
|
||||||
for (const cm of contractMeters) {
|
for (const cm of contractMeters) {
|
||||||
const readings = cm.meter?.readings || [];
|
const readings = cm.meter?.readings || [];
|
||||||
if (readings.length === 0) continue;
|
if (readings.length === 0) continue;
|
||||||
|
|
||||||
// Zeitraum für diesen Zähler bestimmen
|
// Zeitraum für diesen Zähler bestimmen, GE-CLAMPED auf die Vertragslaufzeit.
|
||||||
const meterStart = cm.installedAt || startDate;
|
// Ohne Clamp würden Folgezähler, die nach Vertragsende installiert wurden
|
||||||
const meterEnd = cm.removedAt || endDate;
|
// (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);
|
const result = calculateConsumption(readings, meterStart, meterEnd, contractType);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user