From af7d3dcf28e4e22b9aa8c82ec2892bb84be8597b Mon Sep 17 00:00:00 2001 From: david Date: Fri, 12 Sep 2025 20:07:38 +0200 Subject: [PATCH] Facturas de cliente --- apps/server/package.json | 14 +- .../api/application/presenters/presenter.ts | 4 +- .../express/express-controller.ts | 2 +- modules/customer-invoices/package.json | 6 +- .../customer-invoice.full.presenter.ts | 1 + .../report-customer-invoice.use-case.ts | 7 +- .../reporter/customer-invoice.report.html.ts | 2 +- .../reporter/customer-invoice.report.pdf.ts | 79 ++++----- .../templates/customer-invoice/template.hbs | 44 +++++ .../uecko-footer-logos.jpg | Bin .../uecko-logo.svg | 0 .../reporter/templates/quote/template.hbs | 152 ------------------ .../report-customer-invoice.controller.ts | 4 +- .../customer-invoices-list-grid.tsx | 8 + .../create/customer-invoice-edit-form.tsx | 89 +--------- .../src/web/components/client-selector.tsx | 5 +- pnpm-lock.yaml | 14 +- 17 files changed, 134 insertions(+), 297 deletions(-) create mode 100644 modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/customer-invoice/template.hbs rename modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/{quote => customer-invoice}/uecko-footer-logos.jpg (100%) rename modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/{quote => customer-invoice}/uecko-logo.svg (100%) delete mode 100644 modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/template.hbs diff --git a/apps/server/package.json b/apps/server/package.json index 5bf289a5..f4bae231 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -43,8 +43,8 @@ "dependencies": { "@erp/auth": "workspace:*", "@erp/core": "workspace:*", - "@erp/customers": "workspace:*", "@erp/customer-invoices": "workspace:*", + "@erp/customers": "workspace:*", "bcrypt": "^5.1.1", "cls-rtracer": "^2.6.3", "cors": "^2.8.5", @@ -52,6 +52,7 @@ "dotenv": "^16.5.0", "express": "^4.18.2", "express-list-routes": "^1.3.1", + "handlebars": "^4.7.8", "helmet": "^8.0.0", "http": "0.0.1-security", "jsonwebtoken": "^9.0.2", @@ -62,6 +63,8 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "path": "^0.12.7", + "puppeteer": "^24.20.0", + "puppeteer-report": "^3.2.0", "reflect-metadata": "^0.2.2", "response-time": "^2.3.3", "sequelize": "^6.37.5", @@ -76,9 +79,14 @@ "node": ">=22" }, "tsup": { - "entry": ["src/index.ts"], + "entry": [ + "src/index.ts" + ], "outDir": "dist", - "format": ["esm", "cjs"], + "format": [ + "esm", + "cjs" + ], "target": "ES2022", "sourcemap": true, "clean": true, diff --git a/modules/core/src/api/application/presenters/presenter.ts b/modules/core/src/api/application/presenters/presenter.ts index b323aef4..870bc02a 100644 --- a/modules/core/src/api/application/presenters/presenter.ts +++ b/modules/core/src/api/application/presenters/presenter.ts @@ -5,7 +5,7 @@ export type IPresenterParams = { presenterRegistry: IPresenterRegistry; } & Record; -export abstract class Presenter implements IPresenter { +export abstract class Presenter implements IPresenter { constructor(protected presenterRegistry: IPresenterRegistry) {} - abstract toOutput(source: unknown): unknown; + abstract toOutput(source: T): S; } diff --git a/modules/core/src/api/infrastructure/express/express-controller.ts b/modules/core/src/api/infrastructure/express/express-controller.ts index 53e86a12..dedd4b58 100644 --- a/modules/core/src/api/infrastructure/express/express-controller.ts +++ b/modules/core/src/api/infrastructure/express/express-controller.ts @@ -123,7 +123,7 @@ export abstract class ExpressController { this.res.set({ "Content-Type": "application/pdf", "Content-Disposition": `attachment; filename=${filename}`, - //"Content-Length": buffer.length, + "Content-Length": pdfBuffer.length, }); return this.res.send(pdfBuffer); diff --git a/modules/customer-invoices/package.json b/modules/customer-invoices/package.json index 4f784a33..d5b2cda6 100644 --- a/modules/customer-invoices/package.json +++ b/modules/customer-invoices/package.json @@ -13,7 +13,10 @@ "@tanstack/react-query": "^5.74.11", "dinero.js": "^1.9.1", "express": "^4.18.2", + "handlebars": "^4.7.8", "i18next": "^25.1.1", + "puppeteer": "^24.20.0", + "puppeteer-report": "^3.2.0", "react-hook-form": "^7.58.1", "react-i18next": "^15.5.1", "sequelize": "^6.37.5", @@ -45,11 +48,8 @@ "ag-grid-community": "^33.3.0", "ag-grid-react": "^33.3.0", "date-fns": "^4.1.0", - "handlebars": "^4.7.8", "libphonenumber-js": "^1.12.7", "lucide-react": "^0.503.0", - "puppeteer": "^24.20.0", - "puppeteer-report": "^3.2.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^6.26.0" diff --git a/modules/customer-invoices/src/api/application/presenters/full-domain/customer-invoice.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/full-domain/customer-invoice.full.presenter.ts index 7ddae3ca..97d4d673 100644 --- a/modules/customer-invoices/src/api/application/presenters/full-domain/customer-invoice.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/full-domain/customer-invoice.full.presenter.ts @@ -45,6 +45,7 @@ export class CustomerInvoiceFullPresenter extends Presenter { metadata: { entity: "customer-invoices", + link: "", }, }; } diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/report-customer-invoice.use-case.ts b/modules/customer-invoices/src/api/application/report-customer-invoice/report-customer-invoice.use-case.ts index dedfa196..ce865927 100644 --- a/modules/customer-invoices/src/api/application/report-customer-invoice/report-customer-invoice.use-case.ts +++ b/modules/customer-invoices/src/api/application/report-customer-invoice/report-customer-invoice.use-case.ts @@ -2,6 +2,7 @@ import { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { CustomerInvoiceService } from "../../domain"; +import { CustomerInvoiceReportPDFPresenter } from "./reporter"; type ReportCustomerInvoiceUseCaseInput = { companyId: UniqueID; @@ -15,7 +16,7 @@ export class ReportCustomerInvoiceUseCase { private readonly presenterRegistry: IPresenterRegistry ) {} - public execute(params: ReportCustomerInvoiceUseCaseInput) { + public async execute(params: ReportCustomerInvoiceUseCaseInput) { const { invoice_id, companyId } = params; const idOrError = UniqueID.create(invoice_id); @@ -29,7 +30,7 @@ export class ReportCustomerInvoiceUseCase { resource: "customer-invoice", projection: "REPORT", format: "PDF", - }); + }) as CustomerInvoiceReportPDFPresenter; return this.transactionManager.complete(async (transaction) => { try { @@ -43,7 +44,7 @@ export class ReportCustomerInvoiceUseCase { } const invoice = invoiceOrError.data; - const pdfData = pdfPresenter.toOutput(invoiceOrError.data); + const pdfData = await pdfPresenter.toOutput(invoice); return Result.ok({ data: pdfData, filename: `invoice-${invoice.invoiceNumber}.pdf`, diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.report.html.ts b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.report.html.ts index d26b2b1a..795c61ab 100644 --- a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.report.html.ts +++ b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.report.html.ts @@ -15,7 +15,7 @@ export class CustomerInvoiceReportHTMLPresenter extends Presenter { // Obtener y compilar la plantilla HTML const templateHtml = readFileSync( - path.join(__dirname, "./templates/quote/template.hbs") + path.join(__dirname, "./templates/customer-invoice/template.hbs") ).toString(); const template = handlebars.compile(templateHtml, {}); return template(invoiceDTO); diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.report.pdf.ts b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.report.pdf.ts index 8ceaafa5..cb6c4488 100644 --- a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.report.pdf.ts +++ b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.report.pdf.ts @@ -4,50 +4,53 @@ import report from "puppeteer-report"; import { CustomerInvoice } from "../../../domain"; import { CustomerInvoiceReportHTMLPresenter } from "./customer-invoice.report.html"; -export interface ICustomerInvoiceReporter { - toHTML: (invoice: CustomerInvoice) => Promise; - toPDF: (invoice: CustomerInvoice) => Promise; -} - // https://plnkr.co/edit/lWk6Yd?preview -export class CustomerInvoiceReportPDFPresenter extends Presenter { - async toOutput(customerInvoice: CustomerInvoice): Promise { - const htmlPresenter = this.presenterRegistry.getPresenter({ - resource: "customer-invoice", - projection: "REPORT", - format: "HTML", - }) as CustomerInvoiceReportHTMLPresenter; +export class CustomerInvoiceReportPDFPresenter extends Presenter< + CustomerInvoice, + Promise> +> { + async toOutput(customerInvoice: CustomerInvoice): Promise> { + try { + const htmlPresenter = this.presenterRegistry.getPresenter({ + resource: "customer-invoice", + projection: "REPORT", + format: "HTML", + }) as CustomerInvoiceReportHTMLPresenter; - const htmlData = htmlPresenter.toOutput(customerInvoice); + const htmlData = htmlPresenter.toOutput(customerInvoice); - // Generar el PDF con Puppeteer - const browser = await puppeteer.launch({ - args: [ - "--disable-extensions", - "--no-sandbox", - "--disable-setuid-sandbox", - "--disable-dev-shm-usage", - "--disable-gpu", - ], - }); + // Generar el PDF con Puppeteer + const browser = await puppeteer.launch({ + args: [ + "--disable-extensions", + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-dev-shm-usage", + "--disable-gpu", + ], + }); - const page = await browser.newPage(); - const navigationPromise = page.waitForNavigation(); - await page.setContent(htmlData, { waitUntil: "networkidle2" }); + const page = await browser.newPage(); + const navigationPromise = page.waitForNavigation(); + await page.setContent(htmlData, { waitUntil: "networkidle2" }); - await navigationPromise; - const reportPDF = await report.pdfPage(page, { - format: "A4", - margin: { - bottom: "10mm", - left: "10mm", - right: "10mm", - top: "10mm", - }, - }); + await navigationPromise; + const reportPDF = await report.pdfPage(page, { + format: "A4", + margin: { + bottom: "10mm", + left: "10mm", + right: "10mm", + top: "10mm", + }, + }); - await browser.close(); - return Buffer.from(reportPDF); + await browser.close(); + return Buffer.from(reportPDF); + } catch (err: unknown) { + console.error(err); + throw err as Error; + } } } diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/customer-invoice/template.hbs b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/customer-invoice/template.hbs new file mode 100644 index 00000000..99052b75 --- /dev/null +++ b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/customer-invoice/template.hbs @@ -0,0 +1,44 @@ + + + + + + Presupuesto #{{id}} + + + + + + +
+ +
+ + + + \ No newline at end of file diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/uecko-footer-logos.jpg b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/customer-invoice/uecko-footer-logos.jpg similarity index 100% rename from modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/uecko-footer-logos.jpg rename to modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/customer-invoice/uecko-footer-logos.jpg diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/uecko-logo.svg b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/customer-invoice/uecko-logo.svg similarity index 100% rename from modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/uecko-logo.svg rename to modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/customer-invoice/uecko-logo.svg diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/template.hbs b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/template.hbs deleted file mode 100644 index a14b7fe9..00000000 --- a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/template.hbs +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - Presupuesto #{{id}} - - - - - - -
-
- - - - - - - - - - - - - {{#each items}} - - - - - - - - - {{/each}} - -
Cant.DescripciónPrec. UnitarioSubtotalDto (%)Importe total
{{quantity}}{{description}}{{unit_price}}{{subtotal_price}}{{discount}}{{total_price}}
-
- -
- -
-
-

Forma de pago: {{payment_method}}

-
-
-

Notas: {{notes}}

-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Importe neto{{subtotal_price}}
% Descuento{{discount.amount}}{{discount_price}}
Base imponible{{before_tax_price}}
% IVA{{tax}}{{tax_price}}
Importe total{{total_price}}
-
-
- -
-
- -
- - - - \ No newline at end of file diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/report-customer-invoice.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/report-customer-invoice.controller.ts index a824ec84..37d9dcda 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/report-customer-invoice.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/report-customer-invoice.controller.ts @@ -1,5 +1,5 @@ import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; -import { GetCustomerInvoiceUseCase as ReportCustomerInvoiceUseCase } from "../../../application"; +import { ReportCustomerInvoiceUseCase } from "../../../application"; export class ReportCustomerInvoiceController extends ExpressController { public constructor(private readonly useCase: ReportCustomerInvoiceUseCase) { @@ -15,7 +15,7 @@ export class ReportCustomerInvoiceController extends ExpressController { const result = await this.useCase.execute({ invoice_id, companyId }); return result.match( - ({ pdfData, filename }) => this.downloadPDF(pdfData, filename), + ({ data, filename }) => this.downloadPDF(data, filename), (err) => this.handleError(err) ); } diff --git a/modules/customer-invoices/src/web/components/customer-invoices-list-grid.tsx b/modules/customer-invoices/src/web/components/customer-invoices-list-grid.tsx index e6fe13b1..d8cf346f 100644 --- a/modules/customer-invoices/src/web/components/customer-invoices-list-grid.tsx +++ b/modules/customer-invoices/src/web/components/customer-invoices-list-grid.tsx @@ -11,6 +11,7 @@ import { MoneyDTO } from "@erp/core"; import { formatDate, formatMoney } from "@erp/core/client"; // Core CSS import { AgGridReact } from "ag-grid-react"; +import { Link } from "react-router-dom"; import { useCustomerInvoicesQuery } from "../hooks"; import { useTranslation } from "../i18n"; import { CustomerInvoiceStatusBadge } from "./customer-invoice-status-badge"; @@ -76,6 +77,13 @@ export const CustomerInvoicesListGrid = () => { return formatMoney(rawValue); }, }, + { + field: "id", + headerName: t("pages.list.grid_columns.total_amount"), + cellRenderer: (params: ValueFormatterParams) => { + return Hola; + }, + }, ]); const gridOptions: GridOptions = { diff --git a/modules/customer-invoices/src/web/pages/create/customer-invoice-edit-form.tsx b/modules/customer-invoices/src/web/pages/create/customer-invoice-edit-form.tsx index de8b5b01..03e8ed43 100644 --- a/modules/customer-invoices/src/web/pages/create/customer-invoice-edit-form.tsx +++ b/modules/customer-invoices/src/web/pages/create/customer-invoice-edit-form.tsx @@ -276,98 +276,11 @@ export const CustomerInvoiceEditForm = ({ form.reset(initialData); }; - return ( -
- -
- - - Cliente - Description - - - - - - - - - - - - - - {" "} - - {/* Información básica */} - - - Información Básica - Detalles generales de la factura - - - - - - - - - - - - -
-
- - ); - return (
diff --git a/modules/customers/src/web/components/client-selector.tsx b/modules/customers/src/web/components/client-selector.tsx index 09ede470..97854bc4 100644 --- a/modules/customers/src/web/components/client-selector.tsx +++ b/modules/customers/src/web/components/client-selector.tsx @@ -189,8 +189,9 @@ export const ClientSelector = () => { setSelectedCustomer(customer); setOpen(false); }} - onCreate={() => { - setOpen(false); + onCreate={(e) => { + e.preventDefault(); + setOpen(true); console.log("Crear nuevo cliente"); }} page={pageNumber} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5c8e7e1..0941ee3e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: express-list-routes: specifier: ^1.3.1 version: 1.3.1 + handlebars: + specifier: ^4.7.8 + version: 4.7.8 helmet: specifier: ^8.0.0 version: 8.1.0 @@ -92,6 +95,12 @@ importers: path: specifier: ^0.12.7 version: 0.12.7 + puppeteer: + specifier: ^24.20.0 + version: 24.20.0(typescript@5.8.3) + puppeteer-report: + specifier: ^3.2.0 + version: 3.2.0 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -170,7 +179,7 @@ importers: version: 29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3) + version: 29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3) tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 @@ -12283,7 +12292,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3): + ts-jest@29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 @@ -12301,6 +12310,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.27.4) + esbuild: 0.25.5 jest-util: 29.7.0 ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3):