added place to telecommunication, added contract documents, added invoice to other contracts

This commit is contained in:
2026-03-25 16:55:48 +01:00
parent eaa94e766a
commit 3dd4f7b656
30 changed files with 3424 additions and 90 deletions
+3
View File
@@ -13,5 +13,8 @@ export declare function getSipCredentials(req: Request, res: Response): Promise<
export declare function getCockpit(req: AuthRequest, res: Response): Promise<void>;
export declare function addSuccessorMeter(req: AuthRequest, res: Response): Promise<void>;
export declare function removeContractMeter(req: AuthRequest, res: Response): Promise<void>;
export declare function getContractDocuments(req: AuthRequest, res: Response): Promise<void>;
export declare function uploadContractDocument(req: AuthRequest, res: Response): Promise<void>;
export declare function deleteContractDocument(req: AuthRequest, res: Response): Promise<void>;
export declare function snoozeContract(req: Request, res: Response): Promise<void>;
//# sourceMappingURL=contract.controller.d.ts.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"contract.controller.d.ts","sourceRoot":"","sources":["../../src/controllers/contract.controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAM5C,OAAO,EAAe,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG7D,wBAAsB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAgDjF;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAqChF;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAsEnF;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAkB/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CA8CnF;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBpF;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUtF;AAED,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUvF;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUlF;AAID,wBAAsB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/E;AAID,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CA+DtF;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBxF;AAID,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CA+C/E"}
{"version":3,"file":"contract.controller.d.ts","sourceRoot":"","sources":["../../src/controllers/contract.controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAM5C,OAAO,EAAe,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG7D,wBAAsB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAgDjF;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAqChF;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAsEnF;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAkB/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CA8CnF;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBpF;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUtF;AAED,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUvF;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUlF;AAID,wBAAsB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/E;AAID,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CA+DtF;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBxF;AAID,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAWzF;AAED,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CA2C3F;AAED,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAqC3F;AAID,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CA+C/E"}
+91
View File
@@ -49,6 +49,9 @@ exports.getSipCredentials = getSipCredentials;
exports.getCockpit = getCockpit;
exports.addSuccessorMeter = addSuccessorMeter;
exports.removeContractMeter = removeContractMeter;
exports.getContractDocuments = getContractDocuments;
exports.uploadContractDocument = uploadContractDocument;
exports.deleteContractDocument = deleteContractDocument;
exports.snoozeContract = snoozeContract;
const prisma_js_1 = __importDefault(require("../lib/prisma.js"));
const contractService = __importStar(require("../services/contract.service.js"));
@@ -423,6 +426,94 @@ async function removeContractMeter(req, res) {
});
}
}
// ==================== VERTRAGSDOKUMENTE ====================
async function getContractDocuments(req, res) {
try {
const contractId = parseInt(req.params.id);
const documents = await prisma_js_1.default.contractDocument.findMany({
where: { contractId },
orderBy: { createdAt: 'desc' },
});
res.json({ success: true, data: documents });
}
catch (error) {
res.status(500).json({ success: false, error: 'Fehler beim Laden der Dokumente' });
}
}
async function uploadContractDocument(req, res) {
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' });
return;
}
if (!documentType) {
res.status(400).json({ success: false, error: 'Dokumenttyp erforderlich' });
return;
}
const documentPath = `/uploads/contract-documents/${req.file.filename}`;
const doc = await prisma_js_1.default.contractDocument.create({
data: {
contractId,
documentType,
documentPath,
originalName: req.file.originalname,
notes: notes || null,
uploadedBy: req.user?.email,
},
});
const contract = await prisma_js_1.default.contract.findUnique({ where: { id: contractId }, select: { contractNumber: true, customerId: true } });
await (0, audit_service_js_1.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 });
}
catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Hochladen',
});
}
}
async function deleteContractDocument(req, res) {
try {
const documentId = parseInt(req.params.documentId);
const contractId = parseInt(req.params.id);
const doc = await prisma_js_1.default.contractDocument.findUnique({ where: { id: documentId } });
if (!doc || doc.contractId !== contractId) {
res.status(404).json({ success: false, error: 'Dokument nicht gefunden' });
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_js_1.default.contractDocument.delete({ where: { id: documentId } });
const contract = await prisma_js_1.default.contract.findUnique({ where: { id: contractId }, select: { contractNumber: true, customerId: true } });
await (0, audit_service_js_1.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' });
}
catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen',
});
}
}
// ==================== SNOOZE (VERTRAG ZURÜCKSTELLEN) ====================
async function snoozeContract(req, res) {
try {
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"contract.routes.d.ts","sourceRoot":"","sources":["../../src/routes/contract.routes.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAkCxB,eAAe,MAAM,CAAC"}
{"version":3,"file":"contract.routes.d.ts","sourceRoot":"","sources":["../../src/routes/contract.routes.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAgExB,eAAe,MAAM,CAAC"}
+36
View File
@@ -32,11 +32,40 @@ var __importStar = (this && this.__importStar) || (function () {
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const multer_1 = __importDefault(require("multer"));
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const contractController = __importStar(require("../controllers/contract.controller.js"));
const invoiceController = __importStar(require("../controllers/invoice.controller.js"));
const auth_js_1 = require("../middleware/auth.js");
const router = (0, express_1.Router)();
// Multer für Vertragsdokumente
const docUploadsDir = path_1.default.join(process.cwd(), 'uploads', 'contract-documents');
if (!fs_1.default.existsSync(docUploadsDir)) {
fs_1.default.mkdirSync(docUploadsDir, { recursive: true });
}
const docUpload = (0, multer_1.default)({
storage: multer_1.default.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_1.default.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('/', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:read'), contractController.getContracts);
router.post('/', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:create'), contractController.createContract);
// Vertrags-Cockpit (muss VOR /:id stehen!)
@@ -48,6 +77,13 @@ router.delete('/:id', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('
router.post('/:id/follow-up', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:create'), contractController.createFollowUp);
// Snooze (Vertrag zurückstellen)
router.patch('/:id/snooze', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), contractController.snoozeContract);
// Rechnungen (für alle Vertragstypen)
router.get('/:id/invoices', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:read'), invoiceController.getInvoicesByContract);
router.post('/:id/invoices', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), invoiceController.addInvoiceByContract);
// Vertragsdokumente
router.get('/:id/documents', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:read'), contractController.getContractDocuments);
router.post('/:id/documents', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), docUpload.single('file'), contractController.uploadContractDocument);
router.delete('/:id/documents/:documentId', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), contractController.deleteContractDocument);
// Folgezähler
router.post('/:id/successor-meter', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), contractController.addSuccessorMeter);
router.delete('/:id/contract-meter/:contractMeterId', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), contractController.removeContractMeter);
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"contract.routes.js","sourceRoot":"","sources":["../../src/routes/contract.routes.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qCAAiC;AACjC,0FAA4E;AAC5E,mDAAwE;AAExE,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,YAAY,CAAC,CAAC;AACpG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAEzG,2CAA2C;AAC3C,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;AAEzG,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC;AACtG,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAC3G,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAE9G,qBAAqB;AACrB,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAEtH,iCAAiC;AACjC,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAEpH,cAAc;AACd,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,iBAAiB,CAAC,CAAC;AAC/H,MAAM,CAAC,MAAM,CAAC,sCAAsC,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,mBAAmB,CAAC,CAAC;AAEnJ,yBAAyB;AACzB,MAAM,CAAC,GAAG,CAAC,eAAe,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,mBAAmB,CAAC,CAAC;AAEvH,gCAAgC;AAChC,MAAM,CAAC,GAAG,CAAC,iCAAiC,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,qBAAqB,CAAC,CAAC;AAE3I,kCAAkC;AAClC,MAAM,CAAC,GAAG,CAAC,2BAA2B,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,sBAAsB,CAAC,CAAC;AAEtI,6BAA6B;AAC7B,MAAM,CAAC,GAAG,CAAC,6CAA6C,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,iBAAiB,CAAC,CAAC;AAEnJ,kBAAe,MAAM,CAAC"}
{"version":3,"file":"contract.routes.js","sourceRoot":"","sources":["../../src/routes/contract.routes.ts"],"names":[],"mappings":"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"}
+71 -4
View File
@@ -297,7 +297,8 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
updatedAt: Date;
notes: string | null;
documentPath: string | null;
energyContractDetailsId: number;
energyContractDetailsId: number | null;
contractId: number | null;
invoiceDate: Date;
invoiceType: import(".prisma/client").$Enums.InvoiceType;
}[];
@@ -426,7 +427,8 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
updatedAt: Date;
notes: string | null;
documentPath: string | null;
energyContractDetailsId: number;
energyContractDetailsId: number | null;
contractId: number | null;
invoiceDate: Date;
invoiceType: import(".prisma/client").$Enums.InvoiceType;
}[];
@@ -466,6 +468,9 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
routerModel: string | null;
installationDate: Date | null;
internetUsername: string | null;
propertyType: string | null;
propertyLocation: string | null;
connectionLocation: string | null;
}) | null;
mobileDetails: ({
simCards: {
@@ -630,6 +635,9 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
routerModel: string | null;
installationDate: Date | null;
internetUsername: string | null;
propertyType: string | null;
propertyLocation: string | null;
connectionLocation: string | null;
}) | null;
mobileDetails: ({
simCards: {
@@ -679,6 +687,27 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
deductibleFull: number | null;
previousInsurer: string | null;
} | null;
documents: {
id: number;
createdAt: Date;
notes: string | null;
documentPath: string;
contractId: number;
documentType: string;
originalName: string;
uploadedBy: string | null;
}[];
invoices: {
id: number;
createdAt: Date;
updatedAt: Date;
notes: string | null;
documentPath: string | null;
energyContractDetailsId: number | null;
contractId: number | null;
invoiceDate: Date;
invoiceType: import(".prisma/client").$Enums.InvoiceType;
}[];
} & {
portalPasswordEncrypted: string | null;
id: number;
@@ -772,6 +801,9 @@ interface ContractCreateData {
installationDate?: Date;
internetUsername?: string;
internetPassword?: string;
propertyType?: string;
propertyLocation?: string;
connectionLocation?: string;
homeId?: string;
activationCode?: string;
phoneNumbers?: {
@@ -922,6 +954,9 @@ export declare function createContract(data: ContractCreateData): Promise<{
routerModel: string | null;
installationDate: Date | null;
internetUsername: string | null;
propertyType: string | null;
propertyLocation: string | null;
connectionLocation: string | null;
}) | null;
mobileDetails: ({
simCards: {
@@ -1165,7 +1200,8 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
updatedAt: Date;
notes: string | null;
documentPath: string | null;
energyContractDetailsId: number;
energyContractDetailsId: number | null;
contractId: number | null;
invoiceDate: Date;
invoiceType: import(".prisma/client").$Enums.InvoiceType;
}[];
@@ -1294,7 +1330,8 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
updatedAt: Date;
notes: string | null;
documentPath: string | null;
energyContractDetailsId: number;
energyContractDetailsId: number | null;
contractId: number | null;
invoiceDate: Date;
invoiceType: import(".prisma/client").$Enums.InvoiceType;
}[];
@@ -1334,6 +1371,9 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
routerModel: string | null;
installationDate: Date | null;
internetUsername: string | null;
propertyType: string | null;
propertyLocation: string | null;
connectionLocation: string | null;
}) | null;
mobileDetails: ({
simCards: {
@@ -1498,6 +1538,9 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
routerModel: string | null;
installationDate: Date | null;
internetUsername: string | null;
propertyType: string | null;
propertyLocation: string | null;
connectionLocation: string | null;
}) | null;
mobileDetails: ({
simCards: {
@@ -1547,6 +1590,27 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
deductibleFull: number | null;
previousInsurer: string | null;
} | null;
documents: {
id: number;
createdAt: Date;
notes: string | null;
documentPath: string;
contractId: number;
documentType: string;
originalName: string;
uploadedBy: string | null;
}[];
invoices: {
id: number;
createdAt: Date;
updatedAt: Date;
notes: string | null;
documentPath: string | null;
energyContractDetailsId: number | null;
contractId: number | null;
invoiceDate: Date;
invoiceType: import(".prisma/client").$Enums.InvoiceType;
}[];
} & {
portalPasswordEncrypted: string | null;
id: number;
@@ -1737,6 +1801,9 @@ export declare function createFollowUpContract(previousContractId: number): Prom
routerModel: string | null;
installationDate: Date | null;
internetUsername: string | null;
propertyType: string | null;
propertyLocation: string | null;
connectionLocation: string | null;
}) | null;
mobileDetails: ({
simCards: {
File diff suppressed because one or more lines are too long
+8
View File
@@ -138,6 +138,8 @@ async function getContractById(id, decryptPassword = false) {
tvDetails: true,
carInsuranceDetails: true,
stressfreiEmail: true,
invoices: { orderBy: { invoiceDate: 'desc' } },
documents: { orderBy: { createdAt: 'desc' } },
followUpContract: {
select: { id: true, contractNumber: true, status: true },
},
@@ -183,6 +185,9 @@ async function createContract(data) {
internetPasswordEncrypted: internetDetails.internetPassword
? (0, encryption_js_1.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
@@ -321,6 +326,9 @@ async function updateContract(id, data) {
...(internetPassword
? { internetPasswordEncrypted: (0, encryption_js_1.encrypt)(internetPassword) }
: {}),
propertyType: internetData.propertyType,
propertyLocation: internetData.propertyLocation,
connectionLocation: internetData.connectionLocation,
homeId: internetData.homeId,
activationCode: internetData.activationCode,
};
File diff suppressed because one or more lines are too long
+19 -3
View File
File diff suppressed because one or more lines are too long
+16
View File
@@ -488,6 +488,17 @@ exports.Prisma.ContractScalarFieldEnum = {
updatedAt: 'updatedAt'
};
exports.Prisma.ContractDocumentScalarFieldEnum = {
id: 'id',
contractId: 'contractId',
documentType: 'documentType',
documentPath: 'documentPath',
originalName: 'originalName',
notes: 'notes',
uploadedBy: 'uploadedBy',
createdAt: 'createdAt'
};
exports.Prisma.ContractHistoryEntryScalarFieldEnum = {
id: 'id',
contractId: 'contractId',
@@ -551,6 +562,7 @@ exports.Prisma.ContractMeterScalarFieldEnum = {
exports.Prisma.InvoiceScalarFieldEnum = {
id: 'id',
energyContractDetailsId: 'energyContractDetailsId',
contractId: 'contractId',
invoiceDate: 'invoiceDate',
invoiceType: 'invoiceType',
documentPath: 'documentPath',
@@ -569,6 +581,9 @@ exports.Prisma.InternetContractDetailsScalarFieldEnum = {
installationDate: 'installationDate',
internetUsername: 'internetUsername',
internetPasswordEncrypted: 'internetPasswordEncrypted',
propertyType: 'propertyType',
propertyLocation: 'propertyLocation',
connectionLocation: 'connectionLocation',
homeId: 'homeId',
activationCode: 'activationCode'
};
@@ -870,6 +885,7 @@ exports.Prisma.ModelName = {
Tariff: 'Tariff',
ContractCategory: 'ContractCategory',
Contract: 'Contract',
ContractDocument: 'ContractDocument',
ContractHistoryEntry: 'ContractHistoryEntry',
ContractTask: 'ContractTask',
ContractTaskSubtask: 'ContractTaskSubtask',
+2518 -45
View File
File diff suppressed because it is too large Load Diff
+19 -3
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "prisma-client-45a91d7556f300a75a0048d27fac6a72915779fc4e5c2234b54fe3547ddb1605",
"name": "prisma-client-f8de59fafbd0672a88c2a8e39308517de72556670ae690a7d472709948465d02",
"main": "index.js",
"types": "index.d.ts",
"browser": "index-browser.js",
+30 -5
View File
@@ -656,11 +656,29 @@ model Contract {
tasks ContractTask[]
assignedEmails CachedEmail[] // Zugeordnete E-Mails aus dem E-Mail-Client
historyEntries ContractHistoryEntry[]
documents ContractDocument[]
invoices Invoice[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== CONTRACT DOCUMENTS ====================
model ContractDocument {
id Int @id @default(autoincrement())
contractId Int
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
documentType String // Auftragsformular, Lieferbestätigung, etc.
documentPath String // Dateipfad
originalName String // Originaler Dateiname
notes String? @db.Text
uploadedBy String? // Wer hat hochgeladen
createdAt DateTime @default(now())
@@index([contractId])
}
// ==================== CONTRACT HISTORY ====================
model ContractHistoryEntry {
@@ -752,17 +770,20 @@ model ContractMeter {
}
model Invoice {
id Int @id @default(autoincrement())
energyContractDetailsId Int
energyContractDetails EnergyContractDetails @relation(fields: [energyContractDetailsId], references: [id], onDelete: Cascade)
id Int @id @default(autoincrement())
energyContractDetailsId Int?
energyContractDetails EnergyContractDetails? @relation(fields: [energyContractDetailsId], references: [id], onDelete: Cascade)
contractId Int?
contract Contract? @relation(fields: [contractId], references: [id], onDelete: Cascade)
invoiceDate DateTime
invoiceType InvoiceType
documentPath String? // Pflicht, außer bei NOT_AVAILABLE
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([energyContractDetailsId])
@@index([contractId])
}
// ==================== INTERNET CONTRACT DETAILS ====================
@@ -779,6 +800,10 @@ model InternetContractDetails {
// Internet-Zugangsdaten
internetUsername String?
internetPasswordEncrypted String? // Verschlüsselt gespeichert
// Objekt & Lage
propertyType String? // Objekttyp (Mehrparteienhaus, Freistehendes Haus, etc.)
propertyLocation String? // Lage (Erdgeschoss, OG1, etc.)
connectionLocation String? // Lage des Anschlusses (Flur, HWR, etc.)
// Glasfaser-spezifisch
homeId String?
// Vodafone DSL/Kabel spezifisch
+16
View File
@@ -488,6 +488,17 @@ exports.Prisma.ContractScalarFieldEnum = {
updatedAt: 'updatedAt'
};
exports.Prisma.ContractDocumentScalarFieldEnum = {
id: 'id',
contractId: 'contractId',
documentType: 'documentType',
documentPath: 'documentPath',
originalName: 'originalName',
notes: 'notes',
uploadedBy: 'uploadedBy',
createdAt: 'createdAt'
};
exports.Prisma.ContractHistoryEntryScalarFieldEnum = {
id: 'id',
contractId: 'contractId',
@@ -551,6 +562,7 @@ exports.Prisma.ContractMeterScalarFieldEnum = {
exports.Prisma.InvoiceScalarFieldEnum = {
id: 'id',
energyContractDetailsId: 'energyContractDetailsId',
contractId: 'contractId',
invoiceDate: 'invoiceDate',
invoiceType: 'invoiceType',
documentPath: 'documentPath',
@@ -569,6 +581,9 @@ exports.Prisma.InternetContractDetailsScalarFieldEnum = {
installationDate: 'installationDate',
internetUsername: 'internetUsername',
internetPasswordEncrypted: 'internetPasswordEncrypted',
propertyType: 'propertyType',
propertyLocation: 'propertyLocation',
connectionLocation: 'connectionLocation',
homeId: 'homeId',
activationCode: 'activationCode'
};
@@ -870,6 +885,7 @@ exports.Prisma.ModelName = {
Tariff: 'Tariff',
ContractCategory: 'ContractCategory',
Contract: 'Contract',
ContractDocument: 'ContractDocument',
ContractHistoryEntry: 'ContractHistoryEntry',
ContractTask: 'ContractTask',
ContractTaskSubtask: 'ContractTaskSubtask',
+27 -2
View File
@@ -656,11 +656,29 @@ model Contract {
tasks ContractTask[]
assignedEmails CachedEmail[] // Zugeordnete E-Mails aus dem E-Mail-Client
historyEntries ContractHistoryEntry[]
documents ContractDocument[]
invoices Invoice[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== CONTRACT DOCUMENTS ====================
model ContractDocument {
id Int @id @default(autoincrement())
contractId Int
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
documentType String // Auftragsformular, Lieferbestätigung, etc.
documentPath String // Dateipfad
originalName String // Originaler Dateiname
notes String? @db.Text
uploadedBy String? // Wer hat hochgeladen
createdAt DateTime @default(now())
@@index([contractId])
}
// ==================== CONTRACT HISTORY ====================
model ContractHistoryEntry {
@@ -753,8 +771,10 @@ model ContractMeter {
model Invoice {
id Int @id @default(autoincrement())
energyContractDetailsId Int
energyContractDetails EnergyContractDetails @relation(fields: [energyContractDetailsId], references: [id], onDelete: Cascade)
energyContractDetailsId Int?
energyContractDetails EnergyContractDetails? @relation(fields: [energyContractDetailsId], references: [id], onDelete: Cascade)
contractId Int?
contract Contract? @relation(fields: [contractId], references: [id], onDelete: Cascade)
invoiceDate DateTime
invoiceType InvoiceType
documentPath String? // Pflicht, außer bei NOT_AVAILABLE
@@ -763,6 +783,7 @@ model Invoice {
updatedAt DateTime @updatedAt
@@index([energyContractDetailsId])
@@index([contractId])
}
// ==================== INTERNET CONTRACT DETAILS ====================
@@ -779,6 +800,10 @@ model InternetContractDetails {
// Internet-Zugangsdaten
internetUsername String?
internetPasswordEncrypted String? // Verschlüsselt gespeichert
// Objekt & Lage
propertyType String? // Objekttyp (Mehrparteienhaus, Freistehendes Haus, etc.)
propertyLocation String? // Lage (Erdgeschoss, OG1, etc.)
connectionLocation String? // Lage des Anschlusses (Flur, HWR, etc.)
// Glasfaser-spezifisch
homeId String?
// Vodafone DSL/Kabel spezifisch
@@ -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);
}
}
+34
View File
@@ -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);
+12
View File
@@ -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,
};
+23
View File
@@ -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
*/
+17 -3
View File
@@ -2,6 +2,7 @@ Vertragliste bei Energie mit Anschlussadresse/Lieferadresse noch in der Liste
Bei Mobilfunk die Mobilfunknummer und wenn vorhanden Karteninhaber
Bei Festnetz, die Anschlussadresse/Lieferadresse
Bei KFZ das Kennzeichen
#
#erledigt
Datenschutzerklärung wenn PDF hinterlegt wurde, alle Haken auf Grün setzten.
@@ -11,6 +12,7 @@ Aktuell zählt das PDF als Alternative zu den Online-Haken. Du willst es so:
PDF hochgeladen → alle 4 Online-Consents automatisch auf GRANTED setzen
Kunde entfernt einen Haken im Portal → PDF löschen + Tabs sperren
Entsperrung nur durch: alle Haken wieder setzen ODER neues PDF hochladen
#
#erledigt
Zweitarif (Gibt es auch 3 Tarifuzähler?) Zähler HT/NT bei Strom Zähler hinzufügen.
@@ -21,24 +23,36 @@ Alle Datumsfelder mit 0 davor wenn es ne einstellige Zahl ist
Jetzt : 1.1.2026
Und gewollt 01.01.2026
#
#erledigt
Die Auditmeldungen aussagekräftig
#
Email Log und system testen
Sprich senden und Empfnagen
#
Security System testen
#
#erledigt
Datenschutzerklärung Website unserer Seite und ein impressum im Kundenportal.
Auch wieder über das Einstellungsmenü editirerbar.
Bitte mach mir da auch einen Vorschagstext rein
#
Geburtstagskalender, und Geburtgsgruß als Modal beim ersten Login an dem Tag,
Sollte der Login bis n7 btage nach Geburtsag sein dann Glückwunsch nachträglich
#
Email datenschutzerklärung erst wenn alle hebel drin sind, auf einen bestätigungsbutton klicken, um sicherzustellen, das alle heben drin sind.
Danch bestätigen, nochmals eine Bestätigiguns emails enden
#
Haben wir bei den Vertragen (also alle) kein Dokumentfeld zum Upload von, Auftragsformular, Lieferbestätigung, Vertragsunterlagen.
hier sind wieder png,pdf erlaubt
#