Folgezähler-Deklaration in der Kundenakte (Auto-Propagation)
- Meter.predecessorMeterId (Self-Relation) + Migration
20260530140000_meter_predecessor mit IF NOT EXISTS
- createMeter akzeptiert optional successorOf:
{predecessorMeterId, installedAt?, finalReadingPrevious?}.
Vorgänger wird validiert (gleicher Kunde + Typ); alle Verträge
mit dem Vorgänger als aktuellen Zähler werden analog zu
addSuccessorMeter automatisch auf den neuen Zähler umgestellt
(ContractMeter-Eintrag mit removedAt/finalReading für den
Vorgänger, neuer ContractMeter mit installedAt + nächster
Position, energyDetails.meterId aktualisiert)
- MeterModal: Checkbox "Als Folgezähler deklarieren" + Dropdown
Vorgänger + Wechseldatum + Endstand. Typ/Tarifmodell/Adresse
werden vom Vorgänger übernommen und disabled. Info-Banner über
Vertragsauto-Update
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -457,6 +457,7 @@ export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?
|
||||
onClose={() => setShowMeterModal(false)}
|
||||
customerId={customerId}
|
||||
addresses={c.addresses || []}
|
||||
existingMeters={c.meters || []}
|
||||
/>
|
||||
|
||||
<MeterModal
|
||||
@@ -465,6 +466,7 @@ export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?
|
||||
customerId={customerId}
|
||||
meter={editingMeter}
|
||||
addresses={c.addresses || []}
|
||||
existingMeters={c.meters || []}
|
||||
/>
|
||||
|
||||
<StressfreiEmailModal
|
||||
@@ -2827,16 +2829,19 @@ function MeterModal({
|
||||
customerId,
|
||||
meter,
|
||||
addresses,
|
||||
existingMeters,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
customerId: number;
|
||||
meter?: Meter | null;
|
||||
addresses: Address[];
|
||||
existingMeters: Meter[];
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const isEditing = !!meter;
|
||||
const deliveryAddresses = addresses.filter((a) => a.type === 'DELIVERY_RESIDENCE');
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
const getInitialFormData = () => ({
|
||||
meterNumber: meter?.meterNumber || '',
|
||||
@@ -2845,17 +2850,36 @@ function MeterModal({
|
||||
location: meter?.location || '',
|
||||
isActive: meter?.isActive ?? true,
|
||||
addressId: meter?.addressId?.toString() || '',
|
||||
isSuccessor: false,
|
||||
predecessorMeterId: '',
|
||||
installedAt: today,
|
||||
finalReadingPrevious: '',
|
||||
});
|
||||
|
||||
const [formData, setFormData] = useState(getInitialFormData);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const predecessor = formData.predecessorMeterId
|
||||
? existingMeters.find((m) => m.id.toString() === formData.predecessorMeterId)
|
||||
: null;
|
||||
|
||||
// Wenn Vorgänger gewählt: Typ + Adresse + Tarifmodell vom Vorgänger übernehmen
|
||||
useEffect(() => {
|
||||
if (!formData.isSuccessor || !predecessor) return;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
type: predecessor.type,
|
||||
tariffModel: predecessor.tariffModel,
|
||||
addressId: predecessor.addressId ? predecessor.addressId.toString() : prev.addressId,
|
||||
}));
|
||||
}, [formData.isSuccessor, formData.predecessorMeterId, predecessor?.id]);
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: any) => meterApi.create(customerId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
onClose();
|
||||
setFormData({ meterNumber: '', type: 'ELECTRICITY', tariffModel: 'SINGLE', location: '', isActive: true, addressId: '' });
|
||||
setFormData(getInitialFormData());
|
||||
},
|
||||
onError: (err) => setError(err instanceof Error ? err.message : 'Fehler beim Speichern'),
|
||||
});
|
||||
@@ -2876,7 +2900,11 @@ function MeterModal({
|
||||
setError('Bitte eine Lieferadresse auswählen');
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
if (formData.isSuccessor && !formData.predecessorMeterId) {
|
||||
setError('Bitte einen Vorgänger-Zähler auswählen');
|
||||
return;
|
||||
}
|
||||
const payload: any = {
|
||||
meterNumber: formData.meterNumber,
|
||||
type: formData.type,
|
||||
tariffModel: formData.tariffModel,
|
||||
@@ -2884,6 +2912,15 @@ function MeterModal({
|
||||
isActive: formData.isActive,
|
||||
addressId: parseInt(formData.addressId),
|
||||
};
|
||||
if (!isEditing && formData.isSuccessor && formData.predecessorMeterId) {
|
||||
payload.successorOf = {
|
||||
predecessorMeterId: parseInt(formData.predecessorMeterId),
|
||||
installedAt: formData.installedAt || undefined,
|
||||
finalReadingPrevious: formData.finalReadingPrevious
|
||||
? parseFloat(formData.finalReadingPrevious)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
if (isEditing) {
|
||||
updateMutation.mutate(payload);
|
||||
} else {
|
||||
@@ -2899,6 +2936,7 @@ function MeterModal({
|
||||
}
|
||||
|
||||
const noDeliveryAddresses = deliveryAddresses.length === 0;
|
||||
const successorLocked = !isEditing && formData.isSuccessor && !!predecessor;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title={isEditing ? 'Zähler bearbeiten' : 'Zähler hinzufügen'}>
|
||||
@@ -2920,6 +2958,7 @@ function MeterModal({
|
||||
}))}
|
||||
placeholder="Lieferadresse wählen..."
|
||||
required
|
||||
disabled={successorLocked && !!predecessor?.addressId}
|
||||
/>
|
||||
|
||||
<Input
|
||||
@@ -2937,6 +2976,7 @@ function MeterModal({
|
||||
{ value: 'ELECTRICITY', label: 'Strom' },
|
||||
{ value: 'GAS', label: 'Gas' },
|
||||
]}
|
||||
disabled={successorLocked}
|
||||
/>
|
||||
|
||||
{formData.type === 'ELECTRICITY' && (
|
||||
@@ -2948,6 +2988,7 @@ function MeterModal({
|
||||
{ value: 'SINGLE', label: 'Eintarifzähler (Standard)' },
|
||||
{ value: 'DUAL', label: 'Zweitarifzähler (HT/NT)' },
|
||||
]}
|
||||
disabled={successorLocked}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -2958,6 +2999,54 @@ function MeterModal({
|
||||
placeholder="z.B. Keller, Wohnung"
|
||||
/>
|
||||
|
||||
{!isEditing && existingMeters.length > 0 && (
|
||||
<div className="border-t pt-4">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.isSuccessor}
|
||||
onChange={(e) => setFormData({ ...formData, isSuccessor: e.target.checked })}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm font-medium">Diesen Zähler als Folgezähler deklarieren</span>
|
||||
</label>
|
||||
|
||||
{formData.isSuccessor && (
|
||||
<div className="mt-3 ml-6 space-y-3 bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<Select
|
||||
label="Vorgänger-Zähler *"
|
||||
value={formData.predecessorMeterId}
|
||||
onChange={(e) => setFormData({ ...formData, predecessorMeterId: e.target.value })}
|
||||
options={existingMeters.map((m) => ({
|
||||
value: m.id,
|
||||
label: `${m.meterNumber} (${m.type === 'ELECTRICITY' ? 'Strom' : 'Gas'})${m.location ? ` – ${m.location}` : ''}${!m.isActive ? ' – deaktiviert' : ''}`,
|
||||
}))}
|
||||
placeholder="Bitte wählen..."
|
||||
/>
|
||||
<Input
|
||||
label="Wechseldatum"
|
||||
type="date"
|
||||
value={formData.installedAt}
|
||||
onChange={(e) => setFormData({ ...formData, installedAt: e.target.value })}
|
||||
/>
|
||||
<Input
|
||||
label="Endstand alter Zähler (optional)"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.finalReadingPrevious}
|
||||
onChange={(e) => setFormData({ ...formData, finalReadingPrevious: e.target.value })}
|
||||
placeholder="Optional"
|
||||
/>
|
||||
<p className="text-xs text-blue-700">
|
||||
Typ, Adresse und Tarifmodell werden vom Vorgänger übernommen. Alle Verträge,
|
||||
die den Vorgänger-Zähler verwenden, werden automatisch auf diesen neuen Zähler
|
||||
umgestellt.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isEditing && (
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user