added invoices and status in cockpit, created info button for contract status types
This commit is contained in:
Vendored
+2
@@ -28,6 +28,7 @@ const contractTask_routes_js_1 = __importDefault(require("./routes/contractTask.
|
||||
const appSetting_routes_js_1 = __importDefault(require("./routes/appSetting.routes.js"));
|
||||
const emailProvider_routes_js_1 = __importDefault(require("./routes/emailProvider.routes.js"));
|
||||
const cachedEmail_routes_js_1 = __importDefault(require("./routes/cachedEmail.routes.js"));
|
||||
const invoice_routes_js_1 = __importDefault(require("./routes/invoice.routes.js"));
|
||||
dotenv_1.default.config();
|
||||
const app = (0, express_1.default)();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
@@ -58,6 +59,7 @@ app.use('/api', contractTask_routes_js_1.default);
|
||||
app.use('/api/settings', appSetting_routes_js_1.default);
|
||||
app.use('/api/email-providers', emailProvider_routes_js_1.default);
|
||||
app.use('/api', cachedEmail_routes_js_1.default);
|
||||
app.use('/api/energy-details', invoice_routes_js_1.default);
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAAA,sDAA8B;AAC9B,gDAAwB;AACxB,gDAAwB;AACxB,oDAA4B;AAE5B,6EAAiD;AACjD,qFAAyD;AACzD,mFAAuD;AACvD,qFAAyD;AACzD,qFAAyD;AACzD,+EAAmD;AACnD,mGAAuE;AACvE,qFAAyD;AACzD,qFAAyD;AACzD,2GAA8E;AAC9E,uGAA0E;AAC1E,qFAAyD;AACzD,iFAAqD;AACrD,6EAAiD;AACjD,iFAAqD;AACrD,uFAA2D;AAC3D,qGAAyE;AACzE,6FAAiE;AACjE,yFAA6D;AAC7D,+FAAmE;AACnE,2FAA+D;AAE/D,gBAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;AACtB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;AAEtC,aAAa;AACb,GAAG,CAAC,GAAG,CAAC,IAAA,cAAI,GAAE,CAAC,CAAC;AAChB,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AAExB,gCAAgC;AAChC,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,iBAAO,CAAC,MAAM,CAAC,cAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;AAE7E,SAAS;AACT,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,wBAAU,CAAC,CAAC;AACjC,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,4BAAc,CAAC,CAAC;AAC1C,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,2BAAa,CAAC,CAAC;AACzC,GAAG,CAAC,GAAG,CAAC,iBAAiB,EAAE,4BAAc,CAAC,CAAC;AAC3C,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,4BAAc,CAAC,CAAC;AAC1C,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,yBAAW,CAAC,CAAC;AACpC,GAAG,CAAC,GAAG,CAAC,wBAAwB,EAAE,mCAAqB,CAAC,CAAC;AACzD,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,4BAAc,CAAC,CAAC;AAC1C,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,4BAAc,CAAC,CAAC;AAC1C,GAAG,CAAC,GAAG,CAAC,2BAA2B,EAAE,uCAAwB,CAAC,CAAC;AAC/D,GAAG,CAAC,GAAG,CAAC,yBAAyB,EAAE,qCAAsB,CAAC,CAAC;AAC3D,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,4BAAc,CAAC,CAAC;AAC1C,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,0BAAY,CAAC,CAAC;AACtC,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,wBAAU,CAAC,CAAC;AAClC,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,0BAAY,CAAC,CAAC;AACrC,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,6BAAe,CAAC,CAAC;AAC3C,GAAG,CAAC,GAAG,CAAC,0BAA0B,EAAE,oCAAsB,CAAC,CAAC;AAC5D,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,gCAAkB,CAAC,CAAC;AACpC,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,8BAAgB,CAAC,CAAC;AAC3C,GAAG,CAAC,GAAG,CAAC,sBAAsB,EAAE,iCAAmB,CAAC,CAAC;AACrD,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,+BAAiB,CAAC,CAAC;AAEnC,eAAe;AACf,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAClC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;AAClE,CAAC,CAAC,CAAC;AAEH,iBAAiB;AACjB,GAAG,CAAC,GAAG,CAAC,CAAC,GAAU,EAAE,GAAoB,EAAE,GAAqB,EAAE,IAA0B,EAAE,EAAE;IAC9F,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACzB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAC;AAC3E,CAAC,CAAC,CAAC;AAEH,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IACpB,OAAO,CAAC,GAAG,CAAC,yBAAyB,IAAI,EAAE,CAAC,CAAC;AAC/C,CAAC,CAAC,CAAC"}
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"upload.routes.d.ts","sourceRoot":"","sources":["../../src/routes/upload.routes.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA4oBxB,eAAe,MAAM,CAAC"}
|
||||
{"version":3,"file":"upload.routes.d.ts","sourceRoot":"","sources":["../../src/routes/upload.routes.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA2uBxB,eAAe,MAAM,CAAC"}
|
||||
Vendored
+72
@@ -469,5 +469,77 @@ router.delete('/contracts/:id/cancellation-letter-options', auth_js_1.authentica
|
||||
// Kündigungsbestätigung Optionen
|
||||
router.post('/contracts/:id/cancellation-confirmation-options', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), setUploadDir('cancellation-confirmations-options'), upload.single('document'), (req, res) => handleContractDocumentUpload(req, res, 'cancellationConfirmationOptionsPath', 'cancellation-confirmations-options'));
|
||||
router.delete('/contracts/:id/cancellation-confirmation-options', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), (req, res) => handleContractDocumentDelete(req, res, 'cancellationConfirmationOptionsPath'));
|
||||
// ==================== RECHNUNGS-DOKUMENTE ====================
|
||||
// Upload für Rechnungs-Dokument
|
||||
router.post('/invoices/:id', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), setUploadDir('invoices'), upload.single('document'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' });
|
||||
return;
|
||||
}
|
||||
const invoiceId = parseInt(req.params.id);
|
||||
const relativePath = `/uploads/invoices/${req.file.filename}`;
|
||||
// Alte Datei löschen falls vorhanden
|
||||
const invoice = await prisma.invoice.findUnique({ where: { id: invoiceId } });
|
||||
if (!invoice) {
|
||||
res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
if (invoice.documentPath) {
|
||||
const oldPath = path_1.default.join(process.cwd(), invoice.documentPath);
|
||||
if (fs_1.default.existsSync(oldPath)) {
|
||||
fs_1.default.unlinkSync(oldPath);
|
||||
}
|
||||
}
|
||||
// Invoice in der DB aktualisieren
|
||||
await prisma.invoice.update({
|
||||
where: { id: invoiceId },
|
||||
data: { documentPath: relativePath },
|
||||
});
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
path: relativePath,
|
||||
filename: req.file.filename,
|
||||
originalName: req.file.originalname,
|
||||
size: req.file.size,
|
||||
},
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Invoice upload error:', error);
|
||||
res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' });
|
||||
}
|
||||
});
|
||||
// Löschen von Rechnungs-Dokument
|
||||
router.delete('/invoices/:id', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), async (req, res) => {
|
||||
try {
|
||||
const invoiceId = parseInt(req.params.id);
|
||||
const invoice = await prisma.invoice.findUnique({ where: { id: invoiceId } });
|
||||
if (!invoice) {
|
||||
res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
if (!invoice.documentPath) {
|
||||
res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' });
|
||||
return;
|
||||
}
|
||||
// Datei löschen
|
||||
const filePath = path_1.default.join(process.cwd(), invoice.documentPath);
|
||||
if (fs_1.default.existsSync(filePath)) {
|
||||
fs_1.default.unlinkSync(filePath);
|
||||
}
|
||||
// documentPath in DB auf null setzen
|
||||
await prisma.invoice.update({
|
||||
where: { id: invoiceId },
|
||||
data: { documentPath: null },
|
||||
});
|
||||
res.json({ success: true });
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Invoice delete error:', error);
|
||||
res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' });
|
||||
}
|
||||
});
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=upload.routes.js.map
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+40
@@ -242,6 +242,16 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
|
||||
meterNumber: string;
|
||||
location: string | null;
|
||||
}) | null;
|
||||
invoices: {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
notes: string | null;
|
||||
documentPath: string | null;
|
||||
energyContractDetailsId: number;
|
||||
invoiceDate: Date;
|
||||
invoiceType: import(".prisma/client").$Enums.InvoiceType;
|
||||
}[];
|
||||
} & {
|
||||
id: number;
|
||||
meterId: number | null;
|
||||
@@ -421,6 +431,16 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
|
||||
meterNumber: string;
|
||||
location: string | null;
|
||||
}) | null;
|
||||
invoices: {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
notes: string | null;
|
||||
documentPath: string | null;
|
||||
energyContractDetailsId: number;
|
||||
invoiceDate: Date;
|
||||
invoiceType: import(".prisma/client").$Enums.InvoiceType;
|
||||
}[];
|
||||
} & {
|
||||
id: number;
|
||||
meterId: number | null;
|
||||
@@ -974,6 +994,16 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
|
||||
meterNumber: string;
|
||||
location: string | null;
|
||||
}) | null;
|
||||
invoices: {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
notes: string | null;
|
||||
documentPath: string | null;
|
||||
energyContractDetailsId: number;
|
||||
invoiceDate: Date;
|
||||
invoiceType: import(".prisma/client").$Enums.InvoiceType;
|
||||
}[];
|
||||
} & {
|
||||
id: number;
|
||||
meterId: number | null;
|
||||
@@ -1153,6 +1183,16 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
|
||||
meterNumber: string;
|
||||
location: string | null;
|
||||
}) | null;
|
||||
invoices: {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
notes: string | null;
|
||||
documentPath: string | null;
|
||||
energyContractDetailsId: number;
|
||||
invoiceDate: Date;
|
||||
invoiceType: import(".prisma/client").$Enums.InvoiceType;
|
||||
}[];
|
||||
} & {
|
||||
id: number;
|
||||
meterId: number | null;
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
@@ -121,14 +121,14 @@ async function getContractById(id, decryptPassword = false) {
|
||||
contractCategory: true,
|
||||
previousContract: {
|
||||
include: {
|
||||
energyDetails: { include: { meter: { include: { readings: true } } } },
|
||||
energyDetails: { include: { meter: { include: { readings: true } }, invoices: true } },
|
||||
internetDetails: { include: { phoneNumbers: true } },
|
||||
mobileDetails: { include: { simCards: true } },
|
||||
tvDetails: true,
|
||||
carInsuranceDetails: true,
|
||||
},
|
||||
},
|
||||
energyDetails: { include: { meter: { include: { readings: true } } } },
|
||||
energyDetails: { include: { meter: { include: { readings: true } }, invoices: true } },
|
||||
internetDetails: { include: { phoneNumbers: true } },
|
||||
mobileDetails: { include: { simCards: true } },
|
||||
tvDetails: true,
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -40,6 +40,7 @@ export interface CockpitSummary {
|
||||
contractEnding: number;
|
||||
missingCredentials: number;
|
||||
missingData: number;
|
||||
missingInvoices: number;
|
||||
openTasks: number;
|
||||
pendingContracts: number;
|
||||
};
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"contractCockpit.service.d.ts","sourceRoot":"","sources":["../../src/services/contractCockpit.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,cAAc,EACpC,MAAM,gBAAgB,CAAC;AAMxB,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAAC;AAElE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,YAAY,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,cAAc,EAAE,MAAM,CAAC;QACvB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,QAAQ,CAAC,EAAE;QACT,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,MAAM,CAAC,EAAE;QACP,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,cAAc,EAAE,YAAY,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE;QACV,qBAAqB,EAAE,MAAM,CAAC;QAC9B,cAAc,EAAE,MAAM,CAAC;QACvB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,OAAO,EAAE,cAAc,CAAC;IACxB,UAAU,EAAE;QACV,YAAY,EAAE,MAAM,CAAC;QACrB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAyED,wBAAsB,cAAc,IAAI,OAAO,CAAC,aAAa,CAAC,CA8V7D"}
|
||||
{"version":3,"file":"contractCockpit.service.d.ts","sourceRoot":"","sources":["../../src/services/contractCockpit.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,cAAc,EACpC,MAAM,gBAAgB,CAAC;AAMxB,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAAC;AAElE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,YAAY,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,cAAc,EAAE,MAAM,CAAC;QACvB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,QAAQ,CAAC,EAAE;QACT,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,MAAM,CAAC,EAAE;QACP,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,cAAc,EAAE,YAAY,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE;QACV,qBAAqB,EAAE,MAAM,CAAC;QAC9B,cAAc,EAAE,MAAM,CAAC;QACvB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;QACxB,SAAS,EAAE,MAAM,CAAC;QAClB,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,OAAO,EAAE,cAAc,CAAC;IACxB,UAAU,EAAE;QACV,YAAY,EAAE,MAAM,CAAC;QACrB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAyED,wBAAsB,cAAc,IAAI,OAAO,CAAC,aAAa,CAAC,CAqa7D"}
|
||||
+66
-2
@@ -105,11 +105,11 @@ async function getCockpitData() {
|
||||
const criticalDays = parseInt(settings.deadlineCriticalDays) || 14;
|
||||
const warningDays = parseInt(settings.deadlineWarningDays) || 42;
|
||||
const okDays = parseInt(settings.deadlineOkDays) || 90;
|
||||
// Lade alle aktiven/pending Verträge mit allen relevanten Daten
|
||||
// Lade alle relevanten Verträge (inkl. CANCELLED/DEACTIVATED für Schlussrechnung-Check)
|
||||
const contracts = await prisma.contract.findMany({
|
||||
where: {
|
||||
status: {
|
||||
in: ['ACTIVE', 'PENDING', 'DRAFT'],
|
||||
in: ['ACTIVE', 'PENDING', 'DRAFT', 'CANCELLED', 'DEACTIVATED', 'EXPIRED'],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
@@ -145,6 +145,7 @@ async function getCockpitData() {
|
||||
energyDetails: {
|
||||
include: {
|
||||
meter: true,
|
||||
invoices: true,
|
||||
},
|
||||
},
|
||||
internetDetails: {
|
||||
@@ -186,6 +187,7 @@ async function getCockpitData() {
|
||||
contractEnding: 0,
|
||||
missingCredentials: 0,
|
||||
missingData: 0,
|
||||
missingInvoices: 0,
|
||||
openTasks: 0,
|
||||
pendingContracts: 0,
|
||||
},
|
||||
@@ -367,6 +369,68 @@ async function getCockpitData() {
|
||||
});
|
||||
summary.byCategory.pendingContracts++;
|
||||
}
|
||||
// 13. ENERGIE-RECHNUNGEN (nur für ELECTRICITY und GAS)
|
||||
if (['ELECTRICITY', 'GAS'].includes(contract.type) && contract.energyDetails) {
|
||||
const invoices = contract.energyDetails.invoices || [];
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
// 13a. SCHLUSSRECHNUNG FEHLT (nur wenn Vertrag gekündigt/deaktiviert ist)
|
||||
// "Beendet" = CANCELLED oder DEACTIVATED (nicht nur Laufzeit abgelaufen!)
|
||||
const isContractTerminated = contract.status === 'CANCELLED' || contract.status === 'DEACTIVATED';
|
||||
if (isContractTerminated) {
|
||||
const hasFinalInvoice = invoices.some(inv => inv.invoiceType === 'FINAL');
|
||||
const hasNotAvailable = invoices.some(inv => inv.invoiceType === 'NOT_AVAILABLE');
|
||||
if (!hasFinalInvoice && !hasNotAvailable) {
|
||||
issues.push({
|
||||
type: 'missing_final_invoice',
|
||||
label: 'Schlussrechnung fehlt',
|
||||
urgency: 'warning',
|
||||
details: 'Vertrag gekündigt/deaktiviert, aber keine Schlussrechnung vorhanden',
|
||||
});
|
||||
summary.byCategory.missingInvoices++;
|
||||
}
|
||||
}
|
||||
// 13b. ZWISCHENRECHNUNG FEHLT/ÜBERFÄLLIG (wenn Vertrag > 12 Monate läuft)
|
||||
// Für alle Status außer DRAFT und nicht gekündigt/deaktiviert
|
||||
// Auch EXPIRED zählt hier, da der Vertrag ohne Kündigung weiterläuft!
|
||||
if (contract.startDate && contract.status !== 'DRAFT' && !isContractTerminated) {
|
||||
const startDate = new Date(contract.startDate);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
const daysSinceStart = Math.floor((now.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
if (daysSinceStart > 365) {
|
||||
// Vertrag läuft > 12 Monate
|
||||
if (invoices.length === 0) {
|
||||
// Keine Rechnungen vorhanden
|
||||
issues.push({
|
||||
type: 'missing_interim_invoice',
|
||||
label: 'Zwischenrechnung fehlt',
|
||||
urgency: 'warning',
|
||||
details: 'Vertrag läuft über 12 Monate ohne Rechnung',
|
||||
});
|
||||
summary.byCategory.missingInvoices++;
|
||||
}
|
||||
else {
|
||||
// Prüfen ob letzte Rechnung > 12 Monate alt
|
||||
const latestInvoice = invoices
|
||||
.filter(inv => inv.invoiceType !== 'NOT_AVAILABLE')
|
||||
.sort((a, b) => new Date(b.invoiceDate).getTime() - new Date(a.invoiceDate).getTime())[0];
|
||||
if (latestInvoice) {
|
||||
const invoiceDate = new Date(latestInvoice.invoiceDate);
|
||||
const daysSinceInvoice = Math.floor((now.getTime() - invoiceDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
if (daysSinceInvoice > 365) {
|
||||
issues.push({
|
||||
type: 'overdue_interim_invoice',
|
||||
label: 'Zwischenrechnung überfällig',
|
||||
urgency: 'warning',
|
||||
details: `Letzte Rechnung vor ${Math.floor(daysSinceInvoice / 30)} Monaten`,
|
||||
});
|
||||
summary.byCategory.missingInvoices++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Nur Verträge mit Issues hinzufügen
|
||||
if (issues.length > 0) {
|
||||
const highestUrgency = getHighestUrgency(issues);
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+21
-3
File diff suppressed because one or more lines are too long
+18
@@ -476,6 +476,17 @@ exports.Prisma.EnergyContractDetailsScalarFieldEnum = {
|
||||
previousCustomerNumber: 'previousCustomerNumber'
|
||||
};
|
||||
|
||||
exports.Prisma.InvoiceScalarFieldEnum = {
|
||||
id: 'id',
|
||||
energyContractDetailsId: 'energyContractDetailsId',
|
||||
invoiceDate: 'invoiceDate',
|
||||
invoiceType: 'invoiceType',
|
||||
documentPath: 'documentPath',
|
||||
notes: 'notes',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.InternetContractDetailsScalarFieldEnum = {
|
||||
id: 'id',
|
||||
contractId: 'contractId',
|
||||
@@ -624,6 +635,12 @@ exports.ContractTaskStatus = exports.$Enums.ContractTaskStatus = {
|
||||
COMPLETED: 'COMPLETED'
|
||||
};
|
||||
|
||||
exports.InvoiceType = exports.$Enums.InvoiceType = {
|
||||
INTERIM: 'INTERIM',
|
||||
FINAL: 'FINAL',
|
||||
NOT_AVAILABLE: 'NOT_AVAILABLE'
|
||||
};
|
||||
|
||||
exports.InsuranceType = exports.$Enums.InsuranceType = {
|
||||
LIABILITY: 'LIABILITY',
|
||||
PARTIAL: 'PARTIAL',
|
||||
@@ -657,6 +674,7 @@ exports.Prisma.ModelName = {
|
||||
ContractTask: 'ContractTask',
|
||||
ContractTaskSubtask: 'ContractTaskSubtask',
|
||||
EnergyContractDetails: 'EnergyContractDetails',
|
||||
Invoice: 'Invoice',
|
||||
InternetContractDetails: 'InternetContractDetails',
|
||||
PhoneNumber: 'PhoneNumber',
|
||||
MobileContractDetails: 'MobileContractDetails',
|
||||
|
||||
+1612
-1
File diff suppressed because it is too large
Load Diff
+21
-3
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "prisma-client-86f9a11dd446473e587830adde1df64246ce246e4602839fa2ced48c6aa2f902",
|
||||
"name": "prisma-client-a41de88ab23bcfe6707e2a70a25cdd4736b5f2e024b64f8f8760b0e23260313f",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"browser": "index-browser.js",
|
||||
|
||||
+25
-4
@@ -596,12 +596,18 @@ model ContractTaskSubtask {
|
||||
|
||||
// ==================== ENERGY CONTRACT DETAILS ====================
|
||||
|
||||
enum InvoiceType {
|
||||
INTERIM // Zwischenrechnung
|
||||
FINAL // Schlussrechnung
|
||||
NOT_AVAILABLE // Rechnung nicht mehr zu bekommen
|
||||
}
|
||||
|
||||
model EnergyContractDetails {
|
||||
id Int @id @default(autoincrement())
|
||||
contractId Int @unique
|
||||
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
|
||||
id Int @id @default(autoincrement())
|
||||
contractId Int @unique
|
||||
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
|
||||
meterId Int?
|
||||
meter Meter? @relation(fields: [meterId], references: [id])
|
||||
meter Meter? @relation(fields: [meterId], references: [id])
|
||||
maloId String? // Marktlokations-ID
|
||||
annualConsumption Float? // kWh für Strom, m³ für Gas
|
||||
annualConsumptionKwh Float? // kWh für Gas (zusätzlich zu m³)
|
||||
@@ -610,6 +616,21 @@ model EnergyContractDetails {
|
||||
bonus Float?
|
||||
previousProviderName String?
|
||||
previousCustomerNumber String?
|
||||
invoices Invoice[] // Rechnungen
|
||||
}
|
||||
|
||||
model Invoice {
|
||||
id Int @id @default(autoincrement())
|
||||
energyContractDetailsId Int
|
||||
energyContractDetails EnergyContractDetails @relation(fields: [energyContractDetailsId], 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
|
||||
|
||||
@@index([energyContractDetailsId])
|
||||
}
|
||||
|
||||
// ==================== INTERNET CONTRACT DETAILS ====================
|
||||
|
||||
+18
@@ -476,6 +476,17 @@ exports.Prisma.EnergyContractDetailsScalarFieldEnum = {
|
||||
previousCustomerNumber: 'previousCustomerNumber'
|
||||
};
|
||||
|
||||
exports.Prisma.InvoiceScalarFieldEnum = {
|
||||
id: 'id',
|
||||
energyContractDetailsId: 'energyContractDetailsId',
|
||||
invoiceDate: 'invoiceDate',
|
||||
invoiceType: 'invoiceType',
|
||||
documentPath: 'documentPath',
|
||||
notes: 'notes',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.InternetContractDetailsScalarFieldEnum = {
|
||||
id: 'id',
|
||||
contractId: 'contractId',
|
||||
@@ -624,6 +635,12 @@ exports.ContractTaskStatus = exports.$Enums.ContractTaskStatus = {
|
||||
COMPLETED: 'COMPLETED'
|
||||
};
|
||||
|
||||
exports.InvoiceType = exports.$Enums.InvoiceType = {
|
||||
INTERIM: 'INTERIM',
|
||||
FINAL: 'FINAL',
|
||||
NOT_AVAILABLE: 'NOT_AVAILABLE'
|
||||
};
|
||||
|
||||
exports.InsuranceType = exports.$Enums.InsuranceType = {
|
||||
LIABILITY: 'LIABILITY',
|
||||
PARTIAL: 'PARTIAL',
|
||||
@@ -657,6 +674,7 @@ exports.Prisma.ModelName = {
|
||||
ContractTask: 'ContractTask',
|
||||
ContractTaskSubtask: 'ContractTaskSubtask',
|
||||
EnergyContractDetails: 'EnergyContractDetails',
|
||||
Invoice: 'Invoice',
|
||||
InternetContractDetails: 'InternetContractDetails',
|
||||
PhoneNumber: 'PhoneNumber',
|
||||
MobileContractDetails: 'MobileContractDetails',
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `Invoice` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`energyContractDetailsId` INTEGER NOT NULL,
|
||||
`invoiceDate` DATETIME(3) NOT NULL,
|
||||
`invoiceType` ENUM('INTERIM', 'FINAL', 'NOT_AVAILABLE') NOT NULL,
|
||||
`documentPath` VARCHAR(191) NULL,
|
||||
`notes` VARCHAR(191) NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `Invoice_energyContractDetailsId_idx`(`energyContractDetailsId`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Invoice` ADD CONSTRAINT `Invoice_energyContractDetailsId_fkey` FOREIGN KEY (`energyContractDetailsId`) REFERENCES `EnergyContractDetails`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -596,20 +596,41 @@ model ContractTaskSubtask {
|
||||
|
||||
// ==================== ENERGY CONTRACT DETAILS ====================
|
||||
|
||||
enum InvoiceType {
|
||||
INTERIM // Zwischenrechnung
|
||||
FINAL // Schlussrechnung
|
||||
NOT_AVAILABLE // Rechnung nicht mehr zu bekommen
|
||||
}
|
||||
|
||||
model EnergyContractDetails {
|
||||
id Int @id @default(autoincrement())
|
||||
contractId Int @unique
|
||||
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
|
||||
id Int @id @default(autoincrement())
|
||||
contractId Int @unique
|
||||
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
|
||||
meterId Int?
|
||||
meter Meter? @relation(fields: [meterId], references: [id])
|
||||
maloId String? // Marktlokations-ID
|
||||
annualConsumption Float? // kWh für Strom, m³ für Gas
|
||||
annualConsumptionKwh Float? // kWh für Gas (zusätzlich zu m³)
|
||||
basePrice Float? // €/Monat
|
||||
unitPrice Float? // €/kWh (Arbeitspreis)
|
||||
meter Meter? @relation(fields: [meterId], references: [id])
|
||||
maloId String? // Marktlokations-ID
|
||||
annualConsumption Float? // kWh für Strom, m³ für Gas
|
||||
annualConsumptionKwh Float? // kWh für Gas (zusätzlich zu m³)
|
||||
basePrice Float? // €/Monat
|
||||
unitPrice Float? // €/kWh (Arbeitspreis)
|
||||
bonus Float?
|
||||
previousProviderName String?
|
||||
previousCustomerNumber String?
|
||||
invoices Invoice[] // Rechnungen
|
||||
}
|
||||
|
||||
model Invoice {
|
||||
id Int @id @default(autoincrement())
|
||||
energyContractDetailsId Int
|
||||
energyContractDetails EnergyContractDetails @relation(fields: [energyContractDetailsId], 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
|
||||
|
||||
@@index([energyContractDetailsId])
|
||||
}
|
||||
|
||||
// ==================== INTERNET CONTRACT DETAILS ====================
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as cachedEmailService from '../services/cachedEmail.service.js';
|
||||
import * as stressfreiEmailService from '../services/stressfreiEmail.service.js';
|
||||
import * as invoiceService from '../services/invoice.service.js';
|
||||
import { sendEmail, SmtpCredentials, SendEmailParams, EmailAttachment } from '../services/smtpService.js';
|
||||
import { fetchAttachment, appendToSent, ImapCredentials } from '../services/imapService.js';
|
||||
import { getImapSmtpSettings } from '../services/emailProvider/emailProviderService.js';
|
||||
@@ -885,6 +886,8 @@ export async function getAttachmentTargets(req: Request, res: Response): Promise
|
||||
contract?: {
|
||||
id: number;
|
||||
contractNumber: string;
|
||||
type: string;
|
||||
energyDetailsId?: number;
|
||||
slots: TargetSlot[];
|
||||
};
|
||||
}
|
||||
@@ -954,10 +957,14 @@ export async function getAttachmentTargets(req: Request, res: Response): Promise
|
||||
select: {
|
||||
id: true,
|
||||
contractNumber: true,
|
||||
type: true,
|
||||
cancellationLetterPath: true,
|
||||
cancellationConfirmationPath: true,
|
||||
cancellationLetterOptionsPath: true,
|
||||
cancellationConfirmationOptionsPath: true,
|
||||
energyDetails: {
|
||||
select: { id: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -966,6 +973,8 @@ export async function getAttachmentTargets(req: Request, res: Response): Promise
|
||||
response.contract = {
|
||||
id: contract.id,
|
||||
contractNumber: contract.contractNumber,
|
||||
type: contract.type,
|
||||
energyDetailsId: contract.energyDetails?.id,
|
||||
slots: contractTargets.map(target => ({
|
||||
key: target.key,
|
||||
label: target.label,
|
||||
@@ -1518,3 +1527,309 @@ export async function saveEmailAsPdf(req: Request, res: Response): Promise<void>
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SAVE EMAIL AS INVOICE ====================
|
||||
|
||||
// E-Mail als PDF exportieren und als Rechnung speichern
|
||||
export async function saveEmailAsInvoice(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const emailId = parseInt(req.params.id);
|
||||
const { invoiceDate, invoiceType, notes } = req.body;
|
||||
|
||||
console.log('[saveEmailAsInvoice] Request:', { emailId, invoiceDate, invoiceType, notes });
|
||||
|
||||
// Validierung
|
||||
if (!invoiceDate || !invoiceType) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'invoiceDate und invoiceType sind erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validiere invoiceType
|
||||
if (!['INTERIM', 'FINAL', 'NOT_AVAILABLE'].includes(invoiceType)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Ungültiger Rechnungstyp',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// E-Mail aus Cache laden
|
||||
const email = await cachedEmailService.getCachedEmailById(emailId);
|
||||
if (!email) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'E-Mail nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfen ob E-Mail einem Vertrag zugeordnet ist
|
||||
if (!email.contractId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'E-Mail ist keinem Vertrag zugeordnet',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vertrag laden und prüfen ob es ein Energievertrag ist
|
||||
const contract = await prisma.contract.findUnique({
|
||||
where: { id: email.contractId },
|
||||
include: { energyDetails: true },
|
||||
});
|
||||
|
||||
if (!contract) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Vertrag nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!['ELECTRICITY', 'GAS'].includes(contract.type)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Nur für Strom- und Gas-Verträge verfügbar',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!contract.energyDetails) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Keine Energie-Details für diesen Vertrag vorhanden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Empfänger-Adressen parsen (JSON Array)
|
||||
let toAddresses: string[] = [];
|
||||
let ccAddresses: string[] = [];
|
||||
try {
|
||||
toAddresses = JSON.parse(email.toAddresses);
|
||||
} catch { toAddresses = [email.toAddresses]; }
|
||||
try {
|
||||
if (email.ccAddresses) ccAddresses = JSON.parse(email.ccAddresses);
|
||||
} catch { /* ignore */ }
|
||||
|
||||
// PDF generieren
|
||||
const pdfBuffer = await generateEmailPdf({
|
||||
from: email.fromAddress,
|
||||
to: toAddresses.join(', '),
|
||||
cc: ccAddresses.length > 0 ? ccAddresses.join(', ') : undefined,
|
||||
subject: email.subject || '(Kein Betreff)',
|
||||
date: email.receivedAt,
|
||||
bodyText: email.textBody || undefined,
|
||||
bodyHtml: email.htmlBody || undefined,
|
||||
});
|
||||
|
||||
// Uploads-Verzeichnis erstellen
|
||||
const uploadsDir = path.join(process.cwd(), 'uploads', 'invoices');
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Eindeutigen Dateinamen generieren
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
const newFilename = `invoice-email-${uniqueSuffix}.pdf`;
|
||||
const filePath = path.join(uploadsDir, newFilename);
|
||||
const relativePath = `/uploads/invoices/${newFilename}`;
|
||||
|
||||
// PDF speichern
|
||||
fs.writeFileSync(filePath, pdfBuffer);
|
||||
|
||||
// Invoice in DB erstellen
|
||||
const invoice = await invoiceService.addInvoice(contract.energyDetails.id, {
|
||||
invoiceDate: new Date(invoiceDate),
|
||||
invoiceType,
|
||||
documentPath: relativePath,
|
||||
notes: notes || undefined,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: invoice,
|
||||
} as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('saveEmailAsInvoice error:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: `Fehler beim Erstellen der Rechnung: ${errorMessage}`,
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SAVE ATTACHMENT AS INVOICE ====================
|
||||
|
||||
// E-Mail-Anhang als Rechnung speichern
|
||||
export async function saveAttachmentAsInvoice(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const emailId = parseInt(req.params.id);
|
||||
const filename = decodeURIComponent(req.params.filename);
|
||||
const { invoiceDate, invoiceType, notes } = req.body;
|
||||
|
||||
console.log('[saveAttachmentAsInvoice] Request:', { emailId, filename, invoiceDate, invoiceType, notes });
|
||||
|
||||
// Validierung
|
||||
if (!invoiceDate || !invoiceType) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'invoiceDate und invoiceType sind erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validiere invoiceType
|
||||
if (!['INTERIM', 'FINAL', 'NOT_AVAILABLE'].includes(invoiceType)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Ungültiger Rechnungstyp',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// E-Mail aus Cache laden
|
||||
const email = await cachedEmailService.getCachedEmailById(emailId);
|
||||
if (!email) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'E-Mail nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfen ob E-Mail einem Vertrag zugeordnet ist
|
||||
if (!email.contractId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'E-Mail ist keinem Vertrag zugeordnet',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vertrag laden und prüfen ob es ein Energievertrag ist
|
||||
const contract = await prisma.contract.findUnique({
|
||||
where: { id: email.contractId },
|
||||
include: { energyDetails: true },
|
||||
});
|
||||
|
||||
if (!contract) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Vertrag nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!['ELECTRICITY', 'GAS'].includes(contract.type)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Nur für Strom- und Gas-Verträge verfügbar',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!contract.energyDetails) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Keine Energie-Details für diesen Vertrag vorhanden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Für gesendete E-Mails: Prüfen ob UID vorhanden
|
||||
if (email.folder === 'SENT' && email.uid === 0) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Anhang nicht verfügbar - E-Mail wurde vor der IMAP-Speicherung gesendet',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// StressfreiEmail laden um Zugangsdaten zu bekommen
|
||||
const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(email.stressfreiEmailId);
|
||||
if (!stressfreiEmail || !stressfreiEmail.emailPasswordEncrypted) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Keine Mailbox-Zugangsdaten verfügbar',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// IMAP-Einstellungen laden
|
||||
const settings = await getImapSmtpSettings();
|
||||
if (!settings) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Keine E-Mail-Provider-Einstellungen gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Passwort entschlüsseln
|
||||
const password = decrypt(stressfreiEmail.emailPasswordEncrypted);
|
||||
|
||||
// IMAP-Credentials zusammenstellen
|
||||
const credentials: ImapCredentials = {
|
||||
host: settings.imapServer,
|
||||
port: settings.imapPort,
|
||||
user: stressfreiEmail.email,
|
||||
password,
|
||||
encryption: settings.imapEncryption,
|
||||
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||
};
|
||||
|
||||
// IMAP-Ordner bestimmen
|
||||
const imapFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX';
|
||||
|
||||
// Anhang vom IMAP-Server laden
|
||||
const attachment = await fetchAttachment(credentials, email.uid, filename, imapFolder);
|
||||
if (!attachment) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Anhang nicht gefunden oder nicht mehr verfügbar',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Uploads-Verzeichnis erstellen
|
||||
const uploadsDir = path.join(process.cwd(), 'uploads', 'invoices');
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Dateiendung extrahieren
|
||||
const ext = path.extname(filename) || '.pdf';
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
const newFilename = `invoice-attachment-${uniqueSuffix}${ext}`;
|
||||
const filePath = path.join(uploadsDir, newFilename);
|
||||
const relativePath = `/uploads/invoices/${newFilename}`;
|
||||
|
||||
// Datei speichern
|
||||
fs.writeFileSync(filePath, attachment.content);
|
||||
|
||||
// Invoice in DB erstellen
|
||||
const invoice = await invoiceService.addInvoice(contract.energyDetails.id, {
|
||||
invoiceDate: new Date(invoiceDate),
|
||||
invoiceType,
|
||||
documentPath: relativePath,
|
||||
notes: notes || undefined,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: invoice,
|
||||
} as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('saveAttachmentAsInvoice error:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: `Fehler beim Erstellen der Rechnung: ${errorMessage}`,
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as invoiceService from '../services/invoice.service.js';
|
||||
import { ApiResponse } from '../types/index.js';
|
||||
|
||||
/**
|
||||
* Alle Rechnungen für ein EnergyContractDetails abrufen
|
||||
*/
|
||||
export async function getInvoices(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const ecdId = parseInt(req.params.ecdId);
|
||||
const invoices = await invoiceService.getInvoices(ecdId);
|
||||
res.json({ success: true, data: invoices } as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('getInvoices error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Rechnungen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelne Rechnung abrufen
|
||||
*/
|
||||
export async function getInvoice(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const ecdId = parseInt(req.params.ecdId);
|
||||
const invoiceId = parseInt(req.params.invoiceId);
|
||||
const invoice = await invoiceService.getInvoice(ecdId, invoiceId);
|
||||
|
||||
if (!invoice) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Rechnung nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true, data: invoice } as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('getInvoice error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Rechnung',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Neue Rechnung hinzufügen
|
||||
*/
|
||||
export async function addInvoice(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const ecdId = parseInt(req.params.ecdId);
|
||||
const { invoiceDate, invoiceType, documentPath, notes } = req.body;
|
||||
|
||||
if (!invoiceDate || !invoiceType) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'invoiceDate und invoiceType sind erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const invoice = await invoiceService.addInvoice(ecdId, {
|
||||
invoiceDate: new Date(invoiceDate),
|
||||
invoiceType,
|
||||
documentPath,
|
||||
notes,
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true, data: invoice } as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('addInvoice error:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen der Rechnung',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rechnung aktualisieren
|
||||
*/
|
||||
export async function updateInvoice(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const ecdId = parseInt(req.params.ecdId);
|
||||
const invoiceId = parseInt(req.params.invoiceId);
|
||||
const { invoiceDate, invoiceType, documentPath, notes } = req.body;
|
||||
|
||||
const invoice = await invoiceService.updateInvoice(ecdId, invoiceId, {
|
||||
invoiceDate: invoiceDate ? new Date(invoiceDate) : undefined,
|
||||
invoiceType,
|
||||
documentPath,
|
||||
notes,
|
||||
});
|
||||
|
||||
res.json({ success: true, data: invoice } as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('updateInvoice error:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Rechnung',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rechnung löschen
|
||||
*/
|
||||
export async function deleteInvoice(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const ecdId = parseInt(req.params.ecdId);
|
||||
const invoiceId = parseInt(req.params.invoiceId);
|
||||
|
||||
await invoiceService.deleteInvoice(ecdId, invoiceId);
|
||||
|
||||
res.json({ success: true, data: null } as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('deleteInvoice error:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Rechnung',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import contractTaskRoutes from './routes/contractTask.routes.js';
|
||||
import appSettingRoutes from './routes/appSetting.routes.js';
|
||||
import emailProviderRoutes from './routes/emailProvider.routes.js';
|
||||
import cachedEmailRoutes from './routes/cachedEmail.routes.js';
|
||||
import invoiceRoutes from './routes/invoice.routes.js';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -59,6 +60,7 @@ app.use('/api', contractTaskRoutes);
|
||||
app.use('/api/settings', appSettingRoutes);
|
||||
app.use('/api/email-providers', emailProviderRoutes);
|
||||
app.use('/api', cachedEmailRoutes);
|
||||
app.use('/api/energy-details', invoiceRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
|
||||
@@ -185,6 +185,24 @@ router.post(
|
||||
cachedEmailController.saveEmailAsPdf
|
||||
);
|
||||
|
||||
// E-Mail als PDF exportieren und als Rechnung speichern
|
||||
// POST /api/emails/:id/save-as-invoice { invoiceDate, invoiceType, notes? }
|
||||
router.post(
|
||||
'/emails/:id/save-as-invoice',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
cachedEmailController.saveEmailAsInvoice
|
||||
);
|
||||
|
||||
// Anhang als Rechnung speichern
|
||||
// POST /api/emails/:id/attachments/:filename/save-as-invoice { invoiceDate, invoiceType, notes? }
|
||||
router.post(
|
||||
'/emails/:id/attachments/:filename/save-as-invoice',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
cachedEmailController.saveAttachmentAsInvoice
|
||||
);
|
||||
|
||||
// ==================== VERTRAGSZUORDNUNG ====================
|
||||
|
||||
// E-Mail Vertrag zuordnen
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Router } from 'express';
|
||||
import * as invoiceController from '../controllers/invoice.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// ==================== INVOICE CRUD ====================
|
||||
|
||||
// Alle Rechnungen für ein EnergyContractDetails abrufen
|
||||
// GET /api/energy-details/:ecdId/invoices
|
||||
router.get(
|
||||
'/:ecdId/invoices',
|
||||
authenticate,
|
||||
requirePermission('contracts:read'),
|
||||
invoiceController.getInvoices
|
||||
);
|
||||
|
||||
// Einzelne Rechnung abrufen
|
||||
// GET /api/energy-details/:ecdId/invoices/:invoiceId
|
||||
router.get(
|
||||
'/:ecdId/invoices/:invoiceId',
|
||||
authenticate,
|
||||
requirePermission('contracts:read'),
|
||||
invoiceController.getInvoice
|
||||
);
|
||||
|
||||
// Neue Rechnung hinzufügen
|
||||
// POST /api/energy-details/:ecdId/invoices
|
||||
router.post(
|
||||
'/:ecdId/invoices',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
invoiceController.addInvoice
|
||||
);
|
||||
|
||||
// Rechnung aktualisieren
|
||||
// PUT /api/energy-details/:ecdId/invoices/:invoiceId
|
||||
router.put(
|
||||
'/:ecdId/invoices/:invoiceId',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
invoiceController.updateInvoice
|
||||
);
|
||||
|
||||
// Rechnung löschen
|
||||
// DELETE /api/energy-details/:ecdId/invoices/:invoiceId
|
||||
router.delete(
|
||||
'/:ecdId/invoices/:invoiceId',
|
||||
authenticate,
|
||||
requirePermission('contracts:delete'),
|
||||
invoiceController.deleteInvoice
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -658,4 +658,99 @@ router.delete(
|
||||
(req: AuthRequest, res: Response) => handleContractDocumentDelete(req, res, 'cancellationConfirmationOptionsPath')
|
||||
);
|
||||
|
||||
// ==================== RECHNUNGS-DOKUMENTE ====================
|
||||
|
||||
// Upload für Rechnungs-Dokument
|
||||
router.post(
|
||||
'/invoices/:id',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
setUploadDir('invoices'),
|
||||
upload.single('document'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' });
|
||||
return;
|
||||
}
|
||||
|
||||
const invoiceId = parseInt(req.params.id);
|
||||
const relativePath = `/uploads/invoices/${req.file.filename}`;
|
||||
|
||||
// Alte Datei löschen falls vorhanden
|
||||
const invoice = await prisma.invoice.findUnique({ where: { id: invoiceId } });
|
||||
if (!invoice) {
|
||||
res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (invoice.documentPath) {
|
||||
const oldPath = path.join(process.cwd(), invoice.documentPath);
|
||||
if (fs.existsSync(oldPath)) {
|
||||
fs.unlinkSync(oldPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Invoice in der DB aktualisieren
|
||||
await prisma.invoice.update({
|
||||
where: { id: invoiceId },
|
||||
data: { documentPath: relativePath },
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
path: relativePath,
|
||||
filename: req.file.filename,
|
||||
originalName: req.file.originalname,
|
||||
size: req.file.size,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Invoice upload error:', error);
|
||||
res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Löschen von Rechnungs-Dokument
|
||||
router.delete(
|
||||
'/invoices/:id',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const invoiceId = parseInt(req.params.id);
|
||||
|
||||
const invoice = await prisma.invoice.findUnique({ where: { id: invoiceId } });
|
||||
if (!invoice) {
|
||||
res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!invoice.documentPath) {
|
||||
res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Datei löschen
|
||||
const filePath = path.join(process.cwd(), invoice.documentPath);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
// documentPath in DB auf null setzen
|
||||
await prisma.invoice.update({
|
||||
where: { id: invoiceId },
|
||||
data: { documentPath: null },
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Invoice delete error:', error);
|
||||
res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -124,14 +124,14 @@ export async function getContractById(id: number, decryptPassword = false) {
|
||||
contractCategory: true,
|
||||
previousContract: {
|
||||
include: {
|
||||
energyDetails: { include: { meter: { include: { readings: true } } } },
|
||||
energyDetails: { include: { meter: { include: { readings: true } }, invoices: true } },
|
||||
internetDetails: { include: { phoneNumbers: true } },
|
||||
mobileDetails: { include: { simCards: true } },
|
||||
tvDetails: true,
|
||||
carInsuranceDetails: true,
|
||||
},
|
||||
},
|
||||
energyDetails: { include: { meter: { include: { readings: true } } } },
|
||||
energyDetails: { include: { meter: { include: { readings: true } }, invoices: true } },
|
||||
internetDetails: { include: { phoneNumbers: true } },
|
||||
mobileDetails: { include: { simCards: true } },
|
||||
tvDetails: true,
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface CockpitSummary {
|
||||
contractEnding: number;
|
||||
missingCredentials: number;
|
||||
missingData: number;
|
||||
missingInvoices: number;
|
||||
openTasks: number;
|
||||
pendingContracts: number;
|
||||
};
|
||||
@@ -142,11 +143,11 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
const warningDays = parseInt(settings.deadlineWarningDays) || 42;
|
||||
const okDays = parseInt(settings.deadlineOkDays) || 90;
|
||||
|
||||
// Lade alle aktiven/pending Verträge mit allen relevanten Daten
|
||||
// Lade alle relevanten Verträge (inkl. CANCELLED/DEACTIVATED für Schlussrechnung-Check)
|
||||
const contracts = await prisma.contract.findMany({
|
||||
where: {
|
||||
status: {
|
||||
in: ['ACTIVE', 'PENDING', 'DRAFT'],
|
||||
in: ['ACTIVE', 'PENDING', 'DRAFT', 'CANCELLED', 'DEACTIVATED', 'EXPIRED'],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
@@ -182,6 +183,7 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
energyDetails: {
|
||||
include: {
|
||||
meter: true,
|
||||
invoices: true,
|
||||
},
|
||||
},
|
||||
internetDetails: {
|
||||
@@ -224,6 +226,7 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
contractEnding: 0,
|
||||
missingCredentials: 0,
|
||||
missingData: 0,
|
||||
missingInvoices: 0,
|
||||
openTasks: 0,
|
||||
pendingContracts: 0,
|
||||
},
|
||||
@@ -426,6 +429,75 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
summary.byCategory.pendingContracts++;
|
||||
}
|
||||
|
||||
// 13. ENERGIE-RECHNUNGEN (nur für ELECTRICITY und GAS)
|
||||
if (['ELECTRICITY', 'GAS'].includes(contract.type) && contract.energyDetails) {
|
||||
const invoices = contract.energyDetails.invoices || [];
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
|
||||
// 13a. SCHLUSSRECHNUNG FEHLT (nur wenn Vertrag gekündigt/deaktiviert ist)
|
||||
// "Beendet" = CANCELLED oder DEACTIVATED (nicht nur Laufzeit abgelaufen!)
|
||||
const isContractTerminated = contract.status === 'CANCELLED' || contract.status === 'DEACTIVATED';
|
||||
|
||||
if (isContractTerminated) {
|
||||
const hasFinalInvoice = invoices.some(inv => inv.invoiceType === 'FINAL');
|
||||
const hasNotAvailable = invoices.some(inv => inv.invoiceType === 'NOT_AVAILABLE');
|
||||
|
||||
if (!hasFinalInvoice && !hasNotAvailable) {
|
||||
issues.push({
|
||||
type: 'missing_final_invoice',
|
||||
label: 'Schlussrechnung fehlt',
|
||||
urgency: 'warning',
|
||||
details: 'Vertrag gekündigt/deaktiviert, aber keine Schlussrechnung vorhanden',
|
||||
});
|
||||
summary.byCategory.missingInvoices++;
|
||||
}
|
||||
}
|
||||
|
||||
// 13b. ZWISCHENRECHNUNG FEHLT/ÜBERFÄLLIG (wenn Vertrag > 12 Monate läuft)
|
||||
// Für alle Status außer DRAFT und nicht gekündigt/deaktiviert
|
||||
// Auch EXPIRED zählt hier, da der Vertrag ohne Kündigung weiterläuft!
|
||||
if (contract.startDate && contract.status !== 'DRAFT' && !isContractTerminated) {
|
||||
const startDate = new Date(contract.startDate);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
const daysSinceStart = Math.floor((now.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysSinceStart > 365) {
|
||||
// Vertrag läuft > 12 Monate
|
||||
if (invoices.length === 0) {
|
||||
// Keine Rechnungen vorhanden
|
||||
issues.push({
|
||||
type: 'missing_interim_invoice',
|
||||
label: 'Zwischenrechnung fehlt',
|
||||
urgency: 'warning',
|
||||
details: 'Vertrag läuft über 12 Monate ohne Rechnung',
|
||||
});
|
||||
summary.byCategory.missingInvoices++;
|
||||
} else {
|
||||
// Prüfen ob letzte Rechnung > 12 Monate alt
|
||||
const latestInvoice = invoices
|
||||
.filter(inv => inv.invoiceType !== 'NOT_AVAILABLE')
|
||||
.sort((a, b) => new Date(b.invoiceDate).getTime() - new Date(a.invoiceDate).getTime())[0];
|
||||
|
||||
if (latestInvoice) {
|
||||
const invoiceDate = new Date(latestInvoice.invoiceDate);
|
||||
const daysSinceInvoice = Math.floor((now.getTime() - invoiceDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysSinceInvoice > 365) {
|
||||
issues.push({
|
||||
type: 'overdue_interim_invoice',
|
||||
label: 'Zwischenrechnung überfällig',
|
||||
urgency: 'warning',
|
||||
details: `Letzte Rechnung vor ${Math.floor(daysSinceInvoice / 30)} Monaten`,
|
||||
});
|
||||
summary.byCategory.missingInvoices++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nur Verträge mit Issues hinzufügen
|
||||
if (issues.length > 0) {
|
||||
const highestUrgency = getHighestUrgency(issues);
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { PrismaClient, InvoiceType } from '@prisma/client';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export interface CreateInvoiceData {
|
||||
invoiceDate: Date;
|
||||
invoiceType: InvoiceType;
|
||||
documentPath?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface UpdateInvoiceData {
|
||||
invoiceDate?: Date;
|
||||
invoiceType?: InvoiceType;
|
||||
documentPath?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Rechnungen für ein EnergyContractDetails abrufen
|
||||
*/
|
||||
export async function getInvoices(energyContractDetailsId: number) {
|
||||
return prisma.invoice.findMany({
|
||||
where: { energyContractDetailsId },
|
||||
orderBy: { invoiceDate: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelne Rechnung abrufen
|
||||
*/
|
||||
export async function getInvoice(energyContractDetailsId: number, invoiceId: number) {
|
||||
return prisma.invoice.findFirst({
|
||||
where: { id: invoiceId, energyContractDetailsId },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Neue Rechnung hinzufügen
|
||||
*/
|
||||
export async function addInvoice(energyContractDetailsId: number, data: CreateInvoiceData) {
|
||||
// Validierung: documentPath ist Pflicht, außer bei NOT_AVAILABLE
|
||||
if (data.invoiceType !== 'NOT_AVAILABLE' && !data.documentPath) {
|
||||
throw new Error('Dokument ist Pflicht (außer bei Typ "Nicht verfügbar")');
|
||||
}
|
||||
|
||||
// Prüfen ob EnergyContractDetails existiert
|
||||
const energyDetails = await prisma.energyContractDetails.findUnique({
|
||||
where: { id: energyContractDetailsId },
|
||||
});
|
||||
|
||||
if (!energyDetails) {
|
||||
throw new Error('Energievertrag nicht gefunden');
|
||||
}
|
||||
|
||||
return prisma.invoice.create({
|
||||
data: {
|
||||
energyContractDetailsId,
|
||||
invoiceDate: data.invoiceDate,
|
||||
invoiceType: data.invoiceType,
|
||||
documentPath: data.documentPath,
|
||||
notes: data.notes,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rechnung aktualisieren
|
||||
*/
|
||||
export async function updateInvoice(
|
||||
energyContractDetailsId: number,
|
||||
invoiceId: number,
|
||||
data: UpdateInvoiceData
|
||||
) {
|
||||
// Prüfen ob Rechnung existiert und zum EnergyContractDetails gehört
|
||||
const invoice = await prisma.invoice.findFirst({
|
||||
where: { id: invoiceId, energyContractDetailsId },
|
||||
});
|
||||
|
||||
if (!invoice) {
|
||||
throw new Error('Rechnung nicht gefunden');
|
||||
}
|
||||
|
||||
// Validierung bei Typ-Änderung
|
||||
const newType = data.invoiceType ?? invoice.invoiceType;
|
||||
const newPath = data.documentPath !== undefined ? data.documentPath : invoice.documentPath;
|
||||
|
||||
if (newType !== 'NOT_AVAILABLE' && !newPath) {
|
||||
throw new Error('Dokument ist Pflicht (außer bei Typ "Nicht verfügbar")');
|
||||
}
|
||||
|
||||
return prisma.invoice.update({
|
||||
where: { id: invoiceId },
|
||||
data: {
|
||||
invoiceDate: data.invoiceDate,
|
||||
invoiceType: data.invoiceType,
|
||||
documentPath: data.documentPath,
|
||||
notes: data.notes,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rechnung löschen
|
||||
*/
|
||||
export async function deleteInvoice(energyContractDetailsId: number, invoiceId: number) {
|
||||
const invoice = await prisma.invoice.findFirst({
|
||||
where: { id: invoiceId, energyContractDetailsId },
|
||||
});
|
||||
|
||||
if (!invoice) {
|
||||
throw new Error('Rechnung nicht gefunden');
|
||||
}
|
||||
|
||||
// Datei löschen falls vorhanden
|
||||
if (invoice.documentPath) {
|
||||
const filePath = path.join(process.cwd(), invoice.documentPath);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
return prisma.invoice.delete({ where: { id: invoiceId } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Rechnung direkt erstellen (für E-Mail-Integration)
|
||||
* Erstellt eine Rechnung mit bereits vorhandenem Dokument
|
||||
*/
|
||||
export async function createInvoiceWithDocument(
|
||||
energyContractDetailsId: number,
|
||||
invoiceDate: Date,
|
||||
invoiceType: InvoiceType,
|
||||
documentPath: string,
|
||||
notes?: string
|
||||
) {
|
||||
// Prüfen ob EnergyContractDetails existiert
|
||||
const energyDetails = await prisma.energyContractDetails.findUnique({
|
||||
where: { id: energyContractDetailsId },
|
||||
});
|
||||
|
||||
if (!energyDetails) {
|
||||
throw new Error('Energievertrag nicht gefunden');
|
||||
}
|
||||
|
||||
return prisma.invoice.create({
|
||||
data: {
|
||||
energyContractDetailsId,
|
||||
invoiceDate,
|
||||
invoiceType,
|
||||
documentPath,
|
||||
notes,
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user