diff --git a/docs/todo.md b/docs/todo.md index 5937d2ac..983bbc85 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,26 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🆕 Vorvertrag-Verbrauch als Schätzwert im Folgevertrag** + - **ContractForm** (Strom/Gas): Wenn ein `previousContractId` + gesetzt ist, wird der Vorvertrag samt Readings nachgeladen und + der Verbrauch clientseitig über + `calculateMultiMeterConsumption` / `calculateConsumption` + berechnet. Unter dem Jahresverbrauch-Feld erscheint + `Vorvertrag: 1.698 kWh (hochgerechnet) [Übernehmen]` mit + Ein-Klick-Button, der den Wert ins Feld kopiert. Bei Gas + erscheint der Hinweis sowohl unter „Jahresverbrauch (m³)" + (mit m³-Wert) als auch unter „Jahresverbrauch (kWh)". + - **ContractDetail** (Strom/Gas): Wenn `annualConsumption` leer + ist und ein berechenbarer Vorvertrag existiert, wird die + Jahresverbrauch-Zelle stattdessen mit `~1.698 kWh` in blau + angezeigt, darunter klein „geschätzt aus Vorvertrag + (hochgerechnet)". Verschwindet automatisch, sobald der Wert + im Vertrag eingetragen ist. + - Funktioniert nur bei Verträgen mit explizitem `previousContract` + (Folgevertrag-Kette). Ohne Vorvertrag oder ohne genügend + Zählerstände kommt kein Hinweis. + - [x] **🆕 Endstand alter Zähler fließt in Verbrauchsberechnung ein** - Bisher wurde der Wert „Letzter Stand alter Zähler" zwar als `ContractMeter.finalReading` gespeichert, aber nirgends gelesen diff --git a/frontend/src/pages/contracts/ContractDetail.tsx b/frontend/src/pages/contracts/ContractDetail.tsx index 78245045..71524935 100644 --- a/frontend/src/pages/contracts/ContractDetail.tsx +++ b/frontend/src/pages/contracts/ContractDetail.tsx @@ -1718,6 +1718,23 @@ export default function ContractDetail() { } const c = data.data; + // Verbrauch aus Vorvertrag als Hinweis, wenn der Jahresverbrauch im aktuellen + // Vertrag noch leer ist. Greift nur bei Strom/Gas mit einem berechenbaren + // Vorvertrag. + const previousConsumption = (() => { + const pc = c.previousContract; + if (!pc?.energyDetails || !pc.startDate || !pc.endDate) return null; + if (c.type !== 'ELECTRICITY' && c.type !== 'GAS') return null; + const cms = pc.energyDetails.contractMeters || []; + if (cms.length > 0) { + return calculateMultiMeterConsumption(cms, pc.startDate, pc.endDate, c.type); + } + const readings = pc.energyDetails.meter?.readings || []; + if (readings.length === 0) return null; + return calculateConsumption(readings, pc.startDate, pc.endDate, c.type); + })(); + const previousConsumptionUsable = previousConsumption + && (previousConsumption.type === 'exact' || previousConsumption.type === 'projected'); const fallbackBack = isCustomerPortal ? '/contracts' : (c.customer ? `/customers/${c.customer.id}?tab=contracts` : '/contracts'); const back = popHistory(location.state, fallbackBack); @@ -2595,7 +2612,7 @@ export default function ContractDetail() { )} - {c.energyDetails.annualConsumption && ( + {c.energyDetails.annualConsumption ? (
Jahresverbrauch {c.type === 'ELECTRICITY' ? '' : '(m³)'} @@ -2605,13 +2622,39 @@ export default function ContractDetail() { {c.type === 'ELECTRICITY' ? 'kWh' : 'm³'}
+ ) : previousConsumptionUsable && ( +
+
+ Jahresverbrauch {c.type === 'ELECTRICITY' ? '' : '(m³)'} +
+
+ ~{(c.type === 'GAS' + ? previousConsumption!.consumptionM3 ?? previousConsumption!.consumptionKwh + : previousConsumption!.consumptionKwh + ).toLocaleString('de-DE', { maximumFractionDigits: 0 })}{' '} + {c.type === 'GAS' ? 'm³' : 'kWh'} +
+ geschätzt aus Vorvertrag{previousConsumption!.type === 'projected' ? ' (hochgerechnet)' : ''} +
+
+
)} - {c.type === 'GAS' && c.energyDetails.annualConsumptionKwh && ( + {c.type === 'GAS' && c.energyDetails.annualConsumptionKwh ? (
Jahresverbrauch (kWh)
{c.energyDetails.annualConsumptionKwh.toLocaleString('de-DE')} kWh
- )} + ) : (c.type === 'GAS' && previousConsumptionUsable) ? ( +
+
Jahresverbrauch (kWh)
+
+ ~{previousConsumption!.consumptionKwh.toLocaleString('de-DE', { maximumFractionDigits: 0 })} kWh +
+ geschätzt aus Vorvertrag{previousConsumption!.type === 'projected' ? ' (hochgerechnet)' : ''} +
+
+
+ ) : null} {c.energyDetails.basePrice != null && (
Grundpreis
diff --git a/frontend/src/pages/contracts/ContractForm.tsx b/frontend/src/pages/contracts/ContractForm.tsx index 5ea52d1f..f67e8ace 100644 --- a/frontend/src/pages/contracts/ContractForm.tsx +++ b/frontend/src/pages/contracts/ContractForm.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useNavigate, useParams, useSearchParams, useLocation } from 'react-router-dom'; import { popHistory } from '../../utils/navigation'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; @@ -13,6 +13,7 @@ import type { ContractType } from '../../types'; import { formatDate } from '../../utils/dateFormat'; import { useProviderSettings } from '../../hooks/useProviderSettings'; import { Plus, Trash2, Eye, EyeOff, Info, X, ArrowLeft } from 'lucide-react'; +import { calculateConsumption, calculateMultiMeterConsumption } from '../../utils/energyCalculations'; // Contract types are now loaded dynamically from the database @@ -114,6 +115,14 @@ export default function ContractForm() { enabled: !!customerId, }); + // Vorvertrag (für Verbrauchs-Übernahme bei Energieverträgen). Wird nur geladen, + // wenn ein previousContractId gesetzt ist und der Typ Strom/Gas ist. + const { data: previousContractData } = useQuery({ + queryKey: ['contract', previousContractId], + queryFn: () => contractApi.getById(parseInt(previousContractId)), + enabled: !!previousContractId && ['ELECTRICITY', 'GAS'].includes(contractType), + }); + // Fetch platforms const { data: platformsData } = useQuery({ queryKey: ['platforms'], @@ -624,6 +633,24 @@ export default function ContractForm() { const documents = customer?.identityDocuments?.filter((d) => d.isActive) || []; const meters = customer?.meters || []; const selectedMeter = meters.find(m => m.id.toString() === selectedMeterId); + + // Verbrauch aus Vorvertrag berechnen (für Übernehmen-Hinweis am Jahresverbrauch). + // Nutzt die clientseitige Verbrauchsberechnung – egal ob Single- oder Multi-Meter, + // egal ob exakt oder hochgerechnet. + const previousConsumption = useMemo(() => { + const pc = previousContractData?.data; + if (!pc?.energyDetails || !pc.startDate || !pc.endDate) return null; + if (!['ELECTRICITY', 'GAS'].includes(pc.type)) return null; + const cms = pc.energyDetails.contractMeters || []; + if (cms.length > 0) { + return calculateMultiMeterConsumption(cms, pc.startDate, pc.endDate, pc.type as 'ELECTRICITY' | 'GAS'); + } + const readings = pc.energyDetails.meter?.readings || []; + if (readings.length === 0) return null; + return calculateConsumption(readings, pc.startDate, pc.endDate, pc.type as 'ELECTRICITY' | 'GAS'); + }, [previousContractData]); + const previousConsumptionUsable = previousConsumption + && (previousConsumption.type === 'exact' || previousConsumption.type === 'projected'); const stressfreiEmails = customer?.stressfreiEmails?.filter((e: { isActive: boolean }) => e.isActive) || []; const platforms = platformsData?.data || []; const cancellationPeriods = cancellationPeriodsData?.data || []; @@ -1010,17 +1037,59 @@ export default function ContractForm() { label="MaLo-ID (Marktlokations-ID)" {...register('maloId')} /> - - {contractType === 'GAS' && ( +
+ {previousConsumptionUsable && ( +

+ + Vorvertrag: {(contractType === 'GAS' + ? previousConsumption!.consumptionM3 ?? previousConsumption!.consumptionKwh + : previousConsumption!.consumptionKwh + ).toLocaleString('de-DE', { maximumFractionDigits: 0 })} {contractType === 'GAS' ? 'm³' : 'kWh'} + {previousConsumption!.type === 'projected' && ' (hochgerechnet)'} + + +

+ )} +
+ {contractType === 'GAS' && ( +
+ + {previousConsumptionUsable && ( +

+ + Vorvertrag: {previousConsumption!.consumptionKwh.toLocaleString('de-DE', { maximumFractionDigits: 0 })} kWh + {previousConsumption!.type === 'projected' && ' (hochgerechnet)'} + + +

+ )} +
)}