diff --git a/backend/src/billing/billing.controller.ts b/backend/src/billing/billing.controller.ts index 495656f..67fcc6d 100644 --- a/backend/src/billing/billing.controller.ts +++ b/backend/src/billing/billing.controller.ts @@ -9,13 +9,11 @@ import { Query, UseGuards, Request, - Res, BadRequestException, Inject, forwardRef, ForbiddenException, } from '@nestjs/common'; -import { Response } from 'express'; import { AuthGuard } from '@nestjs/passport'; import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { BillingService } from './billing.service'; @@ -174,20 +172,6 @@ export class BillingController { }); } - @Get('invoices/:id/pdf') - @ApiOperation({ summary: 'Download invoice as PDF' }) - async downloadInvoicePdf( - @Request() req: any, - @Param('id') id: string, - @Res() res: Response, - ) { - const invoice = await this.billingService.getInvoiceForUser(id, req.user); - const pdf = this.billingService.generateInvoicePdf(invoice); - res.setHeader('Content-Type', 'application/pdf'); - res.setHeader('Content-Disposition', `attachment; filename="${invoice.invoiceNumber}.pdf"`); - res.send(pdf); - } - @Get('invoices/:id') @ApiOperation({ summary: 'Get one invoice with line items and transactions' }) async getInvoice(@Request() req: any, @Param('id') id: string) { @@ -372,21 +356,6 @@ export class BillingController { return this.billingService.getInvoiceForUser(id, req.user); } - @Get('admin/invoices/:id/pdf') - @Roles(UserRole.ADMIN) - @ApiOperation({ summary: 'Download invoice PDF (Admin)' }) - async downloadAdminInvoicePdf( - @Request() req: any, - @Param('id') id: string, - @Res() res: Response, - ) { - const invoice = await this.billingService.getInvoiceForUser(id, req.user); - const pdf = this.billingService.generateInvoicePdf(invoice); - res.setHeader('Content-Type', 'application/pdf'); - res.setHeader('Content-Disposition', `attachment; filename="${invoice.invoiceNumber}.pdf"`); - res.send(pdf); - } - @Patch('admin/invoices/:id/status') @Roles(UserRole.ADMIN) @ApiOperation({ summary: 'Update invoice status with reason (Admin)' }) diff --git a/backend/src/billing/billing.service.ts b/backend/src/billing/billing.service.ts index 7455b62..ef54cac 100644 --- a/backend/src/billing/billing.service.ts +++ b/backend/src/billing/billing.service.ts @@ -521,71 +521,6 @@ export class BillingService { return this.invoiceRepo.save(invoice); } - generateInvoicePdf(invoice: Invoice): Buffer { - const formatAmount = (amount: number) => `${Number(amount || 0).toLocaleString('en-US')} Toman`; - const formatDate = (date?: Date) => (date ? new Date(date).toLocaleString('en-US') : '-'); - const clean = (value: unknown) => - String(value ?? '-') - .replace(/[^\x20-\x7E]/g, '?') - .replace(/[\\()]/g, (match) => `\\${match}`); - - const lines = [ - 'CloudHost Invoice', - `Invoice Number: ${invoice.invoiceNumber}`, - `Status: ${invoice.status}`, - `Payment Method: ${invoice.paymentMethod || '-'}`, - `Customer: ${invoice.user?.email || invoice.userId}`, - `Application: ${invoice.application?.name || invoice.applicationId || '-'}`, - `Created At: ${formatDate(invoice.createdAt)}`, - `Paid At: ${formatDate(invoice.paidAt)}`, - '', - 'Line Items:', - ...(invoice.lines || []).map( - (line) => `- ${line.label}${line.description ? ` (${line.description})` : ''}: ${formatAmount(Number(line.amount))}`, - ), - '', - `Subtotal: ${formatAmount(Number(invoice.subtotal))}`, - `Total: ${formatAmount(Number(invoice.total))}`, - `Paid: ${formatAmount(Number(invoice.paidAmount))}`, - `Due: ${formatAmount(Number(invoice.dueAmount))}`, - `Gateway Tracking: ${invoice.gatewayTrackingCode || '-'}`, - ].map(clean); - - const content = [ - 'BT', - '/F1 12 Tf', - '50 790 Td', - '16 TL', - ...lines.map((line, index) => `${index === 0 ? '' : 'T* '}(${line}) Tj`), - 'ET', - ].join('\n'); - - const objects = [ - '1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n', - '2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n', - '3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n', - '4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n', - `5 0 obj\n<< /Length ${Buffer.byteLength(content, 'utf8')} >>\nstream\n${content}\nendstream\nendobj\n`, - ]; - - let pdf = '%PDF-1.4\n'; - const offsets = [0]; - for (const object of objects) { - offsets.push(Buffer.byteLength(pdf, 'utf8')); - pdf += object; - } - - const xrefOffset = Buffer.byteLength(pdf, 'utf8'); - pdf += `xref\n0 ${objects.length + 1}\n`; - pdf += '0000000000 65535 f \n'; - for (let i = 1; i < offsets.length; i += 1) { - pdf += `${String(offsets[i]).padStart(10, '0')} 00000 n \n`; - } - pdf += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF\n`; - - return Buffer.from(pdf, 'utf8'); - } - /** * Calculate cost for an existing Application entity. * Used by lifecycle service for auto-renew. diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..f363a1f --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1,5 @@ +registry=https://registry.npmmirror.com +fetch-timeout=600000 +fetch-retries=8 +fetch-retry-mintimeout=20000 +fetch-retry-maxtimeout=600000 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d25e4f6..cef11a2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,8 @@ "axios": "^1.17.0", "clsx": "^2.1.0", "framer-motion": "^12.40.0", + "html2canvas": "^1.4.1", + "jspdf": "^4.2.1", "lenis": "^1.3.23", "lucide-react": "^1.18.0", "next": "16.2.9", @@ -308,9 +310,9 @@ "license": "Apache-2.0" }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -1761,6 +1763,19 @@ "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", "license": "MIT" }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/react": { "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", @@ -1811,6 +1826,13 @@ "meshoptimizer": "~1.1.1" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/webxr": { "version": "0.5.24", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", @@ -2384,6 +2406,18 @@ "node": ">=14.0.0" } }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", @@ -2395,6 +2429,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", @@ -2779,6 +2824,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2995,6 +3049,26 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmmirror.com/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3073,6 +3147,18 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -3105,6 +3191,15 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -3273,6 +3368,16 @@ "node": ">=0.10.0" } }, + "node_modules/dompurify": { + "version": "3.4.11", + "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/draco3d": { "version": "1.5.7", "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", @@ -3976,6 +4081,17 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmmirror.com/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -4448,6 +4564,19 @@ "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==", "license": "Apache-2.0" }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -4539,6 +4668,12 @@ "node": ">= 0.4" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmmirror.com/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -5106,6 +5241,23 @@ "node": ">=6" } }, + "node_modules/jspdf": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -6007,6 +6159,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6046,6 +6204,13 @@ "dev": true, "license": "MIT" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6192,6 +6357,16 @@ ], "license": "MIT" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmmirror.com/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", @@ -6289,6 +6464,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -6374,6 +6556,16 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6689,6 +6881,16 @@ "dev": true, "license": "MIT" }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/stats-gl": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", @@ -6924,6 +7126,16 @@ "react": ">=17.0" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/tailwindcss": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", @@ -6945,6 +7157,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/three": { "version": "0.184.0", "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", @@ -7411,6 +7632,15 @@ "node": ">= 4" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/webgl-constants": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index b27d936..3e96de6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,8 @@ "axios": "^1.17.0", "clsx": "^2.1.0", "framer-motion": "^12.40.0", + "html2canvas": "^1.4.1", + "jspdf": "^4.2.1", "lenis": "^1.3.23", "lucide-react": "^1.18.0", "next": "16.2.9", diff --git a/frontend/src/app/[lang]/dashboard/admin/invoices/page.tsx b/frontend/src/app/[lang]/dashboard/admin/invoices/page.tsx index bb5a58c..bffd9f1 100644 --- a/frontend/src/app/[lang]/dashboard/admin/invoices/page.tsx +++ b/frontend/src/app/[lang]/dashboard/admin/invoices/page.tsx @@ -6,6 +6,8 @@ import { FileText, Search, User, Wallet, CreditCard, XCircle, Download } from 'l import { notify } from '@/lib/notify'; import api from '@/lib/api'; import { useT, useLocale } from '@/i18n/I18nProvider'; +import { translateInvoiceLabel, translateInvoiceDescription, translateInvoiceReason } from '@/lib/invoice-labels'; +import { downloadInvoicePdf, buildInvoicePdfData } from '@/lib/invoice-pdf'; import type { Invoice, InvoiceStatus, PaymentMethod } from '@/types'; import { Select } from '@/components/ui/select'; @@ -59,18 +61,14 @@ export default function AdminInvoicesPage() { onError: (err: any) => notify.error(err, inv.statusUpdateFailed), }); + const isFa = locale.startsWith('fa'); + const lineLabel = (label?: string) => (isFa ? translateInvoiceLabel(label) : (label ?? '-')); + const lineDesc = (desc?: string) => (isFa ? translateInvoiceDescription(desc) : (desc ?? '')); + const reasonLabel = (reason?: string) => (isFa ? translateInvoiceReason(reason) : (reason ?? '-')); + const downloadPdfMutation = useMutation({ - mutationFn: async (invoice: Invoice) => { - const { data } = await api.get(`/billing/admin/invoices/${invoice.id}/pdf`, { responseType: 'blob' }); - const url = window.URL.createObjectURL(new Blob([data], { type: 'application/pdf' })); - const link = document.createElement('a'); - link.href = url; - link.download = `${invoice.invoiceNumber}.pdf`; - document.body.appendChild(link); - link.click(); - link.remove(); - window.URL.revokeObjectURL(url); - }, + mutationFn: (invoice: Invoice) => + downloadInvoicePdf(buildInvoicePdfData(invoice, { inv, appName: t.common.appName, locale })), onError: () => notify.error(inv.downloadFailed), }); @@ -162,7 +160,7 @@ export default function AdminInvoicesPage() { >
{invoice.invoiceNumber}
-{invoice.application?.name || invoice.reason} · {formatDate(invoice.createdAt)}
+{invoice.application?.name || reasonLabel(invoice.reason)} · {formatDate(invoice.createdAt)}
{invoice.user?.email || invoice.userId}
@@ -193,7 +191,7 @@ export default function AdminInvoicesPage() {{selectedInvoice.application?.name || selectedInvoice.reason}
+{selectedInvoice.application?.name || reasonLabel(selectedInvoice.reason)}
{line.label}
- {line.description &&{line.description}
} +{lineLabel(line.label)}
+ {line.description &&{lineDesc(line.description)}
}{invoice.invoiceNumber}
- {invoice.application?.name || invoice.reason} · {formatDate(invoice.createdAt)} + {invoice.application?.name || reasonLabel(invoice.reason)} · {formatDate(invoice.createdAt)}
{selectedInvoice.application?.name || selectedInvoice.reason}
+{selectedInvoice.application?.name || reasonLabel(selectedInvoice.reason)}
{line.label}
- {line.description &&{line.description}
} +{lineLabel(line.label)}
+ {line.description &&{lineDesc(line.description)}
}