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?
|
||||
contractsAsDelivery Contract[] @relation("DeliveryAddress")
|
||||
contractsAsBilling Contract[] @relation("BillingAddress")
|
||||
meters Meter[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
@@ -479,6 +480,10 @@ model Meter {
|
||||
id Int @id @default(autoincrement())
|
||||
customerId Int
|
||||
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
|
||||
type MeterType
|
||||
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 fieldLabels: Record<string, string> = {
|
||||
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)) {
|
||||
if (['id', 'createdAt', 'updatedAt'].includes(key)) continue;
|
||||
|
||||
@@ -83,6 +83,7 @@ export async function getCustomerById(id: number) {
|
||||
meters: {
|
||||
orderBy: { isActive: 'desc' },
|
||||
include: {
|
||||
address: true,
|
||||
readings: {
|
||||
orderBy: { readingDate: 'desc' },
|
||||
},
|
||||
@@ -410,6 +411,7 @@ export async function getCustomerMeters(
|
||||
return prisma.meter.findMany({
|
||||
where,
|
||||
include: {
|
||||
address: true,
|
||||
readings: {
|
||||
orderBy: { readingDate: 'desc' },
|
||||
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(
|
||||
customerId: number,
|
||||
data: {
|
||||
meterNumber: string;
|
||||
type: 'ELECTRICITY' | 'GAS';
|
||||
tariffModel?: 'SINGLE' | 'DUAL';
|
||||
location?: string;
|
||||
addressId?: number | null;
|
||||
}
|
||||
) {
|
||||
if (data.addressId == null) {
|
||||
throw new Error('Lieferadresse ist erforderlich');
|
||||
}
|
||||
await assertDeliveryAddressBelongsToCustomer(data.addressId, customerId);
|
||||
return prisma.meter.create({
|
||||
data: {
|
||||
customerId,
|
||||
...data,
|
||||
meterNumber: data.meterNumber,
|
||||
type: data.type,
|
||||
tariffModel: data.tariffModel,
|
||||
location: data.location,
|
||||
addressId: data.addressId,
|
||||
isActive: true,
|
||||
},
|
||||
include: { address: true },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -441,13 +466,21 @@ export async function updateMeter(
|
||||
data: {
|
||||
meterNumber?: string;
|
||||
type?: 'ELECTRICITY' | 'GAS';
|
||||
tariffModel?: 'SINGLE' | 'DUAL';
|
||||
location?: string;
|
||||
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({
|
||||
where: { id },
|
||||
data,
|
||||
include: { address: true },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user