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:
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{c.energyDetails.annualConsumption && (
|
||||
{c.energyDetails.annualConsumption ? (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">
|
||||
Jahresverbrauch {c.type === 'ELECTRICITY' ? '' : '(m³)'}
|
||||
@@ -2605,13 +2622,39 @@ export default function ContractDetail() {
|
||||
{c.type === 'ELECTRICITY' ? 'kWh' : 'm³'}
|
||||
</dd>
|
||||
</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>
|
||||
<dt className="text-sm text-gray-500">Jahresverbrauch (kWh)</dt>
|
||||
<dd>{c.energyDetails.annualConsumptionKwh.toLocaleString('de-DE')} kWh</dd>
|
||||
</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 && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Grundpreis</dt>
|
||||
|
||||
@@ -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')}
|
||||
/>
|
||||
<Input
|
||||
label={`Jahresverbrauch (${contractType === 'ELECTRICITY' ? 'kWh' : 'm³'})`}
|
||||
type="number"
|
||||
{...register('annualConsumption')}
|
||||
/>
|
||||
{contractType === 'GAS' && (
|
||||
<div>
|
||||
<Input
|
||||
label="Jahresverbrauch (kWh)"
|
||||
label={`Jahresverbrauch (${contractType === 'ELECTRICITY' ? 'kWh' : 'm³'})`}
|
||||
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')} />
|
||||
<EuroCentInput
|
||||
|
||||
Reference in New Issue
Block a user