Zähler → Lieferadresse-Pflichtfeld + Vertragsfilter

- Meter.addressId (FK → Address, ON DELETE SET NULL) + Migration
  20260530100000_meter_address mit IF NOT EXISTS
- Service erzwingt beim Create: Lieferadresse vorhanden + zum
  Kunden gehörig + Typ DELIVERY_RESIDENCE
- MeterModal: Pflicht-Dropdown "Lieferadresse"; Save disabled
  ohne Adresse; Hinweis-Banner. Bestandszähler ohne Adresse zeigen
  "nicht zugeordnet – bitte über Bearbeiten nachpflegen"
- ContractForm: Zähler-Dropdown filtert auf Vertrags-Lieferadresse;
  deaktivierte Zähler bleiben sichtbar mit "(deaktiviert)"; bei
  Auswahl Toast-Warnung wegen möglichem Altvertrag

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 13:18:24 +02:00
parent d92d0b1eaf
commit c099b41796
8 changed files with 231 additions and 17 deletions
+42 -10
View File
@@ -3,6 +3,7 @@ import { useNavigate, useParams, useSearchParams, useLocation } from 'react-rout
import { popHistory } from '../../utils/navigation';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
import { contractApi, customerApi, platformApi, cancellationPeriodApi, contractDurationApi, providerApi, contractCategoryApi } from '../../services/api';
import Card from '../../components/ui/Card';
import Button from '../../components/ui/Button';
@@ -964,16 +965,47 @@ export default function ContractForm() {
{['ELECTRICITY', 'GAS'].includes(contractType) && (
<Card className="mb-6" title={contractType === 'ELECTRICITY' ? 'Strom-Details' : 'Gas-Details'}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Select
label="Zähler"
{...register('meterId')}
options={meters
.filter((m) => m.type === contractType && (m.isActive || m.id.toString() === watch('meterId')))
.map((m) => ({
value: m.id,
label: `${m.meterNumber}${m.location ? ` (${m.location})` : ''}${!m.isActive ? ' (deaktiviert)' : ''}`,
}))}
/>
{(() => {
const selectedAddressId = watch('addressId');
const meterRegister = register('meterId');
// Zähler werden auf die Lieferadresse des Vertrags gefiltert. Zähler ohne
// Lieferadresse (Bestand) bleiben für den aktuell gewählten Eintrag sichtbar,
// damit die Auswahl nicht verschwindet, bis sie nachgepflegt sind.
const filteredMeters = meters.filter((m) => {
if (m.type !== contractType) return false;
const isCurrentlySelected = m.id.toString() === watch('meterId');
if (isCurrentlySelected) return true;
if (!selectedAddressId) return false;
return m.addressId != null && m.addressId.toString() === selectedAddressId;
});
return (
<Select
label="Zähler"
{...meterRegister}
onChange={(e) => {
meterRegister.onChange(e);
const m = meters.find((x) => x.id.toString() === e.target.value);
if (m && !m.isActive) {
toast(
'Deaktivierter Zähler ausgewählt. Ist das gewollt? Handelt es sich um einen Altvertrag?',
{ icon: '⚠️', duration: 6000 },
);
}
}}
options={filteredMeters.map((m) => ({
value: m.id,
label: `${m.meterNumber}${m.location ? ` (${m.location})` : ''}${!m.isActive ? ' (deaktiviert)' : ''}`,
}))}
placeholder={
!selectedAddressId
? 'Erst Lieferadresse wählen...'
: filteredMeters.length === 0
? 'Kein Zähler für diese Adresse vorhanden'
: 'Zähler wählen...'
}
/>
);
})()}
<Input
label="MaLo-ID (Marktlokations-ID)"
{...register('maloId')}