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

This commit is contained in:
duffyduck 2026-03-25 16:55:48 +01:00
parent eaa94e766a
commit 3dd4f7b656
30 changed files with 3424 additions and 90 deletions

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 getCockpit(req: AuthRequest, res: Response): Promise<void>;
export declare function addSuccessorMeter(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 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>; export declare function snoozeContract(req: Request, res: Response): Promise<void>;
//# sourceMappingURL=contract.controller.d.ts.map //# sourceMappingURL=contract.controller.d.ts.map

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"}

View File

@ -49,6 +49,9 @@ exports.getSipCredentials = getSipCredentials;
exports.getCockpit = getCockpit; exports.getCockpit = getCockpit;
exports.addSuccessorMeter = addSuccessorMeter; exports.addSuccessorMeter = addSuccessorMeter;
exports.removeContractMeter = removeContractMeter; exports.removeContractMeter = removeContractMeter;
exports.getContractDocuments = getContractDocuments;
exports.uploadContractDocument = uploadContractDocument;
exports.deleteContractDocument = deleteContractDocument;
exports.snoozeContract = snoozeContract; exports.snoozeContract = snoozeContract;
const prisma_js_1 = __importDefault(require("../lib/prisma.js")); const prisma_js_1 = __importDefault(require("../lib/prisma.js"));
const contractService = __importStar(require("../services/contract.service.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) ==================== // ==================== SNOOZE (VERTRAG ZURÜCKSTELLEN) ====================
async function snoozeContract(req, res) { async function snoozeContract(req, res) {
try { try {

File diff suppressed because one or more lines are too long

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"}

View File

@ -32,11 +32,40 @@ var __importStar = (this && this.__importStar) || (function () {
return result; return result;
}; };
})(); })();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express"); 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 contractController = __importStar(require("../controllers/contract.controller.js"));
const invoiceController = __importStar(require("../controllers/invoice.controller.js"));
const auth_js_1 = require("../middleware/auth.js"); const auth_js_1 = require("../middleware/auth.js");
const router = (0, express_1.Router)(); 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.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); router.post('/', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:create'), contractController.createContract);
// Vertrags-Cockpit (muss VOR /:id stehen!) // 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); router.post('/:id/follow-up', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:create'), contractController.createFollowUp);
// Snooze (Vertrag zurückstellen) // Snooze (Vertrag zurückstellen)
router.patch('/:id/snooze', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), contractController.snoozeContract); 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 // Folgezähler
router.post('/:id/successor-meter', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), contractController.addSuccessorMeter); 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); router.delete('/:id/contract-meter/:contractMeterId', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), contractController.removeContractMeter);

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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"}

View File

@ -297,7 +297,8 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
updatedAt: Date; updatedAt: Date;
notes: string | null; notes: string | null;
documentPath: string | null; documentPath: string | null;
energyContractDetailsId: number; energyContractDetailsId: number | null;
contractId: number | null;
invoiceDate: Date; invoiceDate: Date;
invoiceType: import(".prisma/client").$Enums.InvoiceType; invoiceType: import(".prisma/client").$Enums.InvoiceType;
}[]; }[];
@ -426,7 +427,8 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
updatedAt: Date; updatedAt: Date;
notes: string | null; notes: string | null;
documentPath: string | null; documentPath: string | null;
energyContractDetailsId: number; energyContractDetailsId: number | null;
contractId: number | null;
invoiceDate: Date; invoiceDate: Date;
invoiceType: import(".prisma/client").$Enums.InvoiceType; invoiceType: import(".prisma/client").$Enums.InvoiceType;
}[]; }[];
@ -466,6 +468,9 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
routerModel: string | null; routerModel: string | null;
installationDate: Date | null; installationDate: Date | null;
internetUsername: string | null; internetUsername: string | null;
propertyType: string | null;
propertyLocation: string | null;
connectionLocation: string | null;
}) | null; }) | null;
mobileDetails: ({ mobileDetails: ({
simCards: { simCards: {
@ -630,6 +635,9 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
routerModel: string | null; routerModel: string | null;
installationDate: Date | null; installationDate: Date | null;
internetUsername: string | null; internetUsername: string | null;
propertyType: string | null;
propertyLocation: string | null;
connectionLocation: string | null;
}) | null; }) | null;
mobileDetails: ({ mobileDetails: ({
simCards: { simCards: {
@ -679,6 +687,27 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
deductibleFull: number | null; deductibleFull: number | null;
previousInsurer: string | null; previousInsurer: string | null;
} | 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; portalPasswordEncrypted: string | null;
id: number; id: number;
@ -772,6 +801,9 @@ interface ContractCreateData {
installationDate?: Date; installationDate?: Date;
internetUsername?: string; internetUsername?: string;
internetPassword?: string; internetPassword?: string;
propertyType?: string;
propertyLocation?: string;
connectionLocation?: string;
homeId?: string; homeId?: string;
activationCode?: string; activationCode?: string;
phoneNumbers?: { phoneNumbers?: {
@ -922,6 +954,9 @@ export declare function createContract(data: ContractCreateData): Promise<{
routerModel: string | null; routerModel: string | null;
installationDate: Date | null; installationDate: Date | null;
internetUsername: string | null; internetUsername: string | null;
propertyType: string | null;
propertyLocation: string | null;
connectionLocation: string | null;
}) | null; }) | null;
mobileDetails: ({ mobileDetails: ({
simCards: { simCards: {
@ -1165,7 +1200,8 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
updatedAt: Date; updatedAt: Date;
notes: string | null; notes: string | null;
documentPath: string | null; documentPath: string | null;
energyContractDetailsId: number; energyContractDetailsId: number | null;
contractId: number | null;
invoiceDate: Date; invoiceDate: Date;
invoiceType: import(".prisma/client").$Enums.InvoiceType; invoiceType: import(".prisma/client").$Enums.InvoiceType;
}[]; }[];
@ -1294,7 +1330,8 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
updatedAt: Date; updatedAt: Date;
notes: string | null; notes: string | null;
documentPath: string | null; documentPath: string | null;
energyContractDetailsId: number; energyContractDetailsId: number | null;
contractId: number | null;
invoiceDate: Date; invoiceDate: Date;
invoiceType: import(".prisma/client").$Enums.InvoiceType; invoiceType: import(".prisma/client").$Enums.InvoiceType;
}[]; }[];
@ -1334,6 +1371,9 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
routerModel: string | null; routerModel: string | null;
installationDate: Date | null; installationDate: Date | null;
internetUsername: string | null; internetUsername: string | null;
propertyType: string | null;
propertyLocation: string | null;
connectionLocation: string | null;
}) | null; }) | null;
mobileDetails: ({ mobileDetails: ({
simCards: { simCards: {
@ -1498,6 +1538,9 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
routerModel: string | null; routerModel: string | null;
installationDate: Date | null; installationDate: Date | null;
internetUsername: string | null; internetUsername: string | null;
propertyType: string | null;
propertyLocation: string | null;
connectionLocation: string | null;
}) | null; }) | null;
mobileDetails: ({ mobileDetails: ({
simCards: { simCards: {
@ -1547,6 +1590,27 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
deductibleFull: number | null; deductibleFull: number | null;
previousInsurer: string | null; previousInsurer: string | null;
} | 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; portalPasswordEncrypted: string | null;
id: number; id: number;
@ -1737,6 +1801,9 @@ export declare function createFollowUpContract(previousContractId: number): Prom
routerModel: string | null; routerModel: string | null;
installationDate: Date | null; installationDate: Date | null;
internetUsername: string | null; internetUsername: string | null;
propertyType: string | null;
propertyLocation: string | null;
connectionLocation: string | null;
}) | null; }) | null;
mobileDetails: ({ mobileDetails: ({
simCards: { simCards: {

File diff suppressed because one or more lines are too long

View File

@ -138,6 +138,8 @@ async function getContractById(id, decryptPassword = false) {
tvDetails: true, tvDetails: true,
carInsuranceDetails: true, carInsuranceDetails: true,
stressfreiEmail: true, stressfreiEmail: true,
invoices: { orderBy: { invoiceDate: 'desc' } },
documents: { orderBy: { createdAt: 'desc' } },
followUpContract: { followUpContract: {
select: { id: true, contractNumber: true, status: true }, select: { id: true, contractNumber: true, status: true },
}, },
@ -183,6 +185,9 @@ async function createContract(data) {
internetPasswordEncrypted: internetDetails.internetPassword internetPasswordEncrypted: internetDetails.internetPassword
? (0, encryption_js_1.encrypt)(internetDetails.internetPassword) ? (0, encryption_js_1.encrypt)(internetDetails.internetPassword)
: undefined, : undefined,
propertyType: internetDetails.propertyType,
propertyLocation: internetDetails.propertyLocation,
connectionLocation: internetDetails.connectionLocation,
homeId: internetDetails.homeId, homeId: internetDetails.homeId,
activationCode: internetDetails.activationCode, activationCode: internetDetails.activationCode,
phoneNumbers: internetDetails.phoneNumbers && internetDetails.phoneNumbers.length > 0 phoneNumbers: internetDetails.phoneNumbers && internetDetails.phoneNumbers.length > 0
@ -321,6 +326,9 @@ async function updateContract(id, data) {
...(internetPassword ...(internetPassword
? { internetPasswordEncrypted: (0, encryption_js_1.encrypt)(internetPassword) } ? { internetPasswordEncrypted: (0, encryption_js_1.encrypt)(internetPassword) }
: {}), : {}),
propertyType: internetData.propertyType,
propertyLocation: internetData.propertyLocation,
connectionLocation: internetData.connectionLocation,
homeId: internetData.homeId, homeId: internetData.homeId,
activationCode: internetData.activationCode, activationCode: internetData.activationCode,
}; };

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -488,6 +488,17 @@ exports.Prisma.ContractScalarFieldEnum = {
updatedAt: 'updatedAt' updatedAt: 'updatedAt'
}; };
exports.Prisma.ContractDocumentScalarFieldEnum = {
id: 'id',
contractId: 'contractId',
documentType: 'documentType',
documentPath: 'documentPath',
originalName: 'originalName',
notes: 'notes',
uploadedBy: 'uploadedBy',
createdAt: 'createdAt'
};
exports.Prisma.ContractHistoryEntryScalarFieldEnum = { exports.Prisma.ContractHistoryEntryScalarFieldEnum = {
id: 'id', id: 'id',
contractId: 'contractId', contractId: 'contractId',
@ -551,6 +562,7 @@ exports.Prisma.ContractMeterScalarFieldEnum = {
exports.Prisma.InvoiceScalarFieldEnum = { exports.Prisma.InvoiceScalarFieldEnum = {
id: 'id', id: 'id',
energyContractDetailsId: 'energyContractDetailsId', energyContractDetailsId: 'energyContractDetailsId',
contractId: 'contractId',
invoiceDate: 'invoiceDate', invoiceDate: 'invoiceDate',
invoiceType: 'invoiceType', invoiceType: 'invoiceType',
documentPath: 'documentPath', documentPath: 'documentPath',
@ -569,6 +581,9 @@ exports.Prisma.InternetContractDetailsScalarFieldEnum = {
installationDate: 'installationDate', installationDate: 'installationDate',
internetUsername: 'internetUsername', internetUsername: 'internetUsername',
internetPasswordEncrypted: 'internetPasswordEncrypted', internetPasswordEncrypted: 'internetPasswordEncrypted',
propertyType: 'propertyType',
propertyLocation: 'propertyLocation',
connectionLocation: 'connectionLocation',
homeId: 'homeId', homeId: 'homeId',
activationCode: 'activationCode' activationCode: 'activationCode'
}; };
@ -870,6 +885,7 @@ exports.Prisma.ModelName = {
Tariff: 'Tariff', Tariff: 'Tariff',
ContractCategory: 'ContractCategory', ContractCategory: 'ContractCategory',
Contract: 'Contract', Contract: 'Contract',
ContractDocument: 'ContractDocument',
ContractHistoryEntry: 'ContractHistoryEntry', ContractHistoryEntry: 'ContractHistoryEntry',
ContractTask: 'ContractTask', ContractTask: 'ContractTask',
ContractTaskSubtask: 'ContractTaskSubtask', ContractTaskSubtask: 'ContractTaskSubtask',

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{ {
"name": "prisma-client-45a91d7556f300a75a0048d27fac6a72915779fc4e5c2234b54fe3547ddb1605", "name": "prisma-client-f8de59fafbd0672a88c2a8e39308517de72556670ae690a7d472709948465d02",
"main": "index.js", "main": "index.js",
"types": "index.d.ts", "types": "index.d.ts",
"browser": "index-browser.js", "browser": "index-browser.js",

View File

@ -656,11 +656,29 @@ model Contract {
tasks ContractTask[] tasks ContractTask[]
assignedEmails CachedEmail[] // Zugeordnete E-Mails aus dem E-Mail-Client assignedEmails CachedEmail[] // Zugeordnete E-Mails aus dem E-Mail-Client
historyEntries ContractHistoryEntry[] historyEntries ContractHistoryEntry[]
documents ContractDocument[]
invoices Invoice[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt 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 ==================== // ==================== CONTRACT HISTORY ====================
model ContractHistoryEntry { model ContractHistoryEntry {
@ -753,8 +771,10 @@ model ContractMeter {
model Invoice { model Invoice {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
energyContractDetailsId Int energyContractDetailsId Int?
energyContractDetails EnergyContractDetails @relation(fields: [energyContractDetailsId], references: [id], onDelete: Cascade) energyContractDetails EnergyContractDetails? @relation(fields: [energyContractDetailsId], references: [id], onDelete: Cascade)
contractId Int?
contract Contract? @relation(fields: [contractId], references: [id], onDelete: Cascade)
invoiceDate DateTime invoiceDate DateTime
invoiceType InvoiceType invoiceType InvoiceType
documentPath String? // Pflicht, außer bei NOT_AVAILABLE documentPath String? // Pflicht, außer bei NOT_AVAILABLE
@ -763,6 +783,7 @@ model Invoice {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@index([energyContractDetailsId]) @@index([energyContractDetailsId])
@@index([contractId])
} }
// ==================== INTERNET CONTRACT DETAILS ==================== // ==================== INTERNET CONTRACT DETAILS ====================
@ -779,6 +800,10 @@ model InternetContractDetails {
// Internet-Zugangsdaten // Internet-Zugangsdaten
internetUsername String? internetUsername String?
internetPasswordEncrypted String? // Verschlüsselt gespeichert 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 // Glasfaser-spezifisch
homeId String? homeId String?
// Vodafone DSL/Kabel spezifisch // Vodafone DSL/Kabel spezifisch

View File

@ -488,6 +488,17 @@ exports.Prisma.ContractScalarFieldEnum = {
updatedAt: 'updatedAt' updatedAt: 'updatedAt'
}; };
exports.Prisma.ContractDocumentScalarFieldEnum = {
id: 'id',
contractId: 'contractId',
documentType: 'documentType',
documentPath: 'documentPath',
originalName: 'originalName',
notes: 'notes',
uploadedBy: 'uploadedBy',
createdAt: 'createdAt'
};
exports.Prisma.ContractHistoryEntryScalarFieldEnum = { exports.Prisma.ContractHistoryEntryScalarFieldEnum = {
id: 'id', id: 'id',
contractId: 'contractId', contractId: 'contractId',
@ -551,6 +562,7 @@ exports.Prisma.ContractMeterScalarFieldEnum = {
exports.Prisma.InvoiceScalarFieldEnum = { exports.Prisma.InvoiceScalarFieldEnum = {
id: 'id', id: 'id',
energyContractDetailsId: 'energyContractDetailsId', energyContractDetailsId: 'energyContractDetailsId',
contractId: 'contractId',
invoiceDate: 'invoiceDate', invoiceDate: 'invoiceDate',
invoiceType: 'invoiceType', invoiceType: 'invoiceType',
documentPath: 'documentPath', documentPath: 'documentPath',
@ -569,6 +581,9 @@ exports.Prisma.InternetContractDetailsScalarFieldEnum = {
installationDate: 'installationDate', installationDate: 'installationDate',
internetUsername: 'internetUsername', internetUsername: 'internetUsername',
internetPasswordEncrypted: 'internetPasswordEncrypted', internetPasswordEncrypted: 'internetPasswordEncrypted',
propertyType: 'propertyType',
propertyLocation: 'propertyLocation',
connectionLocation: 'connectionLocation',
homeId: 'homeId', homeId: 'homeId',
activationCode: 'activationCode' activationCode: 'activationCode'
}; };
@ -870,6 +885,7 @@ exports.Prisma.ModelName = {
Tariff: 'Tariff', Tariff: 'Tariff',
ContractCategory: 'ContractCategory', ContractCategory: 'ContractCategory',
Contract: 'Contract', Contract: 'Contract',
ContractDocument: 'ContractDocument',
ContractHistoryEntry: 'ContractHistoryEntry', ContractHistoryEntry: 'ContractHistoryEntry',
ContractTask: 'ContractTask', ContractTask: 'ContractTask',
ContractTaskSubtask: 'ContractTaskSubtask', ContractTaskSubtask: 'ContractTaskSubtask',

View File

@ -656,11 +656,29 @@ model Contract {
tasks ContractTask[] tasks ContractTask[]
assignedEmails CachedEmail[] // Zugeordnete E-Mails aus dem E-Mail-Client assignedEmails CachedEmail[] // Zugeordnete E-Mails aus dem E-Mail-Client
historyEntries ContractHistoryEntry[] historyEntries ContractHistoryEntry[]
documents ContractDocument[]
invoices Invoice[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt 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 ==================== // ==================== CONTRACT HISTORY ====================
model ContractHistoryEntry { model ContractHistoryEntry {
@ -753,8 +771,10 @@ model ContractMeter {
model Invoice { model Invoice {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
energyContractDetailsId Int energyContractDetailsId Int?
energyContractDetails EnergyContractDetails @relation(fields: [energyContractDetailsId], references: [id], onDelete: Cascade) energyContractDetails EnergyContractDetails? @relation(fields: [energyContractDetailsId], references: [id], onDelete: Cascade)
contractId Int?
contract Contract? @relation(fields: [contractId], references: [id], onDelete: Cascade)
invoiceDate DateTime invoiceDate DateTime
invoiceType InvoiceType invoiceType InvoiceType
documentPath String? // Pflicht, außer bei NOT_AVAILABLE documentPath String? // Pflicht, außer bei NOT_AVAILABLE
@ -763,6 +783,7 @@ model Invoice {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@index([energyContractDetailsId]) @@index([energyContractDetailsId])
@@index([contractId])
} }
// ==================== INTERNET CONTRACT DETAILS ==================== // ==================== INTERNET CONTRACT DETAILS ====================
@ -779,6 +800,10 @@ model InternetContractDetails {
// Internet-Zugangsdaten // Internet-Zugangsdaten
internetUsername String? internetUsername String?
internetPasswordEncrypted String? // Verschlüsselt gespeichert 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 // Glasfaser-spezifisch
homeId String? homeId String?
// Vodafone DSL/Kabel spezifisch // Vodafone DSL/Kabel spezifisch

View File

@ -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) ==================== // ==================== SNOOZE (VERTRAG ZURÜCKSTELLEN) ====================
export async function snoozeContract(req: Request, res: Response): Promise<void> { export async function snoozeContract(req: Request, res: Response): Promise<void> {

View File

@ -143,3 +143,38 @@ export async function deleteInvoice(req: Request, res: Response): Promise<void>
} as ApiResponse); } 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);
}
}

View File

@ -1,9 +1,34 @@
import { Router } from 'express'; 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 contractController from '../controllers/contract.controller.js';
import * as invoiceController from '../controllers/invoice.controller.js';
import { authenticate, requirePermission } from '../middleware/auth.js'; import { authenticate, requirePermission } from '../middleware/auth.js';
const router = Router(); 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.get('/', authenticate, requirePermission('contracts:read'), contractController.getContracts);
router.post('/', authenticate, requirePermission('contracts:create'), contractController.createContract); 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) // Snooze (Vertrag zurückstellen)
router.patch('/:id/snooze', authenticate, requirePermission('contracts:update'), contractController.snoozeContract); 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 // Folgezähler
router.post('/:id/successor-meter', authenticate, requirePermission('contracts:update'), contractController.addSuccessorMeter); router.post('/:id/successor-meter', authenticate, requirePermission('contracts:update'), contractController.addSuccessorMeter);
router.delete('/:id/contract-meter/:contractMeterId', authenticate, requirePermission('contracts:update'), contractController.removeContractMeter); router.delete('/:id/contract-meter/:contractMeterId', authenticate, requirePermission('contracts:update'), contractController.removeContractMeter);

View File

@ -137,6 +137,8 @@ export async function getContractById(id: number, decryptPassword = false) {
tvDetails: true, tvDetails: true,
carInsuranceDetails: true, carInsuranceDetails: true,
stressfreiEmail: true, stressfreiEmail: true,
invoices: { orderBy: { invoiceDate: 'desc' as const } },
documents: { orderBy: { createdAt: 'desc' as const } },
followUpContract: { followUpContract: {
select: { id: true, contractNumber: true, status: true }, select: { id: true, contractNumber: true, status: true },
}, },
@ -210,6 +212,10 @@ interface ContractCreateData {
// Internet-Zugangsdaten // Internet-Zugangsdaten
internetUsername?: string; internetUsername?: string;
internetPassword?: string; internetPassword?: string;
// Objekt & Lage
propertyType?: string;
propertyLocation?: string;
connectionLocation?: string;
// Glasfaser-spezifisch // Glasfaser-spezifisch
homeId?: string; homeId?: string;
// Vodafone DSL/Kabel spezifisch // Vodafone DSL/Kabel spezifisch
@ -302,6 +308,9 @@ export async function createContract(data: ContractCreateData) {
internetPasswordEncrypted: internetDetails.internetPassword internetPasswordEncrypted: internetDetails.internetPassword
? encrypt(internetDetails.internetPassword) ? encrypt(internetDetails.internetPassword)
: undefined, : undefined,
propertyType: internetDetails.propertyType,
propertyLocation: internetDetails.propertyLocation,
connectionLocation: internetDetails.connectionLocation,
homeId: internetDetails.homeId, homeId: internetDetails.homeId,
activationCode: internetDetails.activationCode, activationCode: internetDetails.activationCode,
phoneNumbers: internetDetails.phoneNumbers && internetDetails.phoneNumbers.length > 0 phoneNumbers: internetDetails.phoneNumbers && internetDetails.phoneNumbers.length > 0
@ -462,6 +471,9 @@ export async function updateContract(
...(internetPassword ...(internetPassword
? { internetPasswordEncrypted: encrypt(internetPassword) } ? { internetPasswordEncrypted: encrypt(internetPassword) }
: {}), : {}),
propertyType: internetData.propertyType,
propertyLocation: internetData.propertyLocation,
connectionLocation: internetData.connectionLocation,
homeId: internetData.homeId, homeId: internetData.homeId,
activationCode: internetData.activationCode, activationCode: internetData.activationCode,
}; };

View File

@ -59,6 +59,7 @@ export async function addInvoice(energyContractDetailsId: number, data: CreateIn
return prisma.invoice.create({ return prisma.invoice.create({
data: { data: {
energyContractDetailsId, energyContractDetailsId,
contractId: energyDetails.contractId,
invoiceDate: data.invoiceDate, invoiceDate: data.invoiceDate,
invoiceType: data.invoiceType, invoiceType: data.invoiceType,
documentPath: data.documentPath, 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 * Rechnung aktualisieren
*/ */

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 Mobilfunk die Mobilfunknummer und wenn vorhanden Karteninhaber
Bei Festnetz, die Anschlussadresse/Lieferadresse Bei Festnetz, die Anschlussadresse/Lieferadresse
Bei KFZ das Kennzeichen Bei KFZ das Kennzeichen
#
#erledigt #erledigt
Datenschutzerklärung wenn PDF hinterlegt wurde, alle Haken auf Grün setzten. 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 PDF hochgeladen → alle 4 Online-Consents automatisch auf GRANTED setzen
Kunde entfernt einen Haken im Portal → PDF löschen + Tabs sperren Kunde entfernt einen Haken im Portal → PDF löschen + Tabs sperren
Entsperrung nur durch: alle Haken wieder setzen ODER neues PDF hochladen Entsperrung nur durch: alle Haken wieder setzen ODER neues PDF hochladen
#
#erledigt #erledigt
Zweitarif (Gibt es auch 3 Tarifuzähler?) Zähler HT/NT bei Strom Zähler hinzufügen. 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 Jetzt : 1.1.2026
Und gewollt 01.01.2026 Und gewollt 01.01.2026
#
#erledigt #erledigt
Die Auditmeldungen aussagekräftig Die Auditmeldungen aussagekräftig
#
Email Log und system testen Email Log und system testen
Sprich senden und Empfnagen Sprich senden und Empfnagen
#
Security System testen Security System testen
#
#erledigt
Datenschutzerklärung Website unserer Seite und ein impressum im Kundenportal. Datenschutzerklärung Website unserer Seite und ein impressum im Kundenportal.
Auch wieder über das Einstellungsmenü editirerbar. Auch wieder über das Einstellungsmenü editirerbar.
Bitte mach mir da auch einen Vorschagstext rein Bitte mach mir da auch einen Vorschagstext rein
#
Geburtstagskalender, und Geburtgsgruß als Modal beim ersten Login an dem Tag, 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 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
#

View File

@ -17,15 +17,17 @@ const invoiceTypeLabels: Record<InvoiceType, string> = {
}; };
interface InvoicesSectionProps { interface InvoicesSectionProps {
ecdId: number; // energyContractDetailsId ecdId?: number; // energyContractDetailsId (optional - für Energie-Verträge)
invoices: Invoice[]; invoices: Invoice[];
contractId: number; contractId: number;
canEdit: boolean; canEdit: boolean;
showInvoiceWarnings?: boolean; // Warnungen für fehlende Schluss-/Zwischenrechnung (nur Energie)
} }
export default function InvoicesSection({ export default function InvoicesSection({
ecdId, ecdId,
invoices, invoices,
showInvoiceWarnings = false,
contractId, contractId,
canEdit, canEdit,
}: InvoicesSectionProps) { }: InvoicesSectionProps) {
@ -35,9 +37,9 @@ export default function InvoicesSection({
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const deleteInvoiceMutation = useMutation({ const deleteInvoiceMutation = useMutation({
mutationFn: (invoiceId: number) => invoiceApi.deleteInvoice(ecdId, invoiceId), mutationFn: (invoiceId: number) => ecdId ? invoiceApi.deleteInvoice(ecdId, invoiceId) : invoiceApi.deleteInvoice(0, invoiceId),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contract', contractId.toString()] }); queryClient.invalidateQueries({ queryKey: ['contract'] });
}, },
}); });
@ -56,18 +58,18 @@ export default function InvoicesSection({
<FileText className="w-4 h-4 text-gray-500" /> <FileText className="w-4 h-4 text-gray-500" />
<h4 className="text-sm font-medium text-gray-700">Rechnungen</h4> <h4 className="text-sm font-medium text-gray-700">Rechnungen</h4>
<Badge variant="default">{invoices.length}</Badge> <Badge variant="default">{invoices.length}</Badge>
{/* Status-Indicator */} {/* Status-Indicator (nur bei Energie-Verträgen) */}
{hasFinalInvoice ? ( {showInvoiceWarnings && hasFinalInvoice ? (
<span className="flex items-center gap-1 px-2 py-0.5 text-xs rounded-full bg-green-100 text-green-800"> <span className="flex items-center gap-1 px-2 py-0.5 text-xs rounded-full bg-green-100 text-green-800">
<Check className="w-3 h-3" /> <Check className="w-3 h-3" />
Schlussrechnung Schlussrechnung
</span> </span>
) : hasNotAvailable ? ( ) : showInvoiceWarnings && hasNotAvailable ? (
<span className="flex items-center gap-1 px-2 py-0.5 text-xs rounded-full bg-yellow-100 text-yellow-800"> <span className="flex items-center gap-1 px-2 py-0.5 text-xs rounded-full bg-yellow-100 text-yellow-800">
<AlertTriangle className="w-3 h-3" /> <AlertTriangle className="w-3 h-3" />
Nicht verfügbar Nicht verfügbar
</span> </span>
) : invoices.length > 0 ? ( ) : showInvoiceWarnings && invoices.length > 0 ? (
<span className="flex items-center gap-1 px-2 py-0.5 text-xs rounded-full bg-orange-100 text-orange-800"> <span className="flex items-center gap-1 px-2 py-0.5 text-xs rounded-full bg-orange-100 text-orange-800">
<AlertTriangle className="w-3 h-3" /> <AlertTriangle className="w-3 h-3" />
Schlussrechnung fehlt Schlussrechnung fehlt
@ -198,7 +200,7 @@ function InvoiceModal({
}: { }: {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
ecdId: number; ecdId?: number;
contractId: number; contractId: number;
invoice?: Invoice | null; invoice?: Invoice | null;
}) { }) {
@ -216,10 +218,16 @@ function InvoiceModal({
const [selectedFile, setSelectedFile] = useState<File | null>(null); const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const addInvoiceFn = async (data: { invoiceDate: string; invoiceType: string; notes?: string }) => {
if (ecdId) {
return invoiceApi.addInvoice(ecdId, data as any);
}
return invoiceApi.addInvoiceByContract(contractId, data as any);
};
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: async (file: File) => { mutationFn: async (file: File) => {
// 1. Invoice erstellen const result = await addInvoiceFn({
const result = await invoiceApi.addInvoice(ecdId, {
invoiceDate: formData.invoiceDate, invoiceDate: formData.invoiceDate,
invoiceType: formData.invoiceType, invoiceType: formData.invoiceType,
notes: formData.notes || undefined, notes: formData.notes || undefined,
@ -233,7 +241,7 @@ function InvoiceModal({
return result; return result;
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contract', contractId.toString()] }); queryClient.invalidateQueries({ queryKey: ['contract'] });
onClose(); onClose();
}, },
onError: (err: Error) => { onError: (err: Error) => {
@ -243,15 +251,14 @@ function InvoiceModal({
const createWithoutFileMutation = useMutation({ const createWithoutFileMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
// Für NOT_AVAILABLE Typ - kein Dokument erforderlich return addInvoiceFn({
return await invoiceApi.addInvoice(ecdId, {
invoiceDate: formData.invoiceDate, invoiceDate: formData.invoiceDate,
invoiceType: formData.invoiceType, invoiceType: formData.invoiceType,
notes: formData.notes || undefined, notes: formData.notes || undefined,
}); });
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contract', contractId.toString()] }); queryClient.invalidateQueries({ queryKey: ['contract'] });
onClose(); onClose();
}, },
onError: (err: Error) => { onError: (err: Error) => {
@ -262,7 +269,7 @@ function InvoiceModal({
const updateMutation = useMutation({ const updateMutation = useMutation({
mutationFn: async (file: File | null) => { mutationFn: async (file: File | null) => {
// 1. Invoice aktualisieren // 1. Invoice aktualisieren
const result = await invoiceApi.updateInvoice(ecdId, invoice!.id, { const result = await invoiceApi.updateInvoice(ecdId || 0, invoice!.id, {
invoiceDate: formData.invoiceDate, invoiceDate: formData.invoiceDate,
invoiceType: formData.invoiceType, invoiceType: formData.invoiceType,
notes: formData.notes || undefined, notes: formData.notes || undefined,
@ -276,7 +283,7 @@ function InvoiceModal({
return result; return result;
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contract', contractId.toString()] }); queryClient.invalidateQueries({ queryKey: ['contract'] });
onClose(); onClose();
}, },
onError: (err: Error) => { onError: (err: Error) => {

View File

@ -13,11 +13,11 @@ import Badge from '../../components/ui/Badge';
import Input from '../../components/ui/Input'; import Input from '../../components/ui/Input';
import Modal from '../../components/ui/Modal'; import Modal from '../../components/ui/Modal';
import FileUpload from '../../components/ui/FileUpload'; import FileUpload from '../../components/ui/FileUpload';
import { Edit, Trash2, Copy, Eye, EyeOff, ArrowLeft, ArrowRight, Download, ExternalLink, Plus, ChevronDown, ChevronUp, Gauge, CheckCircle, Circle, ClipboardList, MessageSquare, Calculator, Info, X, BellOff, Lock, Shield } from 'lucide-react'; import { Edit, Trash2, Copy, Eye, EyeOff, ArrowLeft, ArrowRight, Download, ExternalLink, Plus, ChevronDown, ChevronUp, Gauge, CheckCircle, Circle, ClipboardList, MessageSquare, Calculator, Info, X, BellOff, Lock, Shield, FileText } from 'lucide-react';
import { calculateConsumption, calculateCosts, calculateMultiMeterConsumption } from '../../utils/energyCalculations'; import { calculateConsumption, calculateCosts, calculateMultiMeterConsumption } from '../../utils/energyCalculations';
import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton'; import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton';
import { formatDate } from '../../utils/dateFormat'; import { formatDate } from '../../utils/dateFormat';
import type { ContractType, ContractStatus, SimCard, MeterReading, ContractTask, ContractTaskSubtask, ContractMeter } from '../../types'; import type { ContractType, ContractStatus, SimCard, MeterReading, ContractTask, ContractTaskSubtask, ContractMeter, ContractDocument } from '../../types';
const typeLabels: Record<ContractType, string> = { const typeLabels: Record<ContractType, string> = {
ELECTRICITY: 'Strom', ELECTRICITY: 'Strom',
@ -2576,6 +2576,7 @@ export default function ContractDetail() {
invoices={c.energyDetails.invoices || []} invoices={c.energyDetails.invoices || []}
contractId={contractId} contractId={contractId}
canEdit={hasPermission('contracts:update') && !isCustomer} canEdit={hasPermission('contracts:update') && !isCustomer}
showInvoiceWarnings={true}
/> />
</Card> </Card>
)} )}
@ -2614,6 +2615,24 @@ export default function ContractDetail() {
</dd> </dd>
</div> </div>
)} )}
{c.internetDetails.propertyType && (
<div>
<dt className="text-sm text-gray-500">Objekttyp</dt>
<dd>{c.internetDetails.propertyType}</dd>
</div>
)}
{c.internetDetails.propertyLocation && (
<div>
<dt className="text-sm text-gray-500">Lage</dt>
<dd>{c.internetDetails.propertyLocation}</dd>
</div>
)}
{c.internetDetails.connectionLocation && (
<div>
<dt className="text-sm text-gray-500">Anschluss-Lage</dt>
<dd>{c.internetDetails.connectionLocation}</dd>
</div>
)}
{c.internetDetails.installationDate && ( {c.internetDetails.installationDate && (
<div> <div>
<dt className="text-sm text-gray-500">Installation</dt> <dt className="text-sm text-gray-500">Installation</dt>
@ -2960,6 +2979,25 @@ export default function ContractDetail() {
isCustomerPortal={isCustomerPortal} isCustomerPortal={isCustomerPortal}
/> />
{/* Rechnungen (bei allen Vertragstypen, außer Energie - die haben ihre eigene Section) */}
{!['ELECTRICITY', 'GAS'].includes(c.type) && !isCustomerPortal && (
<Card className="mb-6">
<InvoicesSection
invoices={c.invoices || []}
contractId={contractId}
canEdit={hasPermission('contracts:update')}
/>
</Card>
)}
{/* Vertragsdokumente */}
{!isCustomerPortal && (
<ContractDocumentsSection
contractId={contractId}
canEdit={hasPermission('contracts:update')}
/>
)}
{/* Zugeordnete E-Mails */} {/* Zugeordnete E-Mails */}
{!isCustomerPortal && hasPermission('contracts:read') && c.customerId && ( {!isCustomerPortal && hasPermission('contracts:read') && c.customerId && (
<ContractEmailsSection <ContractEmailsSection
@ -3057,3 +3095,177 @@ export default function ContractDetail() {
</div> </div>
); );
} }
// ==================== VERTRAGSDOKUMENTE ====================
const DOCUMENT_TYPES = [
'Auftragsformular',
'Auftragsbestätigung',
'Lieferbestätigung',
'Vertragsunterlagen',
'Vollmacht',
'Widerrufsbelehrung',
'Preisblatt',
'Sonstiges',
];
function ContractDocumentsSection({
contractId,
canEdit,
}: {
contractId: number;
canEdit: boolean;
}) {
const queryClient = useQueryClient();
const [showUpload, setShowUpload] = useState(false);
const [uploadType, setUploadType] = useState(DOCUMENT_TYPES[0]);
const [uploadNotes, setUploadNotes] = useState('');
const { data: docsData } = useQuery({
queryKey: ['contract-documents', contractId],
queryFn: () => contractApi.getDocuments(contractId),
});
const uploadMutation = useMutation({
mutationFn: ({ file, documentType, notes }: { file: File; documentType: string; notes?: string }) =>
contractApi.uploadDocument(contractId, file, documentType, notes),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contract-documents', contractId] });
setShowUpload(false);
setUploadNotes('');
},
});
const deleteMutation = useMutation({
mutationFn: (documentId: number) => contractApi.deleteDocument(contractId, documentId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contract-documents', contractId] });
},
});
const documents: ContractDocument[] = docsData?.data || [];
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
uploadMutation.mutate({ file, documentType: uploadType, notes: uploadNotes || undefined });
}
};
return (
<Card className="mb-6" title={
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<FileText className="w-5 h-5" />
<span>Vertragsdokumente</span>
<span className="text-sm font-normal text-gray-500">({documents.length})</span>
</div>
{canEdit && (
<Button variant="ghost" size="sm" onClick={() => setShowUpload(!showUpload)}>
<Plus className="w-4 h-4" />
</Button>
)}
</div>
}>
{/* Upload-Bereich */}
{showUpload && (
<div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Dokumenttyp</label>
<select
value={uploadType}
onChange={(e) => setUploadType(e.target.value)}
className="block w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
>
{DOCUMENT_TYPES.map((t) => (
<option key={t} value={t}>{t}</option>
))}
</select>
</div>
<div className="sm:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Notiz (optional)</label>
<input
type="text"
value={uploadNotes}
onChange={(e) => setUploadNotes(e.target.value)}
placeholder="z.B. Unterschrieben am 15.03.2026"
className="block w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
/>
</div>
</div>
<div className="flex items-center gap-3">
<label className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer text-sm">
<Plus className="w-4 h-4" />
Datei wählen (PDF, JPG, PNG)
<input type="file" accept=".pdf,.jpg,.jpeg,.png" className="hidden" onChange={handleFileSelect} />
</label>
<Button variant="secondary" size="sm" onClick={() => setShowUpload(false)}>Abbrechen</Button>
{uploadMutation.isPending && <span className="text-sm text-gray-500">Hochladen...</span>}
</div>
{uploadMutation.isError && (
<p className="text-xs text-red-600 mt-2">Fehler beim Hochladen</p>
)}
</div>
)}
{/* Dokumentliste */}
{documents.length === 0 ? (
<p className="text-sm text-gray-500">Keine Dokumente vorhanden.</p>
) : (
<div className="space-y-2">
{documents.map((doc) => (
<div key={doc.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center gap-3">
<FileText className="w-4 h-4 text-gray-400" />
<div>
<div className="flex items-center gap-2">
<span className="text-xs px-2 py-0.5 rounded-full bg-blue-100 text-blue-700">
{doc.documentType}
</span>
<a
href={`/api${doc.documentPath}`}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:underline"
>
{doc.originalName}
</a>
</div>
<div className="flex items-center gap-3 mt-0.5 text-xs text-gray-500">
<span>{formatDate(doc.createdAt)}</span>
{doc.uploadedBy && <span>von {doc.uploadedBy}</span>}
{doc.notes && <span> {doc.notes}</span>}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<a
href={`/api${doc.documentPath}`}
download
className="text-gray-400 hover:text-blue-600"
title="Herunterladen"
>
<Download className="w-4 h-4" />
</a>
{canEdit && (
<button
onClick={() => {
if (confirm(`Dokument "${doc.originalName}" wirklich löschen?`)) {
deleteMutation.mutate(doc.id);
}
}}
className="text-gray-400 hover:text-red-600"
title="Löschen"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
</div>
))}
</div>
)}
</Card>
);
}

View File

@ -288,6 +288,9 @@ export default function ContractForm() {
routerSerialNumber: c.internetDetails?.routerSerialNumber || '', routerSerialNumber: c.internetDetails?.routerSerialNumber || '',
installationDate: c.internetDetails?.installationDate ? c.internetDetails.installationDate.split('T')[0] : '', installationDate: c.internetDetails?.installationDate ? c.internetDetails.installationDate.split('T')[0] : '',
internetUsername: c.internetDetails?.internetUsername || '', internetUsername: c.internetDetails?.internetUsername || '',
propertyType: c.internetDetails?.propertyType || '',
propertyLocation: c.internetDetails?.propertyLocation || '',
connectionLocation: c.internetDetails?.connectionLocation || '',
homeId: c.internetDetails?.homeId || '', homeId: c.internetDetails?.homeId || '',
activationCode: c.internetDetails?.activationCode || '', activationCode: c.internetDetails?.activationCode || '',
// Mobile details // Mobile details
@ -531,6 +534,10 @@ export default function ContractForm() {
// Internet-Zugangsdaten // Internet-Zugangsdaten
internetUsername: emptyToNull(data.internetUsername), internetUsername: emptyToNull(data.internetUsername),
internetPassword: data.internetPassword || undefined, // Passwort: undefined = nicht ändern internetPassword: data.internetPassword || undefined, // Passwort: undefined = nicht ändern
// Objekt & Lage
propertyType: emptyToNull(data.propertyType),
propertyLocation: emptyToNull(data.propertyLocation),
connectionLocation: emptyToNull(data.connectionLocation),
// Glasfaser-spezifisch // Glasfaser-spezifisch
homeId: emptyToNull(data.homeId), homeId: emptyToNull(data.homeId),
// Vodafone DSL/Kabel spezifisch // Vodafone DSL/Kabel spezifisch
@ -1027,6 +1034,65 @@ export default function ContractForm() {
value={watch('installationDate') || ''} value={watch('installationDate') || ''}
onClear={() => setValue('installationDate', '')} onClear={() => setValue('installationDate', '')}
/> />
</div>
{/* Objekt & Lage */}
<h4 className="text-sm font-medium text-gray-700 mt-4 mb-2">Objekt & Lage</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Select
label="Objekttyp"
{...register('propertyType')}
options={[
{ value: '', label: 'Bitte wählen...' },
{ value: 'Mehrparteienhaus', label: 'Mehrparteienhaus' },
{ value: 'Freistehendes Haus', label: 'Freistehendes Haus' },
{ value: 'Doppelhaushälfte', label: 'Doppelhaushälfte' },
{ value: 'Reihenhaus', label: 'Reihenhaus' },
{ value: 'Wohnung', label: 'Wohnung' },
{ value: 'Bürogebäude', label: 'Bürogebäude' },
{ value: 'Gewerbeeinheit', label: 'Gewerbeeinheit' },
]}
/>
<Select
label="Lage"
{...register('propertyLocation')}
options={[
{ value: '', label: 'Bitte wählen...' },
{ value: 'Vorderhaus', label: 'Vorderhaus' },
{ value: 'Hinterhaus', label: 'Hinterhaus' },
{ value: 'Links', label: 'Links' },
{ value: 'Rechts', label: 'Rechts' },
{ value: 'Mitte', label: 'Mitte' },
{ value: 'Keller', label: 'Keller' },
{ value: 'Souterrain', label: 'Souterrain' },
{ value: 'Erdgeschoss', label: 'Erdgeschoss' },
...[...Array(25)].map((_, i) => ({ value: `${i + 1}. OG`, label: `${i + 1}. Obergeschoss` })),
{ value: 'Dachgeschoss', label: 'Dachgeschoss' },
]}
/>
<Select
label="Lage des Anschlusses"
{...register('connectionLocation')}
options={[
{ value: '', label: 'Bitte wählen...' },
{ value: 'Flur', label: 'Flur' },
{ value: 'Wohnzimmer', label: 'Wohnzimmer' },
{ value: 'Schlafzimmer', label: 'Schlafzimmer' },
{ value: 'Kinderzimmer', label: 'Kinderzimmer' },
{ value: 'Küche', label: 'Küche' },
{ value: 'Büro', label: 'Büro' },
{ value: 'HWR', label: 'Hauswirtschaftsraum (HWR)' },
{ value: 'Hausanschlussraum', label: 'Hausanschlussraum' },
{ value: 'Abstellraum', label: 'Abstellraum' },
{ value: 'Garage', label: 'Garage' },
{ value: 'Serverraum', label: 'Serverraum' },
{ value: 'Empfang', label: 'Empfang / Rezeption' },
{ value: 'Keller', label: 'Keller' },
]}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
{/* HomeID nur bei Glasfaser */} {/* HomeID nur bei Glasfaser */}
{contractType === 'FIBER' && ( {contractType === 'FIBER' && (
<Input label="Home-ID" {...register('homeId')} /> <Input label="Home-ID" {...register('homeId')} />

View File

@ -232,6 +232,10 @@ export const invoiceApi = {
const res = await api.post<ApiResponse<Invoice>>(`/energy-details/${ecdId}/invoices`, data); const res = await api.post<ApiResponse<Invoice>>(`/energy-details/${ecdId}/invoices`, data);
return res.data; return res.data;
}, },
addInvoiceByContract: async (contractId: number, data: Partial<Invoice>) => {
const res = await api.post<ApiResponse<Invoice>>(`/contracts/${contractId}/invoices`, data);
return res.data;
},
updateInvoice: async (ecdId: number, invoiceId: number, data: Partial<Invoice>) => { updateInvoice: async (ecdId: number, invoiceId: number, data: Partial<Invoice>) => {
const res = await api.put<ApiResponse<Invoice>>(`/energy-details/${ecdId}/invoices/${invoiceId}`, data); const res = await api.put<ApiResponse<Invoice>>(`/energy-details/${ecdId}/invoices/${invoiceId}`, data);
return res.data; return res.data;
@ -655,6 +659,25 @@ export const contractApi = {
const res = await api.get<ApiResponse<import('../types').CockpitResult>>('/contracts/cockpit'); const res = await api.get<ApiResponse<import('../types').CockpitResult>>('/contracts/cockpit');
return res.data; return res.data;
}, },
// Vertragsdokumente
getDocuments: async (contractId: number) => {
const res = await api.get<ApiResponse<import('../types').ContractDocument[]>>(`/contracts/${contractId}/documents`);
return res.data;
},
uploadDocument: async (contractId: number, file: File, documentType: string, notes?: string) => {
const formData = new FormData();
formData.append('file', file);
formData.append('documentType', documentType);
if (notes) formData.append('notes', notes);
const res = await api.post<ApiResponse<import('../types').ContractDocument>>(`/contracts/${contractId}/documents`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteDocument: async (contractId: number, documentId: number) => {
const res = await api.delete<ApiResponse<void>>(`/contracts/${contractId}/documents/${documentId}`);
return res.data;
},
// Folgezähler // Folgezähler
addSuccessorMeter: async (contractId: number, data: { meterId: number; installedAt?: string; finalReadingPrevious?: number }) => { addSuccessorMeter: async (contractId: number, data: { meterId: number; installedAt?: string; finalReadingPrevious?: number }) => {
const res = await api.post<ApiResponse<any>>(`/contracts/${contractId}/successor-meter`, data); const res = await api.post<ApiResponse<any>>(`/contracts/${contractId}/successor-meter`, data);

View File

@ -152,6 +152,17 @@ export interface IdentityDocument {
licenseIssueDate?: string; licenseIssueDate?: string;
} }
export interface ContractDocument {
id: number;
contractId: number;
documentType: string;
documentPath: string;
originalName: string;
notes?: string;
uploadedBy?: string;
createdAt: string;
}
export type MeterTariffModel = 'SINGLE' | 'DUAL'; export type MeterTariffModel = 'SINGLE' | 'DUAL';
export interface Meter { export interface Meter {
@ -391,6 +402,8 @@ export interface Contract {
mobileDetails?: MobileContractDetails; mobileDetails?: MobileContractDetails;
tvDetails?: TvContractDetails; tvDetails?: TvContractDetails;
carInsuranceDetails?: CarInsuranceDetails; carInsuranceDetails?: CarInsuranceDetails;
invoices?: Invoice[];
documents?: ContractDocument[];
// Snooze: Vertrag zurückstellen // Snooze: Vertrag zurückstellen
nextReviewDate?: string; nextReviewDate?: string;
followUpContract?: { followUpContract?: {
@ -431,6 +444,10 @@ export interface InternetContractDetails {
// Internet-Zugangsdaten // Internet-Zugangsdaten
internetUsername?: string; internetUsername?: string;
internetPasswordEncrypted?: string; internetPasswordEncrypted?: string;
// Objekt & Lage
propertyType?: string;
propertyLocation?: string;
connectionLocation?: string;
// Glasfaser-spezifisch // Glasfaser-spezifisch
homeId?: string; homeId?: string;
// Vodafone DSL/Kabel spezifisch // Vodafone DSL/Kabel spezifisch