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
|
## ✅ 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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user