653 lines
24 KiB
TypeScript
653 lines
24 KiB
TypeScript
import { Request, Response } from 'express';
|
|
import prisma from '../lib/prisma.js';
|
|
import * as contractService from '../services/contract.service.js';
|
|
import * as contractCockpitService from '../services/contractCockpit.service.js';
|
|
import * as contractHistoryService from '../services/contractHistory.service.js';
|
|
import * as authorizationService from '../services/authorization.service.js';
|
|
import { ApiResponse, AuthRequest } from '../types/index.js';
|
|
import { logChange } from '../services/audit.service.js';
|
|
import { canAccessContract } from '../utils/accessControl.js';
|
|
import { maybeActivateOnDeliveryConfirmation } from '../services/contractStatusScheduler.service.js';
|
|
|
|
export async function getContracts(req: AuthRequest, res: Response): Promise<void> {
|
|
try {
|
|
const { customerId, type, status, search, page, limit, tree } = req.query;
|
|
|
|
// Baumstruktur für Kundenansicht
|
|
if (tree === 'true' && customerId) {
|
|
const treeData = await contractService.getContractTreeForCustomer(
|
|
parseInt(customerId as string)
|
|
);
|
|
res.json({ success: true, data: treeData } as ApiResponse);
|
|
return;
|
|
}
|
|
|
|
// Für Kundenportal-Benutzer: nur eigene + vertretene Kunden MIT Vollmacht
|
|
let customerIds: number[] | undefined;
|
|
if (req.user?.isCustomerPortal && req.user.customerId) {
|
|
// Eigene Customer-ID immer
|
|
customerIds = [req.user.customerId];
|
|
// Vertretene Kunden nur wenn Vollmacht erteilt
|
|
const representedIds: number[] = req.user.representedCustomerIds || [];
|
|
for (const repCustId of representedIds) {
|
|
const hasAuth = await authorizationService.hasAuthorization(repCustId, req.user.customerId);
|
|
if (hasAuth) {
|
|
customerIds.push(repCustId);
|
|
}
|
|
}
|
|
}
|
|
|
|
const result = await contractService.getAllContracts({
|
|
customerId: customerId ? parseInt(customerId as string) : undefined,
|
|
customerIds, // Wird nur für Kundenportal-Benutzer gesetzt
|
|
type: type as any,
|
|
status: status as any,
|
|
search: search as string,
|
|
page: page ? parseInt(page as string) : undefined,
|
|
limit: limit ? parseInt(limit as string) : undefined,
|
|
});
|
|
res.json({
|
|
success: true,
|
|
data: result.contracts,
|
|
pagination: result.pagination,
|
|
} as ApiResponse);
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Fehler beim Laden der Verträge',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function getContract(req: AuthRequest, res: Response): Promise<void> {
|
|
try {
|
|
const contract = await contractService.getContractById(parseInt(req.params.id));
|
|
if (!contract) {
|
|
res.status(404).json({
|
|
success: false,
|
|
error: 'Vertrag nicht gefunden',
|
|
} as ApiResponse);
|
|
return;
|
|
}
|
|
|
|
// Für Kundenportal-Benutzer: Zugriff nur auf eigene + vertretene Kunden MIT Vollmacht
|
|
if (req.user?.isCustomerPortal && req.user.customerId) {
|
|
const allowedCustomerIds = [req.user.customerId];
|
|
const representedIds: number[] = req.user.representedCustomerIds || [];
|
|
for (const repCustId of representedIds) {
|
|
const hasAuth = await authorizationService.hasAuthorization(repCustId, req.user.customerId);
|
|
if (hasAuth) {
|
|
allowedCustomerIds.push(repCustId);
|
|
}
|
|
}
|
|
if (!allowedCustomerIds.includes(contract.customerId)) {
|
|
res.status(403).json({
|
|
success: false,
|
|
error: 'Kein Zugriff auf diesen Vertrag',
|
|
} as ApiResponse);
|
|
return;
|
|
}
|
|
}
|
|
|
|
res.json({ success: true, data: contract } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Fehler beim Laden des Vertrags',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function createContract(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const contract = await contractService.createContract(req.body);
|
|
await logChange({
|
|
req, action: 'CREATE', resourceType: 'Contract',
|
|
resourceId: contract.id.toString(),
|
|
label: `Vertrag ${contract.contractNumber} angelegt`,
|
|
customerId: contract.customerId,
|
|
});
|
|
res.status(201).json({ success: true, data: contract } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Vertrags',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function updateContract(req: AuthRequest, res: Response): Promise<void> {
|
|
try {
|
|
const contractId = parseInt(req.params.id);
|
|
// Vorherigen Stand laden für Audit-Vergleich
|
|
const before = await prisma.contract.findUnique({
|
|
where: { id: contractId },
|
|
include: { energyDetails: true, internetDetails: true, mobileDetails: true, tvDetails: true, carInsuranceDetails: true },
|
|
});
|
|
|
|
const contract = await contractService.updateContract(contractId, req.body);
|
|
|
|
// Geänderte Felder ermitteln
|
|
const changes: Record<string, { von: unknown; nach: unknown }> = {};
|
|
const fieldLabels: Record<string, string> = {
|
|
status: 'Status', startDate: 'Vertragsbeginn', endDate: 'Vertragsende',
|
|
portalUsername: 'Portal-Benutzername', customerNumberAtProvider: 'Kundennummer beim Anbieter',
|
|
providerId: 'Anbieter', tariffId: 'Tarif', cancellationPeriodId: 'Kündigungsfrist',
|
|
contractDurationId: 'Vertragslaufzeit', platformId: 'Vertriebsplattform',
|
|
cancellationDate: 'Kündigungsdatum', cancellationSentDate: 'Kündigung gesendet am',
|
|
identityDocumentId: 'Ausweis', bankCardId: 'Bankverbindung', addressId: 'Adresse',
|
|
commission: 'Provision', notes: 'Notizen',
|
|
};
|
|
const energyLabels: Record<string, string> = {
|
|
meterId: 'Zähler', maloId: 'MaLo-ID', annualConsumption: 'Jahresverbrauch',
|
|
basePrice: 'Grundpreis', unitPrice: 'Arbeitspreis', unitPriceNt: 'NT-Arbeitspreis', bonus: 'Bonus',
|
|
};
|
|
|
|
// Hauptfelder vergleichen
|
|
const body = req.body;
|
|
if (before) {
|
|
for (const [key, newVal] of Object.entries(body)) {
|
|
if (['energyDetails', 'internetDetails', 'mobileDetails', 'tvDetails', 'carInsuranceDetails', 'password'].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;
|
|
changes[label] = { von: oldVal ?? '-', nach: newVal ?? '-' };
|
|
}
|
|
}
|
|
// Energie-Details vergleichen
|
|
if (body.energyDetails && before.energyDetails) {
|
|
for (const [key, newVal] of Object.entries(body.energyDetails)) {
|
|
const oldVal = (before.energyDetails 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 = energyLabels[key] || key;
|
|
changes[label] = { von: oldVal ?? '-', nach: newVal ?? '-' };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const changeList = Object.entries(changes).map(([f, c]) => `${f}: ${c.von} → ${c.nach}`).join(', ');
|
|
await logChange({
|
|
req, action: 'UPDATE', resourceType: 'Contract',
|
|
resourceId: contractId.toString(),
|
|
label: changeList
|
|
? `Vertrag ${before?.contractNumber || contractId} aktualisiert: ${changeList}`
|
|
: `Vertrag ${before?.contractNumber || contractId} aktualisiert`,
|
|
details: Object.keys(changes).length > 0 ? changes : undefined,
|
|
customerId: before?.customerId,
|
|
});
|
|
|
|
res.json({ success: true, data: contract } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Vertrags',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function deleteContract(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const contractId = parseInt(req.params.id);
|
|
const contract = await prisma.contract.findUnique({ where: { id: contractId }, select: { contractNumber: true, customerId: true } });
|
|
await contractService.deleteContract(contractId);
|
|
await logChange({
|
|
req, action: 'DELETE', resourceType: 'Contract',
|
|
resourceId: contractId.toString(),
|
|
label: `Vertrag ${contract?.contractNumber} gelöscht`,
|
|
customerId: contract?.customerId,
|
|
});
|
|
res.json({ success: true, message: 'Vertrag gelöscht' } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Vertrags',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function createFollowUp(req: AuthRequest, res: Response): Promise<void> {
|
|
try {
|
|
const previousContractId = parseInt(req.params.id);
|
|
|
|
// Vorgängervertrag laden für Vertragsnummer
|
|
const previousContract = await prisma.contract.findUnique({
|
|
where: { id: previousContractId },
|
|
select: { contractNumber: true },
|
|
});
|
|
|
|
if (!previousContract) {
|
|
res.status(404).json({ success: false, error: 'Vorgängervertrag nicht gefunden' } as ApiResponse);
|
|
return;
|
|
}
|
|
|
|
const contract = await contractService.createFollowUpContract(previousContractId);
|
|
const createdBy = req.user?.email || 'unbekannt';
|
|
|
|
// Historie-Eintrag für den Vorgängervertrag erstellen
|
|
await contractHistoryService.createFollowUpHistoryEntry(
|
|
previousContractId,
|
|
contract.contractNumber,
|
|
createdBy
|
|
);
|
|
|
|
// Historie-Eintrag für den neuen Folgevertrag erstellen
|
|
await contractHistoryService.createNewContractFromPredecessorEntry(
|
|
contract.id,
|
|
previousContract.contractNumber,
|
|
createdBy
|
|
);
|
|
|
|
await logChange({
|
|
req, action: 'CREATE', resourceType: 'Contract',
|
|
resourceId: contract.id.toString(),
|
|
label: `Folgevertrag erstellt für ${previousContract.contractNumber}`,
|
|
customerId: contract.customerId,
|
|
});
|
|
|
|
res.status(201).json({ success: true, data: contract } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Folgevertrags',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* VVL = Vertragsverlängerung beim selben Anbieter.
|
|
* Erstellt einen neuen Vertrag mit allen Daten des Vorgängers (außer
|
|
* Auftragsdokument), Startdatum = altes Start + Vertragslaufzeit.
|
|
*/
|
|
export async function createRenewal(req: AuthRequest, res: Response): Promise<void> {
|
|
try {
|
|
const previousContractId = parseInt(req.params.id);
|
|
|
|
const previousContract = await prisma.contract.findUnique({
|
|
where: { id: previousContractId },
|
|
select: { contractNumber: true },
|
|
});
|
|
if (!previousContract) {
|
|
res.status(404).json({ success: false, error: 'Vorgängervertrag nicht gefunden' } as ApiResponse);
|
|
return;
|
|
}
|
|
|
|
const contract = await contractService.createRenewalContract(previousContractId);
|
|
if (!contract) {
|
|
res.status(500).json({ success: false, error: 'VVL konnte nicht erstellt werden' } as ApiResponse);
|
|
return;
|
|
}
|
|
const createdBy = req.user?.email || 'unbekannt';
|
|
|
|
await contractHistoryService.createRenewalHistoryEntry(
|
|
previousContractId,
|
|
contract.contractNumber,
|
|
createdBy,
|
|
);
|
|
await contractHistoryService.createNewRenewalFromPredecessorEntry(
|
|
contract.id,
|
|
previousContract.contractNumber,
|
|
createdBy,
|
|
);
|
|
|
|
await logChange({
|
|
req, action: 'CREATE', resourceType: 'Contract',
|
|
resourceId: contract.id.toString(),
|
|
label: `VVL erstellt für ${previousContract.contractNumber}`,
|
|
customerId: contract.customerId,
|
|
});
|
|
|
|
res.status(201).json({ success: true, data: contract } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der VVL',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function getContractPassword(req: AuthRequest, res: Response): Promise<void> {
|
|
try {
|
|
const contractId = parseInt(req.params.id);
|
|
if (!(await canAccessContract(req, res, contractId))) return;
|
|
|
|
const password = await contractService.getContractPassword(contractId);
|
|
if (password === null) {
|
|
res.status(404).json({
|
|
success: false,
|
|
error: 'Kein Passwort hinterlegt',
|
|
} as ApiResponse);
|
|
return;
|
|
}
|
|
res.json({ success: true, data: { password } } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Fehler beim Entschlüsseln des Passworts',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function getSimCardCredentials(req: AuthRequest, res: Response): Promise<void> {
|
|
try {
|
|
const simCardId = parseInt(req.params.simCardId);
|
|
// SimCard → MobileDetails → Contract
|
|
const sim = await prisma.simCard.findUnique({
|
|
where: { id: simCardId },
|
|
select: { mobileDetails: { select: { contractId: true } } },
|
|
});
|
|
if (!sim?.mobileDetails) {
|
|
res.status(404).json({ success: false, error: 'SIM-Karte nicht gefunden' } as ApiResponse);
|
|
return;
|
|
}
|
|
if (!(await canAccessContract(req, res, sim.mobileDetails.contractId))) return;
|
|
|
|
const credentials = await contractService.getSimCardCredentials(simCardId);
|
|
res.json({ success: true, data: credentials } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Fehler beim Entschlüsseln der SIM-Karten-Daten',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function getInternetCredentials(req: AuthRequest, res: Response): Promise<void> {
|
|
try {
|
|
const contractId = parseInt(req.params.id);
|
|
if (!(await canAccessContract(req, res, contractId))) return;
|
|
|
|
const credentials = await contractService.getInternetCredentials(contractId);
|
|
res.json({ success: true, data: credentials } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Fehler beim Entschlüsseln des Internet-Passworts',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function getSipCredentials(req: AuthRequest, res: Response): Promise<void> {
|
|
try {
|
|
const phoneNumberId = parseInt(req.params.phoneNumberId);
|
|
// PhoneNumber → InternetDetails → Contract
|
|
const phone = await prisma.phoneNumber.findUnique({
|
|
where: { id: phoneNumberId },
|
|
select: { internetDetails: { select: { contractId: true } } },
|
|
});
|
|
if (!phone?.internetDetails) {
|
|
res.status(404).json({ success: false, error: 'Rufnummer nicht gefunden' } as ApiResponse);
|
|
return;
|
|
}
|
|
if (!(await canAccessContract(req, res, phone.internetDetails.contractId))) return;
|
|
|
|
const credentials = await contractService.getSipCredentials(phoneNumberId);
|
|
res.json({ success: true, data: credentials } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Fehler beim Entschlüsseln des SIP-Passworts',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
// ==================== VERTRAGS-COCKPIT ====================
|
|
|
|
export async function getCockpit(req: AuthRequest, res: Response): Promise<void> {
|
|
try {
|
|
const cockpitData = await contractCockpitService.getCockpitData();
|
|
res.json({ success: true, data: cockpitData } as ApiResponse);
|
|
} catch (error) {
|
|
console.error('Cockpit error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Fehler beim Laden des Vertrags-Cockpits',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
// ==================== 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) },
|
|
});
|
|
|
|
await logChange({
|
|
req, action: 'CREATE', resourceType: 'ContractMeter',
|
|
resourceId: contractMeter.id.toString(),
|
|
label: `Folgezähler hinzugefügt zu Vertrag #${contractId}`,
|
|
customerId: contract.customerId,
|
|
});
|
|
|
|
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);
|
|
const contractId = parseInt(req.params.id);
|
|
await prisma.contractMeter.delete({ where: { id: contractMeterId } });
|
|
await logChange({
|
|
req, action: 'DELETE', resourceType: 'ContractMeter',
|
|
resourceId: contractMeterId.toString(),
|
|
label: `Folgezähler entfernt von Vertrag #${contractId}`,
|
|
});
|
|
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);
|
|
}
|
|
}
|
|
|
|
// ==================== VERTRAGSDOKUMENTE ====================
|
|
|
|
export async function getContractDocuments(req: AuthRequest, res: Response): Promise<void> {
|
|
try {
|
|
const contractId = parseInt(req.params.id);
|
|
if (!(await canAccessContract(req, res, contractId))) return;
|
|
|
|
const documents = await prisma.contractDocument.findMany({
|
|
where: { contractId },
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
res.json({ success: true, data: documents } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(500).json({ success: false, error: 'Fehler beim Laden der Dokumente' } as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function uploadContractDocument(req: AuthRequest, res: Response): Promise<void> {
|
|
try {
|
|
const contractId = parseInt(req.params.id);
|
|
if (!(await canAccessContract(req, res, contractId))) return;
|
|
const { documentType, notes, deliveryDate } = req.body;
|
|
|
|
if (!req.file) {
|
|
res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' } as ApiResponse);
|
|
return;
|
|
}
|
|
|
|
if (!documentType) {
|
|
res.status(400).json({ success: false, error: 'Dokumenttyp erforderlich' } as ApiResponse);
|
|
return;
|
|
}
|
|
|
|
const documentPath = `/uploads/contract-documents/${req.file.filename}`;
|
|
const doc = await prisma.contractDocument.create({
|
|
data: {
|
|
contractId,
|
|
documentType,
|
|
documentPath,
|
|
originalName: req.file.originalname,
|
|
notes: notes || null,
|
|
uploadedBy: req.user?.email,
|
|
},
|
|
});
|
|
|
|
const contract = await prisma.contract.findUnique({ where: { id: contractId }, select: { contractNumber: true, customerId: true } });
|
|
await logChange({
|
|
req, action: 'CREATE', resourceType: 'ContractDocument',
|
|
resourceId: doc.id.toString(),
|
|
label: `Dokument "${documentType}" hochgeladen für Vertrag ${contract?.contractNumber}`,
|
|
details: { typ: documentType, datei: req.file.originalname },
|
|
customerId: contract?.customerId,
|
|
});
|
|
|
|
// Falls Lieferbestätigung: DRAFT → ACTIVE + startDate setzen falls leer
|
|
await maybeActivateOnDeliveryConfirmation(contractId, documentType, req, deliveryDate);
|
|
|
|
res.status(201).json({ success: true, data: doc } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Fehler beim Hochladen',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function deleteContractDocument(req: AuthRequest, res: Response): Promise<void> {
|
|
try {
|
|
const documentId = parseInt(req.params.documentId);
|
|
const contractId = parseInt(req.params.id);
|
|
if (!(await canAccessContract(req, res, contractId))) return;
|
|
|
|
const doc = await prisma.contractDocument.findUnique({ where: { id: documentId } });
|
|
if (!doc || doc.contractId !== contractId) {
|
|
res.status(404).json({ success: false, error: 'Dokument nicht gefunden' } as ApiResponse);
|
|
return;
|
|
}
|
|
|
|
// Datei löschen
|
|
const fs = await import('fs');
|
|
const path = await import('path');
|
|
const filePath = path.join(process.cwd(), doc.documentPath);
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath);
|
|
}
|
|
|
|
await prisma.contractDocument.delete({ where: { id: documentId } });
|
|
|
|
const contract = await prisma.contract.findUnique({ where: { id: contractId }, select: { contractNumber: true, customerId: true } });
|
|
await logChange({
|
|
req, action: 'DELETE', resourceType: 'ContractDocument',
|
|
resourceId: documentId.toString(),
|
|
label: `Dokument "${doc.documentType}" gelöscht von Vertrag ${contract?.contractNumber}`,
|
|
details: { typ: doc.documentType, datei: doc.originalName },
|
|
customerId: contract?.customerId,
|
|
});
|
|
|
|
res.json({ success: true, message: 'Dokument gelöscht' } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Fehler beim Löschen',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
// ==================== SNOOZE (VERTRAG ZURÜCKSTELLEN) ====================
|
|
|
|
export async function snoozeContract(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const { nextReviewDate, months } = req.body;
|
|
|
|
let reviewDate: Date | null = null;
|
|
|
|
if (nextReviewDate) {
|
|
// Explizites Datum angegeben
|
|
reviewDate = new Date(nextReviewDate);
|
|
} else if (months) {
|
|
// Monate angegeben → berechne Datum
|
|
reviewDate = new Date();
|
|
reviewDate.setMonth(reviewDate.getMonth() + months);
|
|
}
|
|
// Wenn beides leer → nextReviewDate wird auf null gesetzt (Snooze aufheben)
|
|
|
|
const updated = await prisma.contract.update({
|
|
where: { id },
|
|
data: { nextReviewDate: reviewDate },
|
|
select: {
|
|
id: true,
|
|
contractNumber: true,
|
|
nextReviewDate: true,
|
|
},
|
|
});
|
|
|
|
await logChange({
|
|
req, action: 'UPDATE', resourceType: 'Contract',
|
|
resourceId: id.toString(),
|
|
label: `Vertrag ${updated.contractNumber} zurückgestellt`,
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: updated,
|
|
message: reviewDate
|
|
? `Vertrag zurückgestellt bis ${reviewDate.toLocaleDateString('de-DE')}`
|
|
: 'Zurückstellung aufgehoben',
|
|
} as ApiResponse);
|
|
} catch (error) {
|
|
console.error('Snooze error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Fehler beim Zurückstellen des Vertrags',
|
|
} as ApiResponse);
|
|
}
|
|
}
|