complete new audit system

This commit is contained in:
2026-03-21 18:23:54 +01:00
parent 4f359df161
commit 219e1930f7
159 changed files with 2841 additions and 736 deletions
+456 -25
View File
@@ -1,11 +1,10 @@
import { Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import prisma from '../lib/prisma.js';
import * as customerService from '../services/customer.service.js';
import * as authService from '../services/auth.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
const prisma = new PrismaClient();
// Customer CRUD
export async function getCustomers(req: Request, res: Response): Promise<void> {
try {
@@ -46,6 +45,12 @@ export async function createCustomer(req: Request, res: Response): Promise<void>
data.birthDate = new Date(data.birthDate);
}
const customer = await customerService.createCustomer(data);
await logChange({
req, action: 'CREATE', resourceType: 'Customer',
resourceId: customer.id.toString(),
label: `Kunde ${customer.customerNumber} angelegt (${customer.firstName} ${customer.lastName})`,
customerId: customer.id,
});
res.status(201).json({ success: true, data: customer } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -57,12 +62,70 @@ export async function createCustomer(req: Request, res: Response): Promise<void>
export async function updateCustomer(req: Request, res: Response): Promise<void> {
try {
const customerId = parseInt(req.params.id);
const data = { ...req.body };
// Convert birthDate string to Date if present
if (data.birthDate) {
// Vorherigen Stand laden für Audit
const before = await prisma.customer.findUnique({ where: { id: customerId } });
// Convert birthDate string to Date if present, empty string to null
if (data.birthDate === '' || data.birthDate === null) {
data.birthDate = null;
} else if (data.birthDate) {
data.birthDate = new Date(data.birthDate);
}
const customer = await customerService.updateCustomer(parseInt(req.params.id), data);
// Leere Strings in optionalen Feldern zu null konvertieren
const nullableFields = ['salutation', 'birthPlace', 'phone', 'mobile', 'email', 'companyName', 'taxNumber', 'businessRegistration', 'commercialRegister', 'commercialRegisterNumber', 'notes'];
for (const field of nullableFields) {
if (data[field] === '') data[field] = null;
}
const customer = await customerService.updateCustomer(customerId, data);
// Audit: Geänderte Felder ermitteln und loggen
if (before) {
const changes: Record<string, { von: unknown; nach: unknown }> = {};
const fieldLabels: Record<string, string> = {
salutation: 'Anrede', firstName: 'Vorname', lastName: 'Nachname', email: 'E-Mail',
phone: 'Telefon', mobile: 'Mobil', birthDate: 'Geburtsdatum', birthPlace: 'Geburtsort',
companyName: 'Firma', type: 'Typ', taxNumber: 'Steuernummer', notes: 'Notizen',
};
for (const [key, value] of Object.entries(data)) {
// Technische/interne Felder überspringen
if (['id', 'createdAt', 'updatedAt', 'customerNumber', 'portalPasswordHash', 'portalPasswordEncrypted'].includes(key)) continue;
const oldVal = (before as any)[key];
const newVal = value;
// Normalisieren: null, undefined, "" werden alle als "leer" behandelt
const normalize = (v: unknown) => {
if (v === null || v === undefined || v === '') return null;
if (v instanceof Date) return v.toISOString().split('T')[0];
return v;
};
const oldNorm = normalize(oldVal);
const newNorm = normalize(newVal);
if (JSON.stringify(oldNorm) !== JSON.stringify(newNorm)) {
const label = fieldLabels[key] || key;
const formatVal = (v: unknown) => {
if (v === null || v === undefined || v === '') return '-';
if (v instanceof Date) return v.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
if (typeof v === 'boolean') return v ? 'Ja' : 'Nein';
return String(v);
};
changes[label] = { von: formatVal(oldVal), nach: formatVal(newVal) };
}
}
if (Object.keys(changes).length > 0) {
const changeList = Object.entries(changes).map(([f, c]) => `${f}: ${c.von}${c.nach}`).join(', ');
await logChange({
req, action: 'UPDATE', resourceType: 'Customer',
resourceId: customerId.toString(),
label: `Kunde ${before.customerNumber} aktualisiert: ${changeList}`,
details: changes,
customerId,
});
}
}
res.json({ success: true, data: customer } as ApiResponse);
} catch (error) {
console.error('Update customer error:', error);
@@ -75,7 +138,15 @@ export async function updateCustomer(req: Request, res: Response): Promise<void>
export async function deleteCustomer(req: Request, res: Response): Promise<void> {
try {
await customerService.deleteCustomer(parseInt(req.params.id));
const customerId = parseInt(req.params.id);
const customer = await prisma.customer.findUnique({ where: { id: customerId }, select: { customerNumber: true, firstName: true, lastName: true } });
await customerService.deleteCustomer(customerId);
await logChange({
req, action: 'DELETE', resourceType: 'Customer',
resourceId: customerId.toString(),
label: `Kunde ${customer?.customerNumber} gelöscht (${customer?.firstName} ${customer?.lastName})`,
customerId,
});
res.json({ success: true, message: 'Kunde gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -97,7 +168,14 @@ export async function getAddresses(req: Request, res: Response): Promise<void> {
export async function createAddress(req: Request, res: Response): Promise<void> {
try {
const address = await customerService.createAddress(parseInt(req.params.customerId), req.body);
const customerId = parseInt(req.params.customerId);
const address = await customerService.createAddress(customerId, req.body);
await logChange({
req, action: 'CREATE', resourceType: 'Address',
resourceId: address.id.toString(),
label: `Adresse hinzugefügt für Kunde #${customerId}`,
customerId,
});
res.status(201).json({ success: true, data: address } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -109,7 +187,53 @@ export async function createAddress(req: Request, res: Response): Promise<void>
export async function updateAddress(req: Request, res: Response): Promise<void> {
try {
const address = await customerService.updateAddress(parseInt(req.params.id), req.body);
const addressId = parseInt(req.params.id);
const data = req.body;
// Vorherigen Stand laden für Audit
const before = await prisma.address.findUnique({ where: { id: addressId } });
const address = await customerService.updateAddress(addressId, data);
const customerId = address.customerId;
// Audit: Geänderte Felder ermitteln und loggen
if (before) {
const changes: Record<string, { von: unknown; nach: unknown }> = {};
const fieldLabels: Record<string, string> = {
street: 'Straße', houseNumber: 'Hausnummer', postalCode: 'PLZ',
city: 'Stadt', country: 'Land', type: 'Typ', isDefault: 'Standard',
};
for (const [key, newVal] of Object.entries(data)) {
if (['id', 'createdAt', 'updatedAt'].includes(key)) continue;
const oldVal = (before as any)[key];
const norm = (v: unknown) => (v === null || v === undefined || v === '' ? null : v);
if (JSON.stringify(norm(oldVal)) !== JSON.stringify(norm(newVal))) {
const label = fieldLabels[key] || key;
const formatVal = (v: unknown) => {
if (v === null || v === undefined || v === '') return '-';
if (typeof v === 'boolean') return v ? 'Ja' : 'Nein';
return String(v);
};
changes[label] = { von: formatVal(oldVal), nach: formatVal(newVal) };
}
}
const changeList = Object.entries(changes).map(([f, c]) => `${f}: ${c.von}${c.nach}`).join(', ');
await logChange({
req, action: 'UPDATE', resourceType: 'Address',
resourceId: address.id.toString(),
label: changeList ? `Adresse aktualisiert für Kunde #${customerId}: ${changeList}` : `Adresse aktualisiert für Kunde #${customerId}`,
details: Object.keys(changes).length > 0 ? changes : undefined,
customerId,
});
} else {
await logChange({
req, action: 'UPDATE', resourceType: 'Address',
resourceId: address.id.toString(),
label: `Adresse aktualisiert für Kunde #${customerId}`,
customerId,
});
}
res.json({ success: true, data: address } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -121,7 +245,16 @@ export async function updateAddress(req: Request, res: Response): Promise<void>
export async function deleteAddress(req: Request, res: Response): Promise<void> {
try {
await customerService.deleteAddress(parseInt(req.params.id));
const addressId = parseInt(req.params.id);
const addr = await prisma.address.findUnique({ where: { id: addressId }, select: { customerId: true } });
const customerId = addr?.customerId;
await customerService.deleteAddress(addressId);
await logChange({
req, action: 'DELETE', resourceType: 'Address',
resourceId: addressId.toString(),
label: `Adresse gelöscht für Kunde #${customerId}`,
customerId: customerId ?? undefined,
});
res.json({ success: true, message: 'Adresse gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -147,7 +280,14 @@ export async function getBankCards(req: Request, res: Response): Promise<void> {
export async function createBankCard(req: Request, res: Response): Promise<void> {
try {
const card = await customerService.createBankCard(parseInt(req.params.customerId), req.body);
const customerId = parseInt(req.params.customerId);
const card = await customerService.createBankCard(customerId, req.body);
await logChange({
req, action: 'CREATE', resourceType: 'BankCard',
resourceId: card.id.toString(),
label: `Bankverbindung hinzugefügt für Kunde #${customerId}`,
customerId,
});
res.status(201).json({ success: true, data: card } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -159,7 +299,53 @@ export async function createBankCard(req: Request, res: Response): Promise<void>
export async function updateBankCard(req: Request, res: Response): Promise<void> {
try {
const card = await customerService.updateBankCard(parseInt(req.params.id), req.body);
const cardId = parseInt(req.params.id);
const data = req.body;
// Vorherigen Stand laden für Audit
const before = await prisma.bankCard.findUnique({ where: { id: cardId } });
const card = await customerService.updateBankCard(cardId, data);
const customerId = card.customerId;
// Audit: Geänderte Felder ermitteln und loggen
if (before) {
const changes: Record<string, { von: unknown; nach: unknown }> = {};
const fieldLabels: Record<string, string> = {
iban: 'IBAN', bic: 'BIC', bankName: 'Bank',
accountHolder: 'Kontoinhaber', isActive: 'Aktiv',
};
for (const [key, newVal] of Object.entries(data)) {
if (['id', 'createdAt', 'updatedAt'].includes(key)) continue;
const oldVal = (before as any)[key];
const norm = (v: unknown) => (v === null || v === undefined || v === '' ? null : v);
if (JSON.stringify(norm(oldVal)) !== JSON.stringify(norm(newVal))) {
const label = fieldLabels[key] || key;
const formatVal = (v: unknown) => {
if (v === null || v === undefined || v === '') return '-';
if (typeof v === 'boolean') return v ? 'Ja' : 'Nein';
return String(v);
};
changes[label] = { von: formatVal(oldVal), nach: formatVal(newVal) };
}
}
const changeList = Object.entries(changes).map(([f, c]) => `${f}: ${c.von}${c.nach}`).join(', ');
await logChange({
req, action: 'UPDATE', resourceType: 'BankCard',
resourceId: card.id.toString(),
label: changeList ? `Bankverbindung aktualisiert für Kunde #${customerId}: ${changeList}` : `Bankverbindung aktualisiert für Kunde #${customerId}`,
details: Object.keys(changes).length > 0 ? changes : undefined,
customerId,
});
} else {
await logChange({
req, action: 'UPDATE', resourceType: 'BankCard',
resourceId: card.id.toString(),
label: `Bankverbindung aktualisiert für Kunde #${customerId}`,
customerId,
});
}
res.json({ success: true, data: card } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -171,7 +357,16 @@ export async function updateBankCard(req: Request, res: Response): Promise<void>
export async function deleteBankCard(req: Request, res: Response): Promise<void> {
try {
await customerService.deleteBankCard(parseInt(req.params.id));
const cardId = parseInt(req.params.id);
const card = await prisma.bankCard.findUnique({ where: { id: cardId }, select: { customerId: true } });
const customerId = card?.customerId;
await customerService.deleteBankCard(cardId);
await logChange({
req, action: 'DELETE', resourceType: 'BankCard',
resourceId: cardId.toString(),
label: `Bankverbindung gelöscht für Kunde #${customerId}`,
customerId: customerId ?? undefined,
});
res.json({ success: true, message: 'Bankkarte gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -197,7 +392,14 @@ export async function getDocuments(req: Request, res: Response): Promise<void> {
export async function createDocument(req: Request, res: Response): Promise<void> {
try {
const doc = await customerService.createDocument(parseInt(req.params.customerId), req.body);
const customerId = parseInt(req.params.customerId);
const doc = await customerService.createDocument(customerId, req.body);
await logChange({
req, action: 'CREATE', resourceType: 'IdentityDocument',
resourceId: doc.id.toString(),
label: `Ausweis hinzugefügt für Kunde #${customerId}`,
customerId,
});
res.status(201).json({ success: true, data: doc } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -209,7 +411,59 @@ export async function createDocument(req: Request, res: Response): Promise<void>
export async function updateDocument(req: Request, res: Response): Promise<void> {
try {
const doc = await customerService.updateDocument(parseInt(req.params.id), req.body);
const docId = parseInt(req.params.id);
const data = req.body;
// Vorherigen Stand laden für Audit
const before = await prisma.identityDocument.findUnique({ where: { id: docId } });
const doc = await customerService.updateDocument(docId, data);
const customerId = doc.customerId;
// Audit: Geänderte Felder ermitteln und loggen
if (before) {
const changes: Record<string, { von: unknown; nach: unknown }> = {};
const fieldLabels: Record<string, string> = {
type: 'Dokumenttyp', documentNumber: 'Dokumentnummer',
issuingAuthority: 'Ausstellungsbehörde', issueDate: 'Ausstellungsdatum',
expiryDate: 'Ablaufdatum', isActive: 'Aktiv', licenseClasses: 'Führerscheinklassen',
};
for (const [key, newVal] of Object.entries(data)) {
if (['id', 'createdAt', 'updatedAt'].includes(key)) continue;
const oldVal = (before as any)[key];
const norm = (v: unknown) => {
if (v === null || v === undefined || v === '') return null;
if (v instanceof Date) return v.toISOString().split('T')[0];
return v;
};
if (JSON.stringify(norm(oldVal)) !== JSON.stringify(norm(newVal))) {
const label = fieldLabels[key] || key;
const formatVal = (v: unknown) => {
if (v === null || v === undefined || v === '') return '-';
if (v instanceof Date) return v.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
if (typeof v === 'boolean') return v ? 'Ja' : 'Nein';
return String(v);
};
changes[label] = { von: formatVal(oldVal), nach: formatVal(newVal) };
}
}
const changeList = Object.entries(changes).map(([f, c]) => `${f}: ${c.von}${c.nach}`).join(', ');
await logChange({
req, action: 'UPDATE', resourceType: 'IdentityDocument',
resourceId: doc.id.toString(),
label: changeList ? `Ausweis aktualisiert für Kunde #${customerId}: ${changeList}` : `Ausweis aktualisiert für Kunde #${customerId}`,
details: Object.keys(changes).length > 0 ? changes : undefined,
customerId,
});
} else {
await logChange({
req, action: 'UPDATE', resourceType: 'IdentityDocument',
resourceId: doc.id.toString(),
label: `Ausweis aktualisiert für Kunde #${customerId}`,
customerId,
});
}
res.json({ success: true, data: doc } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -221,7 +475,16 @@ export async function updateDocument(req: Request, res: Response): Promise<void>
export async function deleteDocument(req: Request, res: Response): Promise<void> {
try {
await customerService.deleteDocument(parseInt(req.params.id));
const docId = parseInt(req.params.id);
const doc = await prisma.identityDocument.findUnique({ where: { id: docId }, select: { customerId: true } });
const customerId = doc?.customerId;
await customerService.deleteDocument(docId);
await logChange({
req, action: 'DELETE', resourceType: 'IdentityDocument',
resourceId: docId.toString(),
label: `Ausweis gelöscht für Kunde #${customerId}`,
customerId: customerId ?? undefined,
});
res.json({ success: true, message: 'Ausweis gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -247,7 +510,14 @@ export async function getMeters(req: Request, res: Response): Promise<void> {
export async function createMeter(req: Request, res: Response): Promise<void> {
try {
const meter = await customerService.createMeter(parseInt(req.params.customerId), req.body);
const customerId = parseInt(req.params.customerId);
const meter = await customerService.createMeter(customerId, req.body);
await logChange({
req, action: 'CREATE', resourceType: 'Meter',
resourceId: meter.id.toString(),
label: `Zähler angelegt für Kunde #${customerId}`,
customerId,
});
res.status(201).json({ success: true, data: meter } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -259,7 +529,52 @@ export async function createMeter(req: Request, res: Response): Promise<void> {
export async function updateMeter(req: Request, res: Response): Promise<void> {
try {
const meter = await customerService.updateMeter(parseInt(req.params.id), req.body);
const meterId = parseInt(req.params.id);
const data = req.body;
// Vorherigen Stand laden für Audit
const before = await prisma.meter.findUnique({ where: { id: meterId } });
const meter = await customerService.updateMeter(meterId, data);
const customerId = meter.customerId;
// Audit: Geänderte Felder ermitteln und loggen
if (before) {
const changes: Record<string, { von: unknown; nach: unknown }> = {};
const fieldLabels: Record<string, string> = {
meterNumber: 'Zählernummer', type: 'Typ', tariffModel: 'Tarifmodell',
location: 'Standort', isActive: 'Aktiv',
};
for (const [key, newVal] of Object.entries(data)) {
if (['id', 'createdAt', 'updatedAt'].includes(key)) continue;
const oldVal = (before as any)[key];
const norm = (v: unknown) => (v === null || v === undefined || v === '' ? null : v);
if (JSON.stringify(norm(oldVal)) !== JSON.stringify(norm(newVal))) {
const label = fieldLabels[key] || key;
const formatVal = (v: unknown) => {
if (v === null || v === undefined || v === '') return '-';
if (typeof v === 'boolean') return v ? 'Ja' : 'Nein';
return String(v);
};
changes[label] = { von: formatVal(oldVal), nach: formatVal(newVal) };
}
}
const changeList = Object.entries(changes).map(([f, c]) => `${f}: ${c.von}${c.nach}`).join(', ');
await logChange({
req, action: 'UPDATE', resourceType: 'Meter',
resourceId: meter.id.toString(),
label: changeList ? `Zähler aktualisiert: ${changeList}` : `Zähler aktualisiert`,
details: Object.keys(changes).length > 0 ? changes : undefined,
customerId,
});
} else {
await logChange({
req, action: 'UPDATE', resourceType: 'Meter',
resourceId: meter.id.toString(),
label: `Zähler aktualisiert`,
});
}
res.json({ success: true, data: meter } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -271,7 +586,13 @@ export async function updateMeter(req: Request, res: Response): Promise<void> {
export async function deleteMeter(req: Request, res: Response): Promise<void> {
try {
await customerService.deleteMeter(parseInt(req.params.id));
const meterId = parseInt(req.params.id);
await customerService.deleteMeter(meterId);
await logChange({
req, action: 'DELETE', resourceType: 'Meter',
resourceId: meterId.toString(),
label: `Zähler gelöscht`,
});
res.json({ success: true, message: 'Zähler gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -294,13 +615,30 @@ export async function getMeterReadings(req: Request, res: Response): Promise<voi
export async function addMeterReading(req: Request, res: Response): Promise<void> {
try {
const { readingDate, value, valueNt, unit, notes } = req.body;
const reading = await customerService.addMeterReading(parseInt(req.params.meterId), {
const meterId = parseInt(req.params.meterId);
const reading = await customerService.addMeterReading(meterId, {
readingDate: new Date(readingDate),
value: parseFloat(value),
valueNt: valueNt !== undefined && valueNt !== null && valueNt !== '' ? parseFloat(valueNt) : undefined,
unit,
notes,
});
// Audit: Zählerstand mit Kontext loggen
const meter = await prisma.meter.findUnique({
where: { id: meterId },
select: { meterNumber: true, customer: { select: { id: true, firstName: true, lastName: true } } },
});
if (meter) {
const ntInfo = valueNt ? ` / NT: ${parseFloat(valueNt)}` : '';
await logChange({
req, action: 'CREATE', resourceType: 'MeterReading',
label: `Zählerstand ${parseFloat(value)}${ntInfo} ${unit || 'kWh'} für Zähler ${meter.meterNumber} erfasst (${meter.customer.firstName} ${meter.customer.lastName})`,
details: { zähler: meter.meterNumber, stand: parseFloat(value), datum: readingDate },
customerId: meter.customer.id,
});
}
res.status(201).json({ success: true, data: reading } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -325,6 +663,11 @@ export async function updateMeterReading(req: Request, res: Response): Promise<v
parseInt(req.params.readingId),
updateData as any
);
await logChange({
req, action: 'UPDATE', resourceType: 'MeterReading',
resourceId: reading.id.toString(),
label: `Zählerstand aktualisiert`,
});
res.json({ success: true, data: reading } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -336,10 +679,16 @@ export async function updateMeterReading(req: Request, res: Response): Promise<v
export async function deleteMeterReading(req: Request, res: Response): Promise<void> {
try {
const readingId = parseInt(req.params.readingId);
await customerService.deleteMeterReading(
parseInt(req.params.meterId),
parseInt(req.params.readingId)
readingId
);
await logChange({
req, action: 'DELETE', resourceType: 'MeterReading',
resourceId: readingId.toString(),
label: `Zählerstand gelöscht`,
});
res.json({ success: true, data: null } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -389,6 +738,15 @@ export async function reportMeterReading(req: AuthRequest, res: Response): Promi
data: { reportedBy: user.email, status: 'REPORTED' },
});
// Audit
const meterInfo = await prisma.meter.findUnique({ where: { id: meterId }, select: { meterNumber: true } });
await logChange({
req, action: 'CREATE', resourceType: 'MeterReading',
label: `Zählerstand ${parsedValue} gemeldet (Zähler ${meterInfo?.meterNumber || meterId})`,
details: { zähler: meterInfo?.meterNumber, stand: parsedValue, datum: parsedDate.toISOString() },
customerId: user.customerId,
});
res.status(201).json({ success: true, data: reading } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -437,6 +795,12 @@ export async function markReadingTransferred(req: AuthRequest, res: Response): P
},
});
await logChange({
req, action: 'UPDATE', resourceType: 'MeterReading',
resourceId: readingId.toString(),
label: `Zählerstand als übertragen markiert`,
});
res.json({ success: true, data: reading } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -476,11 +840,58 @@ export async function getPortalSettings(req: Request, res: Response): Promise<vo
export async function updatePortalSettings(req: Request, res: Response): Promise<void> {
try {
const customerId = parseInt(req.params.customerId);
const { portalEnabled, portalEmail } = req.body;
const settings = await customerService.updatePortalSettings(parseInt(req.params.customerId), {
// Vorherigen Stand laden für Audit
const before = await prisma.customer.findUnique({
where: { id: customerId },
select: { portalEnabled: true, portalEmail: true },
});
const settings = await customerService.updatePortalSettings(customerId, {
portalEnabled,
portalEmail,
});
// Audit: Geänderte Felder ermitteln und loggen
const data: Record<string, unknown> = { portalEnabled, portalEmail };
if (before) {
const changes: Record<string, { von: unknown; nach: unknown }> = {};
const fieldLabels: Record<string, string> = {
portalEnabled: 'Portal aktiv', portalEmail: 'Portal-E-Mail',
};
for (const [key, newVal] of Object.entries(data)) {
if (newVal === undefined) continue;
const oldVal = (before as any)[key];
const norm = (v: unknown) => (v === null || v === undefined || v === '' ? null : v);
if (JSON.stringify(norm(oldVal)) !== JSON.stringify(norm(newVal))) {
const label = fieldLabels[key] || key;
const formatVal = (v: unknown) => {
if (v === null || v === undefined || v === '') return '-';
if (typeof v === 'boolean') return v ? 'Ja' : 'Nein';
return String(v);
};
changes[label] = { von: formatVal(oldVal), nach: formatVal(newVal) };
}
}
const changeList = Object.entries(changes).map(([f, c]) => `${f}: ${c.von}${c.nach}`).join(', ');
await logChange({
req, action: 'UPDATE', resourceType: 'PortalSettings',
resourceId: customerId.toString(),
label: changeList ? `Portal-Einstellungen aktualisiert für Kunde #${customerId}: ${changeList}` : `Portal-Einstellungen aktualisiert für Kunde #${customerId}`,
details: Object.keys(changes).length > 0 ? changes : undefined,
customerId,
});
} else {
await logChange({
req, action: 'UPDATE', resourceType: 'PortalSettings',
resourceId: customerId.toString(),
label: `Portal-Einstellungen aktualisiert für Kunde #${customerId}`,
customerId,
});
}
res.json({ success: true, data: settings } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -500,7 +911,14 @@ export async function setPortalPassword(req: Request, res: Response): Promise<vo
} as ApiResponse);
return;
}
await authService.setCustomerPortalPassword(parseInt(req.params.customerId), password);
const customerId = parseInt(req.params.customerId);
await authService.setCustomerPortalPassword(customerId, password);
await logChange({
req, action: 'UPDATE', resourceType: 'PortalSettings',
resourceId: customerId.toString(),
label: `Portal-Passwort gesetzt für Kunde #${customerId}`,
customerId,
});
res.json({ success: true, message: 'Passwort gesetzt' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -539,12 +957,19 @@ export async function getRepresentatives(req: Request, res: Response): Promise<v
export async function addRepresentative(req: Request, res: Response): Promise<void> {
try {
const customerId = parseInt(req.params.customerId);
const { representativeId, notes } = req.body;
const representative = await customerService.addRepresentative(
parseInt(req.params.customerId),
customerId,
parseInt(representativeId),
notes
);
await logChange({
req, action: 'CREATE', resourceType: 'Representative',
resourceId: representative.id.toString(),
label: `Vertreter hinzugefügt für Kunde #${customerId}`,
customerId,
});
res.status(201).json({ success: true, data: representative } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -556,10 +981,16 @@ export async function addRepresentative(req: Request, res: Response): Promise<vo
export async function removeRepresentative(req: Request, res: Response): Promise<void> {
try {
const customerId = parseInt(req.params.customerId);
await customerService.removeRepresentative(
parseInt(req.params.customerId),
customerId,
parseInt(req.params.representativeId)
);
await logChange({
req, action: 'DELETE', resourceType: 'Representative',
label: `Vertreter entfernt für Kunde #${customerId}`,
customerId,
});
res.json({ success: true, message: 'Vertreter entfernt' } as ApiResponse);
} catch (error) {
res.status(400).json({