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
@@ -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;
+5
View File
@@ -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;
+34 -1
View File
@@ -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 },
}); });
} }
+23
View File
@@ -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
+36 -4
View File
@@ -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">
{(() => {
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 <Select
label="Zähler" label="Zähler"
{...register('meterId')} {...meterRegister}
options={meters onChange={(e) => {
.filter((m) => m.type === contractType && (m.isActive || m.id.toString() === watch('meterId'))) meterRegister.onChange(e);
.map((m) => ({ 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, value: m.id,
label: `${m.meterNumber}${m.location ? ` (${m.location})` : ''}${!m.isActive ? ' (deaktiviert)' : ''}`, 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>
+4
View File
@@ -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[];
} }