save email as pdf likae attachment version 2
This commit is contained in:
parent
d98c97a81f
commit
2d052c76d9
|
|
@ -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;AAG5C,OAAO,EAAe,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAE7D,wBAAsB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CA+BjF;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BhF;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;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"}
|
||||
{"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;AAG5C,OAAO,EAAe,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAE7D,wBAAsB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCjF;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BhF;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;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"}
|
||||
|
|
@ -48,7 +48,13 @@ const contractService = __importStar(require("../services/contract.service.js"))
|
|||
const contractCockpitService = __importStar(require("../services/contractCockpit.service.js"));
|
||||
async function getContracts(req, res) {
|
||||
try {
|
||||
const { customerId, type, status, search, page, limit } = req.query;
|
||||
const { customerId, type, status, search, page, limit, tree } = req.query;
|
||||
// Baumstruktur für Kundenansicht
|
||||
if (tree === 'true' && customerId) {
|
||||
const treeData = await contractService.getContractTreeForCustomer(parseInt(customerId));
|
||||
res.json({ success: true, data: treeData });
|
||||
return;
|
||||
}
|
||||
// Für Kundenportal-Benutzer: nur eigene + vertretene Kunden-Verträge anzeigen
|
||||
let customerIds;
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -67,6 +67,19 @@ export declare function getAllContracts(filters: ContractFilters): Promise<{
|
|||
color: string | null;
|
||||
sortOrder: number;
|
||||
} | null;
|
||||
billingAddress: {
|
||||
id: number;
|
||||
customerId: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
type: import(".prisma/client").$Enums.AddressType;
|
||||
isDefault: boolean;
|
||||
street: string;
|
||||
houseNumber: string;
|
||||
postalCode: string;
|
||||
city: string;
|
||||
country: string;
|
||||
} | null;
|
||||
cancellationPeriod: {
|
||||
id: number;
|
||||
isActive: boolean;
|
||||
|
|
@ -99,6 +112,7 @@ export declare function getAllContracts(filters: ContractFilters): Promise<{
|
|||
contractNumber: string;
|
||||
contractCategoryId: number | null;
|
||||
addressId: number | null;
|
||||
billingAddressId: number | null;
|
||||
bankCardId: number | null;
|
||||
identityDocumentId: number | null;
|
||||
salesPlatformId: number | null;
|
||||
|
|
@ -354,6 +368,19 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
|
|||
color: string | null;
|
||||
sortOrder: number;
|
||||
} | null;
|
||||
billingAddress: {
|
||||
id: number;
|
||||
customerId: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
type: import(".prisma/client").$Enums.AddressType;
|
||||
isDefault: boolean;
|
||||
street: string;
|
||||
houseNumber: string;
|
||||
postalCode: string;
|
||||
city: string;
|
||||
country: string;
|
||||
} | null;
|
||||
cancellationPeriod: {
|
||||
id: number;
|
||||
isActive: boolean;
|
||||
|
|
@ -490,6 +517,7 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
|
|||
contractNumber: string;
|
||||
contractCategoryId: number | null;
|
||||
addressId: number | null;
|
||||
billingAddressId: number | null;
|
||||
bankCardId: number | null;
|
||||
identityDocumentId: number | null;
|
||||
salesPlatformId: number | null;
|
||||
|
|
@ -534,6 +562,7 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
|
|||
contractNumber: string;
|
||||
contractCategoryId: number | null;
|
||||
addressId: number | null;
|
||||
billingAddressId: number | null;
|
||||
bankCardId: number | null;
|
||||
identityDocumentId: number | null;
|
||||
salesPlatformId: number | null;
|
||||
|
|
@ -563,6 +592,7 @@ interface ContractCreateData {
|
|||
contractCategoryId?: number;
|
||||
status?: ContractStatus;
|
||||
addressId?: number;
|
||||
billingAddressId?: number;
|
||||
bankCardId?: number;
|
||||
identityDocumentId?: number;
|
||||
salesPlatformId?: number;
|
||||
|
|
@ -786,6 +816,19 @@ export declare function createContract(data: ContractCreateData): Promise<{
|
|||
internetUsername: string | null;
|
||||
internetPasswordEncrypted: string | null;
|
||||
}) | null;
|
||||
billingAddress: {
|
||||
id: number;
|
||||
customerId: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
type: import(".prisma/client").$Enums.AddressType;
|
||||
isDefault: boolean;
|
||||
street: string;
|
||||
houseNumber: string;
|
||||
postalCode: string;
|
||||
city: string;
|
||||
country: string;
|
||||
} | null;
|
||||
} & {
|
||||
id: number;
|
||||
customerId: number;
|
||||
|
|
@ -802,6 +845,7 @@ export declare function createContract(data: ContractCreateData): Promise<{
|
|||
contractNumber: string;
|
||||
contractCategoryId: number | null;
|
||||
addressId: number | null;
|
||||
billingAddressId: number | null;
|
||||
bankCardId: number | null;
|
||||
identityDocumentId: number | null;
|
||||
salesPlatformId: number | null;
|
||||
|
|
@ -1050,6 +1094,19 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
|
|||
color: string | null;
|
||||
sortOrder: number;
|
||||
} | null;
|
||||
billingAddress: {
|
||||
id: number;
|
||||
customerId: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
type: import(".prisma/client").$Enums.AddressType;
|
||||
isDefault: boolean;
|
||||
street: string;
|
||||
houseNumber: string;
|
||||
postalCode: string;
|
||||
city: string;
|
||||
country: string;
|
||||
} | null;
|
||||
cancellationPeriod: {
|
||||
id: number;
|
||||
isActive: boolean;
|
||||
|
|
@ -1186,6 +1243,7 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
|
|||
contractNumber: string;
|
||||
contractCategoryId: number | null;
|
||||
addressId: number | null;
|
||||
billingAddressId: number | null;
|
||||
bankCardId: number | null;
|
||||
identityDocumentId: number | null;
|
||||
salesPlatformId: number | null;
|
||||
|
|
@ -1230,6 +1288,7 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
|
|||
contractNumber: string;
|
||||
contractCategoryId: number | null;
|
||||
addressId: number | null;
|
||||
billingAddressId: number | null;
|
||||
bankCardId: number | null;
|
||||
identityDocumentId: number | null;
|
||||
salesPlatformId: number | null;
|
||||
|
|
@ -1269,6 +1328,7 @@ export declare function deleteContract(id: number): Promise<{
|
|||
contractNumber: string;
|
||||
contractCategoryId: number | null;
|
||||
addressId: number | null;
|
||||
billingAddressId: number | null;
|
||||
bankCardId: number | null;
|
||||
identityDocumentId: number | null;
|
||||
salesPlatformId: number | null;
|
||||
|
|
@ -1424,6 +1484,19 @@ export declare function createFollowUpContract(previousContractId: number): Prom
|
|||
internetUsername: string | null;
|
||||
internetPasswordEncrypted: string | null;
|
||||
}) | null;
|
||||
billingAddress: {
|
||||
id: number;
|
||||
customerId: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
type: import(".prisma/client").$Enums.AddressType;
|
||||
isDefault: boolean;
|
||||
street: string;
|
||||
houseNumber: string;
|
||||
postalCode: string;
|
||||
city: string;
|
||||
country: string;
|
||||
} | null;
|
||||
} & {
|
||||
id: number;
|
||||
customerId: number;
|
||||
|
|
@ -1440,6 +1513,7 @@ export declare function createFollowUpContract(previousContractId: number): Prom
|
|||
contractNumber: string;
|
||||
contractCategoryId: number | null;
|
||||
addressId: number | null;
|
||||
billingAddressId: number | null;
|
||||
bankCardId: number | null;
|
||||
identityDocumentId: number | null;
|
||||
salesPlatformId: number | null;
|
||||
|
|
@ -1474,5 +1548,38 @@ export declare function getInternetCredentials(contractId: number): Promise<{
|
|||
export declare function getSipCredentials(phoneNumberId: number): Promise<{
|
||||
password: string | null;
|
||||
}>;
|
||||
export interface ContractTreeNode {
|
||||
contract: {
|
||||
id: number;
|
||||
contractNumber: string;
|
||||
type: ContractType;
|
||||
status: ContractStatus;
|
||||
startDate: Date | null;
|
||||
endDate: Date | null;
|
||||
providerName: string | null;
|
||||
tariffName: string | null;
|
||||
previousContractId: number | null;
|
||||
provider?: {
|
||||
id: number;
|
||||
name: string;
|
||||
} | null;
|
||||
tariff?: {
|
||||
id: number;
|
||||
name: string;
|
||||
} | null;
|
||||
contractCategory?: {
|
||||
id: number;
|
||||
name: string;
|
||||
} | null;
|
||||
};
|
||||
predecessors: ContractTreeNode[];
|
||||
hasHistory: boolean;
|
||||
}
|
||||
/**
|
||||
* Verträge eines Kunden als Baumstruktur abrufen.
|
||||
* Wurzelknoten = Verträge ohne Nachfolger (aktuellste Verträge)
|
||||
* Vorgänger werden rekursiv eingebettet.
|
||||
*/
|
||||
export declare function getContractTreeForCustomer(customerId: number): Promise<ContractTreeNode[]>;
|
||||
export {};
|
||||
//# sourceMappingURL=contract.service.d.ts.map
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -10,6 +10,7 @@ exports.getContractPassword = getContractPassword;
|
|||
exports.getSimCardCredentials = getSimCardCredentials;
|
||||
exports.getInternetCredentials = getInternetCredentials;
|
||||
exports.getSipCredentials = getSipCredentials;
|
||||
exports.getContractTreeForCustomer = getContractTreeForCustomer;
|
||||
const client_1 = require("@prisma/client");
|
||||
const helpers_js_1 = require("../utils/helpers.js");
|
||||
const encryption_js_1 = require("../utils/encryption.js");
|
||||
|
|
@ -75,7 +76,7 @@ async function getAllContracts(filters) {
|
|||
where,
|
||||
skip,
|
||||
take,
|
||||
orderBy: [{ startDate: 'desc' }, { createdAt: 'desc' }],
|
||||
orderBy: [{ createdAt: 'desc' }],
|
||||
include: {
|
||||
customer: {
|
||||
select: {
|
||||
|
|
@ -87,6 +88,7 @@ async function getAllContracts(filters) {
|
|||
},
|
||||
},
|
||||
address: true,
|
||||
billingAddress: true,
|
||||
salesPlatform: true,
|
||||
cancellationPeriod: true,
|
||||
contractDuration: true,
|
||||
|
|
@ -108,6 +110,7 @@ async function getContractById(id, decryptPassword = false) {
|
|||
include: {
|
||||
customer: true,
|
||||
address: true,
|
||||
billingAddress: true,
|
||||
bankCard: true,
|
||||
identityDocument: true,
|
||||
salesPlatform: true,
|
||||
|
|
@ -233,6 +236,7 @@ async function createContract(data) {
|
|||
include: {
|
||||
customer: true,
|
||||
address: true,
|
||||
billingAddress: true,
|
||||
salesPlatform: true,
|
||||
energyDetails: true,
|
||||
internetDetails: { include: { phoneNumbers: true } },
|
||||
|
|
@ -598,4 +602,66 @@ async function getSipCredentials(phoneNumberId) {
|
|||
return { password: null };
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Verträge eines Kunden als Baumstruktur abrufen.
|
||||
* Wurzelknoten = Verträge ohne Nachfolger (aktuellste Verträge)
|
||||
* Vorgänger werden rekursiv eingebettet.
|
||||
*/
|
||||
async function getContractTreeForCustomer(customerId) {
|
||||
// Alle Verträge des Kunden laden (außer DEACTIVATED)
|
||||
const allContracts = await prisma.contract.findMany({
|
||||
where: {
|
||||
customerId,
|
||||
status: { not: client_1.ContractStatus.DEACTIVATED },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
contractNumber: true,
|
||||
type: true,
|
||||
status: true,
|
||||
startDate: true,
|
||||
endDate: true,
|
||||
providerName: true,
|
||||
tariffName: true,
|
||||
previousContractId: true,
|
||||
provider: { select: { id: true, name: true } },
|
||||
tariff: { select: { id: true, name: true } },
|
||||
contractCategory: { select: { id: true, name: true } },
|
||||
},
|
||||
orderBy: [{ startDate: 'desc' }, { createdAt: 'desc' }],
|
||||
});
|
||||
// Map für schnellen Zugriff: contractId -> contract
|
||||
const contractMap = new Map(allContracts.map(c => [c.id, c]));
|
||||
// Set der IDs die als Vorgänger referenziert werden
|
||||
const predecessorIds = new Set(allContracts
|
||||
.filter(c => c.previousContractId !== null)
|
||||
.map(c => c.previousContractId));
|
||||
// Wurzelverträge = Verträge die keinen Nachfolger haben
|
||||
// (werden von keinem anderen Vertrag als previousContractId referenziert)
|
||||
const rootContracts = allContracts.filter(c => !predecessorIds.has(c.id));
|
||||
// Rekursive Funktion um Vorgängerkette aufzubauen
|
||||
function buildPredecessorChain(contractId) {
|
||||
if (contractId === null)
|
||||
return [];
|
||||
const contract = contractMap.get(contractId);
|
||||
if (!contract)
|
||||
return [];
|
||||
const predecessors = buildPredecessorChain(contract.previousContractId);
|
||||
return [{
|
||||
contract,
|
||||
predecessors,
|
||||
hasHistory: predecessors.length > 0,
|
||||
}];
|
||||
}
|
||||
// Baumstruktur für jeden Wurzelvertrag aufbauen
|
||||
const tree = rootContracts.map(contract => {
|
||||
const predecessors = buildPredecessorChain(contract.previousContractId);
|
||||
return {
|
||||
contract,
|
||||
predecessors,
|
||||
hasHistory: predecessors.length > 0,
|
||||
};
|
||||
});
|
||||
return tree;
|
||||
}
|
||||
//# sourceMappingURL=contract.service.js.map
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -173,6 +173,7 @@ export declare function getCustomerById(id: number): Promise<({
|
|||
contractNumber: string;
|
||||
contractCategoryId: number | null;
|
||||
addressId: number | null;
|
||||
billingAddressId: number | null;
|
||||
bankCardId: number | null;
|
||||
identityDocumentId: number | null;
|
||||
salesPlatformId: number | null;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -124,6 +124,14 @@
|
|||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz",
|
||||
"integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/adm-zip": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.7.tgz",
|
||||
|
|
@ -277,6 +285,15 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pdfkit": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.4.tgz",
|
||||
"integrity": "sha512-odAmVuuguRxKh1X4pbMrJMp8ecwNqHRw6lweupvzK+wuyNmi6wzlUlGVZ9EqMvp3Bs2+L9Ty0sRlrvKL+gsQZg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||
|
|
@ -580,6 +597,14 @@
|
|||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/brotli": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
|
||||
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
|
|
@ -667,6 +692,14 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
|
@ -843,6 +876,11 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
|
|
@ -876,6 +914,11 @@
|
|||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dfa": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
|
||||
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
|
|
@ -1163,6 +1206,11 @@
|
|||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"node_modules/fast-fifo": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||
|
|
@ -1185,6 +1233,22 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fontkit": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
|
||||
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.12",
|
||||
"brotli": "^1.3.2",
|
||||
"clone": "^2.1.2",
|
||||
"dfa": "^1.2.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"restructure": "^3.0.0",
|
||||
"tiny-inflate": "^1.0.3",
|
||||
"unicode-properties": "^1.4.0",
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
|
|
@ -1513,6 +1577,12 @@
|
|||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jpeg-exif": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz",
|
||||
"integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info."
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
|
|
@ -1609,6 +1679,23 @@
|
|||
"resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz",
|
||||
"integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow=="
|
||||
},
|
||||
"node_modules/linebreak": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
|
||||
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
|
||||
"dependencies": {
|
||||
"base64-js": "0.0.8",
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/linebreak/node_modules/base64-js": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
|
||||
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
|
|
@ -1887,6 +1974,11 @@
|
|||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
||||
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="
|
||||
},
|
||||
"node_modules/parseley": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
|
||||
|
|
@ -1935,6 +2027,18 @@
|
|||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
|
||||
},
|
||||
"node_modules/pdfkit": {
|
||||
"version": "0.17.2",
|
||||
"resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.2.tgz",
|
||||
"integrity": "sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw==",
|
||||
"dependencies": {
|
||||
"crypto-js": "^4.2.0",
|
||||
"fontkit": "^2.0.4",
|
||||
"jpeg-exif": "^1.1.4",
|
||||
"linebreak": "^1.1.0",
|
||||
"png-js": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/peberminta": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
|
||||
|
|
@ -1977,6 +2081,11 @@
|
|||
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
|
||||
"integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="
|
||||
},
|
||||
"node_modules/png-js": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
|
||||
"integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
|
||||
|
|
@ -2140,6 +2249,11 @@
|
|||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/restructure": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
|
||||
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
|
|
@ -2533,6 +2647,11 @@
|
|||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-inflate": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
|
||||
},
|
||||
"node_modules/tlds": {
|
||||
"version": "1.261.0",
|
||||
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz",
|
||||
|
|
@ -2549,6 +2668,11 @@
|
|||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
|
|
@ -2617,6 +2741,24 @@
|
|||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/unicode-properties": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
|
||||
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0",
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unicode-trie": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
|
||||
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
|
||||
"dependencies": {
|
||||
"pako": "^0.2.5",
|
||||
"tiny-inflate": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"mailparser": "^3.9.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^7.0.13",
|
||||
"pdfkit": "^0.17.2",
|
||||
"undici": "^6.23.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -34,6 +35,7 @@
|
|||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/nodemailer": "^7.0.9",
|
||||
"@types/pdfkit": "^0.17.4",
|
||||
"prisma": "^5.22.0",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.6.3"
|
||||
|
|
@ -559,6 +561,14 @@
|
|||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz",
|
||||
"integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/adm-zip": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.7.tgz",
|
||||
|
|
@ -712,6 +722,15 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pdfkit": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.4.tgz",
|
||||
"integrity": "sha512-odAmVuuguRxKh1X4pbMrJMp8ecwNqHRw6lweupvzK+wuyNmi6wzlUlGVZ9EqMvp3Bs2+L9Ty0sRlrvKL+gsQZg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||
|
|
@ -1015,6 +1034,14 @@
|
|||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/brotli": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
|
||||
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
|
|
@ -1102,6 +1129,14 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
|
@ -1278,6 +1313,11 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
|
|
@ -1311,6 +1351,11 @@
|
|||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dfa": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
|
||||
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
|
|
@ -1598,6 +1643,11 @@
|
|||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"node_modules/fast-fifo": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||
|
|
@ -1620,6 +1670,22 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fontkit": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
|
||||
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.12",
|
||||
"brotli": "^1.3.2",
|
||||
"clone": "^2.1.2",
|
||||
"dfa": "^1.2.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"restructure": "^3.0.0",
|
||||
"tiny-inflate": "^1.0.3",
|
||||
"unicode-properties": "^1.4.0",
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
|
|
@ -1962,6 +2028,12 @@
|
|||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jpeg-exif": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz",
|
||||
"integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info."
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
|
|
@ -2058,6 +2130,23 @@
|
|||
"resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz",
|
||||
"integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow=="
|
||||
},
|
||||
"node_modules/linebreak": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
|
||||
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
|
||||
"dependencies": {
|
||||
"base64-js": "0.0.8",
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/linebreak/node_modules/base64-js": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
|
||||
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
|
|
@ -2336,6 +2425,11 @@
|
|||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
||||
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="
|
||||
},
|
||||
"node_modules/parseley": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
|
||||
|
|
@ -2384,6 +2478,18 @@
|
|||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
|
||||
},
|
||||
"node_modules/pdfkit": {
|
||||
"version": "0.17.2",
|
||||
"resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.2.tgz",
|
||||
"integrity": "sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw==",
|
||||
"dependencies": {
|
||||
"crypto-js": "^4.2.0",
|
||||
"fontkit": "^2.0.4",
|
||||
"jpeg-exif": "^1.1.4",
|
||||
"linebreak": "^1.1.0",
|
||||
"png-js": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/peberminta": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
|
||||
|
|
@ -2426,6 +2532,11 @@
|
|||
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
|
||||
"integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="
|
||||
},
|
||||
"node_modules/png-js": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
|
||||
"integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
|
||||
|
|
@ -2589,6 +2700,11 @@
|
|||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/restructure": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
|
||||
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
|
|
@ -2982,6 +3098,11 @@
|
|||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-inflate": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
|
||||
},
|
||||
"node_modules/tlds": {
|
||||
"version": "1.261.0",
|
||||
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz",
|
||||
|
|
@ -2998,6 +3119,11 @@
|
|||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
|
|
@ -3066,6 +3192,24 @@
|
|||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/unicode-properties": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
|
||||
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0",
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unicode-trie": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
|
||||
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
|
||||
"dependencies": {
|
||||
"pako": "^0.2.5",
|
||||
"tiny-inflate": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
"mailparser": "^3.9.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^7.0.13",
|
||||
"pdfkit": "^0.17.2",
|
||||
"undici": "^6.23.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -44,6 +45,7 @@
|
|||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/nodemailer": "^7.0.9",
|
||||
"@types/pdfkit": "^0.17.4",
|
||||
"prisma": "^5.22.0",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.6.3"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { getImapSmtpSettings } from '../services/emailProvider/emailProviderServ
|
|||
import { decrypt } from '../utils/encryption.js';
|
||||
import { ApiResponse } from '../types/index.js';
|
||||
import { getCustomerTargets, getContractTargets, getIdentityDocumentTargets, getBankCardTargets, documentTargets } from '../config/documentTargets.config.js';
|
||||
import { generateEmailPdf } from '../services/pdfService.js';
|
||||
import { PrismaClient, DocumentType } from '@prisma/client';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
|
@ -1270,3 +1271,250 @@ export async function saveAttachmentTo(req: Request, res: Response): Promise<voi
|
|||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SAVE EMAIL AS PDF ====================
|
||||
|
||||
// E-Mail als PDF exportieren und in Dokumentenfeld speichern
|
||||
export async function saveEmailAsPdf(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const emailId = parseInt(req.params.id);
|
||||
const { entityType, entityId, targetKey } = req.body;
|
||||
|
||||
console.log('[saveEmailAsPdf] Request:', { emailId, entityType, entityId, targetKey });
|
||||
|
||||
// Validierung
|
||||
if (!entityType || !targetKey) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'entityType und targetKey sind erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// E-Mail aus Cache laden (mit Body)
|
||||
const email = await cachedEmailService.getCachedEmailById(emailId);
|
||||
if (!email) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'E-Mail nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// StressfreiEmail laden um an den Kunden zu kommen
|
||||
const stressfreiEmail = await prisma.stressfreiEmail.findUnique({
|
||||
where: { id: email.stressfreiEmailId },
|
||||
include: { customer: true },
|
||||
});
|
||||
|
||||
if (!stressfreiEmail) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'E-Mail-Konto nicht gefunden',
|
||||
} 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,
|
||||
});
|
||||
|
||||
// Ziel-Konfiguration finden
|
||||
let targetConfig;
|
||||
let targetDir: string;
|
||||
let targetField: string;
|
||||
|
||||
console.log('[saveEmailAsPdf] Looking for target config:', { entityType, targetKey });
|
||||
|
||||
if (entityType === 'customer') {
|
||||
targetConfig = documentTargets.customer.find(t => t.key === targetKey);
|
||||
} else if (entityType === 'identityDocument') {
|
||||
targetConfig = documentTargets.identityDocument.find(t => t.key === targetKey);
|
||||
} else if (entityType === 'bankCard') {
|
||||
targetConfig = documentTargets.bankCard.find(t => t.key === targetKey);
|
||||
} else if (entityType === 'contract') {
|
||||
targetConfig = documentTargets.contract.find(t => t.key === targetKey);
|
||||
}
|
||||
|
||||
console.log('[saveEmailAsPdf] Found targetConfig:', targetConfig);
|
||||
|
||||
if (!targetConfig) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Unbekanntes Dokumentziel: ${entityType}/${targetKey}`,
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
targetDir = targetConfig.directory;
|
||||
targetField = targetConfig.field;
|
||||
|
||||
// Uploads-Verzeichnis erstellen
|
||||
const uploadsDir = path.join(process.cwd(), 'uploads', targetDir);
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Eindeutigen Dateinamen generieren
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
const newFilename = `email-${uniqueSuffix}.pdf`;
|
||||
const filePath = path.join(uploadsDir, newFilename);
|
||||
const relativePath = `/uploads/${targetDir}/${newFilename}`;
|
||||
|
||||
// PDF speichern
|
||||
fs.writeFileSync(filePath, pdfBuffer);
|
||||
|
||||
// Alte Datei löschen und DB aktualisieren
|
||||
if (entityType === 'customer') {
|
||||
const customer = stressfreiEmail.customer;
|
||||
|
||||
// Alte Datei löschen
|
||||
const oldPath = (customer as any)[targetField];
|
||||
if (oldPath) {
|
||||
const oldFullPath = path.join(process.cwd(), oldPath);
|
||||
if (fs.existsSync(oldFullPath)) {
|
||||
fs.unlinkSync(oldFullPath);
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.customer.update({
|
||||
where: { id: customer.id },
|
||||
data: { [targetField]: relativePath },
|
||||
});
|
||||
} else if (entityType === 'identityDocument') {
|
||||
if (!entityId) {
|
||||
fs.unlinkSync(filePath);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'entityId ist für identityDocument erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = await prisma.identityDocument.findUnique({ where: { id: entityId } });
|
||||
if (!doc) {
|
||||
fs.unlinkSync(filePath);
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Ausweis nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Alte Datei löschen
|
||||
const oldPath = (doc as any)[targetField];
|
||||
if (oldPath) {
|
||||
const oldFullPath = path.join(process.cwd(), oldPath);
|
||||
if (fs.existsSync(oldFullPath)) {
|
||||
fs.unlinkSync(oldFullPath);
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.identityDocument.update({
|
||||
where: { id: entityId },
|
||||
data: { [targetField]: relativePath },
|
||||
});
|
||||
} else if (entityType === 'bankCard') {
|
||||
if (!entityId) {
|
||||
fs.unlinkSync(filePath);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'entityId ist für bankCard erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const card = await prisma.bankCard.findUnique({ where: { id: entityId } });
|
||||
if (!card) {
|
||||
fs.unlinkSync(filePath);
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Bankkarte nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Alte Datei löschen
|
||||
const oldPath = (card as any)[targetField];
|
||||
if (oldPath) {
|
||||
const oldFullPath = path.join(process.cwd(), oldPath);
|
||||
if (fs.existsSync(oldFullPath)) {
|
||||
fs.unlinkSync(oldFullPath);
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.bankCard.update({
|
||||
where: { id: entityId },
|
||||
data: { [targetField]: relativePath },
|
||||
});
|
||||
} else if (entityType === 'contract') {
|
||||
// Contract-ID kommt aus der E-Mail-Zuordnung oder direkt
|
||||
const contractId = email.contractId;
|
||||
if (!contractId) {
|
||||
fs.unlinkSync(filePath);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'E-Mail ist keinem Vertrag zugeordnet',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const contract = await prisma.contract.findUnique({ where: { id: contractId } });
|
||||
if (!contract) {
|
||||
fs.unlinkSync(filePath);
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Vertrag nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Alte Datei löschen
|
||||
const oldPath = (contract as any)[targetField];
|
||||
if (oldPath) {
|
||||
const oldFullPath = path.join(process.cwd(), oldPath);
|
||||
if (fs.existsSync(oldFullPath)) {
|
||||
fs.unlinkSync(oldFullPath);
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.contract.update({
|
||||
where: { id: contractId },
|
||||
data: { [targetField]: relativePath },
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
path: relativePath,
|
||||
filename: newFilename,
|
||||
size: pdfBuffer.length,
|
||||
},
|
||||
} as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('saveEmailAsPdf error:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: `Fehler beim Erstellen der PDF: ${errorMessage}`,
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,6 +176,15 @@ router.post(
|
|||
cachedEmailController.saveAttachmentTo
|
||||
);
|
||||
|
||||
// E-Mail als PDF exportieren und speichern
|
||||
// POST /api/emails/:id/save-as-pdf { entityType, entityId?, targetKey }
|
||||
router.post(
|
||||
'/emails/:id/save-as-pdf',
|
||||
authenticate,
|
||||
requirePermission('customers:update'),
|
||||
cachedEmailController.saveEmailAsPdf
|
||||
);
|
||||
|
||||
// ==================== VERTRAGSZUORDNUNG ====================
|
||||
|
||||
// E-Mail Vertrag zuordnen
|
||||
|
|
|
|||
|
|
@ -1,153 +1,174 @@
|
|||
import puppeteer from 'puppeteer';
|
||||
// ==================== PDF SERVICE ====================
|
||||
|
||||
/**
|
||||
* Konvertiert HTML zu PDF mit Puppeteer
|
||||
*/
|
||||
export async function htmlToPdf(html: string): Promise<Buffer> {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
import PDFDocument from 'pdfkit';
|
||||
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.setContent(html, { waitUntil: 'networkidle0' });
|
||||
|
||||
const pdfBuffer = await page.pdf({
|
||||
format: 'A4',
|
||||
margin: {
|
||||
top: '20mm',
|
||||
right: '15mm',
|
||||
bottom: '20mm',
|
||||
left: '15mm',
|
||||
},
|
||||
printBackground: true,
|
||||
});
|
||||
|
||||
return Buffer.from(pdfBuffer);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
interface EmailData {
|
||||
from: string;
|
||||
to: string;
|
||||
cc?: string;
|
||||
subject: string;
|
||||
date: Date;
|
||||
bodyText?: string;
|
||||
bodyHtml?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut ein HTML-Dokument für eine E-Mail mit Header
|
||||
* Generiert ein PDF aus einer E-Mail
|
||||
*/
|
||||
export function buildEmailHtml(email: {
|
||||
subject?: string | null;
|
||||
fromAddress: string;
|
||||
fromName?: string | null;
|
||||
toAddresses: string;
|
||||
receivedAt: Date;
|
||||
htmlBody?: string | null;
|
||||
textBody?: string | null;
|
||||
}): string {
|
||||
const formatDate = (date: Date) => {
|
||||
return new Date(date).toLocaleString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
// To-Adressen parsen (JSON Array)
|
||||
let toList: string[] = [];
|
||||
export async function generateEmailPdf(email: EmailData): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
toList = JSON.parse(email.toAddresses);
|
||||
} catch {
|
||||
toList = [email.toAddresses];
|
||||
const doc = new PDFDocument({
|
||||
size: 'A4',
|
||||
margins: { top: 50, bottom: 50, left: 50, right: 50 },
|
||||
});
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
doc.on('data', (chunk) => chunks.push(chunk));
|
||||
doc.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
doc.on('error', reject);
|
||||
|
||||
// Header
|
||||
doc
|
||||
.fontSize(18)
|
||||
.font('Helvetica-Bold')
|
||||
.text('E-Mail', { align: 'center' });
|
||||
|
||||
doc.moveDown(1.5);
|
||||
|
||||
// Metadaten-Tabelle
|
||||
doc.fontSize(10).font('Helvetica');
|
||||
|
||||
// Von
|
||||
doc
|
||||
.font('Helvetica-Bold')
|
||||
.text('Von: ', { continued: true })
|
||||
.font('Helvetica')
|
||||
.text(email.from);
|
||||
|
||||
// An
|
||||
doc
|
||||
.font('Helvetica-Bold')
|
||||
.text('An: ', { continued: true })
|
||||
.font('Helvetica')
|
||||
.text(email.to);
|
||||
|
||||
// CC (falls vorhanden)
|
||||
if (email.cc) {
|
||||
doc
|
||||
.font('Helvetica-Bold')
|
||||
.text('CC: ', { continued: true })
|
||||
.font('Helvetica')
|
||||
.text(email.cc);
|
||||
}
|
||||
|
||||
const fromDisplay = email.fromName
|
||||
? `${email.fromName} <${email.fromAddress}>`
|
||||
: email.fromAddress;
|
||||
// Datum
|
||||
const formattedDate = email.date.toLocaleString('de-DE', {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'short',
|
||||
});
|
||||
doc
|
||||
.font('Helvetica-Bold')
|
||||
.text('Datum: ', { continued: true })
|
||||
.font('Helvetica')
|
||||
.text(formattedDate);
|
||||
|
||||
const body = email.htmlBody || `<pre style="white-space: pre-wrap; font-family: inherit;">${email.textBody || ''}</pre>`;
|
||||
// Betreff
|
||||
doc
|
||||
.font('Helvetica-Bold')
|
||||
.text('Betreff: ', { continued: true })
|
||||
.font('Helvetica')
|
||||
.text(email.subject || '(Kein Betreff)');
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 12pt;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.email-header {
|
||||
background: #f5f5f5;
|
||||
border-bottom: 2px solid #ddd;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.email-header h1 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 16pt;
|
||||
color: #222;
|
||||
}
|
||||
.email-header table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.email-header th {
|
||||
text-align: left;
|
||||
width: 60px;
|
||||
padding: 3px 10px 3px 0;
|
||||
color: #666;
|
||||
font-weight: normal;
|
||||
vertical-align: top;
|
||||
}
|
||||
.email-header td {
|
||||
padding: 3px 0;
|
||||
}
|
||||
.email-body {
|
||||
padding: 0 5px;
|
||||
}
|
||||
.email-body img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-header">
|
||||
<h1>${escapeHtml(email.subject || '(Kein Betreff)')}</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Von:</th>
|
||||
<td>${escapeHtml(fromDisplay)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>An:</th>
|
||||
<td>${escapeHtml(toList.join(', '))}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Datum:</th>
|
||||
<td>${formatDate(email.receivedAt)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="email-body">
|
||||
${body}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`.trim();
|
||||
doc.moveDown(1);
|
||||
|
||||
// Trennlinie
|
||||
doc
|
||||
.moveTo(50, doc.y)
|
||||
.lineTo(doc.page.width - 50, doc.y)
|
||||
.stroke();
|
||||
|
||||
doc.moveDown(1);
|
||||
|
||||
// Inhalt
|
||||
doc.fontSize(11);
|
||||
|
||||
// HTML in Text konvertieren (vereinfacht)
|
||||
let content = email.bodyText || '';
|
||||
|
||||
if (!content && email.bodyHtml) {
|
||||
// Einfache HTML-zu-Text Konvertierung
|
||||
content = htmlToPlainText(email.bodyHtml);
|
||||
}
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
if (!content) {
|
||||
content = '(Kein Inhalt)';
|
||||
}
|
||||
|
||||
// Text mit Zeilenumbrüchen ausgeben
|
||||
doc.text(content, {
|
||||
align: 'left',
|
||||
lineGap: 2,
|
||||
});
|
||||
|
||||
// Footer mit Erstellungsdatum
|
||||
const footerY = doc.page.height - 40;
|
||||
doc
|
||||
.fontSize(8)
|
||||
.fillColor('#666666')
|
||||
.text(
|
||||
`Exportiert am ${new Date().toLocaleString('de-DE')}`,
|
||||
50,
|
||||
footerY,
|
||||
{ align: 'center', width: doc.page.width - 100 }
|
||||
);
|
||||
|
||||
doc.end();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert HTML in Plain-Text (vereinfacht)
|
||||
*/
|
||||
function htmlToPlainText(html: string): string {
|
||||
let text = html;
|
||||
|
||||
// Zeilenumbrüche vor Block-Elementen
|
||||
text = text.replace(/<(br|p|div|h[1-6]|li|tr)[^>]*>/gi, '\n');
|
||||
|
||||
// Listen-Elemente
|
||||
text = text.replace(/<li[^>]*>/gi, '\n• ');
|
||||
|
||||
// Links mit URL
|
||||
text = text.replace(/<a[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/gi, '$2 ($1)');
|
||||
|
||||
// Alle anderen Tags entfernen
|
||||
text = text.replace(/<[^>]+>/g, '');
|
||||
|
||||
// HTML-Entities dekodieren
|
||||
text = text.replace(/ /g, ' ');
|
||||
text = text.replace(/&/g, '&');
|
||||
text = text.replace(/</g, '<');
|
||||
text = text.replace(/>/g, '>');
|
||||
text = text.replace(/"/g, '"');
|
||||
text = text.replace(/'/g, "'");
|
||||
text = text.replace(/ä/g, 'ä');
|
||||
text = text.replace(/ö/g, 'ö');
|
||||
text = text.replace(/ü/g, 'ü');
|
||||
text = text.replace(/Ä/g, 'Ä');
|
||||
text = text.replace(/Ö/g, 'Ö');
|
||||
text = text.replace(/Ü/g, 'Ü');
|
||||
text = text.replace(/ß/g, 'ß');
|
||||
|
||||
// Mehrfache Leerzeilen reduzieren
|
||||
text = text.replace(/\n{3,}/g, '\n\n');
|
||||
|
||||
// Führende/folgende Leerzeichen entfernen
|
||||
text = text.trim();
|
||||
|
||||
return text;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,194 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { contractApi } from '../../services/api';
|
||||
import Modal from '../ui/Modal';
|
||||
import Badge from '../ui/Badge';
|
||||
import Card from '../ui/Card';
|
||||
import CopyButton from '../ui/CopyButton';
|
||||
import type { ContractType, ContractStatus } from '../../types';
|
||||
|
||||
const typeLabels: Record<ContractType, string> = {
|
||||
ELECTRICITY: 'Strom',
|
||||
GAS: 'Gas',
|
||||
DSL: 'DSL',
|
||||
CABLE: 'Kabelinternet',
|
||||
FIBER: 'Glasfaser',
|
||||
MOBILE: 'Mobilfunk',
|
||||
TV: 'TV',
|
||||
CAR_INSURANCE: 'KFZ-Versicherung',
|
||||
};
|
||||
|
||||
const statusLabels: Record<ContractStatus, string> = {
|
||||
DRAFT: 'Entwurf',
|
||||
PENDING: 'Ausstehend',
|
||||
ACTIVE: 'Aktiv',
|
||||
CANCELLED: 'Gekündigt',
|
||||
EXPIRED: 'Abgelaufen',
|
||||
DEACTIVATED: 'Deaktiviert',
|
||||
};
|
||||
|
||||
const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' | 'default'> = {
|
||||
ACTIVE: 'success',
|
||||
PENDING: 'warning',
|
||||
CANCELLED: 'danger',
|
||||
EXPIRED: 'danger',
|
||||
DRAFT: 'default',
|
||||
DEACTIVATED: 'default',
|
||||
};
|
||||
|
||||
interface ContractDetailModalProps {
|
||||
contractId: number;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function ContractDetailModal({ contractId, isOpen, onClose }: ContractDetailModalProps) {
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ['contract', contractId],
|
||||
queryFn: () => contractApi.getById(contractId),
|
||||
enabled: isOpen,
|
||||
});
|
||||
|
||||
const c = data?.data;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Vertragsdetails" size="xl">
|
||||
{isLoading && (
|
||||
<div className="text-center py-8 text-gray-500">Laden...</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="text-center py-8 text-red-600">Fehler beim Laden des Vertrags</div>
|
||||
)}
|
||||
|
||||
{c && (
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 pb-4 border-b">
|
||||
<span className="text-xl font-bold font-mono flex items-center gap-2">
|
||||
{c.contractNumber}
|
||||
<CopyButton value={c.contractNumber} />
|
||||
</span>
|
||||
<Badge>{typeLabels[c.type as ContractType] || c.type}</Badge>
|
||||
<Badge variant={statusVariants[c.status as ContractStatus] || 'default'}>
|
||||
{statusLabels[c.status as ContractStatus] || c.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Anbieter & Tarif */}
|
||||
{(c.providerName || c.provider?.name || c.tariffName || c.tariff?.name) && (
|
||||
<Card title="Anbieter & Tarif">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{(c.providerName || c.provider?.name) && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Anbieter</dt>
|
||||
<dd className="flex items-center gap-1">
|
||||
{c.providerName || c.provider?.name}
|
||||
<CopyButton value={c.providerName || c.provider?.name || ''} />
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{(c.tariffName || c.tariff?.name) && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Tarif</dt>
|
||||
<dd className="flex items-center gap-1">
|
||||
{c.tariffName || c.tariff?.name}
|
||||
<CopyButton value={c.tariffName || c.tariff?.name || ''} />
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{c.customerNumberAtProvider && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Kundennummer beim Anbieter</dt>
|
||||
<dd className="font-mono flex items-center gap-1">
|
||||
{c.customerNumberAtProvider}
|
||||
<CopyButton value={c.customerNumberAtProvider} />
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Laufzeit */}
|
||||
<Card title="Laufzeit">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{c.startDate && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Vertragsbeginn</dt>
|
||||
<dd>{new Date(c.startDate).toLocaleDateString('de-DE')}</dd>
|
||||
</div>
|
||||
)}
|
||||
{c.endDate && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Vertragsende</dt>
|
||||
<dd>{new Date(c.endDate).toLocaleDateString('de-DE')}</dd>
|
||||
</div>
|
||||
)}
|
||||
{c.contractDuration && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Laufzeit</dt>
|
||||
<dd>{c.contractDuration.description}</dd>
|
||||
</div>
|
||||
)}
|
||||
{c.cancellationPeriod && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Kündigungsfrist</dt>
|
||||
<dd>{c.cancellationPeriod.description}</dd>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Portal-Zugangsdaten */}
|
||||
{(c.portalUsername || c.provider?.portalUrl) && (
|
||||
<Card title="Portal-Zugangsdaten">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{c.provider?.portalUrl && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Portal-URL</dt>
|
||||
<dd>
|
||||
<a
|
||||
href={c.provider.portalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline"
|
||||
>
|
||||
{c.provider.portalUrl}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{c.portalUsername && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Benutzername</dt>
|
||||
<dd className="font-mono flex items-center gap-1">
|
||||
{c.portalUsername}
|
||||
<CopyButton value={c.portalUsername} />
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Adresse */}
|
||||
{c.address && (
|
||||
<Card title="Lieferadresse">
|
||||
<p>
|
||||
{c.address.street} {c.address.houseNumber}
|
||||
</p>
|
||||
<p>{c.address.postalCode} {c.address.city}</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Notizen */}
|
||||
{c.notes && (
|
||||
<Card title="Notizen">
|
||||
<p className="whitespace-pre-wrap text-gray-700">{c.notes}</p>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as ContractDetailModal } from './ContractDetailModal';
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Reply, Star, Paperclip, Link2, X, Download, ExternalLink, Trash2, Undo2, Save } from 'lucide-react';
|
||||
import { Reply, Star, Paperclip, Link2, X, Download, ExternalLink, Trash2, Undo2, Save, FileDown } from 'lucide-react';
|
||||
import { CachedEmail, cachedEmailApi } from '../../services/api';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import Button from '../ui/Button';
|
||||
|
|
@ -7,6 +7,7 @@ import { Link } from 'react-router-dom';
|
|||
import { useAuth } from '../../context/AuthContext';
|
||||
import toast from 'react-hot-toast';
|
||||
import SaveAttachmentModal from './SaveAttachmentModal';
|
||||
import SaveEmailAsPdfModal from './SaveEmailAsPdfModal';
|
||||
|
||||
interface EmailDetailProps {
|
||||
email: CachedEmail;
|
||||
|
|
@ -37,6 +38,7 @@ export default function EmailDetail({
|
|||
const [showRestoreConfirm, setShowRestoreConfirm] = useState(false);
|
||||
const [showPermanentDeleteConfirm, setShowPermanentDeleteConfirm] = useState(false);
|
||||
const [saveAttachmentFilename, setSaveAttachmentFilename] = useState<string | null>(null);
|
||||
const [showSaveAsPdfModal, setShowSaveAsPdfModal] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
const { hasPermission } = useAuth();
|
||||
|
||||
|
|
@ -219,6 +221,15 @@ export default function EmailDetail({
|
|||
<Reply className="w-4 h-4 mr-1" />
|
||||
Antworten
|
||||
</Button>
|
||||
{/* E-Mail als PDF speichern */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowSaveAsPdfModal(true)}
|
||||
title="E-Mail als PDF speichern"
|
||||
>
|
||||
<FileDown className="w-4 h-4" />
|
||||
</Button>
|
||||
{/* Löschen-Button nur für User mit emails:delete Permission */}
|
||||
{hasPermission('emails:delete') && (
|
||||
<Button
|
||||
|
|
@ -481,6 +492,15 @@ export default function EmailDetail({
|
|||
attachmentFilename={saveAttachmentFilename}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* E-Mail als PDF speichern Modal */}
|
||||
{showSaveAsPdfModal && (
|
||||
<SaveEmailAsPdfModal
|
||||
isOpen={true}
|
||||
onClose={() => setShowSaveAsPdfModal(false)}
|
||||
emailId={email.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@ export { default as ComposeEmailModal } from './ComposeEmailModal';
|
|||
export { default as AssignToContractModal } from './AssignToContractModal';
|
||||
export { default as EmailClientTab } from './EmailClientTab';
|
||||
export { default as ContractEmailsSection } from './ContractEmailsSection';
|
||||
export { default as SaveEmailAsPdfModal } from './SaveEmailAsPdfModal';
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useParams, Link, useNavigate, useLocation } from 'react-router-dom';
|
|||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { contractApi, uploadApi, meterApi, contractTaskApi, appSettingsApi } from '../../services/api';
|
||||
import { ContractEmailsSection } from '../../components/email';
|
||||
import { ContractDetailModal } from '../../components/contracts';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import Card from '../../components/ui/Card';
|
||||
import Button from '../../components/ui/Button';
|
||||
|
|
@ -1062,6 +1063,9 @@ export default function ContractDetail() {
|
|||
const [showSipPasswords, setShowSipPasswords] = useState<Record<number, boolean>>({});
|
||||
const [decryptedSipPasswords, setDecryptedSipPasswords] = useState<Record<number, string | null>>({});
|
||||
|
||||
// Modal für Vorgängervertrag
|
||||
const [showPredecessorModal, setShowPredecessorModal] = useState(false);
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['contract', id],
|
||||
queryFn: () => contractApi.getById(contractId),
|
||||
|
|
@ -1270,6 +1274,15 @@ export default function ContractDetail() {
|
|||
</div>
|
||||
{!isCustomer && (
|
||||
<div className="flex gap-2">
|
||||
{c.previousContract && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setShowPredecessorModal(true)}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Vorgängervertrag
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission('contracts:create') && !c.followUpContract && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
|
@ -1320,12 +1333,12 @@ export default function ContractDetail() {
|
|||
<div>
|
||||
<dt className="text-sm text-gray-500">Vertragsnummer</dt>
|
||||
<dd>
|
||||
<Link
|
||||
to={`/contracts/${c.previousContract.id}`}
|
||||
<button
|
||||
onClick={() => setShowPredecessorModal(true)}
|
||||
className="text-blue-600 hover:underline"
|
||||
>
|
||||
{c.previousContract.contractNumber}
|
||||
</Link>
|
||||
</button>
|
||||
</dd>
|
||||
</div>
|
||||
{c.previousContract.providerName && (
|
||||
|
|
@ -2369,6 +2382,15 @@ export default function ContractDetail() {
|
|||
<p className="whitespace-pre-wrap">{c.notes}</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Vorgängervertrag Modal */}
|
||||
{showPredecessorModal && c.previousContract && (
|
||||
<ContractDetailModal
|
||||
contractId={c.previousContract.id}
|
||||
isOpen={true}
|
||||
onClose={() => setShowPredecessorModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -506,6 +506,14 @@ export const cachedEmailApi = {
|
|||
);
|
||||
return res.data;
|
||||
},
|
||||
// E-Mail als PDF speichern
|
||||
saveEmailAsPdf: async (emailId: number, params: { entityType: string; entityId?: number; targetKey: string }) => {
|
||||
const res = await api.post<ApiResponse<{ path: string; filename: string; size: number }>>(
|
||||
`/emails/${emailId}/save-as-pdf`,
|
||||
params
|
||||
);
|
||||
return res.data;
|
||||
},
|
||||
};
|
||||
|
||||
// Contracts - Vertragsbaum für Kundenansicht
|
||||
|
|
|
|||
Loading…
Reference in New Issue