Datenschutz vollmacht fixed, two time counter added

This commit is contained in:
2026-03-21 16:42:31 +01:00
parent 0121c82412
commit 4f359df161
56 changed files with 4401 additions and 789 deletions
+112 -15
View File
@@ -1,4 +1,4 @@
import type { MeterReading } from '../types';
import type { MeterReading, ContractMeter } from '../types';
// Konstante für Gas → kWh Umrechnung (Standard Erdgas H)
export const GAS_TO_KWH_FACTOR = 10.5;
@@ -8,6 +8,8 @@ export interface ConsumptionCalculation {
type: 'exact' | 'projected' | 'insufficient' | 'none';
consumptionM3?: number; // Nur bei Gas
consumptionKwh: number;
consumptionHt?: number; // Nur bei HT/NT: HT-Verbrauch in kWh
consumptionNt?: number; // Nur bei HT/NT: NT-Verbrauch in kWh
startReading?: MeterReading;
endReading?: MeterReading;
projectedEndDate?: string; // Nur bei Hochrechnung
@@ -17,7 +19,8 @@ export interface ConsumptionCalculation {
// Ergebnis der Kostenberechnung
export interface CostCalculation {
annualBaseCost: number; // basePrice × 12
annualConsumptionCost: number; // verbrauch × unitPrice
annualConsumptionCost: number; // verbrauch × unitPrice (HT bei Zweitarif)
annualConsumptionCostNt?: number; // NT-Verbrauch × unitPriceNt
annualTotalCost: number; // Summe
monthlyPayment: number; // annualTotalCost / 12
bonus?: number;
@@ -94,7 +97,6 @@ export function calculateConsumption(
(a, b) => new Date(a.readingDate).getTime() - new Date(b.readingDate).getTime()
);
const firstReading = sorted[0];
const lastReading = sorted[sorted.length - 1];
// Prüfen ob Endzählerstand am/nach Vertragsende liegt
@@ -103,16 +105,39 @@ export function calculateConsumption(
lastReadingDate.setHours(0, 0, 0, 0);
contractEndDate.setHours(0, 0, 0, 0);
// Fall A: Exakter Verbrauch (Endzählerstand am oder nach Vertragsende)
// Verbrauch zwischen aufeinanderfolgenden Ständen berechnen
// (Erkennt Zählerwechsel: wenn ein Wert sinkt, wird ab dem neuen Stand weitergerechnet)
let totalConsumption = 0;
let totalConsumptionNt = 0;
const hasNt = sorted.some(r => r.valueNt !== undefined && r.valueNt !== null);
let effectiveFirstReading = sorted[0];
for (let i = 1; i < sorted.length; i++) {
const diff = sorted[i].value - sorted[i - 1].value;
if (diff < 0) {
totalConsumption = 0;
totalConsumptionNt = 0;
effectiveFirstReading = sorted[i];
} else {
totalConsumption += diff;
if (hasNt && sorted[i].valueNt != null && sorted[i - 1].valueNt != null) {
const diffNt = sorted[i].valueNt! - sorted[i - 1].valueNt!;
if (diffNt >= 0) totalConsumptionNt += diffNt;
}
}
}
const effectiveLastReading = sorted[sorted.length - 1];
// Fall A: Exakter Verbrauch
if (lastReadingDate >= contractEndDate) {
const consumption = lastReading.value - firstReading.value;
return formatConsumptionResult('exact', consumption, contractType, firstReading, lastReading);
const result = formatConsumptionResult('exact', totalConsumption, contractType, effectiveFirstReading, effectiveLastReading);
if (hasNt) { result.consumptionHt = totalConsumption; result.consumptionNt = totalConsumptionNt; }
return result;
}
// Fall B: Hochrechnung erforderlich
const daysBetweenReadings = daysDiff(firstReading.readingDate, lastReading.readingDate);
const daysBetweenReadings = daysDiff(effectiveFirstReading.readingDate, effectiveLastReading.readingDate);
// Mindestens 1 Tag zwischen den Messungen für sinnvolle Hochrechnung
if (daysBetweenReadings < 1) {
return {
type: 'insufficient',
@@ -122,17 +147,26 @@ export function calculateConsumption(
}
const totalContractDays = daysDiff(startDate, endDate);
const measuredConsumption = lastReading.value - firstReading.value;
const projectedConsumption = (measuredConsumption / daysBetweenReadings) * totalContractDays;
const projectedConsumption = (totalConsumption / daysBetweenReadings) * totalContractDays;
return formatConsumptionResult(
const result = formatConsumptionResult(
'projected',
projectedConsumption,
contractType,
firstReading,
lastReading,
effectiveFirstReading,
effectiveLastReading,
endDate
);
if (hasNt) {
const projectedNt = (totalConsumptionNt / daysBetweenReadings) * totalContractDays;
result.consumptionHt = projectedConsumption;
result.consumptionNt = projectedNt;
// Gesamt-kWh = HT + NT
result.consumptionKwh = projectedConsumption + projectedNt;
}
return result;
}
/**
@@ -173,7 +207,9 @@ export function calculateCosts(
consumptionKwh: number,
basePrice?: number,
unitPrice?: number,
bonus?: number
bonus?: number,
consumptionNtKwh?: number,
unitPriceNt?: number
): CostCalculation | null {
// Mindestens ein Preis muss vorhanden sein
if (basePrice == null && unitPrice == null) {
@@ -181,17 +217,78 @@ export function calculateCosts(
}
const annualBaseCost = (basePrice ?? 0) * 12;
// Bei HT/NT: consumptionKwh ist nur HT, NT wird separat berechnet
const annualConsumptionCost = consumptionKwh * (unitPrice ?? 0);
const annualTotalCost = annualBaseCost + annualConsumptionCost;
const annualConsumptionCostNt = (consumptionNtKwh ?? 0) * (unitPriceNt ?? 0);
const annualTotalCost = annualBaseCost + annualConsumptionCost + annualConsumptionCostNt;
const effectiveAnnualCost = annualTotalCost - (bonus ?? 0);
const monthlyPayment = effectiveAnnualCost / 12;
return {
annualBaseCost,
annualConsumptionCost,
annualConsumptionCostNt: annualConsumptionCostNt > 0 ? annualConsumptionCostNt : undefined,
annualTotalCost,
monthlyPayment,
bonus: bonus ?? undefined,
effectiveAnnualCost,
};
}
/**
* Berechnet den Verbrauch über mehrere Zähler (Folgezähler).
* Pro Zähler wird der Verbrauch einzeln berechnet und dann summiert.
*/
export function calculateMultiMeterConsumption(
contractMeters: ContractMeter[],
startDate: string,
endDate: string,
contractType: 'ELECTRICITY' | 'GAS'
): ConsumptionCalculation {
if (contractMeters.length === 0) {
return { type: 'none', consumptionKwh: 0 };
}
let totalConsumption = 0;
let totalConsumptionM3 = 0;
let hasExact = true;
let hasAny = false;
let firstStart: MeterReading | undefined;
let lastEnd: MeterReading | undefined;
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;
const result = calculateConsumption(readings, meterStart, meterEnd, contractType);
if (result.type === 'none' || result.type === 'insufficient') continue;
hasAny = true;
if (result.type === 'projected') hasExact = false;
totalConsumption += result.consumptionKwh;
if (result.consumptionM3) totalConsumptionM3 += result.consumptionM3;
if (!firstStart && result.startReading) firstStart = result.startReading;
if (result.endReading) lastEnd = result.endReading;
}
if (!hasAny) {
// Fallback: Einzelzähler-Berechnung mit allen Readings
const allReadings = contractMeters.flatMap(cm => cm.meter?.readings || []);
return calculateConsumption(allReadings, startDate, endDate, contractType);
}
return {
type: hasExact ? 'exact' : 'projected',
consumptionKwh: totalConsumption,
consumptionM3: contractType === 'GAS' ? totalConsumptionM3 : undefined,
startReading: firstStart,
endReading: lastEnd,
};
}
+26
View File
@@ -0,0 +1,26 @@
/**
* Navigation-History über location.state.
* Jeder Link fügt die aktuelle URL zum Stack hinzu.
* Der Zurück-Button poppt den letzten Eintrag und gibt den Rest weiter.
*/
export interface NavState {
history?: string[];
}
/**
* Erstellt den state für einen Link - fügt die aktuelle URL zum History-Stack hinzu.
*/
export function pushHistory(currentPath: string, locationState?: unknown): NavState {
const prev = (locationState as NavState)?.history || [];
return { history: [...prev, currentPath] };
}
/**
* Gibt die Zurück-URL und den verbleibenden History-Stack zurück.
*/
export function popHistory(locationState?: unknown, fallback?: string): { to: string; state: NavState } {
const history = [...((locationState as NavState)?.history || [])];
const to = history.pop() || fallback || '/';
return { to, state: { history } };
}