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:
@@ -0,0 +1,43 @@
|
|||||||
|
-- Zähler bekommen eine Lieferadresse, an der sie installiert sind.
|
||||||
|
-- Optional, damit Bestandszähler (vor diesem Feature) nicht brechen –
|
||||||
|
-- die werden manuell nachgepflegt. ON DELETE SET NULL, damit ein
|
||||||
|
-- gelöschter Adresseintrag den Zähler 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 `addressId` 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_addressId_fkey'
|
||||||
|
);
|
||||||
|
SET @sql := IF(
|
||||||
|
@idx_exists = 0,
|
||||||
|
'CREATE INDEX `Meter_addressId_fkey` ON `Meter`(`addressId`)',
|
||||||
|
'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_addressId_fkey'
|
||||||
|
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
|
||||||
|
);
|
||||||
|
SET @sql := IF(
|
||||||
|
@fk_exists = 0,
|
||||||
|
'ALTER TABLE `Meter` ADD CONSTRAINT `Meter_addressId_fkey` FOREIGN KEY (`addressId`) REFERENCES `Address`(`id`) ON DELETE SET NULL ON UPDATE CASCADE',
|
||||||
|
'SELECT "FK existiert bereits"'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@@ -281,6 +281,7 @@ model Address {
|
|||||||
ownerEmail String?
|
ownerEmail String?
|
||||||
contractsAsDelivery Contract[] @relation("DeliveryAddress")
|
contractsAsDelivery Contract[] @relation("DeliveryAddress")
|
||||||
contractsAsBilling Contract[] @relation("BillingAddress")
|
contractsAsBilling Contract[] @relation("BillingAddress")
|
||||||
|
meters Meter[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
@@ -479,6 +480,10 @@ model Meter {
|
|||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
customerId Int
|
customerId Int
|
||||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||||
|
// Lieferadresse, an der der Zähler hängt. Optional, weil Bestandszähler
|
||||||
|
// vor dem Feature noch keine Adresse haben (werden manuell nachgepflegt).
|
||||||
|
addressId Int?
|
||||||
|
address Address? @relation(fields: [addressId], references: [id], onDelete: SetNull)
|
||||||
meterNumber String
|
meterNumber String
|
||||||
type MeterType
|
type MeterType
|
||||||
tariffModel MeterTariffModel @default(SINGLE) // Eintarif oder Zweitarif (HT/NT)
|
tariffModel MeterTariffModel @default(SINGLE) // Eintarif oder Zweitarif (HT/NT)
|
||||||
|
|||||||
@@ -635,7 +635,7 @@ export async function updateMeter(req: AuthRequest, res: Response): Promise<void
|
|||||||
const changes: Record<string, { von: unknown; nach: unknown }> = {};
|
const changes: Record<string, { von: unknown; nach: unknown }> = {};
|
||||||
const fieldLabels: Record<string, string> = {
|
const fieldLabels: Record<string, string> = {
|
||||||
meterNumber: 'Zählernummer', type: 'Typ', tariffModel: 'Tarifmodell',
|
meterNumber: 'Zählernummer', type: 'Typ', tariffModel: 'Tarifmodell',
|
||||||
location: 'Standort', isActive: 'Aktiv',
|
location: 'Standort', isActive: 'Aktiv', addressId: 'Lieferadresse',
|
||||||
};
|
};
|
||||||
for (const [key, newVal] of Object.entries(data)) {
|
for (const [key, newVal] of Object.entries(data)) {
|
||||||
if (['id', 'createdAt', 'updatedAt'].includes(key)) continue;
|
if (['id', 'createdAt', 'updatedAt'].includes(key)) continue;
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ export async function getCustomerById(id: number) {
|
|||||||
meters: {
|
meters: {
|
||||||
orderBy: { isActive: 'desc' },
|
orderBy: { isActive: 'desc' },
|
||||||
include: {
|
include: {
|
||||||
|
address: true,
|
||||||
readings: {
|
readings: {
|
||||||
orderBy: { readingDate: 'desc' },
|
orderBy: { readingDate: 'desc' },
|
||||||
},
|
},
|
||||||
@@ -410,6 +411,7 @@ export async function getCustomerMeters(
|
|||||||
return prisma.meter.findMany({
|
return prisma.meter.findMany({
|
||||||
where,
|
where,
|
||||||
include: {
|
include: {
|
||||||
|
address: true,
|
||||||
readings: {
|
readings: {
|
||||||
orderBy: { readingDate: 'desc' },
|
orderBy: { readingDate: 'desc' },
|
||||||
take: 5,
|
take: 5,
|
||||||
@@ -419,20 +421,43 @@ export async function getCustomerMeters(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lieferadresse muss zum Kunden gehören und vom Typ DELIVERY_RESIDENCE sein.
|
||||||
|
// Wirft eine sprechende Fehlermeldung, die der Controller dem User durchreicht.
|
||||||
|
async function assertDeliveryAddressBelongsToCustomer(addressId: number, customerId: number) {
|
||||||
|
const addr = await prisma.address.findUnique({ where: { id: addressId } });
|
||||||
|
if (!addr || addr.customerId !== customerId) {
|
||||||
|
throw new Error('Ungültige Lieferadresse');
|
||||||
|
}
|
||||||
|
if (addr.type !== 'DELIVERY_RESIDENCE') {
|
||||||
|
throw new Error('Nur Lieferadressen können einem Zähler zugeordnet werden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function createMeter(
|
export async function createMeter(
|
||||||
customerId: number,
|
customerId: number,
|
||||||
data: {
|
data: {
|
||||||
meterNumber: string;
|
meterNumber: string;
|
||||||
type: 'ELECTRICITY' | 'GAS';
|
type: 'ELECTRICITY' | 'GAS';
|
||||||
|
tariffModel?: 'SINGLE' | 'DUAL';
|
||||||
location?: string;
|
location?: string;
|
||||||
|
addressId?: number | null;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
if (data.addressId == null) {
|
||||||
|
throw new Error('Lieferadresse ist erforderlich');
|
||||||
|
}
|
||||||
|
await assertDeliveryAddressBelongsToCustomer(data.addressId, customerId);
|
||||||
return prisma.meter.create({
|
return prisma.meter.create({
|
||||||
data: {
|
data: {
|
||||||
customerId,
|
customerId,
|
||||||
...data,
|
meterNumber: data.meterNumber,
|
||||||
|
type: data.type,
|
||||||
|
tariffModel: data.tariffModel,
|
||||||
|
location: data.location,
|
||||||
|
addressId: data.addressId,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
|
include: { address: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,13 +466,21 @@ export async function updateMeter(
|
|||||||
data: {
|
data: {
|
||||||
meterNumber?: string;
|
meterNumber?: string;
|
||||||
type?: 'ELECTRICITY' | 'GAS';
|
type?: 'ELECTRICITY' | 'GAS';
|
||||||
|
tariffModel?: 'SINGLE' | 'DUAL';
|
||||||
location?: string;
|
location?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
addressId?: number | null;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
if (data.addressId !== undefined && data.addressId !== null) {
|
||||||
|
const meter = await prisma.meter.findUnique({ where: { id }, select: { customerId: true } });
|
||||||
|
if (!meter) throw new Error('Zähler nicht gefunden');
|
||||||
|
await assertDeliveryAddressBelongsToCustomer(data.addressId, meter.customerId);
|
||||||
|
}
|
||||||
return prisma.meter.update({
|
return prisma.meter.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data,
|
data,
|
||||||
|
include: { address: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,29 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ Erledigt
|
||||||
|
|
||||||
|
- [x] **🆕 Zähler → Lieferadresse-Pflichtfeld + Vertragsfilter**
|
||||||
|
- **Backend**: Neues Feld `Meter.addressId` (optional FK auf
|
||||||
|
`Address`, `ON DELETE SET NULL`). Migration
|
||||||
|
`20260530100000_meter_address` mit `IF NOT EXISTS`. Service
|
||||||
|
erzwingt beim Create: Lieferadresse muss vorhanden, zum Kunden
|
||||||
|
gehören und Typ `DELIVERY_RESIDENCE` haben.
|
||||||
|
- **MeterModal** (Kundenakte → Zähler): Pflicht-Dropdown
|
||||||
|
"Lieferadresse" über allen Feldern. Button "Zähler hinzufügen"
|
||||||
|
ist disabled, wenn keine Lieferadresse existiert – mit gelbem
|
||||||
|
Hinweis-Banner. Bestandszähler ohne Adresse zeigen im
|
||||||
|
MetersTab "Lieferadresse: nicht zugeordnet – bitte über
|
||||||
|
Bearbeiten nachpflegen" in gelb.
|
||||||
|
- **ContractForm** (Strom/Gas): Zähler-Dropdown filtert jetzt
|
||||||
|
auf die im Vertrag gewählte Lieferadresse. Deaktivierte
|
||||||
|
Zähler bleiben sichtbar (Label-Suffix `(deaktiviert)`); wenn
|
||||||
|
sie ausgewählt werden, kommt ein Toast: *„Deaktivierter
|
||||||
|
Zähler ausgewählt. Ist das gewollt? Handelt es sich um einen
|
||||||
|
Altvertrag?"*. Platzhalter wechselt zwischen "Erst
|
||||||
|
Lieferadresse wählen…" / "Kein Zähler für diese Adresse
|
||||||
|
vorhanden" / "Zähler wählen…".
|
||||||
|
- Audit-Log loggt Adress-Änderung am Zähler als Feld
|
||||||
|
"Lieferadresse".
|
||||||
|
|
||||||
- [x] **🆕 Backup-Operations-Log + EBUSY-Fix beim Restore**
|
- [x] **🆕 Backup-Operations-Log + EBUSY-Fix beim Restore**
|
||||||
- Zwei neue Log-Panels auf der DB-Backup-Seite: links
|
- Zwei neue Log-Panels auf der DB-Backup-Seite: links
|
||||||
"Backup-Erstellung", rechts "Backup-Wiederherstellung". Jeder
|
"Backup-Erstellung", rechts "Backup-Wiederherstellung". Jeder
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNavigate, useParams, useSearchParams, useLocation } from 'react-rout
|
|||||||
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';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
import { contractApi, customerApi, platformApi, cancellationPeriodApi, contractDurationApi, providerApi, contractCategoryApi } from '../../services/api';
|
import { contractApi, customerApi, platformApi, cancellationPeriodApi, contractDurationApi, providerApi, contractCategoryApi } from '../../services/api';
|
||||||
import Card from '../../components/ui/Card';
|
import Card from '../../components/ui/Card';
|
||||||
import Button from '../../components/ui/Button';
|
import Button from '../../components/ui/Button';
|
||||||
@@ -964,16 +965,47 @@ export default function ContractForm() {
|
|||||||
{['ELECTRICITY', 'GAS'].includes(contractType) && (
|
{['ELECTRICITY', 'GAS'].includes(contractType) && (
|
||||||
<Card className="mb-6" title={contractType === 'ELECTRICITY' ? 'Strom-Details' : 'Gas-Details'}>
|
<Card className="mb-6" title={contractType === 'ELECTRICITY' ? 'Strom-Details' : 'Gas-Details'}>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<Select
|
{(() => {
|
||||||
label="Zähler"
|
const selectedAddressId = watch('addressId');
|
||||||
{...register('meterId')}
|
const meterRegister = register('meterId');
|
||||||
options={meters
|
// Zähler werden auf die Lieferadresse des Vertrags gefiltert. Zähler ohne
|
||||||
.filter((m) => m.type === contractType && (m.isActive || m.id.toString() === watch('meterId')))
|
// Lieferadresse (Bestand) bleiben für den aktuell gewählten Eintrag sichtbar,
|
||||||
.map((m) => ({
|
// damit die Auswahl nicht verschwindet, bis sie nachgepflegt sind.
|
||||||
value: m.id,
|
const filteredMeters = meters.filter((m) => {
|
||||||
label: `${m.meterNumber}${m.location ? ` (${m.location})` : ''}${!m.isActive ? ' (deaktiviert)' : ''}`,
|
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
|
<Input
|
||||||
label="MaLo-ID (Marktlokations-ID)"
|
label="MaLo-ID (Marktlokations-ID)"
|
||||||
{...register('maloId')}
|
{...register('maloId')}
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?
|
|||||||
<MetersTab
|
<MetersTab
|
||||||
customerId={customerId}
|
customerId={customerId}
|
||||||
meters={c.meters || []}
|
meters={c.meters || []}
|
||||||
|
addresses={c.addresses || []}
|
||||||
canEdit={hasPermission('customers:update')}
|
canEdit={hasPermission('customers:update')}
|
||||||
showInactive={showInactive}
|
showInactive={showInactive}
|
||||||
onToggleInactive={() => setShowInactive(!showInactive)}
|
onToggleInactive={() => setShowInactive(!showInactive)}
|
||||||
@@ -455,6 +456,7 @@ export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?
|
|||||||
isOpen={showMeterModal}
|
isOpen={showMeterModal}
|
||||||
onClose={() => setShowMeterModal(false)}
|
onClose={() => setShowMeterModal(false)}
|
||||||
customerId={customerId}
|
customerId={customerId}
|
||||||
|
addresses={c.addresses || []}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MeterModal
|
<MeterModal
|
||||||
@@ -462,6 +464,7 @@ export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?
|
|||||||
onClose={() => setEditingMeter(null)}
|
onClose={() => setEditingMeter(null)}
|
||||||
customerId={customerId}
|
customerId={customerId}
|
||||||
meter={editingMeter}
|
meter={editingMeter}
|
||||||
|
addresses={c.addresses || []}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StressfreiEmailModal
|
<StressfreiEmailModal
|
||||||
@@ -1243,6 +1246,7 @@ function DocumentsTab({
|
|||||||
function MetersTab({
|
function MetersTab({
|
||||||
customerId,
|
customerId,
|
||||||
meters,
|
meters,
|
||||||
|
addresses,
|
||||||
canEdit,
|
canEdit,
|
||||||
showInactive,
|
showInactive,
|
||||||
onToggleInactive,
|
onToggleInactive,
|
||||||
@@ -1251,12 +1255,14 @@ function MetersTab({
|
|||||||
}: {
|
}: {
|
||||||
customerId: number;
|
customerId: number;
|
||||||
meters: Meter[];
|
meters: Meter[];
|
||||||
|
addresses: Address[];
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
showInactive: boolean;
|
showInactive: boolean;
|
||||||
onToggleInactive: () => void;
|
onToggleInactive: () => void;
|
||||||
onAdd: () => void;
|
onAdd: () => void;
|
||||||
onEdit: (meter: Meter) => void;
|
onEdit: (meter: Meter) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const hasDeliveryAddress = addresses.some((a) => a.type === 'DELIVERY_RESIDENCE');
|
||||||
const [showReadingModal, setShowReadingModal] = useState<{ meterId: number; meterType: 'ELECTRICITY' | 'GAS'; tariffModel?: string } | null>(null);
|
const [showReadingModal, setShowReadingModal] = useState<{ meterId: number; meterType: 'ELECTRICITY' | 'GAS'; tariffModel?: string } | null>(null);
|
||||||
const [expandedMeter, setExpandedMeter] = useState<number | null>(null);
|
const [expandedMeter, setExpandedMeter] = useState<number | null>(null);
|
||||||
const [editingReading, setEditingReading] = useState<{ meterId: number; meterType: 'ELECTRICITY' | 'GAS'; tariffModel?: string; reading: any } | null>(null);
|
const [editingReading, setEditingReading] = useState<{ meterId: number; meterType: 'ELECTRICITY' | 'GAS'; tariffModel?: string; reading: any } | null>(null);
|
||||||
@@ -1301,7 +1307,12 @@ function MetersTab({
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-4 mb-4">
|
<div className="flex items-center gap-4 mb-4">
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<Button size="sm" onClick={onAdd}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={onAdd}
|
||||||
|
disabled={!hasDeliveryAddress}
|
||||||
|
title={hasDeliveryAddress ? undefined : 'Erst Lieferadresse anlegen, dann Zähler hinzufügen'}
|
||||||
|
>
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
Zähler hinzufügen
|
Zähler hinzufügen
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1316,6 +1327,12 @@ function MetersTab({
|
|||||||
Inaktive anzeigen
|
Inaktive anzeigen
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
{canEdit && !hasDeliveryAddress && (
|
||||||
|
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg text-sm text-yellow-800">
|
||||||
|
Es ist noch keine Lieferadresse hinterlegt. Bitte zunächst im Tab „Adressen" eine
|
||||||
|
Lieferadresse anlegen, dann können hier Zähler erstellt werden.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{filtered.length > 0 ? (
|
{filtered.length > 0 ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -1405,6 +1422,16 @@ function MetersTab({
|
|||||||
{meter.meterNumber}
|
{meter.meterNumber}
|
||||||
<CopyButton value={meter.meterNumber} />
|
<CopyButton value={meter.meterNumber} />
|
||||||
</p>
|
</p>
|
||||||
|
{meter.address ? (
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Lieferadresse: {meter.address.street} {meter.address.houseNumber},{' '}
|
||||||
|
{meter.address.postalCode} {meter.address.city}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-yellow-700">
|
||||||
|
Lieferadresse: <em>nicht zugeordnet – bitte über „Bearbeiten" nachpflegen</em>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{meter.location && (
|
{meter.location && (
|
||||||
<p className="text-sm text-gray-500 flex items-center gap-1">
|
<p className="text-sm text-gray-500 flex items-center gap-1">
|
||||||
Standort: {meter.location}
|
Standort: {meter.location}
|
||||||
@@ -2799,14 +2826,17 @@ function MeterModal({
|
|||||||
onClose,
|
onClose,
|
||||||
customerId,
|
customerId,
|
||||||
meter,
|
meter,
|
||||||
|
addresses,
|
||||||
}: {
|
}: {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
customerId: number;
|
customerId: number;
|
||||||
meter?: Meter | null;
|
meter?: Meter | null;
|
||||||
|
addresses: Address[];
|
||||||
}) {
|
}) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const isEditing = !!meter;
|
const isEditing = !!meter;
|
||||||
|
const deliveryAddresses = addresses.filter((a) => a.type === 'DELIVERY_RESIDENCE');
|
||||||
|
|
||||||
const getInitialFormData = () => ({
|
const getInitialFormData = () => ({
|
||||||
meterNumber: meter?.meterNumber || '',
|
meterNumber: meter?.meterNumber || '',
|
||||||
@@ -2814,17 +2844,20 @@ function MeterModal({
|
|||||||
tariffModel: meter?.tariffModel || 'SINGLE' as const,
|
tariffModel: meter?.tariffModel || 'SINGLE' as const,
|
||||||
location: meter?.location || '',
|
location: meter?.location || '',
|
||||||
isActive: meter?.isActive ?? true,
|
isActive: meter?.isActive ?? true,
|
||||||
|
addressId: meter?.addressId?.toString() || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [formData, setFormData] = useState(getInitialFormData);
|
const [formData, setFormData] = useState(getInitialFormData);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
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 });
|
setFormData({ meterNumber: '', type: 'ELECTRICITY', tariffModel: 'SINGLE', location: '', isActive: true, addressId: '' });
|
||||||
},
|
},
|
||||||
|
onError: (err) => setError(err instanceof Error ? err.message : 'Fehler beim Speichern'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
@@ -2833,14 +2866,28 @@ function MeterModal({
|
|||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
|
onError: (err) => setError(err instanceof Error ? err.message : 'Fehler beim Speichern'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
setError(null);
|
||||||
|
if (!formData.addressId) {
|
||||||
|
setError('Bitte eine Lieferadresse auswählen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
meterNumber: formData.meterNumber,
|
||||||
|
type: formData.type,
|
||||||
|
tariffModel: formData.tariffModel,
|
||||||
|
location: formData.location,
|
||||||
|
isActive: formData.isActive,
|
||||||
|
addressId: parseInt(formData.addressId),
|
||||||
|
};
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
updateMutation.mutate(formData);
|
updateMutation.mutate(payload);
|
||||||
} else {
|
} else {
|
||||||
createMutation.mutate(formData);
|
createMutation.mutate(payload);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2851,9 +2898,30 @@ function MeterModal({
|
|||||||
setFormData(getInitialFormData());
|
setFormData(getInitialFormData());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const noDeliveryAddresses = deliveryAddresses.length === 0;
|
||||||
|
|
||||||
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'}>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{noDeliveryAddresses && (
|
||||||
|
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg text-sm text-yellow-800">
|
||||||
|
<strong>Keine Lieferadresse vorhanden.</strong> Bitte zunächst im Tab „Adressen" eine
|
||||||
|
Lieferadresse anlegen, bevor ein Zähler erstellt werden kann.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Lieferadresse"
|
||||||
|
value={formData.addressId}
|
||||||
|
onChange={(e) => setFormData({ ...formData, addressId: e.target.value })}
|
||||||
|
options={deliveryAddresses.map((a) => ({
|
||||||
|
value: a.id,
|
||||||
|
label: `${a.street} ${a.houseNumber}, ${a.postalCode} ${a.city}`,
|
||||||
|
}))}
|
||||||
|
placeholder="Lieferadresse wählen..."
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="Zählernummer"
|
label="Zählernummer"
|
||||||
value={formData.meterNumber}
|
value={formData.meterNumber}
|
||||||
@@ -2902,11 +2970,17 @@ function MeterModal({
|
|||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button type="button" variant="secondary" onClick={onClose}>
|
<Button type="button" variant="secondary" onClick={onClose}>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isPending}>
|
<Button type="submit" disabled={isPending || (!isEditing && noDeliveryAddresses)}>
|
||||||
{isPending ? 'Speichern...' : 'Speichern'}
|
{isPending ? 'Speichern...' : 'Speichern'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -216,6 +216,10 @@ export interface Meter {
|
|||||||
tariffModel: MeterTariffModel;
|
tariffModel: MeterTariffModel;
|
||||||
location?: string;
|
location?: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
// Lieferadresse, an der der Zähler hängt. Optional, weil Bestandszähler
|
||||||
|
// (vor Einführung dieses Feldes) noch ohne Adresse existieren können.
|
||||||
|
addressId?: number | null;
|
||||||
|
address?: Address;
|
||||||
readings?: MeterReading[];
|
readings?: MeterReading[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user