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:
@@ -0,0 +1,46 @@
|
|||||||
|
-- Folgezähler-Kette: Meter zeigt optional auf den Vorgänger.
|
||||||
|
-- Beim Wechsel können wir dann sowohl die Kette für die UI anzeigen
|
||||||
|
-- als auch alle Verträge mit dem Vorgänger automatisch auf den
|
||||||
|
-- Nachfolger umstellen.
|
||||||
|
--
|
||||||
|
-- ON DELETE SET NULL, damit ein versehentlich gelöschter Vorgänger
|
||||||
|
-- den Nachfolger nicht killt.
|
||||||
|
--
|
||||||
|
-- IF NOT EXISTS macht den Re-Deploy auf Prod sicher, falls jemand
|
||||||
|
-- schon `prisma db push` gefahren hat.
|
||||||
|
|
||||||
|
ALTER TABLE `Meter`
|
||||||
|
ADD COLUMN IF NOT EXISTS `predecessorMeterId` INT NULL;
|
||||||
|
|
||||||
|
-- Index nur anlegen, wenn er noch nicht da ist
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'Meter'
|
||||||
|
AND INDEX_NAME = 'Meter_predecessorMeterId_fkey'
|
||||||
|
);
|
||||||
|
SET @sql := IF(
|
||||||
|
@idx_exists = 0,
|
||||||
|
'CREATE INDEX `Meter_predecessorMeterId_fkey` ON `Meter`(`predecessorMeterId`)',
|
||||||
|
'SELECT "Index existiert bereits"'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- Foreign Key nur anlegen, wenn er noch nicht da ist
|
||||||
|
SET @fk_exists := (
|
||||||
|
SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'Meter'
|
||||||
|
AND CONSTRAINT_NAME = 'Meter_predecessorMeterId_fkey'
|
||||||
|
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
|
||||||
|
);
|
||||||
|
SET @sql := IF(
|
||||||
|
@fk_exists = 0,
|
||||||
|
'ALTER TABLE `Meter` ADD CONSTRAINT `Meter_predecessorMeterId_fkey` FOREIGN KEY (`predecessorMeterId`) REFERENCES `Meter`(`id`) ON DELETE SET NULL ON UPDATE CASCADE',
|
||||||
|
'SELECT "FK existiert bereits"'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@@ -489,6 +489,12 @@ model Meter {
|
|||||||
tariffModel MeterTariffModel @default(SINGLE) // Eintarif oder Zweitarif (HT/NT)
|
tariffModel MeterTariffModel @default(SINGLE) // Eintarif oder Zweitarif (HT/NT)
|
||||||
location String?
|
location String?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
|
// Folgezähler-Kette: zeigt auf den Vorgänger, den dieser Zähler abgelöst hat.
|
||||||
|
// Wird beim Anlegen als Folgezähler gesetzt; informational + zum Anzeigen
|
||||||
|
// der Kette. Auto-Propagation auf Verträge passiert beim Create.
|
||||||
|
predecessorMeterId Int?
|
||||||
|
predecessor Meter? @relation("MeterSuccessor", fields: [predecessorMeterId], references: [id], onDelete: SetNull)
|
||||||
|
successors Meter[] @relation("MeterSuccessor")
|
||||||
readings MeterReading[]
|
readings MeterReading[]
|
||||||
energyDetails EnergyContractDetails[]
|
energyDetails EnergyContractDetails[]
|
||||||
contractMeters ContractMeter[] @relation("ContractMeters")
|
contractMeters ContractMeter[] @relation("ContractMeters")
|
||||||
|
|||||||
@@ -603,10 +603,13 @@ export async function createMeter(req: AuthRequest, res: Response): Promise<void
|
|||||||
const customerId = parseInt(req.params.customerId);
|
const customerId = parseInt(req.params.customerId);
|
||||||
if (!(await canAccessCustomer(req, res, customerId))) return;
|
if (!(await canAccessCustomer(req, res, customerId))) return;
|
||||||
const meter = await customerService.createMeter(customerId, req.body);
|
const meter = await customerService.createMeter(customerId, req.body);
|
||||||
|
const successorLabel = meter.predecessor
|
||||||
|
? ` als Folgezähler von ${meter.predecessor.meterNumber}`
|
||||||
|
: '';
|
||||||
await logChange({
|
await logChange({
|
||||||
req, action: 'CREATE', resourceType: 'Meter',
|
req, action: 'CREATE', resourceType: 'Meter',
|
||||||
resourceId: meter.id.toString(),
|
resourceId: meter.id.toString(),
|
||||||
label: `Zähler angelegt für Kunde #${customerId}`,
|
label: `Zähler angelegt${successorLabel} für Kunde #${customerId}`,
|
||||||
customerId,
|
customerId,
|
||||||
});
|
});
|
||||||
res.status(201).json({ success: true, data: meter } as ApiResponse);
|
res.status(201).json({ success: true, data: meter } as ApiResponse);
|
||||||
|
|||||||
@@ -441,13 +441,39 @@ export async function createMeter(
|
|||||||
tariffModel?: 'SINGLE' | 'DUAL';
|
tariffModel?: 'SINGLE' | 'DUAL';
|
||||||
location?: string;
|
location?: string;
|
||||||
addressId?: number | null;
|
addressId?: number | null;
|
||||||
|
// Optional: dieser Zähler ersetzt einen bestehenden (Folgezähler).
|
||||||
|
// Beim Create werden alle Verträge, die den Vorgänger als aktuellen
|
||||||
|
// Zähler nutzen, automatisch auf den neuen Zähler umgestellt
|
||||||
|
// (ContractMeter-Eintrag analog zu Vertragsansicht).
|
||||||
|
successorOf?: {
|
||||||
|
predecessorMeterId: number;
|
||||||
|
installedAt?: string;
|
||||||
|
finalReadingPrevious?: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
if (data.addressId == null) {
|
if (data.addressId == null) {
|
||||||
throw new Error('Lieferadresse ist erforderlich');
|
throw new Error('Lieferadresse ist erforderlich');
|
||||||
}
|
}
|
||||||
await assertDeliveryAddressBelongsToCustomer(data.addressId, customerId);
|
await assertDeliveryAddressBelongsToCustomer(data.addressId, customerId);
|
||||||
return prisma.meter.create({
|
|
||||||
|
// Vorgänger validieren (wenn Folgezähler)
|
||||||
|
let predecessor: { id: number; customerId: number; type: 'ELECTRICITY' | 'GAS' } | null = null;
|
||||||
|
if (data.successorOf) {
|
||||||
|
const pred = await prisma.meter.findUnique({
|
||||||
|
where: { id: data.successorOf.predecessorMeterId },
|
||||||
|
select: { id: true, customerId: true, type: true },
|
||||||
|
});
|
||||||
|
if (!pred || pred.customerId !== customerId) {
|
||||||
|
throw new Error('Vorgänger-Zähler nicht gefunden');
|
||||||
|
}
|
||||||
|
if (pred.type !== data.type) {
|
||||||
|
throw new Error('Vorgänger-Zähler muss denselben Typ haben (Strom/Gas)');
|
||||||
|
}
|
||||||
|
predecessor = pred;
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await prisma.meter.create({
|
||||||
data: {
|
data: {
|
||||||
customerId,
|
customerId,
|
||||||
meterNumber: data.meterNumber,
|
meterNumber: data.meterNumber,
|
||||||
@@ -456,9 +482,84 @@ export async function createMeter(
|
|||||||
location: data.location,
|
location: data.location,
|
||||||
addressId: data.addressId,
|
addressId: data.addressId,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
predecessorMeterId: predecessor?.id,
|
||||||
},
|
},
|
||||||
include: { address: true },
|
include: { address: true, predecessor: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Folgezähler-Propagation: alle Verträge, die den Vorgänger als aktuellen
|
||||||
|
// Zähler nutzen, bekommen den neuen Zähler als Nachfolger angehängt
|
||||||
|
// (analog zu addSuccessorMeter im contract.controller).
|
||||||
|
if (predecessor && data.successorOf) {
|
||||||
|
const installedAt = data.successorOf.installedAt
|
||||||
|
? new Date(data.successorOf.installedAt)
|
||||||
|
: new Date();
|
||||||
|
const finalReading = data.successorOf.finalReadingPrevious;
|
||||||
|
|
||||||
|
const affectedContracts = await prisma.energyContractDetails.findMany({
|
||||||
|
where: { meterId: predecessor.id },
|
||||||
|
include: { contractMeters: { orderBy: { position: 'asc' } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const ecd of affectedContracts) {
|
||||||
|
// Vorhandenen ContractMeter für den Vorgänger als gewechselt markieren.
|
||||||
|
// Falls noch kein ContractMeter für den Vorgänger existiert (Single-Meter-
|
||||||
|
// Vertrag vor Multi-Meter-Refactor), legen wir ihn als position 0 an,
|
||||||
|
// damit die Kette lückenlos ist.
|
||||||
|
let predCM = ecd.contractMeters.find((cm) => cm.meterId === predecessor!.id);
|
||||||
|
if (!predCM) {
|
||||||
|
predCM = await prisma.contractMeter.create({
|
||||||
|
data: {
|
||||||
|
energyContractDetailsId: ecd.id,
|
||||||
|
meterId: predecessor.id,
|
||||||
|
position: 0,
|
||||||
|
installedAt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
ecd.contractMeters.push(predCM);
|
||||||
|
}
|
||||||
|
await prisma.contractMeter.update({
|
||||||
|
where: { id: predCM.id },
|
||||||
|
data: {
|
||||||
|
removedAt: installedAt,
|
||||||
|
finalReading: finalReading != null ? finalReading : predCM.finalReading,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextPosition = ecd.contractMeters.length > 0
|
||||||
|
? Math.max(...ecd.contractMeters.map((cm) => cm.position)) + 1
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Idempotenz: falls (durch Doppel-Klick o.ä.) schon ein ContractMeter
|
||||||
|
// mit dem neuen Zähler existiert, nicht doppelt anlegen.
|
||||||
|
const existsForNew = await prisma.contractMeter.findUnique({
|
||||||
|
where: {
|
||||||
|
energyContractDetailsId_meterId: {
|
||||||
|
energyContractDetailsId: ecd.id,
|
||||||
|
meterId: created.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!existsForNew) {
|
||||||
|
await prisma.contractMeter.create({
|
||||||
|
data: {
|
||||||
|
energyContractDetailsId: ecd.id,
|
||||||
|
meterId: created.id,
|
||||||
|
position: nextPosition,
|
||||||
|
installedAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktuellen Zähler am Vertrag aktualisieren
|
||||||
|
await prisma.energyContractDetails.update({
|
||||||
|
where: { id: ecd.id },
|
||||||
|
data: { meterId: created.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateMeter(
|
export async function updateMeter(
|
||||||
|
|||||||
@@ -97,6 +97,34 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ Erledigt
|
||||||
|
|
||||||
|
- [x] **🆕 Folgezähler-Deklaration in der Kundenakte (Auto-Propagation)**
|
||||||
|
- **Backend**: Neues Feld `Meter.predecessorMeterId`
|
||||||
|
(Self-Relation, `ON DELETE SET NULL`). Migration
|
||||||
|
`20260530140000_meter_predecessor` mit `IF NOT EXISTS`.
|
||||||
|
`createMeter` akzeptiert optional `successorOf: { predecessorMeterId,
|
||||||
|
installedAt?, finalReadingPrevious? }`. Wenn gesetzt: Vorgänger
|
||||||
|
wird validiert (gleicher Kunde + gleicher Typ), und für alle
|
||||||
|
Verträge, die den Vorgänger als aktuellen Zähler nutzen, wird
|
||||||
|
der ContractMeter-Eintrag analog zu `addSuccessorMeter`
|
||||||
|
propagiert (vorhandener ContractMeter wird `removedAt` +
|
||||||
|
`finalReading` gesetzt; neuer ContractMeter wird mit nächster
|
||||||
|
Position + `installedAt` angelegt; `energyDetails.meterId`
|
||||||
|
auf den Neuzähler aktualisiert). Idempotent gegen Doppel-Klick.
|
||||||
|
- **MeterModal** (Kundenakte → Zähler): Bei Neuanlage neue
|
||||||
|
Checkbox „Diesen Zähler als Folgezähler deklarieren". Wenn
|
||||||
|
aktiv: Dropdown Vorgänger-Zähler (alle Zähler des Kunden,
|
||||||
|
inkl. inaktive – mit Suffix), Wechseldatum (default heute),
|
||||||
|
Endstand alter Zähler (optional). Bei Vorgänger-Auswahl werden
|
||||||
|
Typ, Tarifmodell und Adresse vom Vorgänger übernommen und
|
||||||
|
disabled. Info-Banner: „Alle Verträge mit dem alten Zähler
|
||||||
|
werden automatisch umgestellt".
|
||||||
|
- Audit-Log: „Zähler angelegt als Folgezähler von X für Kunde #N".
|
||||||
|
|
||||||
|
- [x] **🆕 Vertragsansicht: Standort + Inaktiv-Badge beim Zähler**
|
||||||
|
- In den Strom/Gas-Details neben der Zählernummer zusätzlich ein
|
||||||
|
rotes „Inaktiv"-Badge und eine Zeile mit Standort, falls
|
||||||
|
hinterlegt.
|
||||||
|
|
||||||
- [x] **🆕 Zähler → Lieferadresse-Pflichtfeld + Vertragsfilter**
|
- [x] **🆕 Zähler → Lieferadresse-Pflichtfeld + Vertragsfilter**
|
||||||
- **Backend**: Neues Feld `Meter.addressId` (optional FK auf
|
- **Backend**: Neues Feld `Meter.addressId` (optional FK auf
|
||||||
`Address`, `ON DELETE SET NULL`). Migration
|
`Address`, `ON DELETE SET NULL`). Migration
|
||||||
|
|||||||
@@ -457,6 +457,7 @@ export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?
|
|||||||
onClose={() => setShowMeterModal(false)}
|
onClose={() => setShowMeterModal(false)}
|
||||||
customerId={customerId}
|
customerId={customerId}
|
||||||
addresses={c.addresses || []}
|
addresses={c.addresses || []}
|
||||||
|
existingMeters={c.meters || []}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MeterModal
|
<MeterModal
|
||||||
@@ -465,6 +466,7 @@ export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?
|
|||||||
customerId={customerId}
|
customerId={customerId}
|
||||||
meter={editingMeter}
|
meter={editingMeter}
|
||||||
addresses={c.addresses || []}
|
addresses={c.addresses || []}
|
||||||
|
existingMeters={c.meters || []}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StressfreiEmailModal
|
<StressfreiEmailModal
|
||||||
@@ -2827,16 +2829,19 @@ function MeterModal({
|
|||||||
customerId,
|
customerId,
|
||||||
meter,
|
meter,
|
||||||
addresses,
|
addresses,
|
||||||
|
existingMeters,
|
||||||
}: {
|
}: {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
customerId: number;
|
customerId: number;
|
||||||
meter?: Meter | null;
|
meter?: Meter | null;
|
||||||
addresses: Address[];
|
addresses: Address[];
|
||||||
|
existingMeters: Meter[];
|
||||||
}) {
|
}) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const isEditing = !!meter;
|
const isEditing = !!meter;
|
||||||
const deliveryAddresses = addresses.filter((a) => a.type === 'DELIVERY_RESIDENCE');
|
const deliveryAddresses = addresses.filter((a) => a.type === 'DELIVERY_RESIDENCE');
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
const getInitialFormData = () => ({
|
const getInitialFormData = () => ({
|
||||||
meterNumber: meter?.meterNumber || '',
|
meterNumber: meter?.meterNumber || '',
|
||||||
@@ -2845,17 +2850,36 @@ function MeterModal({
|
|||||||
location: meter?.location || '',
|
location: meter?.location || '',
|
||||||
isActive: meter?.isActive ?? true,
|
isActive: meter?.isActive ?? true,
|
||||||
addressId: meter?.addressId?.toString() || '',
|
addressId: meter?.addressId?.toString() || '',
|
||||||
|
isSuccessor: false,
|
||||||
|
predecessorMeterId: '',
|
||||||
|
installedAt: today,
|
||||||
|
finalReadingPrevious: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [formData, setFormData] = useState(getInitialFormData);
|
const [formData, setFormData] = useState(getInitialFormData);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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({
|
const createMutation = useMutation({
|
||||||
mutationFn: (data: any) => meterApi.create(customerId, data),
|
mutationFn: (data: any) => meterApi.create(customerId, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
onClose();
|
onClose();
|
||||||
setFormData({ meterNumber: '', type: 'ELECTRICITY', tariffModel: 'SINGLE', location: '', isActive: true, addressId: '' });
|
setFormData(getInitialFormData());
|
||||||
},
|
},
|
||||||
onError: (err) => setError(err instanceof Error ? err.message : 'Fehler beim Speichern'),
|
onError: (err) => setError(err instanceof Error ? err.message : 'Fehler beim Speichern'),
|
||||||
});
|
});
|
||||||
@@ -2876,7 +2900,11 @@ function MeterModal({
|
|||||||
setError('Bitte eine Lieferadresse auswählen');
|
setError('Bitte eine Lieferadresse auswählen');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const payload = {
|
if (formData.isSuccessor && !formData.predecessorMeterId) {
|
||||||
|
setError('Bitte einen Vorgänger-Zähler auswählen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload: any = {
|
||||||
meterNumber: formData.meterNumber,
|
meterNumber: formData.meterNumber,
|
||||||
type: formData.type,
|
type: formData.type,
|
||||||
tariffModel: formData.tariffModel,
|
tariffModel: formData.tariffModel,
|
||||||
@@ -2884,6 +2912,15 @@ function MeterModal({
|
|||||||
isActive: formData.isActive,
|
isActive: formData.isActive,
|
||||||
addressId: parseInt(formData.addressId),
|
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) {
|
if (isEditing) {
|
||||||
updateMutation.mutate(payload);
|
updateMutation.mutate(payload);
|
||||||
} else {
|
} else {
|
||||||
@@ -2899,6 +2936,7 @@ function MeterModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const noDeliveryAddresses = deliveryAddresses.length === 0;
|
const noDeliveryAddresses = deliveryAddresses.length === 0;
|
||||||
|
const successorLocked = !isEditing && formData.isSuccessor && !!predecessor;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} title={isEditing ? 'Zähler bearbeiten' : 'Zähler hinzufügen'}>
|
<Modal isOpen={isOpen} onClose={onClose} title={isEditing ? 'Zähler bearbeiten' : 'Zähler hinzufügen'}>
|
||||||
@@ -2920,6 +2958,7 @@ function MeterModal({
|
|||||||
}))}
|
}))}
|
||||||
placeholder="Lieferadresse wählen..."
|
placeholder="Lieferadresse wählen..."
|
||||||
required
|
required
|
||||||
|
disabled={successorLocked && !!predecessor?.addressId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
@@ -2937,6 +2976,7 @@ function MeterModal({
|
|||||||
{ value: 'ELECTRICITY', label: 'Strom' },
|
{ value: 'ELECTRICITY', label: 'Strom' },
|
||||||
{ value: 'GAS', label: 'Gas' },
|
{ value: 'GAS', label: 'Gas' },
|
||||||
]}
|
]}
|
||||||
|
disabled={successorLocked}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{formData.type === 'ELECTRICITY' && (
|
{formData.type === 'ELECTRICITY' && (
|
||||||
@@ -2948,6 +2988,7 @@ function MeterModal({
|
|||||||
{ value: 'SINGLE', label: 'Eintarifzähler (Standard)' },
|
{ value: 'SINGLE', label: 'Eintarifzähler (Standard)' },
|
||||||
{ value: 'DUAL', label: 'Zweitarifzähler (HT/NT)' },
|
{ value: 'DUAL', label: 'Zweitarifzähler (HT/NT)' },
|
||||||
]}
|
]}
|
||||||
|
disabled={successorLocked}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -2958,6 +2999,54 @@ function MeterModal({
|
|||||||
placeholder="z.B. Keller, Wohnung"
|
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 && (
|
{isEditing && (
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
|
|||||||
Reference in New Issue
Block a user