added place to telecommunication, added contract documents, added invoice to other contracts
This commit is contained in:
@@ -410,6 +410,105 @@ export async function removeContractMeter(req: AuthRequest, res: Response): Prom
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== VERTRAGSDOKUMENTE ====================
|
||||
|
||||
export async function getContractDocuments(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const contractId = parseInt(req.params.id);
|
||||
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);
|
||||
const { documentType, notes } = 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,
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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> {
|
||||
|
||||
@@ -143,3 +143,38 @@ export async function deleteInvoice(req: Request, res: Response): Promise<void>
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== CONTRACT-BASIERTE RECHNUNGEN (für alle Vertragstypen) ====================
|
||||
|
||||
export async function getInvoicesByContract(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const contractId = parseInt(req.params.id);
|
||||
const invoices = await invoiceService.getInvoicesByContract(contractId);
|
||||
res.json({ success: true, data: invoices } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden der Rechnungen' } as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function addInvoiceByContract(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const contractId = parseInt(req.params.id);
|
||||
const { invoiceDate, invoiceType, notes } = req.body;
|
||||
const invoice = await invoiceService.addInvoiceByContract(contractId, {
|
||||
invoiceDate: new Date(invoiceDate),
|
||||
invoiceType,
|
||||
notes,
|
||||
});
|
||||
await logChange({
|
||||
req, action: 'CREATE', resourceType: 'Invoice',
|
||||
resourceId: invoice.id.toString(),
|
||||
label: `Rechnung (${invoiceType}) hinzugefügt`,
|
||||
});
|
||||
res.status(201).json({ success: true, data: invoice } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
import { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import * as contractController from '../controllers/contract.controller.js';
|
||||
import * as invoiceController from '../controllers/invoice.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Multer für Vertragsdokumente
|
||||
const docUploadsDir = path.join(process.cwd(), 'uploads', 'contract-documents');
|
||||
if (!fs.existsSync(docUploadsDir)) {
|
||||
fs.mkdirSync(docUploadsDir, { recursive: true });
|
||||
}
|
||||
const docUpload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (_req, _file, cb) => cb(null, docUploadsDir),
|
||||
filename: (_req, file, cb) => {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
cb(null, `doc-${uniqueSuffix}${path.extname(file.originalname)}`);
|
||||
},
|
||||
}),
|
||||
fileFilter: (_req, file, cb) => {
|
||||
const allowed = ['application/pdf', 'image/jpeg', 'image/png', 'image/jpg'];
|
||||
if (allowed.includes(file.mimetype)) cb(null, true);
|
||||
else cb(new Error('Nur PDF, JPG und PNG Dateien sind erlaubt'));
|
||||
},
|
||||
limits: { fileSize: 10 * 1024 * 1024 },
|
||||
});
|
||||
|
||||
router.get('/', authenticate, requirePermission('contracts:read'), contractController.getContracts);
|
||||
router.post('/', authenticate, requirePermission('contracts:create'), contractController.createContract);
|
||||
|
||||
@@ -20,6 +45,15 @@ router.post('/:id/follow-up', authenticate, requirePermission('contracts:create'
|
||||
// Snooze (Vertrag zurückstellen)
|
||||
router.patch('/:id/snooze', authenticate, requirePermission('contracts:update'), contractController.snoozeContract);
|
||||
|
||||
// Rechnungen (für alle Vertragstypen)
|
||||
router.get('/:id/invoices', authenticate, requirePermission('contracts:read'), invoiceController.getInvoicesByContract);
|
||||
router.post('/:id/invoices', authenticate, requirePermission('contracts:update'), invoiceController.addInvoiceByContract);
|
||||
|
||||
// Vertragsdokumente
|
||||
router.get('/:id/documents', authenticate, requirePermission('contracts:read'), contractController.getContractDocuments);
|
||||
router.post('/:id/documents', authenticate, requirePermission('contracts:update'), docUpload.single('file'), contractController.uploadContractDocument);
|
||||
router.delete('/:id/documents/:documentId', authenticate, requirePermission('contracts:update'), contractController.deleteContractDocument);
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -137,6 +137,8 @@ export async function getContractById(id: number, decryptPassword = false) {
|
||||
tvDetails: true,
|
||||
carInsuranceDetails: true,
|
||||
stressfreiEmail: true,
|
||||
invoices: { orderBy: { invoiceDate: 'desc' as const } },
|
||||
documents: { orderBy: { createdAt: 'desc' as const } },
|
||||
followUpContract: {
|
||||
select: { id: true, contractNumber: true, status: true },
|
||||
},
|
||||
@@ -210,6 +212,10 @@ interface ContractCreateData {
|
||||
// Internet-Zugangsdaten
|
||||
internetUsername?: string;
|
||||
internetPassword?: string;
|
||||
// Objekt & Lage
|
||||
propertyType?: string;
|
||||
propertyLocation?: string;
|
||||
connectionLocation?: string;
|
||||
// Glasfaser-spezifisch
|
||||
homeId?: string;
|
||||
// Vodafone DSL/Kabel spezifisch
|
||||
@@ -302,6 +308,9 @@ export async function createContract(data: ContractCreateData) {
|
||||
internetPasswordEncrypted: internetDetails.internetPassword
|
||||
? encrypt(internetDetails.internetPassword)
|
||||
: undefined,
|
||||
propertyType: internetDetails.propertyType,
|
||||
propertyLocation: internetDetails.propertyLocation,
|
||||
connectionLocation: internetDetails.connectionLocation,
|
||||
homeId: internetDetails.homeId,
|
||||
activationCode: internetDetails.activationCode,
|
||||
phoneNumbers: internetDetails.phoneNumbers && internetDetails.phoneNumbers.length > 0
|
||||
@@ -462,6 +471,9 @@ export async function updateContract(
|
||||
...(internetPassword
|
||||
? { internetPasswordEncrypted: encrypt(internetPassword) }
|
||||
: {}),
|
||||
propertyType: internetData.propertyType,
|
||||
propertyLocation: internetData.propertyLocation,
|
||||
connectionLocation: internetData.connectionLocation,
|
||||
homeId: internetData.homeId,
|
||||
activationCode: internetData.activationCode,
|
||||
};
|
||||
|
||||
@@ -59,6 +59,7 @@ export async function addInvoice(energyContractDetailsId: number, data: CreateIn
|
||||
return prisma.invoice.create({
|
||||
data: {
|
||||
energyContractDetailsId,
|
||||
contractId: energyDetails.contractId,
|
||||
invoiceDate: data.invoiceDate,
|
||||
invoiceType: data.invoiceType,
|
||||
documentPath: data.documentPath,
|
||||
@@ -67,6 +68,28 @@ export async function addInvoice(energyContractDetailsId: number, data: CreateIn
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rechnung direkt über contractId hinzufügen (für alle Vertragstypen)
|
||||
*/
|
||||
export async function addInvoiceByContract(contractId: number, data: CreateInvoiceData) {
|
||||
return prisma.invoice.create({
|
||||
data: {
|
||||
contractId,
|
||||
invoiceDate: data.invoiceDate,
|
||||
invoiceType: data.invoiceType,
|
||||
documentPath: data.documentPath,
|
||||
notes: data.notes,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getInvoicesByContract(contractId: number) {
|
||||
return prisma.invoice.findMany({
|
||||
where: { contractId },
|
||||
orderBy: { invoiceDate: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rechnung aktualisieren
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user