Datenschutz vollmacht fixed, two time counter added
This commit is contained in:
@@ -244,6 +244,79 @@ export async function getCockpit(req: AuthRequest, res: Response): Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== FOLGEZÄHLER ====================
|
||||
|
||||
export async function addSuccessorMeter(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const contractId = parseInt(req.params.id);
|
||||
const { meterId, installedAt, finalReadingPrevious } = req.body;
|
||||
|
||||
const contract = await prisma.contract.findUnique({
|
||||
where: { id: contractId },
|
||||
include: { energyDetails: { include: { contractMeters: { orderBy: { position: 'asc' } } } } },
|
||||
});
|
||||
|
||||
if (!contract?.energyDetails) {
|
||||
res.status(404).json({ success: false, error: 'Energievertrag nicht gefunden' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const ecdId = contract.energyDetails.id;
|
||||
const existingMeters = contract.energyDetails.contractMeters;
|
||||
const nextPosition = existingMeters.length > 0
|
||||
? Math.max(...existingMeters.map(m => m.position)) + 1
|
||||
: 0;
|
||||
|
||||
// Vorherigen Zähler als gewechselt markieren
|
||||
if (existingMeters.length > 0 && finalReadingPrevious !== undefined) {
|
||||
const prevMeter = existingMeters[existingMeters.length - 1];
|
||||
await prisma.contractMeter.update({
|
||||
where: { id: prevMeter.id },
|
||||
data: {
|
||||
removedAt: installedAt ? new Date(installedAt) : new Date(),
|
||||
finalReading: parseFloat(finalReadingPrevious),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const contractMeter = await prisma.contractMeter.create({
|
||||
data: {
|
||||
energyContractDetailsId: ecdId,
|
||||
meterId: parseInt(meterId),
|
||||
position: nextPosition,
|
||||
installedAt: installedAt ? new Date(installedAt) : new Date(),
|
||||
},
|
||||
include: { meter: { include: { readings: true } } },
|
||||
});
|
||||
|
||||
// Aktuellen Zähler am Vertrag aktualisieren
|
||||
await prisma.energyContractDetails.update({
|
||||
where: { id: ecdId },
|
||||
data: { meterId: parseInt(meterId) },
|
||||
});
|
||||
|
||||
res.json({ success: true, data: contractMeter } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen des Folgezählers',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeContractMeter(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const contractMeterId = parseInt(req.params.contractMeterId);
|
||||
await prisma.contractMeter.delete({ where: { id: contractMeterId } });
|
||||
res.json({ success: true, data: null } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Entfernen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SNOOZE (VERTRAG ZURÜCKSTELLEN) ====================
|
||||
|
||||
export async function snoozeContract(req: Request, res: Response): Promise<void> {
|
||||
|
||||
@@ -293,7 +293,14 @@ export async function getMeterReadings(req: Request, res: Response): Promise<voi
|
||||
|
||||
export async function addMeterReading(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const reading = await customerService.addMeterReading(parseInt(req.params.meterId), req.body);
|
||||
const { readingDate, value, valueNt, unit, notes } = req.body;
|
||||
const reading = await customerService.addMeterReading(parseInt(req.params.meterId), {
|
||||
readingDate: new Date(readingDate),
|
||||
value: parseFloat(value),
|
||||
valueNt: valueNt !== undefined && valueNt !== null && valueNt !== '' ? parseFloat(valueNt) : undefined,
|
||||
unit,
|
||||
notes,
|
||||
});
|
||||
res.status(201).json({ success: true, data: reading } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
@@ -305,10 +312,18 @@ export async function addMeterReading(req: Request, res: Response): Promise<void
|
||||
|
||||
export async function updateMeterReading(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { readingDate, value, valueNt, unit, notes } = req.body;
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (readingDate !== undefined) updateData.readingDate = new Date(readingDate);
|
||||
if (value !== undefined) updateData.value = parseFloat(value);
|
||||
if (valueNt !== undefined) updateData.valueNt = valueNt !== null && valueNt !== '' ? parseFloat(valueNt) : null;
|
||||
if (unit !== undefined) updateData.unit = unit;
|
||||
if (notes !== undefined) updateData.notes = notes;
|
||||
|
||||
const reading = await customerService.updateMeterReading(
|
||||
parseInt(req.params.meterId),
|
||||
parseInt(req.params.readingId),
|
||||
req.body
|
||||
updateData as any
|
||||
);
|
||||
res.json({ success: true, data: reading } as ApiResponse);
|
||||
} catch (error) {
|
||||
@@ -358,15 +373,20 @@ export async function reportMeterReading(req: AuthRequest, res: Response): Promi
|
||||
return;
|
||||
}
|
||||
|
||||
const reading = await prisma.meterReading.create({
|
||||
data: {
|
||||
meterId,
|
||||
value: parseFloat(value),
|
||||
readingDate: readingDate ? new Date(readingDate) : new Date(),
|
||||
notes,
|
||||
reportedBy: user.email,
|
||||
status: 'REPORTED',
|
||||
},
|
||||
const parsedDate = readingDate ? new Date(readingDate) : new Date();
|
||||
const parsedValue = parseFloat(value);
|
||||
|
||||
// Validierung über den Service (monoton steigend)
|
||||
const reading = await customerService.addMeterReading(meterId, {
|
||||
readingDate: parsedDate,
|
||||
value: parsedValue,
|
||||
notes,
|
||||
});
|
||||
|
||||
// Status auf REPORTED setzen
|
||||
await prisma.meterReading.update({
|
||||
where: { id: reading.id },
|
||||
data: { reportedBy: user.email, status: 'REPORTED' },
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true, data: reading } as ApiResponse);
|
||||
|
||||
+239
-16
@@ -24,6 +24,9 @@ const RESOURCE_MAPPING: Record<string, { type: string; extractId?: (req: AuthReq
|
||||
'/api/contract-durations': { type: 'ContractDuration', extractId: (req) => req.params.id },
|
||||
'/api/settings': { type: 'AppSetting', extractId: (req) => req.params.key },
|
||||
'/api/email-providers': { type: 'EmailProviderConfig', extractId: (req) => req.params.id },
|
||||
'/api/meters': { type: 'Meter', extractId: (req) => req.params.id || req.params.meterId },
|
||||
'/api/upload': { type: 'Upload' },
|
||||
'/api/email-logs': { type: 'EmailLog' },
|
||||
'/api/auth': { type: 'Authentication' },
|
||||
'/api/audit-logs': { type: 'AuditLog', extractId: (req) => req.params.id },
|
||||
'/api/gdpr': { type: 'GDPR' },
|
||||
@@ -116,6 +119,240 @@ function getClientIp(req: AuthRequest): string {
|
||||
return req.socket.remoteAddress || 'unknown';
|
||||
}
|
||||
|
||||
// Menschenlesbare Bezeichnungen für Resource-Typen
|
||||
const RESOURCE_TYPE_LABELS: Record<string, string> = {
|
||||
Customer: 'Kunde',
|
||||
Contract: 'Vertrag',
|
||||
BankCard: 'Bankverbindung',
|
||||
IdentityDocument: 'Ausweis',
|
||||
Address: 'Adresse',
|
||||
Meter: 'Zähler',
|
||||
MeterReading: 'Zählerstand',
|
||||
User: 'Benutzer',
|
||||
Provider: 'Anbieter',
|
||||
Tariff: 'Tarif',
|
||||
SalesPlatform: 'Vertriebsplattform',
|
||||
ContractCategory: 'Vertragskategorie',
|
||||
CancellationPeriod: 'Kündigungsfrist',
|
||||
ContractDuration: 'Vertragslaufzeit',
|
||||
EmailProviderConfig: 'E-Mail-Provider',
|
||||
AppSetting: 'Einstellung',
|
||||
CustomerConsent: 'Einwilligung',
|
||||
ContractTask: 'Aufgabe',
|
||||
ContractHistoryEntry: 'Vertragshistorie',
|
||||
GDPR: 'Datenschutz',
|
||||
Authentication: 'Anmeldung',
|
||||
AuditLog: 'Audit-Protokoll',
|
||||
StressfreiEmail: 'Stressfrei-E-Mail',
|
||||
CachedEmail: 'E-Mail',
|
||||
};
|
||||
|
||||
const ACTION_LABELS: Record<string, string> = {
|
||||
CREATE: 'erstellt',
|
||||
READ: 'aufgerufen',
|
||||
UPDATE: 'aktualisiert',
|
||||
DELETE: 'gelöscht',
|
||||
EXPORT: 'exportiert',
|
||||
ANONYMIZE: 'anonymisiert',
|
||||
LOGIN: 'angemeldet',
|
||||
LOGOUT: 'abgemeldet',
|
||||
LOGIN_FAILED: 'Anmeldung fehlgeschlagen',
|
||||
};
|
||||
|
||||
/**
|
||||
* Erzeugt ein menschenlesbares Label für den Audit-Log-Eintrag
|
||||
*/
|
||||
function generateHumanLabel(
|
||||
action: AuditAction,
|
||||
resourceType: string,
|
||||
req: AuthRequest,
|
||||
responseBody: unknown
|
||||
): string {
|
||||
const typeName = RESOURCE_TYPE_LABELS[resourceType] || resourceType;
|
||||
const actionName = ACTION_LABELS[action] || action;
|
||||
|
||||
// Identifikator aus Response oder Request extrahieren
|
||||
let identifier = '';
|
||||
if (responseBody && typeof responseBody === 'object' && 'data' in responseBody) {
|
||||
const data = (responseBody as { data: Record<string, unknown> }).data;
|
||||
if (data) {
|
||||
identifier =
|
||||
(data.contractNumber as string) ||
|
||||
(data.customerNumber as string) ||
|
||||
(data.meterNumber as string) ||
|
||||
(data.name as string) ||
|
||||
(data.email as string) ||
|
||||
(data.firstName && data.lastName ? `${data.firstName} ${data.lastName}` : '') ||
|
||||
'';
|
||||
}
|
||||
}
|
||||
|
||||
// Spezial-Labels für bestimmte Endpunkte
|
||||
const path = req.path;
|
||||
|
||||
// Auth
|
||||
if (path.includes('/auth/login') || path.includes('/auth/customer-login')) {
|
||||
const email = req.body?.email || '';
|
||||
return action === 'LOGIN'
|
||||
? `Benutzer ${email} hat sich angemeldet`
|
||||
: `Anmeldung fehlgeschlagen für ${email}`;
|
||||
}
|
||||
if (path.includes('/auth/logout')) return 'Benutzer hat sich abgemeldet';
|
||||
|
||||
// Kunden-Operationen
|
||||
if (resourceType === 'Customer') {
|
||||
if (action === 'CREATE') return `Kunde ${identifier} angelegt`;
|
||||
if (action === 'UPDATE') return `Kundendaten ${identifier} aktualisiert`;
|
||||
if (action === 'DELETE') return `Kunde ${identifier} gelöscht`;
|
||||
if (action === 'READ' && req.params.id) return `Kundendaten ${identifier} aufgerufen`;
|
||||
if (action === 'READ') return 'Kundenliste aufgerufen';
|
||||
}
|
||||
|
||||
// Verträge
|
||||
if (resourceType === 'Contract') {
|
||||
if (path.includes('/cockpit')) return 'Vertrags-Cockpit aufgerufen';
|
||||
if (path.includes('/follow-up')) return `Folgevertrag für ${identifier} erstellt`;
|
||||
if (path.includes('/snooze')) return `Vertrag ${identifier} zurückgestellt`;
|
||||
if (path.includes('/password')) return `Passwort für Vertrag ${identifier} abgerufen`;
|
||||
if (path.includes('/sip-credentials')) return 'SIP-Zugangsdaten abgerufen';
|
||||
if (path.includes('/internet-credentials')) return `Internet-Zugangsdaten für Vertrag ${identifier} abgerufen`;
|
||||
if (path.includes('/successor-meter')) return `Folgezähler zu Vertrag ${identifier} hinzugefügt`;
|
||||
if (action === 'CREATE') return `Vertrag ${identifier} angelegt`;
|
||||
if (action === 'UPDATE') return `Vertrag ${identifier} aktualisiert`;
|
||||
if (action === 'DELETE') return `Vertrag ${identifier} gelöscht`;
|
||||
if (action === 'READ' && req.params.id) return `Vertrag ${identifier} aufgerufen`;
|
||||
if (action === 'READ') return 'Vertragsliste aufgerufen';
|
||||
}
|
||||
|
||||
// Bankverbindungen
|
||||
if (resourceType === 'BankCard') {
|
||||
if (action === 'CREATE') return `Bankverbindung hinzugefügt`;
|
||||
if (action === 'UPDATE') return `Bankverbindung aktualisiert`;
|
||||
if (action === 'DELETE') return `Bankverbindung gelöscht`;
|
||||
}
|
||||
|
||||
// Ausweise
|
||||
if (resourceType === 'IdentityDocument') {
|
||||
if (action === 'CREATE') return `Ausweis ${identifier} hinzugefügt`;
|
||||
if (action === 'UPDATE') return `Ausweis ${identifier} aktualisiert`;
|
||||
if (action === 'DELETE') return `Ausweis gelöscht`;
|
||||
}
|
||||
|
||||
// Adressen
|
||||
if (resourceType === 'Address') {
|
||||
if (action === 'CREATE') return `Adresse hinzugefügt`;
|
||||
if (action === 'UPDATE') return `Adresse aktualisiert`;
|
||||
if (action === 'DELETE') return `Adresse gelöscht`;
|
||||
}
|
||||
|
||||
// Zähler
|
||||
if (resourceType === 'Meter') {
|
||||
if (action === 'CREATE') return `Zähler ${identifier} angelegt`;
|
||||
if (action === 'UPDATE') return `Zähler ${identifier} aktualisiert`;
|
||||
if (action === 'DELETE') return `Zähler gelöscht`;
|
||||
}
|
||||
|
||||
// Einwilligungen
|
||||
if (resourceType === 'CustomerConsent') {
|
||||
const consentType = req.params.consentType || '';
|
||||
const consentLabels: Record<string, string> = {
|
||||
DATA_PROCESSING: 'Datenverarbeitung',
|
||||
MARKETING_EMAIL: 'E-Mail-Marketing',
|
||||
MARKETING_PHONE: 'Telefonmarketing',
|
||||
DATA_SHARING_PARTNER: 'Datenweitergabe',
|
||||
};
|
||||
const consentName = consentLabels[consentType] || consentType;
|
||||
if (action === 'UPDATE') {
|
||||
const status = req.body?.status;
|
||||
return status === 'GRANTED'
|
||||
? `Einwilligung "${consentName}" erteilt`
|
||||
: `Einwilligung "${consentName}" widerrufen`;
|
||||
}
|
||||
if (action === 'READ') return 'Einwilligungen abgerufen';
|
||||
}
|
||||
|
||||
// Benutzer
|
||||
if (resourceType === 'User') {
|
||||
if (action === 'CREATE') return `Benutzer ${identifier} angelegt`;
|
||||
if (action === 'UPDATE') return `Benutzer ${identifier} aktualisiert`;
|
||||
if (action === 'DELETE') return `Benutzer ${identifier} gelöscht`;
|
||||
if (action === 'READ' && req.params.id) return `Benutzerdaten ${identifier} aufgerufen`;
|
||||
if (action === 'READ') return 'Benutzerliste aufgerufen';
|
||||
}
|
||||
|
||||
// Aufgaben
|
||||
if (resourceType === 'ContractTask') {
|
||||
if (path.includes('/complete')) return `Aufgabe als erledigt markiert`;
|
||||
if (action === 'CREATE') return `Aufgabe erstellt`;
|
||||
if (action === 'UPDATE') return `Aufgabe aktualisiert`;
|
||||
if (action === 'DELETE') return `Aufgabe gelöscht`;
|
||||
}
|
||||
|
||||
// E-Mail-Provider
|
||||
if (resourceType === 'EmailProviderConfig') {
|
||||
if (path.includes('/test-connection')) return `E-Mail-Provider Verbindungstest`;
|
||||
if (path.includes('/provision')) return `E-Mail-Adresse provisioniert`;
|
||||
if (action === 'CREATE') return `E-Mail-Provider ${identifier} angelegt`;
|
||||
if (action === 'UPDATE') return `E-Mail-Provider ${identifier} aktualisiert`;
|
||||
if (action === 'DELETE') return `E-Mail-Provider ${identifier} gelöscht`;
|
||||
}
|
||||
|
||||
// GDPR
|
||||
if (resourceType === 'GDPR') {
|
||||
if (path.includes('/dashboard')) return 'DSGVO-Dashboard aufgerufen';
|
||||
if (path.includes('/export')) return 'Kundendaten exportiert (DSGVO Art. 15)';
|
||||
if (path.includes('/privacy-policy')) {
|
||||
return action === 'UPDATE' ? 'Datenschutzerklärung aktualisiert' : 'Datenschutzerklärung aufgerufen';
|
||||
}
|
||||
if (path.includes('/authorization-template')) {
|
||||
return action === 'UPDATE' ? 'Vollmacht-Vorlage aktualisiert' : 'Vollmacht-Vorlage aufgerufen';
|
||||
}
|
||||
if (path.includes('/send-consent-link')) return 'Datenschutz-Link versendet';
|
||||
if (path.includes('/authorizations') && path.includes('/send')) return 'Vollmacht-Anfrage versendet';
|
||||
if (path.includes('/authorizations') && path.includes('/grant')) return 'Vollmacht erteilt';
|
||||
if (path.includes('/authorizations') && path.includes('/withdraw')) return 'Vollmacht widerrufen';
|
||||
if (path.includes('/authorizations') && path.includes('/upload')) return 'Vollmacht-PDF hochgeladen';
|
||||
if (path.includes('/authorizations') && path.includes('/document') && action === 'DELETE') return 'Vollmacht-PDF gelöscht';
|
||||
if (path.includes('/my-privacy')) return 'Eigene Datenschutzseite aufgerufen';
|
||||
if (path.includes('/my-consent-status')) return 'Eigener Einwilligungsstatus geprüft';
|
||||
if (path.includes('/my-authorizations')) return 'Eigene Vollmachten aufgerufen';
|
||||
if (path.includes('/deletions')) {
|
||||
if (action === 'CREATE') return 'Löschanfrage erstellt';
|
||||
if (path.includes('/process')) return 'Löschanfrage bearbeitet';
|
||||
return 'Löschanfragen aufgerufen';
|
||||
}
|
||||
if (path.includes('/consent-status')) return 'Einwilligungsstatus geprüft';
|
||||
if (path.includes('/consents/overview')) return 'Einwilligungsübersicht aufgerufen';
|
||||
}
|
||||
|
||||
// Einstellungen
|
||||
if (resourceType === 'AppSetting') {
|
||||
if (action === 'UPDATE') return `Einstellung "${req.params.key || ''}" geändert`;
|
||||
if (action === 'READ') return 'Einstellungen aufgerufen';
|
||||
}
|
||||
|
||||
// Zähler-Readings
|
||||
if (path.includes('/readings')) {
|
||||
if (path.includes('/report')) return 'Zählerstand vom Kunden gemeldet';
|
||||
if (path.includes('/transfer')) return 'Zählerstand als übertragen markiert';
|
||||
if (action === 'CREATE') return 'Zählerstand erfasst';
|
||||
if (action === 'UPDATE') return 'Zählerstand aktualisiert';
|
||||
if (action === 'DELETE') return 'Zählerstand gelöscht';
|
||||
}
|
||||
|
||||
// Upload-Operationen
|
||||
if (path.includes('/upload') || path.includes('/privacy-policy')) {
|
||||
if (path.includes('/privacy-policy') && action === 'DELETE') return 'Datenschutzerklärung-PDF gelöscht';
|
||||
if (path.includes('/privacy-policy')) return 'Datenschutzerklärung-PDF hochgeladen';
|
||||
}
|
||||
|
||||
// Standard-Fallback
|
||||
if (identifier) {
|
||||
return `${typeName} ${identifier} ${actionName}`;
|
||||
}
|
||||
return `${typeName} ${actionName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit Middleware - loggt alle API-Aufrufe asynchron
|
||||
*/
|
||||
@@ -162,22 +399,8 @@ export function auditMiddleware(req: AuthRequest, res: Response, next: NextFunct
|
||||
// Audit-Kontext abrufen (enthält Before/After-Werte von Prisma Middleware)
|
||||
const auditContext = getAuditContext();
|
||||
|
||||
// Label für bessere Lesbarkeit generieren
|
||||
let resourceLabel: string | undefined;
|
||||
if (responseBody && typeof responseBody === 'object' && 'data' in responseBody) {
|
||||
const data = (responseBody as { data: Record<string, unknown> }).data;
|
||||
if (data) {
|
||||
// Versuche verschiedene Label-Felder
|
||||
resourceLabel =
|
||||
(data.contractNumber as string) ||
|
||||
(data.customerNumber as string) ||
|
||||
(data.name as string) ||
|
||||
(data.email as string) ||
|
||||
(data.firstName && data.lastName
|
||||
? `${data.firstName} ${data.lastName}`
|
||||
: undefined);
|
||||
}
|
||||
}
|
||||
// Menschenlesbares Label generieren
|
||||
const resourceLabel = generateHumanLabel(action, mapping.type, req, responseBody);
|
||||
|
||||
await createAuditLog({
|
||||
userId: req.user?.userId,
|
||||
|
||||
@@ -20,6 +20,10 @@ router.post('/:id/follow-up', authenticate, requirePermission('contracts:create'
|
||||
// Snooze (Vertrag zurückstellen)
|
||||
router.patch('/:id/snooze', authenticate, requirePermission('contracts:update'), contractController.snoozeContract);
|
||||
|
||||
// Folgezähler
|
||||
router.post('/:id/successor-meter', authenticate, requirePermission('contracts:update'), contractController.addSuccessorMeter);
|
||||
router.delete('/:id/contract-meter/:contractMeterId', authenticate, requirePermission('contracts:update'), contractController.removeContractMeter);
|
||||
|
||||
// Get decrypted password
|
||||
router.get('/:id/password', authenticate, requirePermission('contracts:read'), contractController.getContractPassword);
|
||||
|
||||
|
||||
@@ -440,6 +440,16 @@ router.post(
|
||||
data: { privacyPolicyPath: relativePath },
|
||||
});
|
||||
|
||||
// Alle Consents auf GRANTED setzen (PDF = vollständige Einwilligung)
|
||||
const consentTypes = ['DATA_PROCESSING', 'MARKETING_EMAIL', 'MARKETING_PHONE', 'DATA_SHARING_PARTNER'] as const;
|
||||
for (const consentType of consentTypes) {
|
||||
await prisma.customerConsent.upsert({
|
||||
where: { customerId_consentType: { customerId, consentType } },
|
||||
update: { status: 'GRANTED', grantedAt: new Date(), source: 'papier' },
|
||||
create: { customerId, consentType, status: 'GRANTED', grantedAt: new Date(), source: 'papier', createdBy: (req as any).user?.email || 'admin' },
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
@@ -488,6 +498,12 @@ router.delete(
|
||||
data: { privacyPolicyPath: null },
|
||||
});
|
||||
|
||||
// Nur Consents widerrufen die per Papier erteilt wurden
|
||||
await prisma.customerConsent.updateMany({
|
||||
where: { customerId, status: 'GRANTED', source: 'papier' },
|
||||
data: { status: 'WITHDRAWN', withdrawnAt: new Date() },
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
|
||||
@@ -146,13 +146,35 @@ export async function updateAuthorizationDocument(
|
||||
* Vollmacht-Dokument löschen
|
||||
*/
|
||||
export async function deleteAuthorizationDocument(customerId: number, representativeId: number) {
|
||||
// Prüfen ob die Vollmacht per Papier erteilt wurde
|
||||
const auth = await prisma.representativeAuthorization.findUnique({
|
||||
where: { customerId_representativeId: { customerId, representativeId } },
|
||||
select: { source: true, documentPath: true },
|
||||
});
|
||||
|
||||
if (!auth) throw new Error('Vollmacht nicht gefunden');
|
||||
|
||||
// Datei löschen
|
||||
if (auth.documentPath) {
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), auth.documentPath);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Löschen der Vollmacht-PDF:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Wenn per Papier erteilt → Vollmacht widerrufen
|
||||
// Wenn per Portal/Online erteilt → nur PDF entfernen, Vollmacht bleibt
|
||||
const withdrawData = auth.source === 'papier'
|
||||
? { documentPath: null, isGranted: false, withdrawnAt: new Date() }
|
||||
: { documentPath: null };
|
||||
|
||||
return prisma.representativeAuthorization.update({
|
||||
where: {
|
||||
customerId_representativeId: { customerId, representativeId },
|
||||
},
|
||||
data: {
|
||||
documentPath: null,
|
||||
},
|
||||
where: { customerId_representativeId: { customerId, representativeId } },
|
||||
data: withdrawData,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -125,14 +125,14 @@ export async function getContractById(id: number, decryptPassword = false) {
|
||||
previousProvider: true,
|
||||
previousContract: {
|
||||
include: {
|
||||
energyDetails: { include: { meter: { include: { readings: true } }, invoices: true } },
|
||||
energyDetails: { include: { meter: { include: { readings: true } }, contractMeters: { include: { meter: { include: { readings: true } } }, orderBy: { position: 'asc' as const } }, invoices: true } },
|
||||
internetDetails: { include: { phoneNumbers: true } },
|
||||
mobileDetails: { include: { simCards: true } },
|
||||
tvDetails: true,
|
||||
carInsuranceDetails: true,
|
||||
},
|
||||
},
|
||||
energyDetails: { include: { meter: { include: { readings: true } }, invoices: true } },
|
||||
energyDetails: { include: { meter: { include: { readings: true } }, contractMeters: { include: { meter: { include: { readings: true } } }, orderBy: { position: 'asc' as const } }, invoices: true } },
|
||||
internetDetails: { include: { phoneNumbers: true } },
|
||||
mobileDetails: { include: { simCards: true } },
|
||||
tvDetails: true,
|
||||
@@ -403,11 +403,45 @@ export async function updateContract(
|
||||
|
||||
// Update type-specific details
|
||||
if (energyDetails) {
|
||||
const existingEcd = await prisma.energyContractDetails.findUnique({
|
||||
where: { contractId: id },
|
||||
select: { id: true, meterId: true },
|
||||
});
|
||||
|
||||
await prisma.energyContractDetails.upsert({
|
||||
where: { contractId: id },
|
||||
update: energyDetails,
|
||||
create: { contractId: id, ...energyDetails },
|
||||
});
|
||||
|
||||
// ContractMeter synchronisieren wenn sich der Zähler ändert
|
||||
if (energyDetails.meterId !== undefined && existingEcd) {
|
||||
const oldMeterId = existingEcd.meterId;
|
||||
const newMeterId = energyDetails.meterId;
|
||||
|
||||
if (oldMeterId !== newMeterId) {
|
||||
// Alle alten ContractMeter-Einträge entfernen
|
||||
await prisma.contractMeter.deleteMany({
|
||||
where: { energyContractDetailsId: existingEcd.id },
|
||||
});
|
||||
|
||||
// Neuen ContractMeter-Eintrag erstellen (wenn ein Zähler gesetzt)
|
||||
if (newMeterId) {
|
||||
const contract = await prisma.contract.findUnique({
|
||||
where: { id },
|
||||
select: { startDate: true },
|
||||
});
|
||||
await prisma.contractMeter.create({
|
||||
data: {
|
||||
energyContractDetailsId: existingEcd.id,
|
||||
meterId: newMeterId,
|
||||
position: 0,
|
||||
installedAt: contract?.startDate,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (internetDetails) {
|
||||
|
||||
@@ -89,6 +89,11 @@ export interface ReportedMeterReading {
|
||||
customerNumber: string;
|
||||
name: string;
|
||||
};
|
||||
// Zugehöriger Vertrag
|
||||
contract?: {
|
||||
id: number;
|
||||
contractNumber: string;
|
||||
};
|
||||
// Anbieter-Info für Quick-Login
|
||||
providerPortal?: {
|
||||
providerName: string;
|
||||
@@ -810,6 +815,7 @@ async function getReportedMeterReadings(): Promise<ReportedMeterReading[]> {
|
||||
contract: {
|
||||
select: {
|
||||
id: true,
|
||||
contractNumber: true,
|
||||
portalUsername: true,
|
||||
provider: {
|
||||
select: { id: true, name: true, portalUrl: true },
|
||||
@@ -847,6 +853,10 @@ async function getReportedMeterReadings(): Promise<ReportedMeterReading[]> {
|
||||
customerNumber: r.meter.customer.customerNumber,
|
||||
name: `${r.meter.customer.firstName} ${r.meter.customer.lastName}`,
|
||||
},
|
||||
contract: contract ? {
|
||||
id: contract.id,
|
||||
contractNumber: contract.contractNumber,
|
||||
} : undefined,
|
||||
providerPortal: provider?.portalUrl ? {
|
||||
providerName: provider.name,
|
||||
portalUrl: provider.portalUrl,
|
||||
|
||||
@@ -443,6 +443,30 @@ export async function updateMeter(
|
||||
}
|
||||
|
||||
export async function deleteMeter(id: number) {
|
||||
// Prüfen ob der Zähler noch an Verträgen hängt
|
||||
const linkedContracts = await prisma.contractMeter.findMany({
|
||||
where: { meterId: id },
|
||||
include: { energyContractDetails: { include: { contract: { select: { contractNumber: true } } } } },
|
||||
});
|
||||
|
||||
if (linkedContracts.length > 0) {
|
||||
const contractNumbers = linkedContracts
|
||||
.map(cm => cm.energyContractDetails.contract.contractNumber)
|
||||
.join(', ');
|
||||
throw new Error(`Zähler kann nicht gelöscht werden – noch an Vertrag/Verträgen zugeordnet: ${contractNumbers}`);
|
||||
}
|
||||
|
||||
// Auch direkte meterId-Referenz auf EnergyContractDetails prüfen
|
||||
const directLinks = await prisma.energyContractDetails.findMany({
|
||||
where: { meterId: id },
|
||||
include: { contract: { select: { contractNumber: true } } },
|
||||
});
|
||||
|
||||
if (directLinks.length > 0) {
|
||||
const contractNumbers = directLinks.map(d => d.contract.contractNumber).join(', ');
|
||||
throw new Error(`Zähler kann nicht gelöscht werden – noch an Vertrag/Verträgen zugeordnet: ${contractNumbers}`);
|
||||
}
|
||||
|
||||
return prisma.meter.delete({ where: { id } });
|
||||
}
|
||||
|
||||
@@ -451,14 +475,25 @@ export async function addMeterReading(
|
||||
data: {
|
||||
readingDate: Date;
|
||||
value: number;
|
||||
valueNt?: number;
|
||||
unit?: string;
|
||||
notes?: string;
|
||||
}
|
||||
) {
|
||||
// Validierung: Zählerstand muss monoton steigend sein
|
||||
await validateReadingValue(meterId, data.readingDate, data.value, undefined, 'HT');
|
||||
if (data.valueNt !== undefined) {
|
||||
await validateReadingValue(meterId, data.readingDate, data.valueNt, undefined, 'NT');
|
||||
}
|
||||
|
||||
return prisma.meterReading.create({
|
||||
data: {
|
||||
meterId,
|
||||
...data,
|
||||
readingDate: data.readingDate,
|
||||
value: data.value,
|
||||
valueNt: data.valueNt,
|
||||
unit: data.unit,
|
||||
notes: data.notes,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -476,6 +511,7 @@ export async function updateMeterReading(
|
||||
data: {
|
||||
readingDate?: Date;
|
||||
value?: number;
|
||||
valueNt?: number | null;
|
||||
unit?: string;
|
||||
notes?: string;
|
||||
}
|
||||
@@ -489,12 +525,65 @@ export async function updateMeterReading(
|
||||
throw new Error('Zählerstand nicht gefunden');
|
||||
}
|
||||
|
||||
// Validierung bei Wertänderung
|
||||
if (data.value !== undefined || data.readingDate !== undefined) {
|
||||
await validateReadingValue(
|
||||
meterId,
|
||||
data.readingDate || reading.readingDate,
|
||||
data.value ?? reading.value,
|
||||
readingId,
|
||||
'HT'
|
||||
);
|
||||
}
|
||||
if (data.valueNt !== undefined || data.readingDate !== undefined) {
|
||||
const ntVal = data.valueNt ?? reading.valueNt;
|
||||
if (ntVal !== undefined && ntVal !== null) {
|
||||
await validateReadingValue(
|
||||
meterId,
|
||||
data.readingDate || reading.readingDate,
|
||||
ntVal,
|
||||
readingId,
|
||||
'NT'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return prisma.meterReading.update({
|
||||
where: { id: readingId },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert, dass ein Zählerstand monoton steigend ist.
|
||||
* tariffLabel: 'HT' für Hochtarif/Eintarif, 'NT' für Niedertarif
|
||||
*/
|
||||
async function validateReadingValue(meterId: number, readingDate: Date, value: number, excludeReadingId?: number, tariffLabel: 'HT' | 'NT' = 'HT') {
|
||||
const existing = await prisma.meterReading.findMany({
|
||||
where: { meterId, ...(excludeReadingId ? { id: { not: excludeReadingId } } : {}) },
|
||||
orderBy: { readingDate: 'asc' },
|
||||
});
|
||||
|
||||
const fmtDate = (d: Date) => d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
const fmtVal = (v: number) => v.toLocaleString('de-DE');
|
||||
const label = tariffLabel === 'NT' ? 'NT-Zählerstand' : 'Zählerstand';
|
||||
|
||||
// Vergleichswert aus bestehendem Reading extrahieren
|
||||
const getVal = (r: typeof existing[0]) => tariffLabel === 'NT' ? (r.valueNt ?? 0) : r.value;
|
||||
|
||||
// Stand vor dem neuen Datum
|
||||
const before = [...existing].filter(r => r.readingDate <= readingDate).pop();
|
||||
if (before && value < getVal(before)) {
|
||||
throw new Error(`${label} (${fmtVal(value)}) darf nicht kleiner sein als der Stand vom ${fmtDate(before.readingDate)} (${fmtVal(getVal(before))})`);
|
||||
}
|
||||
|
||||
// Stand nach dem neuen Datum
|
||||
const after = existing.find(r => r.readingDate > readingDate);
|
||||
if (after && value > getVal(after)) {
|
||||
throw new Error(`${label} (${fmtVal(value)}) darf nicht größer sein als der spätere Stand vom ${fmtDate(after.readingDate)} (${fmtVal(getVal(after))})`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteMeterReading(meterId: number, readingId: number) {
|
||||
// Verify the reading belongs to the meter
|
||||
const reading = await prisma.meterReading.findFirst({
|
||||
|
||||
Reference in New Issue
Block a user