Vorvertrag-Verbrauch als Schätzwert im Folgevertrag

ContractForm (Strom/Gas): Wenn ein previousContractId gesetzt ist,
wird der Vorvertrag samt Readings nachgeladen, der Verbrauch
clientseitig berechnet und als "Vorvertrag: X kWh [Übernehmen]"
unter dem Jahresverbrauch-Feld angezeigt. Bei Gas auch unter
"Jahresverbrauch (kWh)".

ContractDetail (Strom/Gas): Wenn annualConsumption leer ist und
ein berechenbarer Vorvertrag existiert, wird "~X kWh, geschätzt
aus Vorvertrag" in der Jahresverbrauch-Zelle angezeigt – damit
der Wert beim Lesen schon als Anhaltspunkt da steht.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 14:28:55 +02:00
parent 13213846f4
commit e527aebb84
3 changed files with 144 additions and 12 deletions
+20
View File
@@ -97,6 +97,26 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt ## ✅ 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** - [x] **🆕 Endstand alter Zähler fließt in Verbrauchsberechnung ein**
- Bisher wurde der Wert „Letzter Stand alter Zähler" zwar als - Bisher wurde der Wert „Letzter Stand alter Zähler" zwar als
`ContractMeter.finalReading` gespeichert, aber nirgends gelesen `ContractMeter.finalReading` gespeichert, aber nirgends gelesen
@@ -1718,6 +1718,23 @@ export default function ContractDetail() {
} }
const c = data.data; 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 fallbackBack = isCustomerPortal ? '/contracts' : (c.customer ? `/customers/${c.customer.id}?tab=contracts` : '/contracts');
const back = popHistory(location.state, fallbackBack); const back = popHistory(location.state, fallbackBack);
@@ -2595,7 +2612,7 @@ export default function ContractDetail() {
</dd> </dd>
</div> </div>
)} )}
{c.energyDetails.annualConsumption && ( {c.energyDetails.annualConsumption ? (
<div> <div>
<dt className="text-sm text-gray-500"> <dt className="text-sm text-gray-500">
Jahresverbrauch {c.type === 'ELECTRICITY' ? '' : '(m³)'} Jahresverbrauch {c.type === 'ELECTRICITY' ? '' : '(m³)'}
@@ -2605,13 +2622,39 @@ export default function ContractDetail() {
{c.type === 'ELECTRICITY' ? 'kWh' : 'm³'} {c.type === 'ELECTRICITY' ? 'kWh' : 'm³'}
</dd> </dd>
</div> </div>
) : previousConsumptionUsable && (
<div>
<dt className="text-sm text-gray-500">
Jahresverbrauch {c.type === 'ELECTRICITY' ? '' : '(m³)'}
</dt>
<dd className="text-blue-700">
~{(c.type === 'GAS'
? previousConsumption!.consumptionM3 ?? previousConsumption!.consumptionKwh
: previousConsumption!.consumptionKwh
).toLocaleString('de-DE', { maximumFractionDigits: 0 })}{' '}
{c.type === 'GAS' ? 'm³' : 'kWh'}
<div className="text-xs text-gray-500 mt-0.5">
geschätzt aus Vorvertrag{previousConsumption!.type === 'projected' ? ' (hochgerechnet)' : ''}
</div>
</dd>
</div>
)} )}
{c.type === 'GAS' && c.energyDetails.annualConsumptionKwh && ( {c.type === 'GAS' && c.energyDetails.annualConsumptionKwh ? (
<div> <div>
<dt className="text-sm text-gray-500">Jahresverbrauch (kWh)</dt> <dt className="text-sm text-gray-500">Jahresverbrauch (kWh)</dt>
<dd>{c.energyDetails.annualConsumptionKwh.toLocaleString('de-DE')} kWh</dd> <dd>{c.energyDetails.annualConsumptionKwh.toLocaleString('de-DE')} kWh</dd>
</div> </div>
)} ) : (c.type === 'GAS' && previousConsumptionUsable) ? (
<div>
<dt className="text-sm text-gray-500">Jahresverbrauch (kWh)</dt>
<dd className="text-blue-700">
~{previousConsumption!.consumptionKwh.toLocaleString('de-DE', { maximumFractionDigits: 0 })} kWh
<div className="text-xs text-gray-500 mt-0.5">
geschätzt aus Vorvertrag{previousConsumption!.type === 'projected' ? ' (hochgerechnet)' : ''}
</div>
</dd>
</div>
) : null}
{c.energyDetails.basePrice != null && ( {c.energyDetails.basePrice != null && (
<div> <div>
<dt className="text-sm text-gray-500">Grundpreis</dt> <dt className="text-sm text-gray-500">Grundpreis</dt>
+78 -9
View File
@@ -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 { useNavigate, useParams, useSearchParams, useLocation } from 'react-router-dom';
import { popHistory } from '../../utils/navigation'; import { popHistory } from '../../utils/navigation';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
@@ -13,6 +13,7 @@ import type { ContractType } from '../../types';
import { formatDate } from '../../utils/dateFormat'; import { formatDate } from '../../utils/dateFormat';
import { useProviderSettings } from '../../hooks/useProviderSettings'; import { useProviderSettings } from '../../hooks/useProviderSettings';
import { Plus, Trash2, Eye, EyeOff, Info, X, ArrowLeft } from 'lucide-react'; 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 // Contract types are now loaded dynamically from the database
@@ -114,6 +115,14 @@ export default function ContractForm() {
enabled: !!customerId, 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 // Fetch platforms
const { data: platformsData } = useQuery({ const { data: platformsData } = useQuery({
queryKey: ['platforms'], queryKey: ['platforms'],
@@ -624,6 +633,24 @@ export default function ContractForm() {
const documents = customer?.identityDocuments?.filter((d) => d.isActive) || []; const documents = customer?.identityDocuments?.filter((d) => d.isActive) || [];
const meters = customer?.meters || []; const meters = customer?.meters || [];
const selectedMeter = meters.find(m => m.id.toString() === selectedMeterId); 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 stressfreiEmails = customer?.stressfreiEmails?.filter((e: { isActive: boolean }) => e.isActive) || [];
const platforms = platformsData?.data || []; const platforms = platformsData?.data || [];
const cancellationPeriods = cancellationPeriodsData?.data || []; const cancellationPeriods = cancellationPeriodsData?.data || [];
@@ -1010,17 +1037,59 @@ export default function ContractForm() {
label="MaLo-ID (Marktlokations-ID)" label="MaLo-ID (Marktlokations-ID)"
{...register('maloId')} {...register('maloId')}
/> />
<Input <div>
label={`Jahresverbrauch (${contractType === 'ELECTRICITY' ? 'kWh' : 'm³'})`}
type="number"
{...register('annualConsumption')}
/>
{contractType === 'GAS' && (
<Input <Input
label="Jahresverbrauch (kWh)" label={`Jahresverbrauch (${contractType === 'ELECTRICITY' ? 'kWh' : 'm³'})`}
type="number" type="number"
{...register('annualConsumptionKwh')} {...register('annualConsumption')}
/> />
{previousConsumptionUsable && (
<p className="mt-1 text-xs text-blue-600 flex items-center gap-2">
<span>
Vorvertrag: {(contractType === 'GAS'
? previousConsumption!.consumptionM3 ?? previousConsumption!.consumptionKwh
: previousConsumption!.consumptionKwh
).toLocaleString('de-DE', { maximumFractionDigits: 0 })} {contractType === 'GAS' ? 'm³' : 'kWh'}
{previousConsumption!.type === 'projected' && ' (hochgerechnet)'}
</span>
<button
type="button"
className="text-blue-700 hover:underline"
onClick={() => {
const v = contractType === 'GAS'
? previousConsumption!.consumptionM3 ?? previousConsumption!.consumptionKwh
: previousConsumption!.consumptionKwh;
setValue('annualConsumption', Math.round(v) as any, { shouldDirty: true });
}}
>
Übernehmen
</button>
</p>
)}
</div>
{contractType === 'GAS' && (
<div>
<Input
label="Jahresverbrauch (kWh)"
type="number"
{...register('annualConsumptionKwh')}
/>
{previousConsumptionUsable && (
<p className="mt-1 text-xs text-blue-600 flex items-center gap-2">
<span>
Vorvertrag: {previousConsumption!.consumptionKwh.toLocaleString('de-DE', { maximumFractionDigits: 0 })} kWh
{previousConsumption!.type === 'projected' && ' (hochgerechnet)'}
</span>
<button
type="button"
className="text-blue-700 hover:underline"
onClick={() => setValue('annualConsumptionKwh', Math.round(previousConsumption!.consumptionKwh) as any, { shouldDirty: true })}
>
Übernehmen
</button>
</p>
)}
</div>
)} )}
<Input label="Grundpreis (€/Monat)" type="number" step="any" {...register('basePrice')} /> <Input label="Grundpreis (€/Monat)" type="number" step="any" {...register('basePrice')} />
<EuroCentInput <EuroCentInput