From 89e22de2bdd14faf3169822df57632d4a4488272 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 17 Nov 2025 18:20:18 +0100 Subject: [PATCH] Facturas de cliente --- .../presenters/presenter-registry.ts | 15 +- .../validate-request.middleware.ts | 5 +- .../sequelize/sequelize-error-translator.ts | 3 +- .../application/presenters/domain/index.ts | 5 +- .../domain/issued-invoices/index.ts | 4 + .../issued-invoice-items.full.presenter.ts} | 8 +- ...ssued-invoice-recipient.full.presenter.ts} | 10 +- ...issued-invoice-verifactu.full.presenter.ts | 30 + .../issued-invoice.full.presenter.ts} | 29 +- .../presenters/domain/proformas/index.ts | 1 + .../proforma-items.full.presenter.ts | 53 ++ .../proforma-recipient.full.presenter.ts | 46 ++ .../proformas/proforma.full.presenter.ts | 89 +++ .../src/api/application/presenters/index.ts | 1 + .../customer-invoice.report.presenter.ts | 63 -- .../application/presenters/queries/index.ts | 6 +- .../queries/issued-invoices/index.ts | 1 + .../issued-invoice.list.presenter.ts} | 35 +- .../presenters/queries/proformas/index.ts | 1 + .../proformas/proforma.list.presenter.ts | 76 +++ .../application/presenters/reports/index.ts | 2 + .../reports/issued-invoices/index.ts | 3 + .../issued-invoice-items.report.presenter.ts} | 15 +- .../issued-invoice-taxes.report.presenter.ts} | 13 +- .../issued-invoice.report.presenter.ts | 71 ++ .../presenters/reports/proformas/index.ts | 3 + .../proforma-items.report.presenter.ts | 63 ++ .../proforma-taxes.report.presenter.ts | 41 ++ .../proformas/proforma.report.presenter.ts | 58 ++ .../customer-invoice-application.service.ts | 34 +- .../get-issued-invoice.use-case.ts | 6 +- .../use-cases/issued-invoices/index.ts | 2 +- .../list-issued-invoices.use-case.ts | 8 +- .../report-issued-invoices/index.ts | 1 + .../report-issued-invoice.use-case.ts | 9 +- .../report-issued-invoices/reporter/index.ts | 2 + .../reporter/issued-invoice.report.html.ts} | 20 +- .../reporter/issued-invoice.report.pdf.ts} | 12 +- .../create-proforma.use-case.ts | 18 +- .../proformas/get-proforma.use-case.ts | 6 +- .../proformas/issue-proforma.use-case.ts | 6 +- .../proformas/list-proformas.use-case.ts | 12 +- .../report-proforma.use-case.ts | 6 +- .../report-proforma/reporter/index.ts | 4 +- .../reporter/proforma.report.html.ts | 47 ++ .../reporter/proforma.report.pdf.ts | 69 ++ .../logo_acana.jpg | Bin .../logo_rodax.jpg | Bin .../template.hbs | 14 +- .../template.hbs_BAK | 0 .../template_proforma.hbs | 0 .../template_rodax.hbs | 0 .../uecko-footer-logos.jpg | Bin .../uecko-logo.svg | 0 .../update-proforma.use-case.ts | 6 +- .../api/domain/aggregates/customer-invoice.ts | 15 +- .../customer-invoice-item.ts | 15 +- .../src/api/domain/entities/index.ts | 1 + .../api/domain/entities/verifactu-record.ts | 50 ++ .../customer-invoice-repository.interface.ts | 40 +- .../src/api/domain/value-objects/index.ts | 1 + .../domain/value-objects/verifactu-status.ts} | 20 +- modules/customer-invoices/src/api/index.ts | 4 +- .../src/api/infrastructure/dependencies.bak | 169 ----- .../express/issued-invoices.routes.ts | 4 +- .../express/proformas.routes.ts | 4 +- .../src/api/infrastructure/index.ts | 2 +- .../issued-invoices-dependencies.ts | 148 +++++ .../queries/customer-invoice.list.mapper.ts | 25 +- .../queries/invoice-recipient.list.mapper.ts | 16 +- .../queries/verifactu-record.list.mapper.ts | 64 ++ ...endencies.ts => proformas-dependencies.ts} | 90 ++- .../sequelize/customer-invoice.repository.ts | 229 ++++++- .../src/api/infrastructure/sequelize/index.ts | 3 + .../models/customer-invoice-tax.model.ts | 4 +- .../models/customer-invoice.model.ts | 13 +- .../infrastructure/sequelize/models/index.ts | 7 +- .../models/verifactu-record.model.ts | 40 +- .../get-issued-invoice-by-id.response.dto.ts | 6 + .../list-issued-invoices.response.dto.ts | 6 + .../src/common/locales/en.json | 17 + .../src/common/locales/es.json | 19 +- .../use-issued-invoices-grid-columns.tsx | 67 +- .../pages/list/ui/issued-invoices-grid.tsx | 37 ++ .../update/ui/blocks/proforma-totals.tsx | 1 - .../ui/components/amount-input-field.tsx | 56 ++ .../proformas/ui/components/amount-input.tsx | 233 +++++++ .../src/web/proformas/ui/components/index.ts | 3 + .../ui/components}/input-utils.ts | 37 +- .../ui/components/percentage-input-field.tsx | 56 ++ .../ui/components/percentage-input.tsx | 254 ++++++++ .../ui/components/quantity-input-field.tsx | 56 ++ .../ui/components/quantity-input.tsx | 269 ++++++++ .../customer-invoices/src/web/shared/index.ts | 1 - .../buttons/append-empty-row-button.tsx | 26 - .../web/shared/ui/components/buttons/index.ts | 1 - .../buttons/update-commit-button-group.tsx | 154 ----- .../customer-invoice-prices-card.tsx | 89 --- .../src/web/shared/ui/components/data.json | 614 ------------------ .../web/shared/ui/components/editor/index.ts | 1 - .../ui/components/editor/invoice-notes.tsx | 35 - .../ui/components/editor/invoice-totals.tsx | 156 ----- .../editor/items/amount-input-field.tsx | 53 -- .../components/editor/items/amount-input.tsx | 213 ------ .../components/editor/items/blocks-view.tsx | 139 ---- .../components/editor/items/debug-id-col.tsx | 22 - .../editor/items/hover-card-total-summary.tsx | 128 ---- .../ui/components/editor/items/index.ts | 1 - .../ui/components/editor/items/item-row.tsx | 253 -------- .../items/items-data-table-row-actions.tsx | 115 ---- .../editor/items/items-editor copy.tsx | 165 ----- .../editor/items/items-editor-toolbar.tsx | 127 ---- .../editor/items/last-cell-tab-hook.tsx | 34 - .../editor/items/percentage-input-field.tsx | 51 -- .../editor/items/percentage-input.tsx | 248 ------- .../editor/items/quantity-input-field.tsx | 55 -- .../editor/items/quantity-input.tsx | 244 ------- .../ui/components/editor/items/table-view.tsx | 312 --------- .../ui/components/editor/items/types.d.ts | 63 -- .../src/web/shared/ui/components/index.tsx | 4 - .../customer-invoice-items-card-editor.tsx | 315 --------- ...voice-items-sortable-datatable-toolbar.tsx | 87 --- ...tomer-invoice-items-sortable-datatable.tsx | 496 -------------- ...tomer-invoice-items-sortable-table-row.tsx | 75 --- .../web/shared/ui/components/items/index.ts | 2 - .../src/web/shared/ui/index.ts | 2 - .../ui/layouts/customer-invoices-layout.tsx | 5 - .../src/web/shared/ui/layouts/index.ts | 1 - .../src/api/domain/aggregates/customer.ts | 29 +- .../editor/customer-basic-info-fields.tsx | 4 +- .../editor/customer-contact-fields.tsx | 140 ++-- .../editor/customer-taxes-multi-select.tsx} | 6 +- modules/verifactu/package.json | 35 - .../verifactu/src/api/application/index.ts | 2 - .../application/presenters/domain/index.ts | 2 - .../domain/verifactu-record.full.presenter.ts | 24 - .../src/api/application/use-cases/index.ts | 1 - .../api/application/use-cases/send/index.ts | 1 - .../use-cases/send/send-invoice.use-case.ts | 69 -- .../src/api/domain/aggregates/index.ts | 1 - .../domain/aggregates/value-objects/index.ts | 2 - .../value-objects/verifactu-record-url.ts | 55 -- .../api/domain/aggregates/verifactu-record.ts | 42 -- modules/verifactu/src/api/domain/index.ts | 7 - .../src/api/domain/repositories/index.ts | 1 - .../verifactu-repository.interface.ts | 29 - .../src/api/domain/services/index.ts | 1 - .../services/verifactu-record.service.ts | 49 -- modules/verifactu/src/api/helpers/index.ts | 1 - modules/verifactu/src/api/helpers/logger.ts | 3 - modules/verifactu/src/api/index.ts | 29 - .../src/api/infrastructure/dependencies.ts | 86 --- .../express/controllers/index.ts | 1 - .../send-invoice-verifactu.controller.ts | 25 - .../src/api/infrastructure/express/index.ts | 1 - .../express/verifactu.routes.ts | 56 -- .../verifactu/src/api/infrastructure/index.ts | 3 - .../infrastructure/mappers/domain/index.ts | 1 - .../mappers/domain/verifactu-record.mapper.ts | 119 ---- .../src/api/infrastructure/mappers/index.ts | 2 - .../src/api/infrastructure/sequelize/index.ts | 7 - .../infrastructure/sequelize/models/index.ts | 1 - .../sequelize/verifactu-record.repository.ts | 251 ------- modules/verifactu/src/common/dto/index.ts | 2 - ...get-verifactu-record-by-id.response.dto.ts | 74 --- .../verifactu/src/common/dto/request/index.ts | 2 - .../request/send-invoice-by-id.request.dto.ts | 7 - modules/verifactu/src/common/index.ts | 1 - modules/verifactu/tsconfig.json | 33 - .../src/helpers/extract-or-push-error.ts | 5 +- pnpm-lock.yaml | 37 -- 171 files changed, 2704 insertions(+), 5943 deletions(-) create mode 100644 modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/index.ts rename modules/customer-invoices/src/api/application/presenters/domain/{customer-invoice-items.full.presenter.ts => issued-invoices/issued-invoice-items.full.presenter.ts} (89%) rename modules/customer-invoices/src/api/application/presenters/domain/{recipient-invoice.full.representer.ts => issued-invoices/issued-invoice-recipient.full.presenter.ts} (83%) create mode 100644 modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-verifactu.full.presenter.ts rename modules/customer-invoices/src/api/application/presenters/domain/{customer-invoice.full.presenter.ts => issued-invoices/issued-invoice.full.presenter.ts} (74%) create mode 100644 modules/customer-invoices/src/api/application/presenters/domain/proformas/index.ts create mode 100644 modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-items.full.presenter.ts create mode 100644 modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-recipient.full.presenter.ts create mode 100644 modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma.full.presenter.ts delete mode 100644 modules/customer-invoices/src/api/application/presenters/queries/customer-invoice.report.presenter.ts create mode 100644 modules/customer-invoices/src/api/application/presenters/queries/issued-invoices/index.ts rename modules/customer-invoices/src/api/application/presenters/queries/{list-customer-invoices.presenter.ts => issued-invoices/issued-invoice.list.presenter.ts} (75%) create mode 100644 modules/customer-invoices/src/api/application/presenters/queries/proformas/index.ts create mode 100644 modules/customer-invoices/src/api/application/presenters/queries/proformas/proforma.list.presenter.ts create mode 100644 modules/customer-invoices/src/api/application/presenters/reports/index.ts create mode 100644 modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/index.ts rename modules/customer-invoices/src/api/application/presenters/{queries/customer-invoice-items.report.presenter.ts => reports/issued-invoices/issued-invoice-items.report.presenter.ts} (77%) rename modules/customer-invoices/src/api/application/presenters/{queries/customer-invoice-taxes.report.presenter.ts => reports/issued-invoices/issued-invoice-taxes.report.presenter.ts} (73%) create mode 100644 modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice.report.presenter.ts create mode 100644 modules/customer-invoices/src/api/application/presenters/reports/proformas/index.ts create mode 100644 modules/customer-invoices/src/api/application/presenters/reports/proformas/proforma-items.report.presenter.ts create mode 100644 modules/customer-invoices/src/api/application/presenters/reports/proformas/proforma-taxes.report.presenter.ts create mode 100644 modules/customer-invoices/src/api/application/presenters/reports/proformas/proforma.report.presenter.ts create mode 100644 modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/index.ts rename modules/customer-invoices/src/api/application/use-cases/issued-invoices/{ => report-issued-invoices}/report-issued-invoice.use-case.ts (86%) create mode 100644 modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/index.ts rename modules/customer-invoices/src/api/application/use-cases/{proformas/report-proforma/reporter/customer-invoice.report.html.ts => issued-invoices/report-issued-invoices/reporter/issued-invoice.report.html.ts} (71%) rename modules/customer-invoices/src/api/application/use-cases/{proformas/report-proforma/reporter/customer-invoice.report.pdf.ts => issued-invoices/report-issued-invoices/reporter/issued-invoice.report.pdf.ts} (81%) create mode 100644 modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.html.ts create mode 100644 modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.pdf.ts rename modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/{customer-invoice => proforma}/logo_acana.jpg (100%) rename modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/{customer-invoice => proforma}/logo_rodax.jpg (100%) rename modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/{customer-invoice => proforma}/template.hbs (88%) rename modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/{customer-invoice => proforma}/template.hbs_BAK (100%) rename modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/{customer-invoice => proforma}/template_proforma.hbs (100%) rename modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/{customer-invoice => proforma}/template_rodax.hbs (100%) rename modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/{customer-invoice => proforma}/uecko-footer-logos.jpg (100%) rename modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/{customer-invoice => proforma}/uecko-logo.svg (100%) create mode 100644 modules/customer-invoices/src/api/domain/entities/verifactu-record.ts rename modules/{verifactu/src/api/domain/aggregates/value-objects/verifactu-record-estado.ts => customer-invoices/src/api/domain/value-objects/verifactu-status.ts} (87%) delete mode 100644 modules/customer-invoices/src/api/infrastructure/dependencies.bak create mode 100644 modules/customer-invoices/src/api/infrastructure/issued-invoices-dependencies.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/mappers/queries/verifactu-record.list.mapper.ts rename modules/customer-invoices/src/api/infrastructure/{dependencies.ts => proformas-dependencies.ts} (59%) rename modules/{verifactu => customer-invoices}/src/api/infrastructure/sequelize/models/verifactu-record.model.ts (66%) create mode 100644 modules/customer-invoices/src/web/proformas/ui/components/amount-input-field.tsx create mode 100644 modules/customer-invoices/src/web/proformas/ui/components/amount-input.tsx create mode 100644 modules/customer-invoices/src/web/proformas/ui/components/index.ts rename modules/customer-invoices/src/web/{shared/ui/components/editor/items => proformas/ui/components}/input-utils.ts (68%) create mode 100644 modules/customer-invoices/src/web/proformas/ui/components/percentage-input-field.tsx create mode 100644 modules/customer-invoices/src/web/proformas/ui/components/percentage-input.tsx create mode 100644 modules/customer-invoices/src/web/proformas/ui/components/quantity-input-field.tsx create mode 100644 modules/customer-invoices/src/web/proformas/ui/components/quantity-input.tsx delete mode 100644 modules/customer-invoices/src/web/shared/index.ts delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/buttons/append-empty-row-button.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/buttons/index.ts delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/buttons/update-commit-button-group.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/customer-invoice-prices-card.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/data.json delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/index.ts delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/invoice-notes.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/invoice-totals.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/items/amount-input-field.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/items/amount-input.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/items/blocks-view.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/items/debug-id-col.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/items/hover-card-total-summary.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/items/index.ts delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/items/item-row.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/items/items-data-table-row-actions.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/items/items-editor copy.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/items/items-editor-toolbar.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/items/last-cell-tab-hook.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/items/percentage-input-field.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/items/percentage-input.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/items/quantity-input-field.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/items/quantity-input.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/items/table-view.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/editor/items/types.d.ts delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/index.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/items/customer-invoice-items-card-editor.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/items/customer-invoice-items-sortable-datatable-toolbar.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/items/customer-invoice-items-sortable-datatable.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/items/customer-invoice-items-sortable-table-row.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/components/items/index.ts delete mode 100644 modules/customer-invoices/src/web/shared/ui/index.ts delete mode 100644 modules/customer-invoices/src/web/shared/ui/layouts/customer-invoices-layout.tsx delete mode 100644 modules/customer-invoices/src/web/shared/ui/layouts/index.ts rename modules/{customer-invoices/src/web/shared/ui/components/proforma-taxes-multi-select.tsx => customers/src/web/components/editor/customer-taxes-multi-select.tsx} (93%) delete mode 100644 modules/verifactu/package.json delete mode 100644 modules/verifactu/src/api/application/index.ts delete mode 100644 modules/verifactu/src/api/application/presenters/domain/index.ts delete mode 100644 modules/verifactu/src/api/application/presenters/domain/verifactu-record.full.presenter.ts delete mode 100644 modules/verifactu/src/api/application/use-cases/index.ts delete mode 100644 modules/verifactu/src/api/application/use-cases/send/index.ts delete mode 100644 modules/verifactu/src/api/application/use-cases/send/send-invoice.use-case.ts delete mode 100644 modules/verifactu/src/api/domain/aggregates/index.ts delete mode 100644 modules/verifactu/src/api/domain/aggregates/value-objects/index.ts delete mode 100644 modules/verifactu/src/api/domain/aggregates/value-objects/verifactu-record-url.ts delete mode 100644 modules/verifactu/src/api/domain/aggregates/verifactu-record.ts delete mode 100644 modules/verifactu/src/api/domain/index.ts delete mode 100644 modules/verifactu/src/api/domain/repositories/index.ts delete mode 100644 modules/verifactu/src/api/domain/repositories/verifactu-repository.interface.ts delete mode 100644 modules/verifactu/src/api/domain/services/index.ts delete mode 100644 modules/verifactu/src/api/domain/services/verifactu-record.service.ts delete mode 100644 modules/verifactu/src/api/helpers/index.ts delete mode 100644 modules/verifactu/src/api/helpers/logger.ts delete mode 100644 modules/verifactu/src/api/index.ts delete mode 100644 modules/verifactu/src/api/infrastructure/dependencies.ts delete mode 100644 modules/verifactu/src/api/infrastructure/express/controllers/index.ts delete mode 100644 modules/verifactu/src/api/infrastructure/express/controllers/send-invoice-verifactu.controller.ts delete mode 100644 modules/verifactu/src/api/infrastructure/express/index.ts delete mode 100644 modules/verifactu/src/api/infrastructure/express/verifactu.routes.ts delete mode 100644 modules/verifactu/src/api/infrastructure/index.ts delete mode 100644 modules/verifactu/src/api/infrastructure/mappers/domain/index.ts delete mode 100644 modules/verifactu/src/api/infrastructure/mappers/domain/verifactu-record.mapper.ts delete mode 100644 modules/verifactu/src/api/infrastructure/mappers/index.ts delete mode 100644 modules/verifactu/src/api/infrastructure/sequelize/index.ts delete mode 100644 modules/verifactu/src/api/infrastructure/sequelize/models/index.ts delete mode 100644 modules/verifactu/src/api/infrastructure/sequelize/verifactu-record.repository.ts delete mode 100644 modules/verifactu/src/common/dto/index.ts delete mode 100644 modules/verifactu/src/common/dto/request/get-verifactu-record-by-id.response.dto.ts delete mode 100644 modules/verifactu/src/common/dto/request/index.ts delete mode 100644 modules/verifactu/src/common/dto/request/send-invoice-by-id.request.dto.ts delete mode 100644 modules/verifactu/src/common/index.ts delete mode 100644 modules/verifactu/tsconfig.json diff --git a/modules/core/src/api/application/presenters/presenter-registry.ts b/modules/core/src/api/application/presenters/presenter-registry.ts index b05eefbf..183dfd7c 100644 --- a/modules/core/src/api/application/presenters/presenter-registry.ts +++ b/modules/core/src/api/application/presenters/presenter-registry.ts @@ -1,6 +1,7 @@ import { ApplicationError } from "../errors"; -import { IPresenterRegistry, PresenterKey } from "./presenter-registry.interface"; -import { IPresenter } from "./presenter.interface"; + +import type { IPresenter } from "./presenter.interface"; +import type { IPresenterRegistry, PresenterKey } from "./presenter-registry.interface"; export class InMemoryPresenterRegistry implements IPresenterRegistry { private registry: Map> = new Map(); @@ -16,7 +17,7 @@ export class InMemoryPresenterRegistry implements IPresenterRegistry { * 🔹 Construye la clave única para el registro. */ private _buildKey(key: PresenterKey): string { - const { resource, projection, format, version, locale } = key; + const { resource, projection, format, version, locale } = this._normalizeKey(key); return [ resource.toLowerCase(), projection.toLowerCase(), @@ -30,12 +31,12 @@ export class InMemoryPresenterRegistry implements IPresenterRegistry { key: PresenterKey, presenter: IPresenter ): void { - const exactKey = this._buildKey(this._normalizeKey(key)); + const exactKey = this._buildKey(key); this.registry.set(exactKey, presenter); } getPresenter(key: PresenterKey): IPresenter { - const exactKey = this._buildKey(this._normalizeKey(key)); + const exactKey = this._buildKey(key); // 1) Intentar clave exacta if (this.registry.has(exactKey)) { @@ -86,7 +87,9 @@ export class InMemoryPresenterRegistry implements IPresenterRegistry { registerPresenters( presenters: Array<{ key: PresenterKey; presenter: IPresenter }> ): this { - presenters.forEach(({ key, presenter }) => this._registerPresenter(key, presenter)); + for (const { key, presenter } of presenters) { + this._registerPresenter(key, presenter); + } return this; } } diff --git a/modules/core/src/api/infrastructure/express/middlewares/validate-request.middleware.ts b/modules/core/src/api/infrastructure/express/middlewares/validate-request.middleware.ts index ed57d9c3..7f252db6 100644 --- a/modules/core/src/api/infrastructure/express/middlewares/validate-request.middleware.ts +++ b/modules/core/src/api/infrastructure/express/middlewares/validate-request.middleware.ts @@ -1,5 +1,6 @@ -import { RequestHandler } from "express"; -import { z } from "zod/v4"; +import type { RequestHandler } from "express"; +import type { z } from "zod/v4"; + import { InternalApiError, ValidationApiError } from "../errors"; import { ExpressController } from "../express-controller"; diff --git a/modules/core/src/api/infrastructure/sequelize/sequelize-error-translator.ts b/modules/core/src/api/infrastructure/sequelize/sequelize-error-translator.ts index eeedaf4e..3b70afb7 100644 --- a/modules/core/src/api/infrastructure/sequelize/sequelize-error-translator.ts +++ b/modules/core/src/api/infrastructure/sequelize/sequelize-error-translator.ts @@ -6,6 +6,7 @@ import { ValidationError as SequelizeValidationError, UniqueConstraintError, } from "sequelize"; + import { DuplicateEntityError, EntityNotFoundError } from "../../domain"; import { InfrastructureRepositoryError } from "../errors/infrastructure-repository-error"; import { InfrastructureUnavailableError } from "../errors/infrastructure-unavailable-error"; @@ -17,7 +18,7 @@ import { InfrastructureUnavailableError } from "../errors/infrastructure-unavail * 👉 Este traductor pertenece a la infraestructura (persistencia) */ export function translateSequelizeError(err: unknown): Error { - console.log(err); + console.error(err); // 1) Duplicados (índices únicos) if (err instanceof UniqueConstraintError) { diff --git a/modules/customer-invoices/src/api/application/presenters/domain/index.ts b/modules/customer-invoices/src/api/application/presenters/domain/index.ts index 19f03545..de2b09ca 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/index.ts +++ b/modules/customer-invoices/src/api/application/presenters/domain/index.ts @@ -1,3 +1,2 @@ -export * from "./customer-invoice-items.full.presenter"; -export * from "./customer-invoice.full.presenter"; -export * from "./recipient-invoice.full.representer"; +export * from "./issued-invoices"; +export * from "./proformas"; diff --git a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/index.ts b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/index.ts new file mode 100644 index 00000000..2276c1de --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/index.ts @@ -0,0 +1,4 @@ +export * from "./issued-invoice.full.presenter"; +export * from "./issued-invoice-items.full.presenter"; +export * from "./issued-invoice-recipient.full.presenter"; +export * from "./issued-invoice-verifactu.full.presenter"; diff --git a/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice-items.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-items.full.presenter.ts similarity index 89% rename from modules/customer-invoices/src/api/application/presenters/domain/customer-invoice-items.full.presenter.ts rename to modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-items.full.presenter.ts index e4f9d8ae..f6cd5f08 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice-items.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-items.full.presenter.ts @@ -3,17 +3,17 @@ import type { GetIssuedInvoiceByIdResponseDTO } from "@erp/customer-invoices/com import { toEmptyString } from "@repo/rdx-ddd"; import type { ArrayElement } from "@repo/rdx-utils"; -import type { CustomerInvoiceItem, CustomerInvoiceItems } from "../../../domain"; +import type { CustomerInvoiceItem, CustomerInvoiceItems } from "../../../../domain"; -type GetCustomerInvoiceItemByInvoiceIdResponseDTO = ArrayElement< +type GetIssuedInvoiceItemByInvoiceIdResponseDTO = ArrayElement< GetIssuedInvoiceByIdResponseDTO["items"] >; -export class CustomerInvoiceItemsFullPresenter extends Presenter { +export class IssuedInvoiceItemsFullPresenter extends Presenter { private _mapItem( invoiceItem: CustomerInvoiceItem, index: number - ): GetCustomerInvoiceItemByInvoiceIdResponseDTO { + ): GetIssuedInvoiceItemByInvoiceIdResponseDTO { const allAmounts = invoiceItem.getAllAmounts(); return { diff --git a/modules/customer-invoices/src/api/application/presenters/domain/recipient-invoice.full.representer.ts b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-recipient.full.presenter.ts similarity index 83% rename from modules/customer-invoices/src/api/application/presenters/domain/recipient-invoice.full.representer.ts rename to modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-recipient.full.presenter.ts index c9a89111..0ff3db64 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/recipient-invoice.full.representer.ts +++ b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-recipient.full.presenter.ts @@ -1,13 +1,13 @@ import { Presenter } from "@erp/core/api"; import { DomainValidationError, toEmptyString } from "@repo/rdx-ddd"; -import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../common/dto"; -import type { CustomerInvoice, InvoiceRecipient } from "../../../domain"; +import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../../common/dto"; +import type { CustomerInvoice, InvoiceRecipient } from "../../../../domain"; -type GetRecipientInvoiceByInvoiceIdResponseDTO = GetIssuedInvoiceByIdResponseDTO["recipient"]; +type GetIssuedInvoiceRecipientByIdResponseDTO = GetIssuedInvoiceByIdResponseDTO["recipient"]; -export class RecipientInvoiceFullPresenter extends Presenter { - toOutput(invoice: CustomerInvoice): GetRecipientInvoiceByInvoiceIdResponseDTO { +export class IssuedInvoiceRecipientFullPresenter extends Presenter { + toOutput(invoice: CustomerInvoice): GetIssuedInvoiceRecipientByIdResponseDTO { if (!invoice.recipient) { throw DomainValidationError.requiredValue("recipient", { cause: invoice, diff --git a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-verifactu.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-verifactu.full.presenter.ts new file mode 100644 index 00000000..a60de91f --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-verifactu.full.presenter.ts @@ -0,0 +1,30 @@ +import { Presenter } from "@erp/core/api"; +import { DomainValidationError } from "@repo/rdx-ddd"; + +import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../../common/dto"; +import type { CustomerInvoice } from "../../../../domain"; + +type GetIssuedInvoiceVerifactuByIdResponseDTO = GetIssuedInvoiceByIdResponseDTO["verifactu"]; + +export class IssuedInvoiceVerifactuFullPresenter extends Presenter { + toOutput(invoice: CustomerInvoice): GetIssuedInvoiceVerifactuByIdResponseDTO { + if (!invoice.verifactu) { + throw DomainValidationError.requiredValue("verifactu", { + cause: invoice, + }); + } + + return invoice.verifactu.match( + (verifactu) => ({ + id: verifactu.id.toString(), + ...verifactu.toObjectString(), + }), + () => ({ + id: "", + status: "", + url: "", + qr_code: "", + }) + ); + } +} diff --git a/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice.full.presenter.ts similarity index 74% rename from modules/customer-invoices/src/api/application/presenters/domain/customer-invoice.full.presenter.ts rename to modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice.full.presenter.ts index 69ebca47..2eef7f79 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice.full.presenter.ts @@ -1,29 +1,36 @@ import { Presenter } from "@erp/core/api"; import { toEmptyString } from "@repo/rdx-ddd"; -import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../common/dto"; -import type { CustomerInvoice } from "../../../domain"; +import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../../common/dto"; +import type { CustomerInvoice } from "../../../../domain"; -import type { CustomerInvoiceItemsFullPresenter } from "./customer-invoice-items.full.presenter"; -import type { RecipientInvoiceFullPresenter } from "./recipient-invoice.full.representer"; +import type { IssuedInvoiceItemsFullPresenter } from "./issued-invoice-items.full.presenter"; +import type { IssuedInvoiceRecipientFullPresenter } from "./issued-invoice-recipient.full.presenter"; +import type { IssuedInvoiceVerifactuFullPresenter } from "./issued-invoice-verifactu.full.presenter"; -export class CustomerInvoiceFullPresenter extends Presenter< +export class IssuedInvoiceFullPresenter extends Presenter< CustomerInvoice, GetIssuedInvoiceByIdResponseDTO > { toOutput(invoice: CustomerInvoice): GetIssuedInvoiceByIdResponseDTO { const itemsPresenter = this.presenterRegistry.getPresenter({ - resource: "customer-invoice-items", + resource: "issued-invoice-items", projection: "FULL", - }) as CustomerInvoiceItemsFullPresenter; + }) as IssuedInvoiceItemsFullPresenter; const recipientPresenter = this.presenterRegistry.getPresenter({ - resource: "recipient-invoice", + resource: "issued-invoice-recipient", projection: "FULL", - }) as RecipientInvoiceFullPresenter; + }) as IssuedInvoiceRecipientFullPresenter; + + const verifactuPresenter = this.presenterRegistry.getPresenter({ + resource: "issued-invoice-verifactu", + projection: "FULL", + }) as IssuedInvoiceVerifactuFullPresenter; const recipient = recipientPresenter.toOutput(invoice); const items = itemsPresenter.toOutput(invoice.items); + const verifactu = verifactuPresenter.toOutput(invoice); const allAmounts = invoice.getAllAmounts(); const payment = invoice.paymentMethod.match( @@ -81,10 +88,12 @@ export class CustomerInvoiceFullPresenter extends Presenter< taxes_amount: allAmounts.taxesAmount.toObjectString(), total_amount: allAmounts.totalAmount.toObjectString(), + verifactu, + items, metadata: { - entity: "customer-invoices", + entity: "issued-invoices", link: "", }, }; diff --git a/modules/customer-invoices/src/api/application/presenters/domain/proformas/index.ts b/modules/customer-invoices/src/api/application/presenters/domain/proformas/index.ts new file mode 100644 index 00000000..011078a1 --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/domain/proformas/index.ts @@ -0,0 +1 @@ +export * from "./proforma.full.presenter"; diff --git a/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-items.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-items.full.presenter.ts new file mode 100644 index 00000000..a135802d --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-items.full.presenter.ts @@ -0,0 +1,53 @@ +import { Presenter } from "@erp/core/api"; +import type { GetProformaByIdResponseDTO } from "@erp/customer-invoices/common"; +import { toEmptyString } from "@repo/rdx-ddd"; +import type { ArrayElement } from "@repo/rdx-utils"; + +import type { CustomerInvoiceItem, CustomerInvoiceItems } from "../../../../domain"; + +type GetProformaItemByIdResponseDTO = ArrayElement; + +export class ProformaItemsFullPresenter extends Presenter { + private _mapItem( + proformaItem: CustomerInvoiceItem, + index: number + ): GetProformaItemByIdResponseDTO { + const allAmounts = proformaItem.getAllAmounts(); + + return { + id: proformaItem.id.toPrimitive(), + is_valued: String(proformaItem.isValued), + position: String(index), + description: toEmptyString(proformaItem.description, (value) => value.toPrimitive()), + + quantity: proformaItem.quantity.match( + (quantity) => quantity.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + unit_amount: proformaItem.unitAmount.match( + (unitAmount) => unitAmount.toObjectString(), + () => ({ value: "", scale: "", currency_code: "" }) + ), + + subtotal_amount: allAmounts.subtotalAmount.toObjectString(), + + discount_percentage: proformaItem.discountPercentage.match( + (discountPercentage) => discountPercentage.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + discount_amount: allAmounts.discountAmount.toObjectString(), + + taxable_amount: allAmounts.taxableAmount.toObjectString(), + tax_codes: proformaItem.taxes.getCodesToString().split(","), + taxes_amount: allAmounts.taxesAmount.toObjectString(), + + total_amount: allAmounts.totalAmount.toObjectString(), + }; + } + + toOutput(proformaItems: CustomerInvoiceItems): GetProformaByIdResponseDTO["items"] { + return proformaItems.map(this._mapItem); + } +} diff --git a/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-recipient.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-recipient.full.presenter.ts new file mode 100644 index 00000000..360bf5de --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-recipient.full.presenter.ts @@ -0,0 +1,46 @@ +import { Presenter } from "@erp/core/api"; +import { DomainValidationError, toEmptyString } from "@repo/rdx-ddd"; + +import type { GetIssuedInvoiceByIdResponseDTO as GetProformaByIdResponseDTO } from "../../../../../common/dto"; +import type { CustomerInvoice, InvoiceRecipient } from "../../../../domain"; + +type GetProformaRecipientByIdResponseDTO = GetProformaByIdResponseDTO["recipient"]; + +export class ProformaRecipientFullPresenter extends Presenter { + toOutput(proforma: CustomerInvoice): GetProformaRecipientByIdResponseDTO { + if (!proforma.recipient) { + throw DomainValidationError.requiredValue("recipient", { + cause: proforma, + }); + } + + return proforma.recipient.match( + (recipient: InvoiceRecipient) => { + return { + id: proforma.customerId.toString(), + name: recipient.name.toString(), + tin: recipient.tin.toString(), + street: toEmptyString(recipient.street, (value) => value.toString()), + street2: toEmptyString(recipient.street2, (value) => value.toString()), + city: toEmptyString(recipient.city, (value) => value.toString()), + province: toEmptyString(recipient.province, (value) => value.toString()), + postal_code: toEmptyString(recipient.postalCode, (value) => value.toString()), + country: toEmptyString(recipient.country, (value) => value.toString()), + }; + }, + () => { + return { + id: "", + name: "", + tin: "", + street: "", + street2: "", + city: "", + province: "", + postal_code: "", + country: "", + }; + } + ); + } +} diff --git a/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma.full.presenter.ts new file mode 100644 index 00000000..37ee2341 --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma.full.presenter.ts @@ -0,0 +1,89 @@ +import { Presenter } from "@erp/core/api"; +import { toEmptyString } from "@repo/rdx-ddd"; + +import type { GetProformaByIdResponseDTO } from "../../../../../common/dto"; +import type { CustomerInvoice } from "../../../../domain"; + +import type { ProformaItemsFullPresenter } from "./proforma-items.full.presenter"; +import type { ProformaRecipientFullPresenter } from "./proforma-recipient.full.presenter"; + +export class ProformaFullPresenter extends Presenter { + toOutput(proforma: CustomerInvoice): GetProformaByIdResponseDTO { + const itemsPresenter = this.presenterRegistry.getPresenter({ + resource: "proforma-items", + projection: "FULL", + }) as ProformaItemsFullPresenter; + + const recipientPresenter = this.presenterRegistry.getPresenter({ + resource: "proforma-recipient", + projection: "FULL", + }) as ProformaRecipientFullPresenter; + + const recipient = recipientPresenter.toOutput(proforma); + const items = itemsPresenter.toOutput(proforma.items); + const allAmounts = proforma.getAllAmounts(); + + const payment = proforma.paymentMethod.match( + (payment) => { + const { id, payment_description } = payment.toObjectString(); + return { + payment_id: id, + payment_description, + }; + }, + () => undefined + ); + + const invoiceTaxes = proforma.getTaxes().map((taxItem) => { + return { + tax_code: taxItem.tax.code, + taxable_amount: taxItem.taxableAmount.toObjectString(), + taxes_amount: taxItem.taxesAmount.toObjectString(), + }; + }); + + return { + id: proforma.id.toString(), + company_id: proforma.companyId.toString(), + + is_proforma: proforma.isProforma ? "true" : "false", + invoice_number: proforma.invoiceNumber.toString(), + status: proforma.status.toPrimitive(), + series: toEmptyString(proforma.series, (value) => value.toString()), + + invoice_date: proforma.invoiceDate.toDateString(), + operation_date: toEmptyString(proforma.operationDate, (value) => value.toDateString()), + + reference: toEmptyString(proforma.reference, (value) => value.toString()), + description: toEmptyString(proforma.description, (value) => value.toString()), + notes: toEmptyString(proforma.notes, (value) => value.toString()), + + language_code: proforma.languageCode.toString(), + currency_code: proforma.currencyCode.toString(), + + customer_id: proforma.customerId.toString(), + recipient, + + taxes: invoiceTaxes, + + payment_method: payment, + + subtotal_amount: allAmounts.subtotalAmount.toObjectString(), + items_discount_amount: allAmounts.itemDiscountAmount.toObjectString(), + + discount_percentage: proforma.discountPercentage.toObjectString(), + discount_amount: allAmounts.headerDiscountAmount.toObjectString(), + + taxable_amount: allAmounts.taxableAmount.toObjectString(), + taxes_amount: allAmounts.taxesAmount.toObjectString(), + total_amount: allAmounts.totalAmount.toObjectString(), + + items, + + metadata: { + entity: "proforma", + link: "", + }, + }; + } +} diff --git a/modules/customer-invoices/src/api/application/presenters/index.ts b/modules/customer-invoices/src/api/application/presenters/index.ts index 9e03d7a9..8d7e8eb5 100644 --- a/modules/customer-invoices/src/api/application/presenters/index.ts +++ b/modules/customer-invoices/src/api/application/presenters/index.ts @@ -1,2 +1,3 @@ export * from "./domain"; export * from "./queries"; +export * from "./reports"; diff --git a/modules/customer-invoices/src/api/application/presenters/queries/customer-invoice.report.presenter.ts b/modules/customer-invoices/src/api/application/presenters/queries/customer-invoice.report.presenter.ts deleted file mode 100644 index 2509ffd7..00000000 --- a/modules/customer-invoices/src/api/application/presenters/queries/customer-invoice.report.presenter.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { DateHelper, MoneyDTOHelper, PercentageDTOHelper } from "@erp/core"; -import { Presenter } from "@erp/core/api"; - -import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../common/dto"; - -export class CustomerInvoiceReportPresenter extends Presenter< - GetIssuedInvoiceByIdResponseDTO, - unknown -> { - private _formatPaymentMethodDTO( - paymentMethod?: GetIssuedInvoiceByIdResponseDTO["payment_method"] - ) { - if (!paymentMethod) { - return ""; - } - - return paymentMethod.payment_description ?? ""; - } - - toOutput(invoiceDTO: GetIssuedInvoiceByIdResponseDTO) { - const itemsPresenter = this.presenterRegistry.getPresenter({ - resource: "customer-invoice-items", - projection: "REPORT", - format: "JSON", - }); - - const taxesPresenter = this.presenterRegistry.getPresenter({ - resource: "customer-invoice-taxes", - projection: "REPORT", - format: "JSON", - }); - - const locale = invoiceDTO.language_code; - const itemsDTO = itemsPresenter.toOutput(invoiceDTO.items, { - locale, - }); - - const taxesDTO = taxesPresenter.toOutput(invoiceDTO.taxes, { - locale, - }); - - const moneyOptions = { - hideZeros: true, - minimumFractionDigits: 0, - }; - - return { - ...invoiceDTO, - taxes: taxesDTO, - items: itemsDTO, - - invoice_date: DateHelper.format(invoiceDTO.invoice_date, locale), - subtotal_amount: MoneyDTOHelper.format(invoiceDTO.subtotal_amount, locale, moneyOptions), - discount_percentage: PercentageDTOHelper.format(invoiceDTO.discount_percentage, locale), - discount_amount: MoneyDTOHelper.format(invoiceDTO.discount_amount, locale, moneyOptions), - taxable_amount: MoneyDTOHelper.format(invoiceDTO.taxable_amount, locale, moneyOptions), - taxes_amount: MoneyDTOHelper.format(invoiceDTO.taxes_amount, locale, moneyOptions), - total_amount: MoneyDTOHelper.format(invoiceDTO.total_amount, locale, moneyOptions), - - payment_method: this._formatPaymentMethodDTO(invoiceDTO.payment_method), - }; - } -} diff --git a/modules/customer-invoices/src/api/application/presenters/queries/index.ts b/modules/customer-invoices/src/api/application/presenters/queries/index.ts index dd86ec0a..de2b09ca 100644 --- a/modules/customer-invoices/src/api/application/presenters/queries/index.ts +++ b/modules/customer-invoices/src/api/application/presenters/queries/index.ts @@ -1,4 +1,2 @@ -export * from "./customer-invoice.report.presenter"; -export * from "./customer-invoice-items.report.presenter"; -export * from "./customer-invoice-taxes.report.presenter"; -export * from "./list-customer-invoices.presenter"; +export * from "./issued-invoices"; +export * from "./proformas"; diff --git a/modules/customer-invoices/src/api/application/presenters/queries/issued-invoices/index.ts b/modules/customer-invoices/src/api/application/presenters/queries/issued-invoices/index.ts new file mode 100644 index 00000000..3ed1ac53 --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/queries/issued-invoices/index.ts @@ -0,0 +1 @@ +export * from "./issued-invoice.list.presenter"; diff --git a/modules/customer-invoices/src/api/application/presenters/queries/list-customer-invoices.presenter.ts b/modules/customer-invoices/src/api/application/presenters/queries/issued-invoices/issued-invoice.list.presenter.ts similarity index 75% rename from modules/customer-invoices/src/api/application/presenters/queries/list-customer-invoices.presenter.ts rename to modules/customer-invoices/src/api/application/presenters/queries/issued-invoices/issued-invoice.list.presenter.ts index 375b6172..53f62427 100644 --- a/modules/customer-invoices/src/api/application/presenters/queries/list-customer-invoices.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/queries/issued-invoices/issued-invoice.list.presenter.ts @@ -3,13 +3,22 @@ import type { Criteria } from "@repo/rdx-criteria/server"; import { toEmptyString } from "@repo/rdx-ddd"; import type { ArrayElement, Collection } from "@repo/rdx-utils"; -import type { ListIssuedInvoicesResponseDTO } from "../../../../common/dto"; -import type { CustomerInvoiceListDTO } from "../../../infrastructure"; +import type { ListIssuedInvoicesResponseDTO } from "../../../../../common/dto"; +import type { CustomerInvoiceListDTO } from "../../../../infrastructure"; -export class ListCustomerInvoicesPresenter extends Presenter { +export class IssuedInvoiceListPresenter extends Presenter { protected _mapInvoice(invoice: CustomerInvoiceListDTO) { const recipientDTO = invoice.recipient.toObjectString(); + const verifactuDTO = invoice.verifactu.match( + (verifactu) => verifactu.toObjectString(), + () => ({ + status: "", + url: "", + qr_code: "", + }) + ); + const invoiceDTO: ArrayElement = { id: invoice.id.toString(), company_id: invoice.companyId.toString(), @@ -39,8 +48,10 @@ export class ListCustomerInvoicesPresenter extends Presenter { taxes_amount: invoice.taxesAmount.toObjectString(), total_amount: invoice.totalAmount.toObjectString(), + verifactu: verifactuDTO, + metadata: { - entity: "customer-invoice", + entity: "issued-invoice", }, }; @@ -48,22 +59,22 @@ export class ListCustomerInvoicesPresenter extends Presenter { } toOutput(params: { - customerInvoices: Collection; + invoices: Collection; criteria: Criteria; }): ListIssuedInvoicesResponseDTO { - const { customerInvoices, criteria } = params; + const { invoices, criteria } = params; - const invoices = customerInvoices.map((invoice) => this._mapInvoice(invoice)); - const totalItems = customerInvoices.total(); + const _invoices = invoices.map((invoice) => this._mapInvoice(invoice)); + const _totalItems = invoices.total(); return { page: criteria.pageNumber, per_page: criteria.pageSize, - total_pages: Math.ceil(totalItems / criteria.pageSize), - total_items: totalItems, - items: invoices, + total_pages: Math.ceil(_totalItems / criteria.pageSize), + total_items: _totalItems, + items: _invoices, metadata: { - entity: "customer-invoices", + entity: "issued-invoices", criteria: criteria.toJSON(), //links: { // self: `/api/customer-invoices?page=${criteria.pageNumber}&per_page=${criteria.pageSize}`, diff --git a/modules/customer-invoices/src/api/application/presenters/queries/proformas/index.ts b/modules/customer-invoices/src/api/application/presenters/queries/proformas/index.ts new file mode 100644 index 00000000..3c1b3639 --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/queries/proformas/index.ts @@ -0,0 +1 @@ +export * from "./proforma.list.presenter"; diff --git a/modules/customer-invoices/src/api/application/presenters/queries/proformas/proforma.list.presenter.ts b/modules/customer-invoices/src/api/application/presenters/queries/proformas/proforma.list.presenter.ts new file mode 100644 index 00000000..c946c225 --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/queries/proformas/proforma.list.presenter.ts @@ -0,0 +1,76 @@ +import { Presenter } from "@erp/core/api"; +import type { Criteria } from "@repo/rdx-criteria/server"; +import { toEmptyString } from "@repo/rdx-ddd"; +import type { ArrayElement, Collection } from "@repo/rdx-utils"; + +import type { ListProformasResponseDTO } from "../../../../../common/dto"; +import type { CustomerInvoiceListDTO } from "../../../../infrastructure"; + +export class ProformaListPresenter extends Presenter { + protected _mapProforma(proforma: CustomerInvoiceListDTO) { + const recipientDTO = proforma.recipient.toObjectString(); + + const invoiceDTO: ArrayElement = { + id: proforma.id.toString(), + company_id: proforma.companyId.toString(), + is_proforma: proforma.isProforma, + customer_id: proforma.customerId.toString(), + + invoice_number: proforma.invoiceNumber.toString(), + status: proforma.status.toPrimitive(), + series: toEmptyString(proforma.series, (value) => value.toString()), + + invoice_date: proforma.invoiceDate.toDateString(), + operation_date: toEmptyString(proforma.operationDate, (value) => value.toDateString()), + reference: toEmptyString(proforma.reference, (value) => value.toString()), + description: toEmptyString(proforma.description, (value) => value.toString()), + + recipient: recipientDTO, + + language_code: proforma.languageCode.code, + currency_code: proforma.currencyCode.code, + + taxes: proforma.taxes, + + subtotal_amount: proforma.subtotalAmount.toObjectString(), + discount_percentage: proforma.discountPercentage.toObjectString(), + discount_amount: proforma.discountAmount.toObjectString(), + taxable_amount: proforma.taxableAmount.toObjectString(), + taxes_amount: proforma.taxesAmount.toObjectString(), + total_amount: proforma.totalAmount.toObjectString(), + + metadata: { + entity: "proforma", + }, + }; + + return invoiceDTO; + } + + toOutput(params: { + proformas: Collection; + criteria: Criteria; + }): ListProformasResponseDTO { + const { proformas, criteria } = params; + + const _proformas = proformas.map((proforma) => this._mapProforma(proforma)); + const _totalItems = proformas.total(); + + return { + page: criteria.pageNumber, + per_page: criteria.pageSize, + total_pages: Math.ceil(_totalItems / criteria.pageSize), + total_items: _totalItems, + items: _proformas, + metadata: { + entity: "proformas", + criteria: criteria.toJSON(), + //links: { + // self: `/api/customer-invoices?page=${criteria.pageNumber}&per_page=${criteria.pageSize}`, + // first: `/api/customer-invoices?page=1&per_page=${criteria.pageSize}`, + // last: `/api/customer-invoices?page=${Math.ceil(totalItems / criteria.pageSize)}&per_page=${criteria.pageSize}`, + //}, + }, + }; + } +} diff --git a/modules/customer-invoices/src/api/application/presenters/reports/index.ts b/modules/customer-invoices/src/api/application/presenters/reports/index.ts new file mode 100644 index 00000000..de2b09ca --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/reports/index.ts @@ -0,0 +1,2 @@ +export * from "./issued-invoices"; +export * from "./proformas"; diff --git a/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/index.ts b/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/index.ts new file mode 100644 index 00000000..8433c115 --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/index.ts @@ -0,0 +1,3 @@ +export * from "./issued-invoice.report.presenter"; +export * from "./issued-invoice-items.report.presenter"; +export * from "./issued-invoice-taxes.report.presenter"; diff --git a/modules/customer-invoices/src/api/application/presenters/queries/customer-invoice-items.report.presenter.ts b/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice-items.report.presenter.ts similarity index 77% rename from modules/customer-invoices/src/api/application/presenters/queries/customer-invoice-items.report.presenter.ts rename to modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice-items.report.presenter.ts index dc1bb006..54f91f2e 100644 --- a/modules/customer-invoices/src/api/application/presenters/queries/customer-invoice-items.report.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice-items.report.presenter.ts @@ -3,16 +3,13 @@ import { type IPresenterOutputParams, Presenter } from "@erp/core/api"; import type { GetIssuedInvoiceByIdResponseDTO } from "@erp/customer-invoices/common"; import type { ArrayElement } from "@repo/rdx-utils"; -type CustomerInvoiceItemsDTO = GetIssuedInvoiceByIdResponseDTO["items"]; -type CustomerInvoiceItemDTO = ArrayElement; +type IssuedInvoiceItemsDTO = GetIssuedInvoiceByIdResponseDTO["items"]; +type IssuedInvoiceItemDTO = ArrayElement; -export class CustomerInvoiceItemsReportPersenter extends Presenter< - CustomerInvoiceItemsDTO, - unknown -> { +export class IssuedInvoiceItemsReportPresenter extends Presenter { private _locale!: string; - private _mapItem(invoiceItem: CustomerInvoiceItemDTO, _index: number) { + private _mapItem(invoiceItem: IssuedInvoiceItemDTO, _index: number) { const moneyOptions = { hideZeros: true, minimumFractionDigits: 0, @@ -48,14 +45,14 @@ export class CustomerInvoiceItemsReportPersenter extends Presenter< }; } - toOutput(invoiceItems: CustomerInvoiceItemsDTO, params: IPresenterOutputParams): unknown { + toOutput(issuedInvoiceItems: IssuedInvoiceItemsDTO, params: IPresenterOutputParams): unknown { const { locale } = params as { locale: string; }; this._locale = locale; - return invoiceItems.map((item, index) => { + return issuedInvoiceItems.map((item, index) => { return this._mapItem(item, index); }); } diff --git a/modules/customer-invoices/src/api/application/presenters/queries/customer-invoice-taxes.report.presenter.ts b/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice-taxes.report.presenter.ts similarity index 73% rename from modules/customer-invoices/src/api/application/presenters/queries/customer-invoice-taxes.report.presenter.ts rename to modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice-taxes.report.presenter.ts index 43b0dd4a..946f5d6a 100644 --- a/modules/customer-invoices/src/api/application/presenters/queries/customer-invoice-taxes.report.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice-taxes.report.presenter.ts @@ -3,17 +3,14 @@ import { type IPresenterOutputParams, Presenter } from "@erp/core/api"; import type { GetIssuedInvoiceByIdResponseDTO } from "@erp/customer-invoices/common"; import type { ArrayElement } from "@repo/rdx-utils"; -type CustomerInvoiceTaxesDTO = GetIssuedInvoiceByIdResponseDTO["taxes"]; -type CustomerInvoiceTaxDTO = ArrayElement; +type IssuedInvoiceTaxesDTO = GetIssuedInvoiceByIdResponseDTO["taxes"]; +type IssuedInvoiceTaxDTO = ArrayElement; -export class CustomerInvoiceTaxesReportPresenter extends Presenter< - CustomerInvoiceTaxesDTO, - unknown -> { +export class IssuedInvoiceTaxesReportPresenter extends Presenter { private _locale!: string; private _taxCatalog!: JsonTaxCatalogProvider; - private _mapTax(taxItem: CustomerInvoiceTaxDTO) { + private _mapTax(taxItem: IssuedInvoiceTaxDTO) { const moneyOptions = { hideZeros: true, minimumFractionDigits: 0, @@ -29,7 +26,7 @@ export class CustomerInvoiceTaxesReportPresenter extends Presenter< }; } - toOutput(taxes: CustomerInvoiceTaxesDTO, params: IPresenterOutputParams): unknown { + toOutput(taxes: IssuedInvoiceTaxesDTO, params: IPresenterOutputParams): unknown { const { locale } = params as { locale: string; }; diff --git a/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice.report.presenter.ts b/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice.report.presenter.ts new file mode 100644 index 00000000..7310af43 --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice.report.presenter.ts @@ -0,0 +1,71 @@ +import { DateHelper, MoneyDTOHelper, PercentageDTOHelper } from "@erp/core"; +import { Presenter } from "@erp/core/api"; + +import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../../common/dto"; + +export class IssuedInvoiceReportPresenter extends Presenter< + GetIssuedInvoiceByIdResponseDTO, + unknown +> { + private _formatPaymentMethodDTO( + paymentMethod?: GetIssuedInvoiceByIdResponseDTO["payment_method"] + ) { + if (!paymentMethod) { + return ""; + } + + return paymentMethod.payment_description ?? ""; + } + + toOutput(issuedInvoiceDTO: GetIssuedInvoiceByIdResponseDTO) { + const itemsPresenter = this.presenterRegistry.getPresenter({ + resource: "issued-invoice-items", + projection: "REPORT", + format: "JSON", + }); + + const taxesPresenter = this.presenterRegistry.getPresenter({ + resource: "issued-invoice-taxes", + projection: "REPORT", + format: "JSON", + }); + + const locale = issuedInvoiceDTO.language_code; + const itemsDTO = itemsPresenter.toOutput(issuedInvoiceDTO.items, { + locale, + }); + + const taxesDTO = taxesPresenter.toOutput(issuedInvoiceDTO.taxes, { + locale, + }); + + const moneyOptions = { + hideZeros: true, + minimumFractionDigits: 0, + }; + + return { + ...issuedInvoiceDTO, + taxes: taxesDTO, + items: itemsDTO, + + invoice_date: DateHelper.format(issuedInvoiceDTO.invoice_date, locale), + subtotal_amount: MoneyDTOHelper.format( + issuedInvoiceDTO.subtotal_amount, + locale, + moneyOptions + ), + discount_percentage: PercentageDTOHelper.format(issuedInvoiceDTO.discount_percentage, locale), + discount_amount: MoneyDTOHelper.format( + issuedInvoiceDTO.discount_amount, + locale, + moneyOptions + ), + taxable_amount: MoneyDTOHelper.format(issuedInvoiceDTO.taxable_amount, locale, moneyOptions), + taxes_amount: MoneyDTOHelper.format(issuedInvoiceDTO.taxes_amount, locale, moneyOptions), + total_amount: MoneyDTOHelper.format(issuedInvoiceDTO.total_amount, locale, moneyOptions), + + payment_method: this._formatPaymentMethodDTO(issuedInvoiceDTO.payment_method), + }; + } +} diff --git a/modules/customer-invoices/src/api/application/presenters/reports/proformas/index.ts b/modules/customer-invoices/src/api/application/presenters/reports/proformas/index.ts new file mode 100644 index 00000000..4e86d5bb --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/reports/proformas/index.ts @@ -0,0 +1,3 @@ +export * from "./proforma.report.presenter"; +export * from "./proforma-items.report.presenter"; +export * from "./proforma-taxes.report.presenter"; diff --git a/modules/customer-invoices/src/api/application/presenters/reports/proformas/proforma-items.report.presenter.ts b/modules/customer-invoices/src/api/application/presenters/reports/proformas/proforma-items.report.presenter.ts new file mode 100644 index 00000000..a899a690 --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/reports/proformas/proforma-items.report.presenter.ts @@ -0,0 +1,63 @@ +import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core"; +import { type IPresenterOutputParams, Presenter } from "@erp/core/api"; +import type { GetProformaByIdResponseDTO } from "@erp/customer-invoices/common"; +import type { ArrayElement } from "@repo/rdx-utils"; + +type ProformaItemsDTO = GetProformaByIdResponseDTO["items"]; +type ProformaItemDTO = ArrayElement; + +export class ProformaItemsReportPresenter extends Presenter { + private _locale!: string; + + private _mapItem(proformaItem: ProformaItemDTO, _index: number) { + const moneyOptions = { + hideZeros: true, + minimumFractionDigits: 0, + }; + + return { + ...proformaItem, + + quantity: QuantityDTOHelper.format(proformaItem.quantity, this._locale, { + minimumFractionDigits: 0, + }), + unit_amount: MoneyDTOHelper.format(proformaItem.unit_amount, this._locale, moneyOptions), + subtotal_amount: MoneyDTOHelper.format( + proformaItem.subtotal_amount, + this._locale, + moneyOptions + ), + discount_percentage: PercentageDTOHelper.format( + proformaItem.discount_percentage, + this._locale, + { + minimumFractionDigits: 0, + } + ), + discount_amount: MoneyDTOHelper.format( + proformaItem.discount_amount, + this._locale, + moneyOptions + ), + taxable_amount: MoneyDTOHelper.format( + proformaItem.taxable_amount, + this._locale, + moneyOptions + ), + taxes_amount: MoneyDTOHelper.format(proformaItem.taxes_amount, this._locale, moneyOptions), + total_amount: MoneyDTOHelper.format(proformaItem.total_amount, this._locale, moneyOptions), + }; + } + + toOutput(proformaItems: ProformaItemsDTO, params: IPresenterOutputParams): unknown { + const { locale } = params as { + locale: string; + }; + + this._locale = locale; + + return proformaItems.map((item, index) => { + return this._mapItem(item, index); + }); + } +} diff --git a/modules/customer-invoices/src/api/application/presenters/reports/proformas/proforma-taxes.report.presenter.ts b/modules/customer-invoices/src/api/application/presenters/reports/proformas/proforma-taxes.report.presenter.ts new file mode 100644 index 00000000..d2ba0a6d --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/reports/proformas/proforma-taxes.report.presenter.ts @@ -0,0 +1,41 @@ +import { type JsonTaxCatalogProvider, MoneyDTOHelper, SpainTaxCatalogProvider } from "@erp/core"; +import { type IPresenterOutputParams, Presenter } from "@erp/core/api"; +import type { GetProformaByIdResponseDTO } from "@erp/customer-invoices/common"; +import type { ArrayElement } from "@repo/rdx-utils"; + +type ProformaTaxesDTO = GetProformaByIdResponseDTO["taxes"]; +type ProformaTaxDTO = ArrayElement; + +export class ProformaTaxesReportPresenter extends Presenter { + private _locale!: string; + private _taxCatalog!: JsonTaxCatalogProvider; + + private _mapTax(taxItem: ProformaTaxDTO) { + const moneyOptions = { + hideZeros: true, + minimumFractionDigits: 0, + }; + + const taxCatalogItem = this._taxCatalog.findByCode(taxItem.tax_code); + + return { + tax_code: taxItem.tax_code, + tax_name: taxCatalogItem.unwrap().name, + taxable_amount: MoneyDTOHelper.format(taxItem.taxable_amount, this._locale, moneyOptions), + taxes_amount: MoneyDTOHelper.format(taxItem.taxes_amount, this._locale, moneyOptions), + }; + } + + toOutput(taxes: ProformaTaxesDTO, params: IPresenterOutputParams): unknown { + const { locale } = params as { + locale: string; + }; + + this._locale = locale; + this._taxCatalog = SpainTaxCatalogProvider(); + + return taxes.map((item, _index) => { + return this._mapTax(item); + }); + } +} diff --git a/modules/customer-invoices/src/api/application/presenters/reports/proformas/proforma.report.presenter.ts b/modules/customer-invoices/src/api/application/presenters/reports/proformas/proforma.report.presenter.ts new file mode 100644 index 00000000..58276915 --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/reports/proformas/proforma.report.presenter.ts @@ -0,0 +1,58 @@ +import { DateHelper, MoneyDTOHelper, PercentageDTOHelper } from "@erp/core"; +import { Presenter } from "@erp/core/api"; + +import type { GetProformaByIdResponseDTO } from "../../../../../common/dto"; + +export class ProformaReportPresenter extends Presenter { + private _formatPaymentMethodDTO(paymentMethod?: GetProformaByIdResponseDTO["payment_method"]) { + if (!paymentMethod) { + return ""; + } + + return paymentMethod.payment_description ?? ""; + } + + toOutput(proformaDTO: GetProformaByIdResponseDTO) { + const itemsPresenter = this.presenterRegistry.getPresenter({ + resource: "proforma-items", + projection: "REPORT", + format: "JSON", + }); + + const taxesPresenter = this.presenterRegistry.getPresenter({ + resource: "proforma-taxes", + projection: "REPORT", + format: "JSON", + }); + + const locale = proformaDTO.language_code; + const itemsDTO = itemsPresenter.toOutput(proformaDTO.items, { + locale, + }); + + const taxesDTO = taxesPresenter.toOutput(proformaDTO.taxes, { + locale, + }); + + const moneyOptions = { + hideZeros: true, + minimumFractionDigits: 0, + }; + + return { + ...proformaDTO, + taxes: taxesDTO, + items: itemsDTO, + + invoice_date: DateHelper.format(proformaDTO.invoice_date, locale), + subtotal_amount: MoneyDTOHelper.format(proformaDTO.subtotal_amount, locale, moneyOptions), + discount_percentage: PercentageDTOHelper.format(proformaDTO.discount_percentage, locale), + discount_amount: MoneyDTOHelper.format(proformaDTO.discount_amount, locale, moneyOptions), + taxable_amount: MoneyDTOHelper.format(proformaDTO.taxable_amount, locale, moneyOptions), + taxes_amount: MoneyDTOHelper.format(proformaDTO.taxes_amount, locale, moneyOptions), + total_amount: MoneyDTOHelper.format(proformaDTO.total_amount, locale, moneyOptions), + + payment_method: this._formatPaymentMethodDTO(proformaDTO.payment_method), + }; + } +} diff --git a/modules/customer-invoices/src/api/application/services/customer-invoice-application.service.ts b/modules/customer-invoices/src/api/application/services/customer-invoice-application.service.ts index 75dabd0e..33d699f6 100644 --- a/modules/customer-invoices/src/api/application/services/customer-invoice-application.service.ts +++ b/modules/customer-invoices/src/api/application/services/customer-invoice-application.service.ts @@ -188,11 +188,7 @@ export class CustomerInvoiceApplicationService { criteria: Criteria, transaction?: Transaction ): Promise, Error>> { - return this.repository.findByCriteriaInCompany(companyId, criteria, transaction, { - where: { - is_proforma: true, - }, - }); + return this.repository.findProformasByCriteriaInCompany(companyId, criteria, transaction, {}); } /** @@ -208,11 +204,12 @@ export class CustomerInvoiceApplicationService { criteria: Criteria, transaction?: Transaction ): Promise, Error>> { - return this.repository.findByCriteriaInCompany(companyId, criteria, transaction, { - where: { - is_proforma: false, - }, - }); + return this.repository.findIssuedInvoicesByCriteriaInCompany( + companyId, + criteria, + transaction, + {} + ); } /** @@ -227,11 +224,12 @@ export class CustomerInvoiceApplicationService { invoiceId: UniqueID, transaction?: Transaction ): Promise> { - return await this.repository.getByIdInCompany(companyId, invoiceId, transaction, { - where: { - is_proforma: false, - }, - }); + return await this.repository.getIssuedInvoiceByIdInCompany( + companyId, + invoiceId, + transaction, + {} + ); } /** @@ -246,11 +244,7 @@ export class CustomerInvoiceApplicationService { proformaId: UniqueID, transaction?: Transaction ): Promise> { - return await this.repository.getByIdInCompany(companyId, proformaId, transaction, { - where: { - is_proforma: true, - }, - }); + return await this.repository.getProformaByIdInCompany(companyId, proformaId, transaction, {}); } /** diff --git a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/get-issued-invoice.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/get-issued-invoice.use-case.ts index 4713dd5b..72091d4c 100644 --- a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/get-issued-invoice.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/get-issued-invoice.use-case.ts @@ -2,7 +2,7 @@ import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { CustomerInvoiceFullPresenter } from "../../presenters/domain"; +import type { ProformaFullPresenter } from "../../presenters/domain"; import type { CustomerInvoiceApplicationService } from "../../services"; type GetIssuedInvoiceUseCaseInput = { @@ -27,9 +27,9 @@ export class GetIssuedInvoiceUseCase { const invoiceId = idOrError.data; const presenter = this.presenterRegistry.getPresenter({ - resource: "customer-invoice", + resource: "issued-invoice", projection: "FULL", - }) as CustomerInvoiceFullPresenter; + }) as ProformaFullPresenter; return this.transactionManager.complete(async (transaction) => { try { diff --git a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/index.ts b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/index.ts index e5ef5e1e..ea278f6a 100644 --- a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/index.ts +++ b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/index.ts @@ -1,3 +1,3 @@ export * from "./get-issued-invoice.use-case"; export * from "./list-issued-invoices.use-case"; -export * from "./report-issued-invoice.use-case"; +export * from "./report-issued-invoices"; diff --git a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/list-issued-invoices.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/list-issued-invoices.use-case.ts index bb44c35b..c92d4482 100644 --- a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/list-issued-invoices.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/list-issued-invoices.use-case.ts @@ -5,7 +5,7 @@ import { Result } from "@repo/rdx-utils"; import type { Transaction } from "sequelize"; import type { ListIssuedInvoicesResponseDTO } from "../../../../common/dto"; -import type { ListCustomerInvoicesPresenter } from "../../presenters"; +import type { IssuedInvoiceListPresenter } from "../../presenters"; import type { CustomerInvoiceApplicationService } from "../../services"; type ListIssuedInvoicesUseCaseInput = { @@ -25,9 +25,9 @@ export class ListIssuedInvoicesUseCase { ): Promise> { const { criteria, companyId } = params; const presenter = this.presenterRegistry.getPresenter({ - resource: "customer-invoice", + resource: "issued-invoice", projection: "LIST", - }) as ListCustomerInvoicesPresenter; + }) as IssuedInvoiceListPresenter; return this.transactionManager.complete(async (transaction: Transaction) => { try { @@ -43,7 +43,7 @@ export class ListIssuedInvoicesUseCase { const invoices = result.data; const dto = presenter.toOutput({ - customerInvoices: invoices, + invoices: invoices, criteria, }); diff --git a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/index.ts b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/index.ts new file mode 100644 index 00000000..9d2fabc2 --- /dev/null +++ b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/index.ts @@ -0,0 +1 @@ +export * from "./report-issued-invoice.use-case"; diff --git a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoice.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/report-issued-invoice.use-case.ts similarity index 86% rename from modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoice.use-case.ts rename to modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/report-issued-invoice.use-case.ts index dc84a5b4..afe83ee6 100644 --- a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoice.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/report-issued-invoice.use-case.ts @@ -2,8 +2,9 @@ import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { CustomerInvoiceApplicationService } from "../../services"; -import type { CustomerInvoiceReportPDFPresenter } from "../proformas"; +import type { CustomerInvoiceApplicationService } from "../../../services"; + +import type { IssuedInvoiceReportPDFPresenter } from "./reporter/issued-invoice.report.pdf"; type ReportIssuedInvoiceUseCaseInput = { companyId: UniqueID; @@ -28,10 +29,10 @@ export class ReportIssuedInvoiceUseCase { const invoiceId = idOrError.data; const pdfPresenter = this.presenterRegistry.getPresenter({ - resource: "customer-invoice", + resource: "issued-invoice", projection: "REPORT", format: "PDF", - }) as CustomerInvoiceReportPDFPresenter; + }) as IssuedInvoiceReportPDFPresenter; return this.transactionManager.complete(async (transaction) => { try { diff --git a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/index.ts b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/index.ts new file mode 100644 index 00000000..57b3a2da --- /dev/null +++ b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/index.ts @@ -0,0 +1,2 @@ +export * from "./issued-invoice.report.html"; +export * from "./issued-invoice.report.pdf"; diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/customer-invoice.report.html.ts b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.html.ts similarity index 71% rename from modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/customer-invoice.report.html.ts rename to modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.html.ts index 9ceae5dd..b27f75aa 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/customer-invoice.report.html.ts +++ b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.html.ts @@ -7,8 +7,8 @@ import Handlebars from "handlebars"; import type { CustomerInvoice } from "../../../../../domain"; import type { - CustomerInvoiceFullPresenter, - CustomerInvoiceReportPresenter, + IssuedInvoiceFullPresenter, + IssuedInvoiceReportPresenter, } from "../../../../presenters"; /** Helper para trabajar relativo al fichero actual (ESM) */ @@ -23,26 +23,26 @@ export function fromHere(metaUrl: string) { }; } -export class CustomerInvoiceReportHTMLPresenter extends Presenter { - toOutput(customerInvoice: CustomerInvoice): string { +export class IssuedInvoiceReportHTMLPresenter extends Presenter { + toOutput(invoice: CustomerInvoice): string { const dtoPresenter = this.presenterRegistry.getPresenter({ - resource: "customer-invoice", + resource: "issued-invoice", projection: "FULL", - }) as CustomerInvoiceFullPresenter; + }) as IssuedInvoiceFullPresenter; const prePresenter = this.presenterRegistry.getPresenter({ - resource: "customer-invoice", + resource: "issued-invoice", projection: "REPORT", format: "JSON", - }) as CustomerInvoiceReportPresenter; + }) as IssuedInvoiceReportPresenter; - const invoiceDTO = dtoPresenter.toOutput(customerInvoice); + const invoiceDTO = dtoPresenter.toOutput(invoice); const prettyDTO = prePresenter.toOutput(invoiceDTO); // Obtener y compilar la plantilla HTML const here = fromHere(import.meta.url); - const templatePath = here.resolve("./templates/customer-invoice/template.hbs"); + const templatePath = here.resolve("./templates/proforma/template.hbs"); const templateHtml = readFileSync(templatePath).toString(); const template = Handlebars.compile(templateHtml, {}); return template(prettyDTO); diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/customer-invoice.report.pdf.ts b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.pdf.ts similarity index 81% rename from modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/customer-invoice.report.pdf.ts rename to modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.pdf.ts index da15949f..4d3ac52e 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/customer-invoice.report.pdf.ts +++ b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.pdf.ts @@ -4,23 +4,23 @@ import report from "puppeteer-report"; import type { CustomerInvoice } from "../../../../../domain"; -import type { CustomerInvoiceReportHTMLPresenter } from "./customer-invoice.report.html"; +import type { IssuedInvoiceReportHTMLPresenter } from "./issued-invoice.report.html"; // https://plnkr.co/edit/lWk6Yd?preview -export class CustomerInvoiceReportPDFPresenter extends Presenter< +export class IssuedInvoiceReportPDFPresenter extends Presenter< CustomerInvoice, Promise> > { - async toOutput(customerInvoice: CustomerInvoice): Promise> { + async toOutput(invoice: CustomerInvoice): Promise> { try { const htmlPresenter = this.presenterRegistry.getPresenter({ - resource: "customer-invoice", + resource: "issued-invoice", projection: "REPORT", format: "HTML", - }) as CustomerInvoiceReportHTMLPresenter; + }) as IssuedInvoiceReportHTMLPresenter; - const htmlData = htmlPresenter.toOutput(customerInvoice); + const htmlData = htmlPresenter.toOutput(invoice); // Generar el PDF con Puppeteer const browser = await puppeteer.launch({ diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/create-proforma.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/create-proforma.use-case.ts index e0ab717e..5f597597 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/create-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/create-proforma.use-case.ts @@ -9,7 +9,7 @@ import { Result } from "@repo/rdx-utils"; import type { Transaction } from "sequelize"; import type { CreateProformaRequestDTO } from "../../../../../common"; -import type { CustomerInvoiceFullPresenter } from "../../../presenters"; +import type { ProformaFullPresenter } from "../../../presenters"; import type { CustomerInvoiceApplicationService } from "../../../services"; import { CreateCustomerInvoicePropsMapper } from "./map-dto-to-create-proforma-props"; @@ -30,9 +30,9 @@ export class CreateProformaUseCase { public async execute(params: CreateProformaUseCaseInput) { const { dto, companyId } = params; const presenter = this.presenterRegistry.getPresenter({ - resource: "customer-invoice", + resource: "proforma", projection: "FULL", - }) as CustomerInvoiceFullPresenter; + }) as ProformaFullPresenter; // 1) Mapear DTO → props de dominio const dtoMapper = new CreateCustomerInvoicePropsMapper({ taxCatalog: this.taxCatalog }); @@ -65,24 +65,24 @@ export class CreateProformaUseCase { return Result.fail(buildResult.error); } - const newInvoice = buildResult.data; + const newProforma = buildResult.data; const existsGuard = await this.ensureNotExists(companyId, id, transaction); if (existsGuard.isFailure) { return Result.fail(existsGuard.error); } - const saveResult = await this.service.createInvoiceInCompany( + const saveResult = await this.service.createProformaInCompany( companyId, - newInvoice, + newProforma, transaction ); if (saveResult.isFailure) { return Result.fail(saveResult.error); } - const invoice = saveResult.data; - const dto = presenter.toOutput(invoice); + const proforma = saveResult.data; + const dto = presenter.toOutput(proforma); return Result.ok(dto); } catch (error: unknown) { @@ -99,7 +99,7 @@ export class CreateProformaUseCase { id: UniqueID, transaction: Transaction ): Promise> { - const existsResult = await this.service.existsByIdInCompany(companyId, id, transaction); + const existsResult = await this.service.existsProformaByIdInCompany(companyId, id, transaction); if (existsResult.isFailure) { return Result.fail(existsResult.error); } diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/get-proforma.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/get-proforma.use-case.ts index 82a610a8..0abe1e72 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/get-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/get-proforma.use-case.ts @@ -2,7 +2,7 @@ import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { CustomerInvoiceFullPresenter } from "../../presenters/domain"; +import type { ProformaFullPresenter } from "../../presenters/domain"; import type { CustomerInvoiceApplicationService } from "../../services"; type GetProformaUseCaseInput = { @@ -27,9 +27,9 @@ export class GetProformaUseCase { const proformaId = idOrError.data; const presenter = this.presenterRegistry.getPresenter({ - resource: "customer-invoice", + resource: "proforma", projection: "FULL", - }) as CustomerInvoiceFullPresenter; + }) as ProformaFullPresenter; return this.transactionManager.complete(async (transaction) => { try { diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/issue-proforma.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/issue-proforma.use-case.ts index 35c0400b..6cfabf73 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/issue-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/issue-proforma.use-case.ts @@ -6,7 +6,7 @@ import { IssueCustomerInvoiceDomainService, ProformaCustomerInvoiceDomainService, } from "../../../domain"; -import type { CustomerInvoiceFullPresenter } from "../../presenters"; +import type { ProformaFullPresenter } from "../../presenters"; import type { CustomerInvoiceApplicationService } from "../../services"; type IssueProformaUseCaseInput = { @@ -43,9 +43,9 @@ export class IssueProformaUseCase { const proformaId = idOrError.data; const presenter = this.presenterRegistry.getPresenter({ - resource: "customer-invoice", + resource: "proforma", projection: "FULL", - }) as CustomerInvoiceFullPresenter; + }) as ProformaFullPresenter; return this.transactionManager.complete(async (transaction) => { try { diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/list-proformas.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/list-proformas.use-case.ts index 377897de..03abe6b6 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/list-proformas.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/list-proformas.use-case.ts @@ -1,11 +1,11 @@ import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; +import type { ListProformasResponseDTO } from "@erp/customer-invoices/common"; import type { Criteria } from "@repo/rdx-criteria/server"; import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import type { Transaction } from "sequelize"; -import type { ListIssuedInvoicesResponseDTO } from "../../../../common/dto"; -import type { ListCustomerInvoicesPresenter } from "../../presenters"; +import type { ProformaListPresenter } from "../../presenters"; import type { CustomerInvoiceApplicationService } from "../../services"; type ListProformasUseCaseInput = { @@ -22,12 +22,12 @@ export class ListProformasUseCase { public execute( params: ListProformasUseCaseInput - ): Promise> { + ): Promise> { const { criteria, companyId } = params; const presenter = this.presenterRegistry.getPresenter({ - resource: "customer-invoice", + resource: "proforma", projection: "LIST", - }) as ListCustomerInvoicesPresenter; + }) as ProformaListPresenter; return this.transactionManager.complete(async (transaction: Transaction) => { try { @@ -43,7 +43,7 @@ export class ListProformasUseCase { const proformas = result.data; const dto = presenter.toOutput({ - customerInvoices: proformas, + proformas, criteria, }); diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/report-proforma.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/report-proforma.use-case.ts index c011cb7c..a48e1fac 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/report-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/report-proforma.use-case.ts @@ -4,7 +4,7 @@ import { Result } from "@repo/rdx-utils"; import type { CustomerInvoiceApplicationService } from "../../../services/customer-invoice-application.service"; -import type { CustomerInvoiceReportPDFPresenter } from "./reporter"; +import type { ProformaReportPDFPresenter } from "./reporter"; type ReportProformaUseCaseInput = { companyId: UniqueID; @@ -29,10 +29,10 @@ export class ReportProformaUseCase { const proformaId = idOrError.data; const pdfPresenter = this.presenterRegistry.getPresenter({ - resource: "customer-invoice", + resource: "proforma", projection: "REPORT", format: "PDF", - }) as CustomerInvoiceReportPDFPresenter; + }) as ProformaReportPDFPresenter; return this.transactionManager.complete(async (transaction) => { try { diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/index.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/index.ts index ede4d997..42872fb1 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/index.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/index.ts @@ -1,2 +1,2 @@ -export * from "./customer-invoice.report.html"; -export * from "./customer-invoice.report.pdf"; +export * from "./proforma.report.html"; +export * from "./proforma.report.pdf"; diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.html.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.html.ts new file mode 100644 index 00000000..64ac0e30 --- /dev/null +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.html.ts @@ -0,0 +1,47 @@ +import { readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { Presenter } from "@erp/core/api"; +import Handlebars from "handlebars"; + +import type { CustomerInvoice } from "../../../../../domain"; +import type { ProformaFullPresenter, ProformaReportPresenter } from "../../../../presenters"; + +/** Helper para trabajar relativo al fichero actual (ESM) */ +export function fromHere(metaUrl: string) { + const file = fileURLToPath(metaUrl); + const dir = dirname(file); + return { + file, // ruta absoluta al fichero actual + dir, // ruta absoluta al directorio actual + resolve: (...parts: string[]) => resolve(dir, ...parts), + join: (...parts: string[]) => join(dir, ...parts), + }; +} + +export class ProformaReportHTMLPresenter extends Presenter { + toOutput(proforma: CustomerInvoice): string { + const dtoPresenter = this.presenterRegistry.getPresenter({ + resource: "proforma", + projection: "FULL", + }) as ProformaFullPresenter; + + const prePresenter = this.presenterRegistry.getPresenter({ + resource: "proforma", + projection: "REPORT", + format: "JSON", + }) as ProformaReportPresenter; + + const invoiceDTO = dtoPresenter.toOutput(proforma); + const prettyDTO = prePresenter.toOutput(invoiceDTO); + + // Obtener y compilar la plantilla HTML + const here = fromHere(import.meta.url); + + const templatePath = here.resolve("./templates/proforma/template.hbs"); + const templateHtml = readFileSync(templatePath).toString(); + const template = Handlebars.compile(templateHtml, {}); + return template(prettyDTO); + } +} diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.pdf.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.pdf.ts new file mode 100644 index 00000000..0229dd91 --- /dev/null +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.pdf.ts @@ -0,0 +1,69 @@ +import { Presenter } from "@erp/core/api"; +import puppeteer from "puppeteer"; +import report from "puppeteer-report"; + +import type { CustomerInvoice } from "../../../../../domain"; + +import type { ProformaReportHTMLPresenter } from "./proforma.report.html"; + +// https://plnkr.co/edit/lWk6Yd?preview + +export class ProformaReportPDFPresenter extends Presenter< + CustomerInvoice, + Promise> +> { + async toOutput(proforma: CustomerInvoice): Promise> { + try { + const htmlPresenter = this.presenterRegistry.getPresenter({ + resource: "proforma", + projection: "REPORT", + format: "HTML", + }) as ProformaReportHTMLPresenter; + + const htmlData = htmlPresenter.toOutput(proforma); + + // Generar el PDF con Puppeteer + const browser = await puppeteer.launch({ + executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, + headless: true, + 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" }); + + await navigationPromise; + + const reportPDF = await report.pdfPage(page, { + format: "A4", + margin: { + bottom: "10mm", + left: "10mm", + right: "10mm", + top: "10mm", + }, + landscape: false, + preferCSSPageSize: true, + omitBackground: false, + printBackground: true, + displayHeaderFooter: false, + headerTemplate: "
", + footerTemplate: + '
Página de
', + }); + + 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/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/logo_acana.jpg b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/logo_acana.jpg similarity index 100% rename from modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/logo_acana.jpg rename to modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/logo_acana.jpg diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/logo_rodax.jpg b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/logo_rodax.jpg similarity index 100% rename from modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/logo_rodax.jpg rename to modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/logo_rodax.jpg diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/template.hbs b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template.hbs similarity index 88% rename from modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/template.hbs rename to modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template.hbs index e36500ea..1a4459ed 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/template.hbs +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template.hbs @@ -16,6 +16,7 @@ } header { + font-family: Tahoma, sans-serif; display: flex; justify-content: space-between; margin-bottom: 20px; @@ -253,9 +254,16 @@
diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/template.hbs_BAK b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template.hbs_BAK similarity index 100% rename from modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/template.hbs_BAK rename to modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template.hbs_BAK diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/template_proforma.hbs b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template_proforma.hbs similarity index 100% rename from modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/template_proforma.hbs rename to modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template_proforma.hbs diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/template_rodax.hbs b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template_rodax.hbs similarity index 100% rename from modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/template_rodax.hbs rename to modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template_rodax.hbs diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/uecko-footer-logos.jpg b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/uecko-footer-logos.jpg similarity index 100% rename from modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/uecko-footer-logos.jpg rename to modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/uecko-footer-logos.jpg diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/uecko-logo.svg b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/uecko-logo.svg similarity index 100% rename from modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/uecko-logo.svg rename to modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/uecko-logo.svg diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/update-proforma.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/update-proforma.use-case.ts index 6bd13536..bba5c439 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/update-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/update-proforma.use-case.ts @@ -5,7 +5,7 @@ import type { Transaction } from "sequelize"; import type { UpdateProformaByIdRequestDTO } from "../../../../../common"; import type { CustomerInvoicePatchProps } from "../../../../domain"; -import type { CustomerInvoiceFullPresenter } from "../../../presenters"; +import type { ProformaFullPresenter } from "../../../presenters"; import type { CustomerInvoiceApplicationService } from "../../../services/customer-invoice-application.service"; import { mapDTOToUpdateCustomerInvoicePatchProps } from "./map-dto-to-update-customer-invoice-props"; @@ -33,9 +33,9 @@ export class UpdateProformaUseCase { const invoiceId = idOrError.data; const presenter = this.presenterRegistry.getPresenter({ - resource: "customer-invoice", + resource: "proforma", projection: "FULL", - }) as CustomerInvoiceFullPresenter; + }) as ProformaFullPresenter; // Mapear DTO → props de dominio const patchPropsResult = mapDTOToUpdateCustomerInvoicePatchProps(dto); diff --git a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts index 2cffb720..62f32696 100644 --- a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts +++ b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts @@ -10,7 +10,12 @@ import { } from "@repo/rdx-ddd"; import { type Maybe, Result } from "@repo/rdx-utils"; -import { CustomerInvoiceItems, type InvoicePaymentMethod, type InvoiceTaxTotal } from "../entities"; +import { + CustomerInvoiceItems, + type InvoicePaymentMethod, + type InvoiceTaxTotal, + type VerifactuRecord, +} from "../entities"; import { type CustomerInvoiceNumber, type CustomerInvoiceSerie, @@ -49,9 +54,7 @@ export interface CustomerInvoiceProps { discountPercentage: Percentage; - /*verifactu_qr: string; - verifactu_url: string; - verifactu_status: string;*/ + verifactu: Maybe; } export type CustomerInvoicePatchProps = Partial< @@ -212,6 +215,10 @@ export class CustomerInvoice return this.props.currencyCode; } + public get verifactu(): Maybe { + return this.props.verifactu; + } + public get discountPercentage(): Percentage { return this.props.discountPercentage; } diff --git a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts index 1016da15..c491b0b0 100644 --- a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts +++ b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts @@ -1,12 +1,19 @@ -import { CurrencyCode, DomainEntity, LanguageCode, Percentage, UniqueID } from "@repo/rdx-ddd"; -import { Maybe, Result } from "@repo/rdx-utils"; import { - CustomerInvoiceItemDescription, + type CurrencyCode, + DomainEntity, + type LanguageCode, + type Percentage, + type UniqueID, +} from "@repo/rdx-ddd"; +import { type Maybe, Result } from "@repo/rdx-utils"; + +import { + type CustomerInvoiceItemDescription, ItemAmount, ItemDiscount, ItemQuantity, } from "../../value-objects"; -import { ItemTaxes, ItemTaxTotal } from "../item-taxes"; +import type { ItemTaxTotal, ItemTaxes } from "../item-taxes"; export interface CustomerInvoiceItemProps { description: Maybe; diff --git a/modules/customer-invoices/src/api/domain/entities/index.ts b/modules/customer-invoices/src/api/domain/entities/index.ts index 164601cf..1ab175ce 100644 --- a/modules/customer-invoices/src/api/domain/entities/index.ts +++ b/modules/customer-invoices/src/api/domain/entities/index.ts @@ -2,3 +2,4 @@ export * from "./customer-invoice-items"; export * from "./invoice-payment-method"; export * from "./invoice-taxes"; export * from "./item-taxes"; +export * from "./verifactu-record"; diff --git a/modules/customer-invoices/src/api/domain/entities/verifactu-record.ts b/modules/customer-invoices/src/api/domain/entities/verifactu-record.ts new file mode 100644 index 00000000..c38fef96 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/entities/verifactu-record.ts @@ -0,0 +1,50 @@ +import { DomainEntity, type URLAddress, type UniqueID, toEmptyString } from "@repo/rdx-ddd"; +import { type Maybe, Result } from "@repo/rdx-utils"; + +import type { VerifactuRecordEstado } from "../value-objects"; + +export interface VerifactuRecordProps { + estado: VerifactuRecordEstado; + url: Maybe; + qrCode: Maybe; +} + +export class VerifactuRecord extends DomainEntity { + public static create(props: VerifactuRecordProps, id?: UniqueID): Result { + const record = new VerifactuRecord(props, id); + + // Reglas de negocio / validaciones + // ... + // ... + + return Result.ok(record); + } + + get estado(): VerifactuRecordEstado { + return this.props.estado; + } + + get url(): Maybe { + return this.props.url; + } + + get qrCode(): Maybe { + return this.props.qrCode; + } + + getProps(): VerifactuRecordProps { + return this.props; + } + + toPrimitive() { + return this.getProps(); + } + + toObjectString() { + return { + status: this.estado.toString(), + url: toEmptyString(this.url, (value) => value.toString()), + qr_code: toEmptyString(this.qrCode, (value) => value.toString()), + }; + } +} diff --git a/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts b/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts index e04b147d..3ab451a2 100644 --- a/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts +++ b/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts @@ -41,10 +41,21 @@ export interface ICustomerInvoiceRepository { ): Promise>; /** - * Recupera una factura/proforma por su ID y companyId. + * Recupera una proforma por su ID y companyId. * Devuelve un `NotFoundError` si no se encuentra. */ - getByIdInCompany( + getProformaByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction: unknown, + options: unknown + ): Promise>; + + /** + * Recupera una factura por su ID y companyId. + * Devuelve un `NotFoundError` si no se encuentra. + */ + getIssuedInvoiceByIdInCompany( companyId: UniqueID, id: UniqueID, transaction: unknown, @@ -53,7 +64,7 @@ export interface ICustomerInvoiceRepository { /** * - * Consulta facturas/proformas dentro de una empresa usando un + * Consulta proformas dentro de una empresa usando un * objeto Criteria (filtros, orden, paginación). * El resultado está encapsulado en un objeto `Collection`. * @@ -65,7 +76,28 @@ export interface ICustomerInvoiceRepository { * * @see Criteria */ - findByCriteriaInCompany( + findProformasByCriteriaInCompany( + companyId: UniqueID, + criteria: Criteria, + transaction: unknown, + options: unknown + ): Promise, Error>>; + + /** + * + * Consulta facturas dentro de una empresa usando un + * objeto Criteria (filtros, orden, paginación). + * El resultado está encapsulado en un objeto `Collection`. + * + * @param companyId - ID de la empresa. + * @param criteria - Criterios de búsqueda. + * @param transaction - Transacción activa para la operación. + * @param options - Opciones adicionales para la consulta (Sequelize FindOptions) + * @returns Result, Error> + * + * @see Criteria + */ + findIssuedInvoicesByCriteriaInCompany( companyId: UniqueID, criteria: Criteria, transaction: unknown, diff --git a/modules/customer-invoices/src/api/domain/value-objects/index.ts b/modules/customer-invoices/src/api/domain/value-objects/index.ts index 6008f147..5a0fe743 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/index.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/index.ts @@ -8,3 +8,4 @@ export * from "./invoice-recipient"; export * from "./item-amount"; export * from "./item-discount"; export * from "./item-quantity"; +export * from "./verifactu-status"; diff --git a/modules/verifactu/src/api/domain/aggregates/value-objects/verifactu-record-estado.ts b/modules/customer-invoices/src/api/domain/value-objects/verifactu-status.ts similarity index 87% rename from modules/verifactu/src/api/domain/aggregates/value-objects/verifactu-record-estado.ts rename to modules/customer-invoices/src/api/domain/value-objects/verifactu-status.ts index b7f6c82f..643a2d2e 100644 --- a/modules/verifactu/src/api/domain/aggregates/value-objects/verifactu-record-estado.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/verifactu-status.ts @@ -16,6 +16,7 @@ export enum VERIFACTU_RECORD_STATUS { RECHAZADO = "No registrado", // <- Registro rechazado por la AEAT ERROR = "Error servidor AEAT", // <- Error en el servidor de la AEAT. Se intentará reenviar el registro de facturación de nuevo } + export class VerifactuRecordEstado extends ValueObject { private static readonly ALLOWED_STATUSES = [ "Pendiente", @@ -30,17 +31,10 @@ export class VerifactuRecordEstado extends ValueObject = { - draft: [INVOICE_STATUS.SENT], - sent: [INVOICE_STATUS.APPROVED, INVOICE_STATUS.REJECTED], - approved: [INVOICE_STATUS.ISSUED], - rejected: [INVOICE_STATUS.DRAFT], - }; -*/ + static create(value: string): Result { if (!VerifactuRecordEstado.ALLOWED_STATUSES.includes(value)) { - const detail = `Estado de la factura no válido: ${value}`; + const detail = `Estado de verifactu no válido: ${value}`; return Result.fail( new DomainValidationError( VerifactuRecordEstado.ERROR_CODE, @@ -53,6 +47,14 @@ export class VerifactuRecordEstado extends ValueObject ListCustomerInvoicesUseCase; - get: () => GetCustomerInvoiceUseCase; - create: () => CreateCustomerInvoiceUseCase; - update: () => UpdateCustomerInvoiceUseCase; - //delete: () => DeleteCustomerInvoiceUseCase; - report: () => ReportCustomerInvoiceUseCase; - issue: () => IssueCustomerInvoiceUseCase; - }; - getService: (name: string) => any; - listServices: () => string[]; -}; - -export function buildCustomerInvoiceDependencies(params: ModuleParams): CustomerInvoiceDeps { - const { database, listServices, getService } = params; - const transactionManager = new SequelizeTransactionManager(database); - const catalogs = { taxes: SpainTaxCatalogProvider() }; - - // Mapper Registry - const mapperRegistry = new InMemoryMapperRegistry(); - mapperRegistry - .registerDomainMapper( - { resource: "customer-invoice" }, - new CustomerInvoiceDomainMapper({ taxCatalog: catalogs.taxes }) - ) - .registerQueryMappers([ - { - key: { resource: "customer-invoice", query: "LIST" }, - mapper: new CustomerInvoiceListMapper(), - }, - ]); - - // Repository & Services - const repo = new CustomerInvoiceRepository({ mapperRegistry, database }); - const numberGenerator = new SequelizeInvoiceNumberGenerator(); - const service = new CustomerInvoiceApplicationService(repo, numberGenerator); - - // Presenter Registry - const presenterRegistry = new InMemoryPresenterRegistry(); - presenterRegistry.registerPresenters([ - { - key: { - resource: "customer-invoice-items", - projection: "FULL", - }, - presenter: new CustomerInvoiceItemsFullPresenter(presenterRegistry), - }, - { - key: { - resource: "recipient-invoice", - projection: "FULL", - }, - presenter: new RecipientInvoiceFullPresenter(presenterRegistry), - }, - { - key: { - resource: "customer-invoice", - projection: "FULL", - }, - presenter: new CustomerInvoiceFullPresenter(presenterRegistry), - }, - { - key: { - resource: "customer-invoice", - projection: "LIST", - }, - presenter: new ListCustomerInvoicesPresenter(presenterRegistry), - }, - { - key: { - resource: "customer-invoice", - projection: "REPORT", - format: "JSON", - }, - presenter: new CustomerInvoiceReportPresenter(presenterRegistry), - }, - { - key: { - resource: "customer-invoice-items", - projection: "REPORT", - format: "JSON", - }, - presenter: new CustomerInvoiceItemsReportPersenter(presenterRegistry), - }, - { - key: { - resource: "customer-invoice", - projection: "REPORT", - format: "HTML", - }, - presenter: new CustomerInvoiceReportHTMLPresenter(presenterRegistry), - }, - { - key: { - resource: "customer-invoice", - projection: "REPORT", - format: "PDF", - }, - presenter: new CustomerInvoiceReportPDFPresenter(presenterRegistry), - }, - ]); - - return { - transactionManager, - repo, - mapperRegistry, - presenterRegistry, - service, - catalogs, - build: { - list: () => new ListCustomerInvoicesUseCase(service, transactionManager, presenterRegistry), - get: () => new GetCustomerInvoiceUseCase(service, transactionManager, presenterRegistry), - create: () => - new CreateCustomerInvoiceUseCase( - service, - transactionManager, - presenterRegistry, - catalogs.taxes - ), - update: () => - new UpdateCustomerInvoiceUseCase(service, transactionManager, presenterRegistry), - // delete: () => new DeleteCustomerInvoiceUseCase(service, transactionManager), - report: () => - new ReportCustomerInvoiceUseCase(service, transactionManager, presenterRegistry), - issue: () => new IssueCustomerInvoiceUseCase(service, transactionManager), - }, - listServices, - getService, - }; -} diff --git a/modules/customer-invoices/src/api/infrastructure/express/issued-invoices.routes.ts b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices.routes.ts index b1404a0c..a1140437 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/issued-invoices.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices.routes.ts @@ -9,7 +9,7 @@ import { ListIssuedInvoicesRequestSchema, ReportIssueInvoiceByIdRequestSchema, } from "../../../common/dto"; -import { buildCustomerInvoiceDependencies } from "../dependencies"; +import { buildIssuedInvoicesDependencies } from "../issued-invoices-dependencies"; import { GetIssueInvoiceController, @@ -25,7 +25,7 @@ export const issuedInvoicesRouter = (params: ModuleParams) => { logger: ILogger; }; - const deps = buildCustomerInvoiceDependencies(params); + const deps = buildIssuedInvoicesDependencies(params); const router: Router = Router({ mergeParams: true }); if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") { diff --git a/modules/customer-invoices/src/api/infrastructure/express/proformas.routes.ts b/modules/customer-invoices/src/api/infrastructure/express/proformas.routes.ts index ea6ba2e1..af6bc332 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/proformas.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/proformas.routes.ts @@ -16,7 +16,7 @@ import { UpdateProformaByIdParamsRequestSchema, UpdateProformaByIdRequestSchema, } from "../../../common"; -import { buildCustomerInvoiceDependencies } from "../dependencies"; +import { buildProformasDependencies } from "../proformas-dependencies"; import { ChangeStatusProformaController, @@ -37,7 +37,7 @@ export const proformasRouter = (params: ModuleParams) => { logger: ILogger; }; - const deps = buildCustomerInvoiceDependencies(params); + const deps = buildProformasDependencies(params); const router: Router = Router({ mergeParams: true }); if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") { diff --git a/modules/customer-invoices/src/api/infrastructure/index.ts b/modules/customer-invoices/src/api/infrastructure/index.ts index c86dd682..1b90dabf 100644 --- a/modules/customer-invoices/src/api/infrastructure/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/index.ts @@ -1,4 +1,4 @@ -export * from "./dependencies"; export * from "./express"; export * from "./mappers"; +export * from "./proformas-dependencies"; export * from "./sequelize"; diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices-dependencies.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices-dependencies.ts new file mode 100644 index 00000000..a0ffcec5 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices-dependencies.ts @@ -0,0 +1,148 @@ +// modules/invoice/infrastructure/invoice-dependencies.factory.ts + +import { type JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "@erp/core"; +import type { IMapperRegistry, IPresenterRegistry, ModuleParams } from "@erp/core/api"; +import { + InMemoryMapperRegistry, + InMemoryPresenterRegistry, + SequelizeTransactionManager, +} from "@erp/core/api"; + +import { + CustomerInvoiceApplicationService, + GetIssuedInvoiceUseCase, + IssuedInvoiceFullPresenter, + IssuedInvoiceItemsFullPresenter, + IssuedInvoiceItemsReportPresenter, + IssuedInvoiceListPresenter, + IssuedInvoiceRecipientFullPresenter, + IssuedInvoiceReportPresenter, + IssuedInvoiceTaxesReportPresenter, + IssuedInvoiceVerifactuFullPresenter, + ListIssuedInvoicesUseCase, + ReportIssuedInvoiceUseCase, +} from "../application"; +import { + IssuedInvoiceReportHTMLPresenter, + IssuedInvoiceReportPDFPresenter, +} from "../application/use-cases/issued-invoices/report-issued-invoices/reporter"; + +import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers"; +import { CustomerInvoiceRepository } from "./sequelize"; +import { SequelizeInvoiceNumberGenerator } from "./services"; + +export type IssuedInvoicesDeps = { + transactionManager: SequelizeTransactionManager; + mapperRegistry: IMapperRegistry; + presenterRegistry: IPresenterRegistry; + repo: CustomerInvoiceRepository; + appService: CustomerInvoiceApplicationService; + catalogs: { + taxes: JsonTaxCatalogProvider; + }; + useCases: { + list_issued_invoices: () => ListIssuedInvoicesUseCase; + get_issued_invoice: () => GetIssuedInvoiceUseCase; + report_issued_invoice: () => ReportIssuedInvoiceUseCase; + }; +}; + +export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInvoicesDeps { + const { database } = params; + + /** Dominio */ + const catalogs = { taxes: SpainTaxCatalogProvider() }; + + /** Infraestructura */ + const transactionManager = new SequelizeTransactionManager(database); + + const mapperRegistry = new InMemoryMapperRegistry(); + mapperRegistry + .registerDomainMapper( + { resource: "customer-invoice" }, + new CustomerInvoiceDomainMapper({ taxCatalog: catalogs.taxes }) + ) + .registerQueryMappers([ + { + key: { resource: "customer-invoice", query: "LIST" }, + mapper: new CustomerInvoiceListMapper(), + }, + ]); + + // Repository & Services + const repository = new CustomerInvoiceRepository({ mapperRegistry, database }); + const numberGenerator = new SequelizeInvoiceNumberGenerator(); + + /** Aplicación */ + const appService = new CustomerInvoiceApplicationService(repository, numberGenerator); + + // Presenter Registry + const presenterRegistry = new InMemoryPresenterRegistry(); + presenterRegistry.registerPresenters([ + // FULL + { + key: { resource: "issued-invoice-items", projection: "FULL" }, + presenter: new IssuedInvoiceItemsFullPresenter(presenterRegistry), + }, + { + key: { resource: "issued-invoice-recipient", projection: "FULL" }, + presenter: new IssuedInvoiceRecipientFullPresenter(presenterRegistry), + }, + { + key: { resource: "issued-invoice-verifactu", projection: "FULL" }, + presenter: new IssuedInvoiceVerifactuFullPresenter(presenterRegistry), + }, + { + key: { resource: "issued-invoice", projection: "FULL" }, + presenter: new IssuedInvoiceFullPresenter(presenterRegistry), + }, + + // LIST + { + key: { resource: "issued-invoice", projection: "LIST" }, + presenter: new IssuedInvoiceListPresenter(presenterRegistry), + }, + + // REPORT + { + key: { resource: "issued-invoice", projection: "REPORT", format: "JSON" }, + presenter: new IssuedInvoiceReportPresenter(presenterRegistry), + }, + { + key: { resource: "issued-invoice-taxes", projection: "REPORT", format: "JSON" }, + presenter: new IssuedInvoiceTaxesReportPresenter(presenterRegistry), + }, + { + key: { resource: "issued-invoice-items", projection: "REPORT", format: "JSON" }, + presenter: new IssuedInvoiceItemsReportPresenter(presenterRegistry), + }, + { + key: { resource: "issued-invoice", projection: "REPORT", format: "HTML" }, + presenter: new IssuedInvoiceReportHTMLPresenter(presenterRegistry), + }, + { + key: { resource: "issued-invoice", projection: "REPORT", format: "PDF" }, + presenter: new IssuedInvoiceReportPDFPresenter(presenterRegistry), + }, + ]); + + const useCases: IssuedInvoicesDeps["useCases"] = { + // Issue Invoices + list_issued_invoices: () => + new ListIssuedInvoicesUseCase(appService, transactionManager, presenterRegistry), + get_issued_invoice: () => + new GetIssuedInvoiceUseCase(appService, transactionManager, presenterRegistry), + report_issued_invoice: () => + new ReportIssuedInvoiceUseCase(appService, transactionManager, presenterRegistry), + }; + + return { + transactionManager, + repo: repository, + mapperRegistry, + presenterRegistry, + appService, + catalogs, + useCases, + }; +} diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/queries/customer-invoice.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/queries/customer-invoice.list.mapper.ts index b72d2e65..8037bb85 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/queries/customer-invoice.list.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/queries/customer-invoice.list.mapper.ts @@ -14,7 +14,7 @@ import { extractOrPushError, maybeFromNullableVO, } from "@repo/rdx-ddd"; -import { type Maybe, Result } from "@repo/rdx-utils"; +import { Maybe, Result } from "@repo/rdx-utils"; import { CustomerInvoiceNumber, @@ -22,10 +22,12 @@ import { CustomerInvoiceStatus, InvoiceAmount, type InvoiceRecipient, + type VerifactuRecord, } from "../../../domain"; import type { CustomerInvoiceModel } from "../../sequelize"; import { InvoiceRecipientListMapper } from "./invoice-recipient.list.mapper"; +import { VerifactuRecordListMapper } from "./verifactu-record.list.mapper"; export type CustomerInvoiceListDTO = { id: UniqueID; @@ -57,6 +59,8 @@ export type CustomerInvoiceListDTO = { taxableAmount: InvoiceAmount; taxesAmount: InvoiceAmount; totalAmount: InvoiceAmount; + + verifactu: Maybe; }; export interface ICustomerInvoiceListMapper @@ -67,10 +71,12 @@ export class CustomerInvoiceListMapper implements ICustomerInvoiceListMapper { private _recipientMapper: InvoiceRecipientListMapper; + private _verifactuMapper: VerifactuRecordListMapper; constructor() { super(); this._recipientMapper = new InvoiceRecipientListMapper(); + this._verifactuMapper = new VerifactuRecordListMapper(); } public mapToDTO( @@ -99,6 +105,21 @@ export class CustomerInvoiceListMapper // 3) Taxes const taxes = raw.taxes.map((tax) => tax.tax_code).join(", "); + // 4) Verifactu record + let verifactu: Maybe = Maybe.none(); + if (raw.verifactu) { + const verifactuResult = this._verifactuMapper.mapToDTO(raw.verifactu, { errors, ...params }); + + if (verifactuResult.isFailure) { + errors.push({ + path: "verifactu", + message: verifactuResult.error.message, + }); + } else { + verifactu = Maybe.some(verifactuResult.data); + } + } + // 5) Si hubo errores de mapeo, devolvemos colección de validación if (errors.length > 0) { return Result.fail( @@ -133,6 +154,8 @@ export class CustomerInvoiceListMapper taxableAmount: attributes.taxableAmount!, taxesAmount: attributes.taxesAmount!, totalAmount: attributes.totalAmount!, + + verifactu, }); } diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/queries/invoice-recipient.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/queries/invoice-recipient.list.mapper.ts index c87f37b0..4baaab39 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/queries/invoice-recipient.list.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/queries/invoice-recipient.list.mapper.ts @@ -1,3 +1,8 @@ +import { + type IQueryMapperWithBulk, + type MapperParamsType, + SequelizeQueryMapper, +} from "@erp/core/api"; import { City, Country, @@ -6,17 +11,16 @@ import { Province, Street, TINNumber, - ValidationErrorDetail, + type ValidationErrorDetail, extractOrPushError, maybeFromNullableVO, } from "@repo/rdx-ddd"; +import type { Result } from "@repo/rdx-utils"; -import { IQueryMapperWithBulk, MapperParamsType, SequelizeQueryMapper } from "@erp/core/api"; - -import { Result } from "@repo/rdx-utils"; import { InvoiceRecipient } from "../../../domain"; -import { CustomerInvoiceModel } from "../../sequelize"; -import { CustomerInvoiceListDTO } from "./customer-invoice.list.mapper"; +import type { CustomerInvoiceModel } from "../../sequelize"; + +import type { CustomerInvoiceListDTO } from "./customer-invoice.list.mapper"; interface IInvoiceRecipientListMapper extends IQueryMapperWithBulk {} diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/queries/verifactu-record.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/queries/verifactu-record.list.mapper.ts new file mode 100644 index 00000000..3f03edd4 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/mappers/queries/verifactu-record.list.mapper.ts @@ -0,0 +1,64 @@ +import { + type ISequelizeQueryMapper, + type MapperParamsType, + SequelizeQueryMapper, +} from "@erp/core/api"; +import { + URLAddress, + UniqueID, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, + maybeFromNullableVO, +} from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import { VerifactuRecord, VerifactuRecordEstado } from "../../../domain/"; +import type { VerifactuRecordModel } from "../../sequelize"; + +export interface IVerifactuRecordListMapper + extends ISequelizeQueryMapper { + // +} + +export class VerifactuRecordListMapper + extends SequelizeQueryMapper + implements IVerifactuRecordListMapper +{ + public mapToDTO( + raw: VerifactuRecordModel, + params?: MapperParamsType + ): Result { + const errors: ValidationErrorDetail[] = []; + + const recordId = extractOrPushError(UniqueID.create(raw.id), "id", errors); + const estado = extractOrPushError(VerifactuRecordEstado.create(raw.estado), "estado", errors); + + const qr = extractOrPushError( + maybeFromNullableVO(raw.qr, (value) => Result.ok(String(value))), + "qr", + errors + ); + + const url = extractOrPushError( + maybeFromNullableVO(raw.url, (value) => URLAddress.create(value)), + "url", + errors + ); + + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Verifactu record mapping failed [mapToDTO]", errors) + ); + } + + return VerifactuRecord.create( + { + estado: estado!, + qrCode: qr!, + url: url!, + }, + recordId! + ); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/dependencies.ts b/modules/customer-invoices/src/api/infrastructure/proformas-dependencies.ts similarity index 59% rename from modules/customer-invoices/src/api/infrastructure/dependencies.ts rename to modules/customer-invoices/src/api/infrastructure/proformas-dependencies.ts index 893a0cfe..ba919048 100644 --- a/modules/customer-invoices/src/api/infrastructure/dependencies.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas-dependencies.ts @@ -12,31 +12,30 @@ import { ChangeStatusProformaUseCase, CreateProformaUseCase, CustomerInvoiceApplicationService, - CustomerInvoiceFullPresenter, - CustomerInvoiceItemsFullPresenter, - CustomerInvoiceItemsReportPersenter, - CustomerInvoiceReportHTMLPresenter, - CustomerInvoiceReportPDFPresenter, - CustomerInvoiceReportPresenter, - CustomerInvoiceTaxesReportPresenter, DeleteProformaUseCase, - GetIssuedInvoiceUseCase, GetProformaUseCase, IssueProformaUseCase, - ListCustomerInvoicesPresenter, - ListIssuedInvoicesUseCase, ListProformasUseCase, - RecipientInvoiceFullPresenter, - ReportIssuedInvoiceUseCase, + ProformaFullPresenter, + ProformaListPresenter, + ProformaReportHTMLPresenter, + ProformaReportPDFPresenter, ReportProformaUseCase, UpdateProformaUseCase, } from "../application"; +import { ProformaItemsFullPresenter } from "../application/presenters/domain/proformas/proforma-items.full.presenter"; +import { ProformaRecipientFullPresenter } from "../application/presenters/domain/proformas/proforma-recipient.full.presenter"; +import { + IssuedInvoiceTaxesReportPresenter, + ProformaItemsReportPresenter, + ProformaReportPresenter, +} from "../application/presenters/reports"; import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers"; import { CustomerInvoiceRepository } from "./sequelize"; import { SequelizeInvoiceNumberGenerator } from "./services"; -export type CustomerInvoiceDeps = { +export type ProformasDeps = { transactionManager: SequelizeTransactionManager; mapperRegistry: IMapperRegistry; presenterRegistry: IPresenterRegistry; @@ -54,14 +53,10 @@ export type CustomerInvoiceDeps = { report_proforma: () => ReportProformaUseCase; issue_proforma: () => IssueProformaUseCase; changeStatus_proforma: () => ChangeStatusProformaUseCase; - - list_issued_invoices: () => ListIssuedInvoicesUseCase; - get_issued_invoice: () => GetIssuedInvoiceUseCase; - report_issued_invoice: () => ReportIssuedInvoiceUseCase; }; }; -export function buildCustomerInvoiceDependencies(params: ModuleParams): CustomerInvoiceDeps { +export function buildProformasDependencies(params: ModuleParams): ProformasDeps { const { database } = params; /** Dominio */ @@ -93,45 +88,50 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer // Presenter Registry const presenterRegistry = new InMemoryPresenterRegistry(); presenterRegistry.registerPresenters([ + // FULL { - key: { resource: "customer-invoice-items", projection: "FULL" }, - presenter: new CustomerInvoiceItemsFullPresenter(presenterRegistry), + key: { resource: "proforma-items", projection: "FULL" }, + presenter: new ProformaItemsFullPresenter(presenterRegistry), }, { - key: { resource: "recipient-invoice", projection: "FULL" }, - presenter: new RecipientInvoiceFullPresenter(presenterRegistry), + key: { resource: "proforma-recipient", projection: "FULL" }, + presenter: new ProformaRecipientFullPresenter(presenterRegistry), }, { - key: { resource: "customer-invoice", projection: "FULL" }, - presenter: new CustomerInvoiceFullPresenter(presenterRegistry), + key: { resource: "proforma", projection: "FULL" }, + presenter: new ProformaFullPresenter(presenterRegistry), + }, + + // LIST + { + key: { resource: "proforma", projection: "LIST" }, + presenter: new ProformaListPresenter(presenterRegistry), + }, + + // REPORT + { + key: { resource: "proforma", projection: "REPORT", format: "JSON" }, + presenter: new ProformaReportPresenter(presenterRegistry), }, { - key: { resource: "customer-invoice", projection: "LIST" }, - presenter: new ListCustomerInvoicesPresenter(presenterRegistry), + key: { resource: "proforma-taxes", projection: "REPORT", format: "JSON" }, + presenter: new IssuedInvoiceTaxesReportPresenter(presenterRegistry), }, { - key: { resource: "customer-invoice", projection: "REPORT", format: "JSON" }, - presenter: new CustomerInvoiceReportPresenter(presenterRegistry), + key: { resource: "proforma-items", projection: "REPORT", format: "JSON" }, + presenter: new ProformaItemsReportPresenter(presenterRegistry), }, { - key: { resource: "customer-invoice-taxes", projection: "REPORT", format: "JSON" }, - presenter: new CustomerInvoiceTaxesReportPresenter(presenterRegistry), + key: { resource: "proforma", projection: "REPORT", format: "HTML" }, + presenter: new ProformaReportHTMLPresenter(presenterRegistry), }, { - key: { resource: "customer-invoice-items", projection: "REPORT", format: "JSON" }, - presenter: new CustomerInvoiceItemsReportPersenter(presenterRegistry), - }, - { - key: { resource: "customer-invoice", projection: "REPORT", format: "HTML" }, - presenter: new CustomerInvoiceReportHTMLPresenter(presenterRegistry), - }, - { - key: { resource: "customer-invoice", projection: "REPORT", format: "PDF" }, - presenter: new CustomerInvoiceReportPDFPresenter(presenterRegistry), + key: { resource: "proforma", projection: "REPORT", format: "PDF" }, + presenter: new ProformaReportPDFPresenter(presenterRegistry), }, ]); - const useCases: CustomerInvoiceDeps["useCases"] = { + const useCases: ProformasDeps["useCases"] = { // Proformas list_proformas: () => new ListProformasUseCase(appService, transactionManager, presenterRegistry), @@ -146,14 +146,6 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer issue_proforma: () => new IssueProformaUseCase(appService, transactionManager, presenterRegistry), changeStatus_proforma: () => new ChangeStatusProformaUseCase(appService, transactionManager), - - // Issue Invoices - list_issued_invoices: () => - new ListIssuedInvoicesUseCase(appService, transactionManager, presenterRegistry), - get_issued_invoice: () => - new GetIssuedInvoiceUseCase(appService, transactionManager, presenterRegistry), - report_issued_invoice: () => - new ReportIssuedInvoiceUseCase(appService, transactionManager, presenterRegistry), }; return { diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts index a3c1bdb9..902fc981 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts @@ -24,6 +24,7 @@ import { CustomerInvoiceModel } from "./models/customer-invoice.model"; import { CustomerInvoiceItemModel } from "./models/customer-invoice-item.model"; import { CustomerInvoiceItemTaxModel } from "./models/customer-invoice-item-tax.model"; import { CustomerInvoiceTaxModel } from "./models/customer-invoice-tax.model"; +import { VerifactuRecordModel } from "./models/verifactu-record.model"; export class CustomerInvoiceRepository extends SequelizeRepository @@ -119,8 +120,6 @@ export class CustomerInvoiceRepository transaction, }); - console.log(affectedCount); - if (affectedCount === 0) { return Result.fail( new InfrastructureRepositoryError(`Invoice ${id} not found or concurrency issue`) @@ -193,15 +192,15 @@ export class CustomerInvoiceRepository /** * - * Busca una factura por su identificador único. + * Busca una proforma por su identificador único. * - * @param companyId - Identificador UUID de la empresa a la que pertenece la factura. - * @param id - UUID de la factura. + * @param companyId - Identificador UUID de la empresa a la que pertenece la proforma. + * @param id - UUID de la proforma. * @param transaction - Transacción activa para la operación. * @param options - Opciones adicionales para la consulta (Sequelize FindOptions) * @returns Result */ - async getByIdInCompany( + async getProformaByIdInCompany( companyId: UniqueID, id: UniqueID, transaction: Transaction, @@ -230,9 +229,10 @@ export class CustomerInvoiceRepository const mergedOptions: FindOptions> = { ...options, where: { - id: id.toString(), - company_id: companyId.toString(), ...(options.where ?? {}), + id: id.toString(), + is_proforma: true, + company_id: companyId.toString(), }, order: [ ...normalizedOrder, @@ -281,7 +281,96 @@ export class CustomerInvoiceRepository /** * - * Consulta facturas usando un objeto Criteria (filtros, orden, paginación). + * Busca una factura por su identificador único. + * + * @param companyId - Identificador UUID de la empresa a la que pertenece la factura. + * @param id - UUID de la factura. + * @param transaction - Transacción activa para la operación. + * @param options - Opciones adicionales para la consulta (Sequelize FindOptions) + * @returns Result + */ + async getIssuedInvoiceByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction: Transaction, + options: FindOptions> = {} + ): Promise> { + const { CustomerModel } = this._database.models; + + try { + const mapper: ICustomerInvoiceDomainMapper = this._registry.getDomainMapper({ + resource: "customer-invoice", + }); + + // Normalización defensiva de order/include + const normalizedOrder = Array.isArray(options.order) + ? options.order + : options.order + ? [options.order] + : []; + + const normalizedInclude = Array.isArray(options.include) + ? options.include + : options.include + ? [options.include] + : []; + + const mergedOptions: FindOptions> = { + ...options, + where: { + ...(options.where ?? {}), + id: id.toString(), + is_proforma: false, + company_id: companyId.toString(), + }, + order: [ + ...normalizedOrder, + [{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"], + ], + include: [ + ...normalizedInclude, + { + model: CustomerModel, + as: "current_customer", + required: false, + }, + { + model: CustomerInvoiceItemModel, + as: "items", + required: false, + include: [ + { + model: CustomerInvoiceItemTaxModel, + as: "taxes", + required: false, + }, + ], + }, + { + model: CustomerInvoiceTaxModel, + as: "taxes", + required: false, + }, + ], + transaction, + }; + + const row = await CustomerInvoiceModel.findOne(mergedOptions); + + if (!row) { + return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString())); + } + + const invoice = mapper.mapToDomain(row); + return invoice; + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + + /** + * + * Consulta proformas usando un objeto Criteria (filtros, orden, paginación). * * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. * @param criteria - Criterios de búsqueda. @@ -290,7 +379,7 @@ export class CustomerInvoiceRepository * * @see Criteria */ - public async findByCriteriaInCompany( + public async findProformasByCriteriaInCompany( companyId: UniqueID, criteria: Criteria, transaction: Transaction, @@ -331,9 +420,10 @@ export class CustomerInvoiceRepository query.where = { ...query.where, + ...(options.where ?? {}), + is_proforma: true, company_id: companyId.toString(), deleted_at: null, - ...(options.where ?? {}), }; query.order = [...(query.order as OrderItem[]), ...normalizedOrder]; @@ -389,6 +479,123 @@ export class CustomerInvoiceRepository } } + /** + * + * Consulta facturas usando un objeto Criteria (filtros, orden, paginación). + * + * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. + * @param criteria - Criterios de búsqueda. + * @param transaction - Transacción activa para la operación. + * @returns Result + * + * @see Criteria + */ + public async findIssuedInvoicesByCriteriaInCompany( + companyId: UniqueID, + criteria: Criteria, + transaction: Transaction, + options: FindOptions> = {} + ): Promise, Error>> { + const { CustomerModel } = this._database.models; + + try { + const mapper: ICustomerInvoiceListMapper = this._registry.getQueryMapper({ + resource: "customer-invoice", + query: "LIST", + }); + + const converter = new CriteriaToSequelizeConverter(); + const query = converter.convert(criteria, { + searchableFields: ["invoice_number", "reference", "description"], + mappings: { + reference: "CustomerInvoiceModel.reference", + }, + allowedFields: ["invoice_date", "id", "created_at"], + enableFullText: true, + database: this._database, + strictMode: true, // fuerza error si ORDER BY no permitido + }); + + // Normalización defensiva de order/include + const normalizedOrder = Array.isArray(options.order) + ? options.order + : options.order + ? [options.order] + : []; + + const normalizedInclude = Array.isArray(options.include) + ? options.include + : options.include + ? [options.include] + : []; + + query.where = { + ...query.where, + ...(options.where ?? {}), + is_proforma: false, + company_id: companyId.toString(), + deleted_at: null, + }; + + query.order = [...(query.order as OrderItem[]), ...normalizedOrder]; + + query.include = [ + ...normalizedInclude, + { + model: VerifactuRecordModel, + as: "verifactu", + required: false, + attributes: ["id", "estado", "url", "uuid"], + }, + { + model: CustomerModel, + as: "current_customer", + required: false, // false => LEFT JOIN + attributes: [ + "name", + "trade_name", + "tin", + "street", + "street2", + "city", + "postal_code", + "province", + "country", + ], + }, + { + model: CustomerInvoiceTaxModel, + as: "taxes", + required: false, + separate: true, // => query aparte, devuelve siempre array + attributes: ["tax_id", "tax_code"], + }, + ]; + + // Reemplazar findAndCountAll por findAll + count (más control y mejor rendimiento) + /*const { rows, count } = await CustomerInvoiceModel.findAndCountAll({ + ...query, + transaction, + });*/ + + const [rows, count] = await Promise.all([ + CustomerInvoiceModel.findAll({ + ...query, + transaction, + }), + CustomerInvoiceModel.count({ + where: query.where, + distinct: true, // evita duplicados por LEFT JOIN + transaction, + }), + ]); + + return mapper.mapToDTOCollection(rows, count); + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + /** * * Elimina o marca como eliminada una proforma dentro de una empresa. diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/index.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/index.ts index 535f7a8a..fdad48c6 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/index.ts @@ -2,6 +2,7 @@ import customerInvoiceModelInit from "./models/customer-invoice.model"; import customerInvoiceItemModelInit from "./models/customer-invoice-item.model"; import customerInvoiceItemTaxesModelInit from "./models/customer-invoice-item-tax.model"; import customerInvoiceTaxesModelInit from "./models/customer-invoice-tax.model"; +import verifactuRecordModelInit from "./models/verifactu-record.model"; export * from "./customer-invoice.repository"; export * from "./models"; @@ -13,4 +14,6 @@ export const models = [ customerInvoiceTaxesModelInit, customerInvoiceItemTaxesModelInit, + + verifactuRecordModelInit, ]; diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-tax.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-tax.model.ts index 4c56bb70..98012d97 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-tax.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-tax.model.ts @@ -55,7 +55,9 @@ export class CustomerInvoiceTaxModel extends Model< }); } - static hooks(_database: Sequelize) {} + static hooks(_database: Sequelize) { + // + } } export default (database: Sequelize) => { diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts index f5fb217d..531cba0d 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts @@ -17,6 +17,7 @@ import type { CustomerInvoiceTaxCreationAttributes, CustomerInvoiceTaxModel, } from "./customer-invoice-tax.model"; +import type { VerifactuRecordModel } from "./verifactu-record.model"; export type CustomerInvoiceCreationAttributes = InferCreationAttributes< CustomerInvoiceModel, @@ -101,6 +102,7 @@ export class CustomerInvoiceModel extends Model< declare items: NonAttribute; declare taxes: NonAttribute; declare current_customer: NonAttribute; + declare verifactu: NonAttribute; static associate(database: Sequelize) { const models = database.models; @@ -109,6 +111,7 @@ export class CustomerInvoiceModel extends Model< "CustomerInvoiceItemModel", "CustomerModel", "CustomerInvoiceTaxModel", + "VerifactuRecordModel", ]; // Comprobamos que los modelos existan @@ -124,8 +127,14 @@ export class CustomerInvoiceModel extends Model< CustomerInvoiceItemModel, CustomerModel, CustomerInvoiceTaxModel, + VerifactuRecordModel, } = models; + CustomerInvoiceModel.hasOne(VerifactuRecordModel, { + as: "verifactu", + foreignKey: "invoice_id", + }); + CustomerInvoiceModel.belongsTo(CustomerModel, { as: "current_customer", foreignKey: "customer_id", @@ -151,7 +160,9 @@ export class CustomerInvoiceModel extends Model< }); } - static hooks(_database: Sequelize) {} + static hooks(_database: Sequelize) { + // + } } export default (database: Sequelize) => { diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/index.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/models/index.ts index b3f2db3b..4c131a10 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/models/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/models/index.ts @@ -1,4 +1,5 @@ -export * from "./customer-invoice-item-tax.model"; -export * from "./customer-invoice-item.model"; -export * from "./customer-invoice-tax.model"; export * from "./customer-invoice.model"; +export * from "./customer-invoice-item.model"; +export * from "./customer-invoice-item-tax.model"; +export * from "./customer-invoice-tax.model"; +export * from "./verifactu-record.model"; diff --git a/modules/verifactu/src/api/infrastructure/sequelize/models/verifactu-record.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/models/verifactu-record.model.ts similarity index 66% rename from modules/verifactu/src/api/infrastructure/sequelize/models/verifactu-record.model.ts rename to modules/customer-invoices/src/api/infrastructure/sequelize/models/verifactu-record.model.ts index fe34ec5c..b102d8ef 100644 --- a/modules/verifactu/src/api/infrastructure/sequelize/models/verifactu-record.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/models/verifactu-record.model.ts @@ -1,9 +1,10 @@ -import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize"; -/*import { - CustomerInvoiceItemTaxCreationAttributes, - CustomerInvoiceItemTaxModel, -} from "./customer-invoice-item-tax.model"; - */ +import { + DataTypes, + type InferAttributes, + type InferCreationAttributes, + Model, + type Sequelize, +} from "sequelize"; export type VerifactuRecordCreationAttributes = InferCreationAttributes; @@ -22,16 +23,30 @@ export class VerifactuRecordModel extends Model< declare operacion: string; static associate(database: Sequelize) { - const { CustomerInvoiceModel } = database.models; + const models = database.models; + const requiredModels = ["CustomerInvoiceModel"]; + + // Comprobamos que los modelos existan + for (const name of requiredModels) { + if (!models[name]) { + throw new Error(`[VerifactuRecordModel.associate] Missing model: ${name}`); + } + } + + const { CustomerInvoiceModel } = models; VerifactuRecordModel.belongsTo(CustomerInvoiceModel, { - as: "verifactu_records", + as: "verifactu", targetKey: "id", foreignKey: "invoice_id", onDelete: "CASCADE", onUpdate: "CASCADE", }); } + + static hooks(_database: Sequelize) { + // + } } export default (database: Sequelize) => { @@ -76,11 +91,18 @@ export default (database: Sequelize) => { }, { sequelize: database, + modelName: "VerifactuRecordModel", tableName: "verifactu_records", underscored: true, + paranoid: true, // softs deletes + timestamps: true, - indexes: [], + createdAt: "created_at", + updatedAt: "updated_at", + deletedAt: "deleted_at", + + indexes: [{ name: "idx_invoice_id", fields: ["invoice_id"] }], // <- para relación con CustomerInvoiceModel whereMergeStrategy: "and", // <- cómo tratar el merge de un scope diff --git a/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts b/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts index 28104031..b3c4dc4c 100644 --- a/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts @@ -56,6 +56,12 @@ export const GetIssuedInvoiceByIdResponseSchema = z.object({ taxes_amount: MoneySchema, total_amount: MoneySchema, + verifactu: z.object({ + status: z.string(), + url: z.string(), + qr_code: z.string(), + }), + items: z.array( z.object({ id: z.uuid(), diff --git a/modules/customer-invoices/src/common/dto/response/issued-invoices/list-issued-invoices.response.dto.ts b/modules/customer-invoices/src/common/dto/response/issued-invoices/list-issued-invoices.response.dto.ts index 293536af..318c99d8 100644 --- a/modules/customer-invoices/src/common/dto/response/issued-invoices/list-issued-invoices.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/issued-invoices/list-issued-invoices.response.dto.ts @@ -47,6 +47,12 @@ export const ListIssuedInvoicesResponseSchema = createPaginatedListSchema( taxes_amount: MoneySchema, total_amount: MoneySchema, + verifactu: z.object({ + status: z.string(), + url: z.string(), + qr_code: z.string(), + }), + metadata: MetadataSchema.optional(), }) ); diff --git a/modules/customer-invoices/src/common/locales/en.json b/modules/customer-invoices/src/common/locales/en.json index d255619a..ed3034bc 100644 --- a/modules/customer-invoices/src/common/locales/en.json +++ b/modules/customer-invoices/src/common/locales/en.json @@ -35,6 +35,19 @@ "rejected": "Rejected", "issued": "Issued" } + }, + "issued_invoices": { + "status": { + "all": "Todos", + "pendiente": "Pendiente", + "aceptado_con_error": "Aceptado con error", + "incorrecto": "Incorrecto", + "duplicado": "Duplicado", + "anulado": "Anulado", + "factura_inexistente": "Factura inexistente", + "rechazado": "Rechazado", + "error": "Error" + } } }, "pages": { @@ -90,6 +103,10 @@ "title": "Customer invoices", "description": "List all customer invoices", "grid_columns": { + "series_invoice_number": "Serie & #", + "verifactu_status": "Status", + "verifactu_qr_code": "QR Code", + "verifactu_url": "URL", "invoice_number": "Inv. number", "series": "Serie", "reference": "Reference", diff --git a/modules/customer-invoices/src/common/locales/es.json b/modules/customer-invoices/src/common/locales/es.json index e9190f03..4cb20b2f 100644 --- a/modules/customer-invoices/src/common/locales/es.json +++ b/modules/customer-invoices/src/common/locales/es.json @@ -34,6 +34,19 @@ "rejected": "Rechazadas", "issued": "Emitidas" } + }, + "issued_invoices": { + "status": { + "all": "All", + "pendiente": "Pendiente", + "aceptado_con_error": "Aceptado con error", + "incorrecto": "Incorrecto", + "duplicado": "Duplicado", + "anulado": "Anulado", + "factura_inexistente": "Factura inexistente", + "rechazado": "Rechazado", + "error": "Error" + } } }, "pages": { @@ -89,10 +102,14 @@ "title": "Facturas de cliente", "description": "Lista todas las facturas de cliente", "grid_columns": { + "series_invoice_number": "Serie & #", + "verifactu_status": "Estado", + "verifactu_qr_code": "Código QR", + "verifactu_url": "URL", "invoice_number": "Nº factura", "series": "Serie", "reference": "Reference", - "invoice_date": "Fecha de proforma", + "invoice_date": "Fecha de factura", "operation_date": "Fecha de operación", "recipient": "Cliente", "recipient_tin": "NIF/CIF", diff --git a/modules/customer-invoices/src/web/issued-invoices/pages/list/hooks/use-issued-invoices-grid-columns.tsx b/modules/customer-invoices/src/web/issued-invoices/pages/list/hooks/use-issued-invoices-grid-columns.tsx index b675eea6..29f2e881 100644 --- a/modules/customer-invoices/src/web/issued-invoices/pages/list/hooks/use-issued-invoices-grid-columns.tsx +++ b/modules/customer-invoices/src/web/issued-invoices/pages/list/hooks/use-issued-invoices-grid-columns.tsx @@ -49,14 +49,73 @@ export function useIssuedInvoicesGridColumns( ), enableHiding: false, enableSorting: false, - maxSize: 48, - size: 48, - minSize: 48, + maxSize: 64, + size: 64, + minSize: 64, meta: { title: t("pages.issued_invoices.list.grid_columns.series_invoice_number"), }, }, - + { + id: "verifactu_status", + header: ({ column }) => ( + + ), + accessorFn: (row) => row.verifactu.status, // para ordenar/buscar por nombre + enableHiding: false, + enableSorting: false, + size: 140, + minSize: 120, + meta: { + title: t("pages.issued_invoices.list.grid_columns.verifactu_status"), + }, + }, + { + id: "verifactu_qr_code", + header: ({ column }) => ( + + ), + accessorFn: (row) => row.verifactu.qr_code, // para ordenar/buscar por nombre + cell: ({ row }) => ( +
{row.original.verifactu.qr_code}
+ ), + enableHiding: false, + enableSorting: false, + size: 140, + minSize: 120, + meta: { + title: t("pages.issued_invoices.list.grid_columns.verifactu_qr_code"), + }, + }, + { + id: "verifactu_url", + header: ({ column }) => ( + + ), + accessorFn: (row) => row.verifactu.url, // para ordenar/buscar por nombre + cell: ({ row }) => ( +
{row.original.verifactu.url}
+ ), + enableHiding: false, + enableSorting: false, + size: 140, + minSize: 120, + meta: { + title: t("pages.issued_invoices.list.grid_columns.verifactu_url"), + }, + }, { id: "recipient", header: ({ column }) => ( diff --git a/modules/customer-invoices/src/web/issued-invoices/pages/list/ui/issued-invoices-grid.tsx b/modules/customer-invoices/src/web/issued-invoices/pages/list/ui/issued-invoices-grid.tsx index fe4206d9..87da3aaf 100644 --- a/modules/customer-invoices/src/web/issued-invoices/pages/list/ui/issued-invoices-grid.tsx +++ b/modules/customer-invoices/src/web/issued-invoices/pages/list/ui/issued-invoices-grid.tsx @@ -1,5 +1,13 @@ import { SimpleSearchInput } from "@erp/core/components"; import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@repo/shadcn-ui/components"; +import { FilterIcon } from "lucide-react"; import { useTranslation } from "../../../../i18n"; import type { IssuedInvoiceSummaryPageData } from "../../../schema/issued-invoice-summary.web.schema"; @@ -52,6 +60,35 @@ export const IssuedInvoicesGrid = ({
+
= { + inputId?: string; + control: Control; + name: FieldPath; + label?: string; + description?: string; + required?: boolean; +} & Omit; + +export function AmountInputField({ + inputId, + control, + name, + label, + description, + required = false, + ...inputProps +}: AmountInputFieldProps) { + return ( + ( + + {label ? ( + + {label} {required ? : null} + + ) : null} + + + + {description ? {description} : null} + + + )} + /> + ); +} diff --git a/modules/customer-invoices/src/web/proformas/ui/components/amount-input.tsx b/modules/customer-invoices/src/web/proformas/ui/components/amount-input.tsx new file mode 100644 index 00000000..88525b52 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/ui/components/amount-input.tsx @@ -0,0 +1,233 @@ +import { formatCurrency } from "@erp/core"; +import { useMoney } from "@erp/core/hooks"; +import { Input } from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; +import * as React from "react"; + +import { + type InputEmptyMode, + type InputReadOnlyMode, + findFocusableInCell, + focusAndSelect, +} from "./input-utils"; + +export type AmountInputProps = { + value: number | string; // "" → no mostrar nada; string puede venir con separadores + onChange: (next: number | string) => void; + readOnly?: boolean; + readOnlyMode?: InputReadOnlyMode; // default "textlike-input" + id?: string; + "aria-label"?: string; + step?: number; // ↑/↓; default 0.01 + emptyMode?: InputEmptyMode; // cómo presentar vacío + emptyText?: string; // texto en vacío para value/placeholder + scale?: number; // decimales; default 2 (ej. 4 para unit_amount) + languageCode?: string; // p.ej. "es-ES" + currencyCode?: string; // p.ej. "EUR" + className?: string; +}; + +export function AmountInput({ + value, + onChange, + readOnly = false, + readOnlyMode = "textlike-input", + id, + "aria-label": ariaLabel = "Amount", + emptyMode = "blank", + emptyText = "", + scale = 2, + languageCode = "es", + currencyCode = "EUR", + className, + ...inputProps +}: AmountInputProps) { + // Hook de dinero para parseo/redondeo consistente con el resto de la app + const { parse, roundToScale } = useMoney({ + locale: languageCode, + fallbackCurrency: currencyCode as any, + }); + + const [raw, setRaw] = React.useState(""); + const [focused, setFocused] = React.useState(false); + + const formatCurrencyNumber = React.useCallback( + (n: number) => formatCurrency(n, scale, currencyCode, languageCode), + [languageCode, currencyCode, scale] + ); + + // Derivar texto visual desde prop `value` + const visualText = React.useMemo(() => { + if (value === "" || value == null) { + return emptyMode === "value" ? emptyText : ""; + } + const numeric = + typeof value === "number" + ? value + : (parse(String(value)) ?? + Number( + String(value) + .replace(/[^\d.,-]/g, "") + .replace(/\./g, "") + .replace(",", ".") + )); + if (!Number.isFinite(numeric)) return emptyMode === "value" ? emptyText : ""; + const n = roundToScale(numeric, scale); + return formatCurrencyNumber(n); + }, [value, emptyMode, emptyText, parse, roundToScale, scale, formatCurrencyNumber]); + + const isShowingEmptyValue = emptyMode === "value" && raw === emptyText; + + // Sin foco → mantener visual + React.useEffect(() => { + if (!focused) setRaw(visualText); + }, [visualText, focused]); + + const handleChange = React.useCallback((e: React.ChangeEvent) => { + setRaw(e.currentTarget.value); + }, []); + + const handleFocus = React.useCallback( + (e: React.FocusEvent) => { + setFocused(true); + // pasar de visual con símbolo → crudo + if (emptyMode === "value" && e.currentTarget.value === emptyText) { + setRaw(""); + return; + } + const current = + parse(e.currentTarget.value) ?? + (value === "" || value == null + ? null + : typeof value === "number" + ? value + : parse(String(value))); + setRaw(current !== null && current !== undefined ? String(current) : ""); + }, + [emptyMode, emptyText, parse, value] + ); + + const handleBlur = React.useCallback( + (e: React.FocusEvent) => { + setFocused(false); + const txt = e.currentTarget.value.trim(); + if (txt === "" || isShowingEmptyValue) { + onChange(""); + setRaw(emptyMode === "value" ? emptyText : ""); + return; + } + const n = parse(txt); + if (n === null) { + onChange(""); + setRaw(emptyMode === "value" ? emptyText : ""); + return; + } + const rounded = roundToScale(n, scale); + onChange(rounded); + setRaw(formatCurrencyNumber(rounded)); // vuelve a visual con símbolo + }, + [ + isShowingEmptyValue, + onChange, + emptyMode, + emptyText, + parse, + roundToScale, + scale, + formatCurrencyNumber, + ] + ); + + const handleKeyDown = React.useCallback( + (e: React.KeyboardEvent) => { + if (readOnly) return; + + const keys = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]; + if (!keys.includes(e.key)) return; + + e.preventDefault(); + + const current = e.currentTarget as HTMLElement; + const rowIndex = Number(current.dataset.rowIndex); + const colIndex = Number(current.dataset.colIndex); + + let nextRow = rowIndex; + let nextCol = colIndex; + + switch (e.key) { + case "ArrowUp": + nextRow--; + break; + case "ArrowDown": + nextRow++; + break; + case "ArrowLeft": + nextCol--; + break; + case "ArrowRight": + nextCol++; + break; + } + + const nextElement = findFocusableInCell(nextRow, nextCol); + console.log(nextElement); + if (nextElement) { + focusAndSelect(nextElement); + } + }, + [readOnly] + ); + + const handleBlock = React.useCallback((e: React.SyntheticEvent) => { + e.preventDefault(); + (e.target as HTMLInputElement).blur(); + }, []); + + if (readOnly && readOnlyMode === "textlike-input") { + return ( + e.preventDefault()} + onMouseDown={handleBlock} + readOnly + tabIndex={-1} + value={visualText} + {...inputProps} + /> + ); + } + + return ( + + ); +} diff --git a/modules/customer-invoices/src/web/proformas/ui/components/index.ts b/modules/customer-invoices/src/web/proformas/ui/components/index.ts new file mode 100644 index 00000000..b663be07 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/ui/components/index.ts @@ -0,0 +1,3 @@ +export * from "./amount-input-field"; +export * from "./percentage-input-field"; +export * from "./quantity-input-field"; diff --git a/modules/customer-invoices/src/web/shared/ui/components/editor/items/input-utils.ts b/modules/customer-invoices/src/web/proformas/ui/components/input-utils.ts similarity index 68% rename from modules/customer-invoices/src/web/shared/ui/components/editor/items/input-utils.ts rename to modules/customer-invoices/src/web/proformas/ui/components/input-utils.ts index dfd9e3fe..a5d53ade 100644 --- a/modules/customer-invoices/src/web/shared/ui/components/editor/items/input-utils.ts +++ b/modules/customer-invoices/src/web/proformas/ui/components/input-utils.ts @@ -1,14 +1,18 @@ +export type InputEmptyMode = "blank" | "placeholder" | "value"; +export type InputReadOnlyMode = "textlike-input" | "normal"; +export type InputSuffixMap = { one: string; other: string; zero?: string }; + // Selectores típicos de elementos que son editables o permite foco const FOCUSABLE_SELECTOR = [ - '[data-cell-focus]', // permite marcar manualmente el target dentro de la celda - 'input:not([disabled])', - 'textarea:not([disabled])', - 'select:not([disabled])', + "[data-cell-focus]", // permite marcar manualmente el target dentro de la celda + "input:not([disabled])", + "textarea:not([disabled])", + "select:not([disabled])", '[contenteditable="true"]', - 'button:not([disabled])', - 'a[href]', - '[tabindex]:not([tabindex="-1"])' -].join(','); + "button:not([disabled])", + "a[href]", + '[tabindex]:not([tabindex="-1"])', +].join(","); // Busca el elemento focuseable dentro de la "celda" destino. // Puedes poner data-row-index / data-col-index en la propia celda o en el control. @@ -16,10 +20,9 @@ const FOCUSABLE_SELECTOR = [ export function findFocusableInCell(row: number, col: number): HTMLElement | null { // 1) ¿Hay un control que ya tenga los data-* directamente? - let el = - document.querySelector( - `[data-row-index="${row}"][data-col-index="${col}"]${FOCUSABLE_SELECTOR.startsWith('[') ? '' : ''}` - ); + let el = document.querySelector( + `[data-row-index="${row}"][data-col-index="${col}"]${FOCUSABLE_SELECTOR.startsWith("[") ? "" : ""}` + ); // Si lo anterior no funcionó o seleccionó un contenedor, intenta: if (!el) { @@ -30,7 +33,9 @@ export function findFocusableInCell(row: number, col: number): HTMLElement | nul if (!cell) return null; // 3) Dentro de la celda, busca el primer foco válido - el = cell.matches(FOCUSABLE_SELECTOR) ? cell : cell.querySelector(FOCUSABLE_SELECTOR); + el = cell.matches(FOCUSABLE_SELECTOR) + ? cell + : cell.querySelector(FOCUSABLE_SELECTOR); } return el || null; @@ -48,8 +53,8 @@ export function focusAndSelect(el: HTMLElement) { // select() funciona en la mayoría; si es type="number", cae en setSelectionRange el.select?.(); // Asegura selección completa si select() no aplica (p.ej. type="number") - if (typeof (el as any).setSelectionRange === 'function') { - const val = (el as any).value ?? ''; + if (typeof (el as any).setSelectionRange === "function") { + const val = (el as any).value ?? ""; (el as any).setSelectionRange(0, String(val).length); } } catch { @@ -65,4 +70,4 @@ export function focusAndSelect(el: HTMLElement) { } // Para select/button/otros focuseables no hacemos selección de texto. }); -} \ No newline at end of file +} diff --git a/modules/customer-invoices/src/web/proformas/ui/components/percentage-input-field.tsx b/modules/customer-invoices/src/web/proformas/ui/components/percentage-input-field.tsx new file mode 100644 index 00000000..28a09727 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/ui/components/percentage-input-field.tsx @@ -0,0 +1,56 @@ +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@repo/shadcn-ui/components"; +import type { Control, FieldPath, FieldValues } from "react-hook-form"; + +import { PercentageInput, type PercentageInputProps } from "./percentage-input"; + +type PercentageInputFieldProps = { + inputId?: string; + control: Control; + name: FieldPath; + label?: string; + description?: string; + required?: boolean; +} & Omit; + +export function PercentageInputField({ + inputId, + control, + name, + label, + description, + required = false, + ...inputProps +}: PercentageInputFieldProps) { + return ( + ( + + {label ? ( + + {label} {required ? : null} + + ) : null} + + + + {description ? {description} : null} + + + )} + /> + ); +} diff --git a/modules/customer-invoices/src/web/proformas/ui/components/percentage-input.tsx b/modules/customer-invoices/src/web/proformas/ui/components/percentage-input.tsx new file mode 100644 index 00000000..e9adaeb8 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/ui/components/percentage-input.tsx @@ -0,0 +1,254 @@ +import { Input } from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; +import * as React from "react"; + +import { + type InputEmptyMode, + type InputReadOnlyMode, + findFocusableInCell, + focusAndSelect, +} from "./input-utils"; + +export type PercentageInputProps = { + value: number | "" | string; // "" → no mostrar nada; string puede venir con separadores + onChange: (next: number | "") => void; + readOnly?: boolean; + readOnlyMode?: InputReadOnlyMode; // default "textlike-input" + id?: string; + "aria-label"?: string; + step?: number; // ↑/↓; default 0.1 + emptyMode?: InputEmptyMode; // cómo presentar vacío + emptyText?: string; // texto en vacío para value/placeholder + scale?: number; // decimales; default 2 + min?: number; // default 0 (p. ej. descuentos) + max?: number; // default 100 + showSuffix?: boolean; // “%” en visual; default true + locale?: string; // para formateo numérico + className?: string; +}; + +export function PercentageInput({ + value, + onChange, + readOnly = false, + readOnlyMode = "textlike-input", + id, + "aria-label": ariaLabel = "Percentage", + step = 0.1, + emptyMode = "blank", + emptyText = "", + scale = 2, + min = 0, + max = 100, + showSuffix = true, + locale, + className, + ...inputProps +}: PercentageInputProps) { + const stripNumberish = (s: string) => s.replace(/[^\d.,-]/g, "").trim(); + + // biome-ignore lint/correctness/useExhaustiveDependencies: + const parseLocaleNumber = React.useCallback((raw: string): number | null => { + if (!raw) return null; + const s = stripNumberish(raw); + if (!s) return null; + const lastComma = s.lastIndexOf(","); + const lastDot = s.lastIndexOf("."); + let normalized = s; + if (lastComma > -1 && lastDot > -1) { + normalized = + lastComma > lastDot ? s.replace(/\./g, "").replace(",", ".") : s.replace(/,/g, ""); + } else if (lastComma > -1) { + normalized = s.replace(",", "."); + } + const n = Number(normalized); + return Number.isFinite(n) ? n : null; + }, []); + + const roundToScale = React.useCallback((n: number, sc: number) => { + const f = 10 ** sc; + return Math.round(n * f) / f; + }, []); + + const clamp = React.useCallback((n: number) => Math.min(Math.max(n, min), max), [min, max]); + + const [raw, setRaw] = React.useState(""); + const [focused, setFocused] = React.useState(false); + + const formatVisual = React.useCallback( + (n: number) => { + const txt = new Intl.NumberFormat(locale ?? undefined, { + maximumFractionDigits: scale, + minimumFractionDigits: Number.isInteger(n) ? 0 : 0, + useGrouping: false, + }).format(n); + return showSuffix ? `${txt}%` : txt; + }, + [locale, scale, showSuffix] + ); + + const visualText = React.useMemo(() => { + if (value === "" || value == null) { + return emptyMode === "value" ? emptyText : ""; + } + const numeric = typeof value === "number" ? value : parseLocaleNumber(String(value)); + if (!Number.isFinite(numeric as number)) return emptyMode === "value" ? emptyText : ""; + const n = roundToScale(clamp(numeric as number), scale); + return formatVisual(n); + }, [value, emptyMode, emptyText, parseLocaleNumber, roundToScale, clamp, scale, formatVisual]); + + const isShowingEmptyValue = emptyMode === "value" && raw === emptyText; + + React.useEffect(() => { + if (!focused) setRaw(visualText); + }, [visualText, focused]); + + const handleChange = React.useCallback((e: React.ChangeEvent) => { + setRaw(e.currentTarget.value); + }, []); + + const handleFocus = React.useCallback( + (e: React.FocusEvent) => { + setFocused(true); + if (emptyMode === "value" && e.currentTarget.value === emptyText) { + setRaw(""); + return; + } + const n = + parseLocaleNumber(e.currentTarget.value) ?? + (value === "" || value == null + ? null + : typeof value === "number" + ? value + : parseLocaleNumber(String(value))); + setRaw(n !== null && n !== undefined ? String(n) : ""); + }, + [emptyMode, emptyText, parseLocaleNumber, value] + ); + + const handleBlur = React.useCallback( + (e: React.FocusEvent) => { + setFocused(false); + const txt = e.currentTarget.value.trim().replace("%", ""); + if (txt === "" || isShowingEmptyValue) { + onChange(""); + setRaw(emptyMode === "value" ? emptyText : ""); + return; + } + const parsed = parseLocaleNumber(txt); + if (parsed === null) { + onChange(""); + setRaw(emptyMode === "value" ? emptyText : ""); + return; + } + const rounded = roundToScale(clamp(parsed), scale); + onChange(rounded); + setRaw(formatVisual(rounded)); // vuelve a visual con % + }, + [ + isShowingEmptyValue, + onChange, + emptyMode, + emptyText, + parseLocaleNumber, + roundToScale, + clamp, + scale, + formatVisual, + ] + ); + + const handleKeyDown = React.useCallback( + (e: React.KeyboardEvent) => { + if (readOnly) return; + + const keys = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]; + if (!keys.includes(e.key)) return; + + e.preventDefault(); + + const current = e.currentTarget as HTMLElement; + const rowIndex = Number(current.dataset.rowIndex); + const colIndex = Number(current.dataset.colIndex); + + let nextRow = rowIndex; + let nextCol = colIndex; + + switch (e.key) { + case "ArrowUp": + nextRow--; + break; + case "ArrowDown": + nextRow++; + break; + case "ArrowLeft": + nextCol--; + break; + case "ArrowRight": + nextCol++; + break; + } + + const nextElement = findFocusableInCell(nextRow, nextCol); + console.log(nextElement); + if (nextElement) { + focusAndSelect(nextElement); + } + }, + [readOnly] + ); + + // Bloquear foco/edición en modo texto + const handleBlock = React.useCallback((e: React.SyntheticEvent) => { + e.preventDefault(); + (e.target as HTMLInputElement).blur(); + }, []); + + if (readOnly && readOnlyMode === "textlike-input") { + return ( + e.preventDefault()} + onMouseDown={handleBlock} + readOnly + tabIndex={-1} + value={visualText} + {...inputProps} + /> + ); + } + + return ( + + ); +} diff --git a/modules/customer-invoices/src/web/proformas/ui/components/quantity-input-field.tsx b/modules/customer-invoices/src/web/proformas/ui/components/quantity-input-field.tsx new file mode 100644 index 00000000..93637554 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/ui/components/quantity-input-field.tsx @@ -0,0 +1,56 @@ +import type { CommonInputProps } from "@repo/rdx-ui/components"; +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@repo/shadcn-ui/components"; +import type { Control, FieldPath, FieldValues } from "react-hook-form"; + +import { QuantityInput, type QuantityInputProps } from "./quantity-input"; + +type QuantityInputFieldProps = CommonInputProps & { + inputId?: string; + control: Control; + name: FieldPath; + label?: string; + description?: string; + required?: boolean; +} & Omit; + +export function QuantityInputField({ + inputId, + control, + name, + label, + description, + required = false, + ...inputProps +}: QuantityInputFieldProps) { + return ( + { + const { value, onChange } = field; + + return ( + + {label ? ( + + {label} {required ? : null} + + ) : null} + + + + {description ? {description} : null} + + + ); + }} + /> + ); +} diff --git a/modules/customer-invoices/src/web/proformas/ui/components/quantity-input.tsx b/modules/customer-invoices/src/web/proformas/ui/components/quantity-input.tsx new file mode 100644 index 00000000..c04e495a --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/ui/components/quantity-input.tsx @@ -0,0 +1,269 @@ +// QuantityNumberInput.tsx — valor primitivo (number | "" | string numérica) +// Comentarios en español. TS estricto. + +import { useQuantity } from "@erp/core/hooks"; +import { Input } from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; +import * as React from "react"; + +import { + type InputEmptyMode, + type InputReadOnlyMode, + type InputSuffixMap, + findFocusableInCell, + focusAndSelect, +} from "./input-utils"; + +export type QuantityInputProps = { + value: number | "" | string; // "" → no mostrar nada; string puede venir con separadores + onChange: (next: number | "") => void; + readOnly?: boolean; + readOnlyMode?: InputReadOnlyMode; + id?: string; + "aria-label"?: string; + emptyMode?: InputEmptyMode; // cómo presentar vacío + emptyText?: string; // texto de vacío para value-mode/placeholder + scale?: number; // default 2 + locale?: string; // para plural/sufijo y formateo + className?: string; + + // Sufijo solo en visual, p.ej. {one:"caja", other:"cajas"} + displaySuffix?: InputSuffixMap | ((n: number) => string); + nbspBeforeSuffix?: boolean; // separador no rompible +}; + +export function QuantityInput({ + value, + onChange, + readOnly = false, + readOnlyMode = "textlike-input", + id, + "aria-label": ariaLabel = "Quantity", + emptyMode = "blank", + emptyText = "", + scale = 2, + locale, + className, + displaySuffix, + nbspBeforeSuffix = true, + ...inputProps +}: QuantityInputProps) { + const { parse, roundToScale } = useQuantity({ defaultScale: scale }); + const [raw, setRaw] = React.useState(""); + const [focused, setFocused] = React.useState(false); + + const plural = React.useMemo(() => new Intl.PluralRules(locale ?? undefined), [locale]); + + const suffixFor = React.useCallback( + (n: number): string => { + if (!displaySuffix) return ""; + if (typeof displaySuffix === "function") return displaySuffix(n); + const cat = plural.select(Math.abs(n)); + if (n === 0 && displaySuffix.zero) return displaySuffix.zero; + return displaySuffix[cat as "one" | "other"] ?? displaySuffix.other; + }, + [displaySuffix, plural] + ); + + const formatNumber = React.useCallback( + (n: number) => { + return new Intl.NumberFormat(locale ?? undefined, { + maximumFractionDigits: scale, + minimumFractionDigits: Number.isInteger(n) ? 0 : 0, + useGrouping: false, + }).format(n); + }, + [locale, scale] + ); + + // Derivar texto visual desde prop `value` + const visualText = React.useMemo(() => { + if (value === "" || value === null || value === undefined) { + return emptyMode === "value" ? emptyText : ""; + } + const numeric = + typeof value === "number" + ? value + : (parse(String(value)) ?? Number(String(value).replaceAll(",", ""))); // tolera string numérico + + if (!Number.isFinite(numeric)) return emptyMode === "value" ? emptyText : ""; + const n = roundToScale(numeric, scale); + const numTxt = formatNumber(n); + const suf = suffixFor(n); + return suf ? `${numTxt}${nbspBeforeSuffix ? "\u00A0" : " "}${suf}` : numTxt; + }, [ + value, + emptyMode, + emptyText, + parse, + roundToScale, + scale, + formatNumber, + suffixFor, + nbspBeforeSuffix, + ]); + + const isShowingEmptyValue = emptyMode === "value" && raw === emptyText; + + // Sin foco → mantener visual + React.useEffect(() => { + if (!focused) setRaw(visualText); + }, [visualText, focused]); + + const handleChange = React.useCallback((e: React.ChangeEvent) => { + setRaw(e.currentTarget.value); + }, []); + + const handleFocus = React.useCallback( + (e: React.FocusEvent) => { + setFocused(true); + if (emptyMode === "value" && e.currentTarget.value === emptyText) { + setRaw(""); + return; + } + const n = + parse(e.currentTarget.value) ?? + (value === "" || value == null + ? null + : typeof value === "number" + ? value + : parse(String(value))); + setRaw(n !== null && n !== undefined ? String(n) : ""); + }, + [emptyMode, emptyText, parse, value] + ); + + const handleBlur = React.useCallback( + (e: React.FocusEvent) => { + setFocused(false); + const txt = e.currentTarget.value.trim(); + + if (txt === "" || isShowingEmptyValue) { + onChange(""); + setRaw(emptyMode === "value" ? emptyText : ""); + return; + } + const n = parse(txt); + if (n === null) { + onChange(""); + setRaw(emptyMode === "value" ? emptyText : ""); + return; + } + const rounded = roundToScale(n, scale); + onChange(rounded); + const numTxt = formatNumber(rounded); + const suf = suffixFor(rounded); + setRaw(suf ? `${numTxt}${nbspBeforeSuffix ? "\u00A0" : " "}${suf}` : numTxt); + }, + [ + isShowingEmptyValue, + onChange, + emptyMode, + emptyText, + parse, + roundToScale, + scale, + formatNumber, + suffixFor, + nbspBeforeSuffix, + ] + ); + + const handleKeyDown = React.useCallback( + (e: React.KeyboardEvent) => { + if (readOnly) return; + + const keys = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]; + if (!keys.includes(e.key)) return; + + e.preventDefault(); + + const current = e.currentTarget as HTMLElement; + const rowIndex = Number(current.dataset.rowIndex); + const colIndex = Number(current.dataset.colIndex); + + let nextRow = rowIndex; + let nextCol = colIndex; + + switch (e.key) { + case "ArrowUp": + nextRow--; + break; + case "ArrowDown": + nextRow++; + break; + case "ArrowLeft": + nextCol--; + break; + case "ArrowRight": + nextCol++; + break; + } + + const nextElement = findFocusableInCell(nextRow, nextCol); + console.log(nextElement); + if (nextElement) { + focusAndSelect(nextElement); + } + }, + [readOnly] + ); + + // ── READ-ONLY como input que parece texto ─────────────────────────────── + if (readOnly && readOnlyMode === "textlike-input") { + const handleBlockFocus = React.useCallback((e: React.SyntheticEvent) => { + e.preventDefault(); + (e.target as HTMLInputElement).blur(); + }, []); + const handleBlockKey = React.useCallback((e: React.KeyboardEvent) => { + e.preventDefault(); + }, []); + + return ( + + ); + } + + // ── Editable / readOnly normal ────────────────────────────────────────── + return ( + + ); +} diff --git a/modules/customer-invoices/src/web/shared/index.ts b/modules/customer-invoices/src/web/shared/index.ts deleted file mode 100644 index 4aedf593..00000000 --- a/modules/customer-invoices/src/web/shared/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./ui"; diff --git a/modules/customer-invoices/src/web/shared/ui/components/buttons/append-empty-row-button.tsx b/modules/customer-invoices/src/web/shared/ui/components/buttons/append-empty-row-button.tsx deleted file mode 100644 index 844ea7df..00000000 --- a/modules/customer-invoices/src/web/shared/ui/components/buttons/append-empty-row-button.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Button } from "@repo/shadcn-ui/components"; -import { PlusCircleIcon } from "lucide-react"; -import { type JSX, forwardRef } from "react"; - -import { useTranslation } from "../../../../i18n"; - -export interface AppendEmptyRowButtonProps extends React.ComponentProps { - label?: string; - className?: string; -} - -export const AppendEmptyRowButton = forwardRef( - ({ label, className, ...props }: AppendEmptyRowButtonProps, ref): JSX.Element => { - const { t } = useTranslation(); - const _label = label || t("common.append_empty_row"); - - return ( - - ); - } -); - -AppendEmptyRowButton.displayName = "AppendEmptyRowButton"; diff --git a/modules/customer-invoices/src/web/shared/ui/components/buttons/index.ts b/modules/customer-invoices/src/web/shared/ui/components/buttons/index.ts deleted file mode 100644 index e5658673..00000000 --- a/modules/customer-invoices/src/web/shared/ui/components/buttons/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./append-empty-row-button"; diff --git a/modules/customer-invoices/src/web/shared/ui/components/buttons/update-commit-button-group.tsx b/modules/customer-invoices/src/web/shared/ui/components/buttons/update-commit-button-group.tsx deleted file mode 100644 index 00f1fc5c..00000000 --- a/modules/customer-invoices/src/web/shared/ui/components/buttons/update-commit-button-group.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@repo/shadcn-ui/components"; -import { cn } from "@repo/shadcn-ui/lib/utils"; -import { - ArrowLeftIcon, - CopyIcon, - EyeIcon, - MoreHorizontalIcon, - RotateCcwIcon, - Trash2Icon, -} from "lucide-react"; -import { useFormContext } from "react-hook-form"; -import { CancelFormButton, CancelFormButtonProps } from "./cancel-form-button"; -import { SubmitButtonProps, SubmitFormButton } from "./submit-form-button"; - -type Align = "start" | "center" | "end" | "between"; - -type GroupSubmitButtonProps = Omit; - -export type FormCommitButtonGroupProps = { - className?: string; - align?: Align; // default "end" - gap?: string; // default "gap-2" - reverseOrderOnMobile?: boolean; // default true (Cancel debajo en móvil) - - isLoading?: boolean; - disabled?: boolean; - preventDoubleSubmit?: boolean; // Evita múltiples submits mientras loading - - cancel?: CancelFormButtonProps & { show?: boolean }; - submit?: GroupSubmitButtonProps; // props directas a SubmitButton - - onReset?: () => void; - onDelete?: () => void; - onPreview?: () => void; - onDuplicate?: () => void; - onBack?: () => void; -}; - -const alignToJustify: Record = { - start: "justify-start", - center: "justify-center", - end: "justify-end", - between: "justify-between", -}; - -export const FormCommitButtonGroup = ({ - className, - align = "end", - gap = "gap-2", - reverseOrderOnMobile = true, - - isLoading, - disabled = false, - preventDoubleSubmit = true, - - cancel, - submit, - - onReset, - onDelete, - onPreview, - onDuplicate, - onBack, -}: FormCommitButtonGroupProps) => { - const showCancel = cancel?.show ?? true; - const hasSecondaryActions = onReset || onPreview || onDuplicate || onBack || onDelete; - - // ⛳️ RHF opcional: auto-detectar isSubmitting si no se pasó isLoading - let rhfIsSubmitting = false; - try { - const ctx = useFormContext(); - rhfIsSubmitting = !!ctx?.formState?.isSubmitting; - } catch { - // No hay provider de RHF; ignorar - } - const busy = isLoading ?? rhfIsSubmitting; - const computedDisabled = !!(disabled || (preventDoubleSubmit && busy)); - - return ( -
- {submit && } - {showCancel && } - - {/* Menú de acciones adicionales */} - {hasSecondaryActions && ( - - - - - - {onReset && ( - - - Deshacer cambios - - )} - {onPreview && ( - - - Vista previa - - )} - {onDuplicate && ( - - - Duplicar - - )} - {onBack && ( - - - Volver - - )} - {onDelete && ( - <> - - - - Eliminar - - - )} - - - )} -
- ); -}; diff --git a/modules/customer-invoices/src/web/shared/ui/components/customer-invoice-prices-card.tsx b/modules/customer-invoices/src/web/shared/ui/components/customer-invoice-prices-card.tsx deleted file mode 100644 index 916e72a2..00000000 --- a/modules/customer-invoices/src/web/shared/ui/components/customer-invoice-prices-card.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Separator, -} from "@repo/shadcn-ui/components"; -import { useFormContext } from "react-hook-form"; - -import { useTranslation } from "../../../i18n"; -import { formatCurrency } from "../../../pages/create/utils"; - -export const CustomerInvoicePricesCard = () => { - const { t } = useTranslation(); - const { register, formState, control, watch } = useFormContext(); - - /*const pricesWatch = useWatch({ control, name: ["subtotal_price", "discount", "tax"] }); - - const totals = calculateQuoteTotals(pricesWatch); - - const subtotal_price = formatNumber(totals.subtotalPrice); - const discount_price = formatNumber(totals.discountPrice); - const tax_price = formatNumber(totals.taxesPrice); - const total_price = formatNumber(totals.totalPrice);*/ - - const currency_symbol = watch("currency"); - - return ( - - - Impuestos y Totales - Configuración de impuestos y resumen de totales - - - -
-
- - {t("form_fields.subtotal_price.label")} - - - {formatCurrency(watch("subtotal_price.amount"), 2, watch("currency"))} - -
-
- -
-
- {t("form_fields.discount.label")} -
-
- - {t("form_fields.discount_price.label")} - - - {"-"} {formatCurrency(watch("discount_price.amount"), 2, watch("currency"))} - -
-
- -
-
- {t("form_fields.tax.label")} -
-
- - {t("form_fields.tax_price.label")} - - - {formatCurrency(watch("tax_price.amount"), 2, watch("currency"))} - -
-
{" "} - -
-
- - {t("form_fields.total_price.label")} - - - {formatCurrency(watch("total_price.amount"), 2, watch("currency"))} - -
-
-
-
- ); -}; diff --git a/modules/customer-invoices/src/web/shared/ui/components/data.json b/modules/customer-invoices/src/web/shared/ui/components/data.json deleted file mode 100644 index ec087364..00000000 --- a/modules/customer-invoices/src/web/shared/ui/components/data.json +++ /dev/null @@ -1,614 +0,0 @@ -[ - { - "id": 1, - "header": "Cover page", - "type": "Cover page", - "status": "In Process", - "target": "18", - "limit": "5", - "reviewer": "Eddie Lake" - }, - { - "id": 2, - "header": "Table of contents", - "type": "Table of contents", - "status": "Done", - "target": "29", - "limit": "24", - "reviewer": "Eddie Lake" - }, - { - "id": 3, - "header": "Executive summary", - "type": "Narrative", - "status": "Done", - "target": "10", - "limit": "13", - "reviewer": "Eddie Lake" - }, - { - "id": 4, - "header": "Technical approach", - "type": "Narrative", - "status": "Done", - "target": "27", - "limit": "23", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 5, - "header": "Design", - "type": "Narrative", - "status": "In Process", - "target": "2", - "limit": "16", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 6, - "header": "Capabilities", - "type": "Narrative", - "status": "In Process", - "target": "20", - "limit": "8", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 7, - "header": "Integration with existing systems", - "type": "Narrative", - "status": "In Process", - "target": "19", - "limit": "21", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 8, - "header": "Innovation and Advantages", - "type": "Narrative", - "status": "Done", - "target": "25", - "limit": "26", - "reviewer": "Assign reviewer" - }, - { - "id": 9, - "header": "Overview of EMR's Innovative Solutions", - "type": "Technical content", - "status": "Done", - "target": "7", - "limit": "23", - "reviewer": "Assign reviewer" - }, - { - "id": 10, - "header": "Advanced Algorithms and Machine Learning", - "type": "Narrative", - "status": "Done", - "target": "30", - "limit": "28", - "reviewer": "Assign reviewer" - }, - { - "id": 11, - "header": "Adaptive Communication Protocols", - "type": "Narrative", - "status": "Done", - "target": "9", - "limit": "31", - "reviewer": "Assign reviewer" - }, - { - "id": 12, - "header": "Advantages Over Current Technologies", - "type": "Narrative", - "status": "Done", - "target": "12", - "limit": "0", - "reviewer": "Assign reviewer" - }, - { - "id": 13, - "header": "Past Performance", - "type": "Narrative", - "status": "Done", - "target": "22", - "limit": "33", - "reviewer": "Assign reviewer" - }, - { - "id": 14, - "header": "Customer Feedback and Satisfaction Levels", - "type": "Narrative", - "status": "Done", - "target": "15", - "limit": "34", - "reviewer": "Assign reviewer" - }, - { - "id": 15, - "header": "Implementation Challenges and Solutions", - "type": "Narrative", - "status": "Done", - "target": "3", - "limit": "35", - "reviewer": "Assign reviewer" - }, - { - "id": 16, - "header": "Security Measures and Data Protection Policies", - "type": "Narrative", - "status": "In Process", - "target": "6", - "limit": "36", - "reviewer": "Assign reviewer" - }, - { - "id": 17, - "header": "Scalability and Future Proofing", - "type": "Narrative", - "status": "Done", - "target": "4", - "limit": "37", - "reviewer": "Assign reviewer" - }, - { - "id": 18, - "header": "Cost-Benefit Analysis", - "type": "Plain language", - "status": "Done", - "target": "14", - "limit": "38", - "reviewer": "Assign reviewer" - }, - { - "id": 19, - "header": "User Training and Onboarding Experience", - "type": "Narrative", - "status": "Done", - "target": "17", - "limit": "39", - "reviewer": "Assign reviewer" - }, - { - "id": 20, - "header": "Future Development Roadmap", - "type": "Narrative", - "status": "Done", - "target": "11", - "limit": "40", - "reviewer": "Assign reviewer" - }, - { - "id": 21, - "header": "System Architecture Overview", - "type": "Technical content", - "status": "In Process", - "target": "24", - "limit": "18", - "reviewer": "Maya Johnson" - }, - { - "id": 22, - "header": "Risk Management Plan", - "type": "Narrative", - "status": "Done", - "target": "15", - "limit": "22", - "reviewer": "Carlos Rodriguez" - }, - { - "id": 23, - "header": "Compliance Documentation", - "type": "Legal", - "status": "In Process", - "target": "31", - "limit": "27", - "reviewer": "Sarah Chen" - }, - { - "id": 24, - "header": "API Documentation", - "type": "Technical content", - "status": "Done", - "target": "8", - "limit": "12", - "reviewer": "Raj Patel" - }, - { - "id": 25, - "header": "User Interface Mockups", - "type": "Visual", - "status": "In Process", - "target": "19", - "limit": "25", - "reviewer": "Leila Ahmadi" - }, - { - "id": 26, - "header": "Database Schema", - "type": "Technical content", - "status": "Done", - "target": "22", - "limit": "20", - "reviewer": "Thomas Wilson" - }, - { - "id": 27, - "header": "Testing Methodology", - "type": "Technical content", - "status": "In Process", - "target": "17", - "limit": "14", - "reviewer": "Assign reviewer" - }, - { - "id": 28, - "header": "Deployment Strategy", - "type": "Narrative", - "status": "Done", - "target": "26", - "limit": "30", - "reviewer": "Eddie Lake" - }, - { - "id": 29, - "header": "Budget Breakdown", - "type": "Financial", - "status": "In Process", - "target": "13", - "limit": "16", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 30, - "header": "Market Analysis", - "type": "Research", - "status": "Done", - "target": "29", - "limit": "32", - "reviewer": "Sophia Martinez" - }, - { - "id": 31, - "header": "Competitor Comparison", - "type": "Research", - "status": "In Process", - "target": "21", - "limit": "19", - "reviewer": "Assign reviewer" - }, - { - "id": 32, - "header": "Maintenance Plan", - "type": "Technical content", - "status": "Done", - "target": "16", - "limit": "23", - "reviewer": "Alex Thompson" - }, - { - "id": 33, - "header": "User Personas", - "type": "Research", - "status": "In Process", - "target": "27", - "limit": "24", - "reviewer": "Nina Patel" - }, - { - "id": 34, - "header": "Accessibility Compliance", - "type": "Legal", - "status": "Done", - "target": "18", - "limit": "21", - "reviewer": "Assign reviewer" - }, - { - "id": 35, - "header": "Performance Metrics", - "type": "Technical content", - "status": "In Process", - "target": "23", - "limit": "26", - "reviewer": "David Kim" - }, - { - "id": 36, - "header": "Disaster Recovery Plan", - "type": "Technical content", - "status": "Done", - "target": "14", - "limit": "17", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 37, - "header": "Third-party Integrations", - "type": "Technical content", - "status": "In Process", - "target": "25", - "limit": "28", - "reviewer": "Eddie Lake" - }, - { - "id": 38, - "header": "User Feedback Summary", - "type": "Research", - "status": "Done", - "target": "20", - "limit": "15", - "reviewer": "Assign reviewer" - }, - { - "id": 39, - "header": "Localization Strategy", - "type": "Narrative", - "status": "In Process", - "target": "12", - "limit": "19", - "reviewer": "Maria Garcia" - }, - { - "id": 40, - "header": "Mobile Compatibility", - "type": "Technical content", - "status": "Done", - "target": "28", - "limit": "31", - "reviewer": "James Wilson" - }, - { - "id": 41, - "header": "Data Migration Plan", - "type": "Technical content", - "status": "In Process", - "target": "19", - "limit": "22", - "reviewer": "Assign reviewer" - }, - { - "id": 42, - "header": "Quality Assurance Protocols", - "type": "Technical content", - "status": "Done", - "target": "30", - "limit": "33", - "reviewer": "Priya Singh" - }, - { - "id": 43, - "header": "Stakeholder Analysis", - "type": "Research", - "status": "In Process", - "target": "11", - "limit": "14", - "reviewer": "Eddie Lake" - }, - { - "id": 44, - "header": "Environmental Impact Assessment", - "type": "Research", - "status": "Done", - "target": "24", - "limit": "27", - "reviewer": "Assign reviewer" - }, - { - "id": 45, - "header": "Intellectual Property Rights", - "type": "Legal", - "status": "In Process", - "target": "17", - "limit": "20", - "reviewer": "Sarah Johnson" - }, - { - "id": 46, - "header": "Customer Support Framework", - "type": "Narrative", - "status": "Done", - "target": "22", - "limit": "25", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 47, - "header": "Version Control Strategy", - "type": "Technical content", - "status": "In Process", - "target": "15", - "limit": "18", - "reviewer": "Assign reviewer" - }, - { - "id": 48, - "header": "Continuous Integration Pipeline", - "type": "Technical content", - "status": "Done", - "target": "26", - "limit": "29", - "reviewer": "Michael Chen" - }, - { - "id": 49, - "header": "Regulatory Compliance", - "type": "Legal", - "status": "In Process", - "target": "13", - "limit": "16", - "reviewer": "Assign reviewer" - }, - { - "id": 50, - "header": "User Authentication System", - "type": "Technical content", - "status": "Done", - "target": "28", - "limit": "31", - "reviewer": "Eddie Lake" - }, - { - "id": 51, - "header": "Data Analytics Framework", - "type": "Technical content", - "status": "In Process", - "target": "21", - "limit": "24", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 52, - "header": "Cloud Infrastructure", - "type": "Technical content", - "status": "Done", - "target": "16", - "limit": "19", - "reviewer": "Assign reviewer" - }, - { - "id": 53, - "header": "Network Security Measures", - "type": "Technical content", - "status": "In Process", - "target": "29", - "limit": "32", - "reviewer": "Lisa Wong" - }, - { - "id": 54, - "header": "Project Timeline", - "type": "Planning", - "status": "Done", - "target": "14", - "limit": "17", - "reviewer": "Eddie Lake" - }, - { - "id": 55, - "header": "Resource Allocation", - "type": "Planning", - "status": "In Process", - "target": "27", - "limit": "30", - "reviewer": "Assign reviewer" - }, - { - "id": 56, - "header": "Team Structure and Roles", - "type": "Planning", - "status": "Done", - "target": "20", - "limit": "23", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 57, - "header": "Communication Protocols", - "type": "Planning", - "status": "In Process", - "target": "15", - "limit": "18", - "reviewer": "Assign reviewer" - }, - { - "id": 58, - "header": "Success Metrics", - "type": "Planning", - "status": "Done", - "target": "30", - "limit": "33", - "reviewer": "Eddie Lake" - }, - { - "id": 59, - "header": "Internationalization Support", - "type": "Technical content", - "status": "In Process", - "target": "23", - "limit": "26", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 60, - "header": "Backup and Recovery Procedures", - "type": "Technical content", - "status": "Done", - "target": "18", - "limit": "21", - "reviewer": "Assign reviewer" - }, - { - "id": 61, - "header": "Monitoring and Alerting System", - "type": "Technical content", - "status": "In Process", - "target": "25", - "limit": "28", - "reviewer": "Daniel Park" - }, - { - "id": 62, - "header": "Code Review Guidelines", - "type": "Technical content", - "status": "Done", - "target": "12", - "limit": "15", - "reviewer": "Eddie Lake" - }, - { - "id": 63, - "header": "Documentation Standards", - "type": "Technical content", - "status": "In Process", - "target": "27", - "limit": "30", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 64, - "header": "Release Management Process", - "type": "Planning", - "status": "Done", - "target": "22", - "limit": "25", - "reviewer": "Assign reviewer" - }, - { - "id": 65, - "header": "Feature Prioritization Matrix", - "type": "Planning", - "status": "In Process", - "target": "19", - "limit": "22", - "reviewer": "Emma Davis" - }, - { - "id": 66, - "header": "Technical Debt Assessment", - "type": "Technical content", - "status": "Done", - "target": "24", - "limit": "27", - "reviewer": "Eddie Lake" - }, - { - "id": 67, - "header": "Capacity Planning", - "type": "Planning", - "status": "In Process", - "target": "21", - "limit": "24", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 68, - "header": "Service Level Agreements", - "type": "Legal", - "status": "Done", - "target": "26", - "limit": "29", - "reviewer": "Assign reviewer" - } -] diff --git a/modules/customer-invoices/src/web/shared/ui/components/editor/index.ts b/modules/customer-invoices/src/web/shared/ui/components/editor/index.ts deleted file mode 100644 index 780fb801..00000000 --- a/modules/customer-invoices/src/web/shared/ui/components/editor/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./items"; diff --git a/modules/customer-invoices/src/web/shared/ui/components/editor/invoice-notes.tsx b/modules/customer-invoices/src/web/shared/ui/components/editor/invoice-notes.tsx deleted file mode 100644 index c608f3b3..00000000 --- a/modules/customer-invoices/src/web/shared/ui/components/editor/invoice-notes.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { TextAreaField } from "@repo/rdx-ui/components"; -import { FieldDescription, FieldGroup, FieldLegend, FieldSet } from "@repo/shadcn-ui/components"; -import { StickyNoteIcon } from "lucide-react"; -import type { ComponentProps } from "react"; -import { useFormContext } from "react-hook-form"; - -import { useTranslation } from "../../../../i18n"; -import type { InvoiceFormData } from "../../../../schemas"; - -export const InvoiceNotes = (props: ComponentProps<"fieldset">) => { - const { t } = useTranslation(); - const { control } = useFormContext(); - - return ( -
- - - {t("form_groups.basic_info.title")} - - - {t("form_groups.basic_info.description")} - - - -
- ); -}; diff --git a/modules/customer-invoices/src/web/shared/ui/components/editor/invoice-totals.tsx b/modules/customer-invoices/src/web/shared/ui/components/editor/invoice-totals.tsx deleted file mode 100644 index 049b3536..00000000 --- a/modules/customer-invoices/src/web/shared/ui/components/editor/invoice-totals.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { formatCurrency } from "@erp/core"; -import { - FieldDescription, - FieldGroup, - FieldLegend, - FieldSet, - Separator, -} from "@repo/shadcn-ui/components"; -import { cn } from "@repo/shadcn-ui/lib/utils"; -import { ReceiptIcon } from "lucide-react"; -import type { ComponentProps } from "react"; -import { useFormContext, useWatch } from "react-hook-form"; - -import { useTranslation } from "../../../../i18n"; -import { useInvoiceContext } from "../../../../proformas/pages/update/context"; -import type { InvoiceFormData } from "../../../../schemas"; - -import { PercentageInputField } from "./items/percentage-input-field"; - -export const InvoiceTotals = (props: ComponentProps<"fieldset">) => { - const { t } = useTranslation(); - const { control } = useFormContext(); - const { currency_code, language_code, readOnly, taxCatalog } = useInvoiceContext(); - - const displayTaxes = useWatch({ control, name: "taxes", defaultValue: [] }); - const subtotal_amount = useWatch({ control, name: "subtotal_amount", defaultValue: 0 }); - const items_discount_amount = useWatch({ - control, - name: "items_discount_amount", - defaultValue: 0, - }); - const discount_amount = useWatch({ control, name: "discount_amount", defaultValue: 0 }); - const taxable_amount = useWatch({ control, name: "taxable_amount", defaultValue: 0 }); - const taxes_amount = useWatch({ control, name: "taxes_amount", defaultValue: 0 }); - const total_amount = useWatch({ control, name: "total_amount", defaultValue: 0 }); - - return ( -
- - - {t("form_groups.totals.title")} - - - {t("form_groups.totals.description")} - -
- {/* Sección: Subtotal y Descuentos */} -
- Subtotal sin descuentos - - {formatCurrency(subtotal_amount, 2, currency_code, language_code)} - -
- -
-
- Descuento en líneas -
- - -{formatCurrency(items_discount_amount, 2, currency_code, language_code)} - -
- -
-
- Descuento global - -
- - -{formatCurrency(discount_amount, 2, currency_code, language_code)} - -
- - {/* Sección: Base Imponible */} -
- Base imponible - - {formatCurrency(taxable_amount, 2, currency_code, language_code)} - -
-
- - - - {/* Sección: Impuestos */} -
-

- Impuestos y retenciones -

- - {taxCatalog.groups().map((group) => { - // Filtra impuestos de ese grupo - const taxesInGroup = displayTaxes?.filter((item) => { - const tax = taxCatalog.findByCode(item.tax_code).match( - (t) => t, - () => undefined - ); - return tax?.group === group; - }); - - // Si el grupo no tiene impuestos, no renderiza nada - if (taxesInGroup?.length === 0) return null; - - return ( -
- {taxesInGroup?.map((item) => { - const tax = taxCatalog.findByCode(item.tax_code).match( - (t) => t, - () => undefined - ); - return ( -
- {tax?.name} - - {formatCurrency(item.taxes_amount, 2, currency_code, language_code)} - -
- ); - })} -
- ); - })} - -
- Total de impuestos - - {formatCurrency(taxes_amount, 2, currency_code, language_code)} - -
-
- - - -
- Total de la factura - - {formatCurrency(total_amount, 2, currency_code, language_code)} - -
-
-
- ); -}; diff --git a/modules/customer-invoices/src/web/shared/ui/components/editor/items/amount-input-field.tsx b/modules/customer-invoices/src/web/shared/ui/components/editor/items/amount-input-field.tsx deleted file mode 100644 index a2b1ae1a..00000000 --- a/modules/customer-invoices/src/web/shared/ui/components/editor/items/amount-input-field.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { - FormControl, FormDescription, - FormField, FormItem, FormLabel, FormMessage, -} from "@repo/shadcn-ui/components"; -import { Control, FieldPath, FieldValues } from "react-hook-form"; -import { AmountInput, AmountInputProps } from './amount-input'; - - -type AmountInputFieldProps = { - inputId?: string; - control: Control; - name: FieldPath; - label?: string; - description?: string; - required?: boolean; -} & Omit - -export function AmountInputField({ - inputId, - control, - name, - label, - description, - required = false, - ...inputProps - -}: AmountInputFieldProps) { - return ( - ( - - {label ? ( - - {label} {required ? : null} - - ) : null} - - - - {description ? {description} : null} - - - )} - /> - ); -} \ No newline at end of file diff --git a/modules/customer-invoices/src/web/shared/ui/components/editor/items/amount-input.tsx b/modules/customer-invoices/src/web/shared/ui/components/editor/items/amount-input.tsx deleted file mode 100644 index a933e4ba..00000000 --- a/modules/customer-invoices/src/web/shared/ui/components/editor/items/amount-input.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { formatCurrency } from '@erp/core'; -import { useMoney } from '@erp/core/hooks'; -import { Input } from '@repo/shadcn-ui/components'; -import { cn } from '@repo/shadcn-ui/lib/utils'; -import * as React from "react"; -import { findFocusableInCell, focusAndSelect } from './input-utils'; -import { InputEmptyMode, InputReadOnlyMode } from './quantity-input'; - - -export type AmountInputProps = { - value: number | string; // "" → no mostrar nada; string puede venir con separadores - onChange: (next: number | string) => void; - readOnly?: boolean; - readOnlyMode?: InputReadOnlyMode; // default "textlike-input" - id?: string; - "aria-label"?: string; - step?: number; // ↑/↓; default 0.01 - emptyMode?: InputEmptyMode; // cómo presentar vacío - emptyText?: string; // texto en vacío para value/placeholder - scale?: number; // decimales; default 2 (ej. 4 para unit_amount) - languageCode?: string; // p.ej. "es-ES" - currencyCode?: string; // p.ej. "EUR" - className?: string; -}; - -export function AmountInput({ - value, - onChange, - readOnly = false, - readOnlyMode = "textlike-input", - id, - "aria-label": ariaLabel = "Amount", - emptyMode = "blank", - emptyText = "", - scale = 2, - languageCode = 'es', - currencyCode = "EUR", - className, - ...inputProps -}: AmountInputProps) { - - // Hook de dinero para parseo/redondeo consistente con el resto de la app - const { parse, roundToScale } = useMoney({ locale: languageCode, fallbackCurrency: currencyCode as any }); - - const [raw, setRaw] = React.useState(""); - const [focused, setFocused] = React.useState(false); - - const formatCurrencyNumber = React.useCallback( - (n: number) => formatCurrency(n, scale, currencyCode, languageCode), - [languageCode, currencyCode, scale] - ); - - // Derivar texto visual desde prop `value` - const visualText = React.useMemo(() => { - if (value === "" || value == null) { - return emptyMode === "value" ? emptyText : ""; - } - const numeric = - typeof value === "number" - ? value - : (parse(String(value)) ?? Number(String(value).replace(/[^\d.,\-]/g, "").replace(/\./g, "").replace(",", "."))); - if (!Number.isFinite(numeric)) return emptyMode === "value" ? emptyText : ""; - const n = roundToScale(numeric, scale); - return formatCurrencyNumber(n); - }, [value, emptyMode, emptyText, parse, roundToScale, scale, formatCurrencyNumber]); - - const isShowingEmptyValue = emptyMode === "value" && raw === emptyText; - - // Sin foco → mantener visual - React.useEffect(() => { - if (!focused) setRaw(visualText); - }, [visualText, focused]); - - const handleChange = React.useCallback( - (e: React.ChangeEvent) => { - setRaw(e.currentTarget.value); - }, - [] - ); - - const handleFocus = React.useCallback( - (e: React.FocusEvent) => { - setFocused(true); - // pasar de visual con símbolo → crudo - if (emptyMode === "value" && e.currentTarget.value === emptyText) { - setRaw(""); - return; - } - const current = - parse(e.currentTarget.value) ?? - (value === "" || value == null - ? null - : typeof value === "number" - ? value - : parse(String(value))); - setRaw(current !== null && current !== undefined ? String(current) : ""); - }, - [emptyMode, emptyText, parse, value] - ); - - const handleBlur = React.useCallback( - (e: React.FocusEvent) => { - setFocused(false); - const txt = e.currentTarget.value.trim(); - if (txt === "" || isShowingEmptyValue) { - onChange(""); - setRaw(emptyMode === "value" ? emptyText : ""); - return; - } - const n = parse(txt); - if (n === null) { - onChange(""); - setRaw(emptyMode === "value" ? emptyText : ""); - return; - } - const rounded = roundToScale(n, scale); - onChange(rounded); - setRaw(formatCurrencyNumber(rounded)); // vuelve a visual con símbolo - }, - [isShowingEmptyValue, onChange, emptyMode, emptyText, parse, roundToScale, scale, formatCurrencyNumber] - ); - - const handleKeyDown = React.useCallback( - (e: React.KeyboardEvent) => { - if (readOnly) return; - - const keys = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]; - if (!keys.includes(e.key)) return; - - e.preventDefault(); - - const current = e.currentTarget as HTMLElement; - const rowIndex = Number(current.dataset.rowIndex); - const colIndex = Number(current.dataset.colIndex); - - let nextRow = rowIndex; - let nextCol = colIndex; - - switch (e.key) { - case "ArrowUp": - nextRow--; - break; - case "ArrowDown": - nextRow++; - break; - case "ArrowLeft": - nextCol--; - break; - case "ArrowRight": - nextCol++; - break; - } - - const nextElement = findFocusableInCell(nextRow, nextCol); - console.log(nextElement); - if (nextElement) { - focusAndSelect(nextElement); - } - }, - [readOnly] - ); - - const handleBlock = React.useCallback((e: React.SyntheticEvent) => { - e.preventDefault(); - (e.target as HTMLInputElement).blur(); - }, []); - - if (readOnly && readOnlyMode === "textlike-input") { - return ( - e.preventDefault()} - value={visualText} - className={cn( - "w-full bg-transparent p-0 text-right tabular-nums border-0 shadow-none", - "focus:outline-none focus:ring-0 [caret-color:transparent] cursor-default", - className - )} - {...inputProps} - /> - ); - } - - return ( - - ); -} diff --git a/modules/customer-invoices/src/web/shared/ui/components/editor/items/blocks-view.tsx b/modules/customer-invoices/src/web/shared/ui/components/editor/items/blocks-view.tsx deleted file mode 100644 index e5bb3ed3..00000000 --- a/modules/customer-invoices/src/web/shared/ui/components/editor/items/blocks-view.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { Badge, Button, Input, Label } from "@repo/shadcn-ui/components"; -import { Trash2 } from "lucide-react"; -import { useFormContext } from "react-hook-form"; - -import { useTranslation } from "../../../../../i18n"; -import type { InvoiceFormData } from "../../../../../schemas"; -import { ProformaTaxesMultiSelect } from "../../proforma-taxes-multi-select"; - -import type { CustomItemViewProps } from "./types"; - -export interface BlocksViewProps extends CustomItemViewProps {} - -export const BlocksView = ({ items, removeItem, updateItem }: BlocksViewProps) => { - const { t } = useTranslation(); - const { control } = useFormContext(); - - return ( -
- {items.map((item: any, index: number) => ( -
-
- - Línea {item.position} - - {items.length > 1 && ( - - )} -
- -
-
- - updateItem(index, "description", e.target.value)} - placeholder="Descripción del producto o servicio..." - value={item.description} - /> -
- -
- - - updateItem(index, "quantity", Number.parseFloat(e.target.value) || 0) - } - step="0.01" - type="number" - value={item.quantity} - /> -
- -
- - - updateItem(index, "unit_amount", Number.parseFloat(e.target.value) || 0) - } - step="0.0001" - type="number" - value={item.unit_amount} - /> -
- -
- - - updateItem(index, "discount_percentage", Number.parseFloat(e.target.value) || 0) - } - step="0.0001" - type="number" - value={item.discount_percentage} - /> -
- -
- - - {/* - - - */} -
-
- - {/* Calculated amounts */} -
-
-
- -

{formatCurrency(item.subtotal_amount)}

-
-
- -

{formatCurrency(item.discount_amount)}

-
-
- -

{formatCurrency(item.taxable_amount)}

-
-
- -

{formatCurrency(item.taxes_amount)}

-
-
- -

{formatCurrency(item.total_amount)}

-
-
-
-
- ))} -
- ); -}; diff --git a/modules/customer-invoices/src/web/shared/ui/components/editor/items/debug-id-col.tsx b/modules/customer-invoices/src/web/shared/ui/components/editor/items/debug-id-col.tsx deleted file mode 100644 index 38104467..00000000 --- a/modules/customer-invoices/src/web/shared/ui/components/editor/items/debug-id-col.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { ColumnDef } from '@tanstack/react-table'; - -// columna de depuración: muestra el row.id interno de TanStack -export const debugIdCol: ColumnDef = ({ - id: "__debug_row_id", - header: () => row.id, - cell: ({ row }) => ( - - {row.id}
-
- ), - enableSorting: false, - enableHiding: false, // ponlo en true si quieres que sea ocultable en ViewOptions - size: 160, - minSize: 120, - maxSize: 260, -}); diff --git a/modules/customer-invoices/src/web/shared/ui/components/editor/items/hover-card-total-summary.tsx b/modules/customer-invoices/src/web/shared/ui/components/editor/items/hover-card-total-summary.tsx deleted file mode 100644 index 93c270d0..00000000 --- a/modules/customer-invoices/src/web/shared/ui/components/editor/items/hover-card-total-summary.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { formatCurrency } from "@erp/core"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, - HoverCard, - HoverCardContent, - HoverCardTrigger, -} from "@repo/shadcn-ui/components"; -import type { PropsWithChildren } from "react"; -import { useFormContext, useWatch } from "react-hook-form"; - -import { useTranslation } from "../../../../../i18n"; -import { useInvoiceContext } from "../../../../../proformas/pages/update/context"; - -type HoverCardTotalsSummaryProps = PropsWithChildren & { - rowIndex: number; -}; - -/** - * Muestra un desglose financiero del total de línea. - * Lee directamente los importes del formulario vía react-hook-form. - */ - -// Aparcado por ahora - -export const HoverCardTotalsSummary = ({ children, rowIndex }: HoverCardTotalsSummaryProps) => ( - <>{children} -); - -const HoverCardTotalsSummary2 = ({ children, rowIndex }: HoverCardTotalsSummaryProps) => { - const { t } = useTranslation(); - const { control } = useFormContext(); - const { currency_code, language_code } = useInvoiceContext(); - - // Observar los valores actuales del formulario - const [subtotal, discountPercentage, discountAmount, taxableBase, total] = useWatch({ - control, - name: [ - `items.${rowIndex}.subtotal_amount`, - `items.${rowIndex}.discount_percentage`, - `items.${rowIndex}.discount_amount`, - `items.${rowIndex}.taxable_base`, - `items.${rowIndex}.total_amount`, - ], - }); - - const SummaryBlock = () => ( -
-

- {t("components.hover_card_totals_summary.label")} -

- - {/* Subtotal */} -
- - {t("components.hover_card_totals_summary.fields.subtotal_amount")}: - - - {formatCurrency(subtotal, 4, currency_code, language_code)} - -
- - {/* Descuento (si aplica) */} - {discountPercentage && Number(discountPercentage.value) > 0 && ( -
- - {t("components.hover_card_totals_summary.fields.discount_percentage")} ( - {discountPercentage && discountPercentage.value - ? (Number(discountPercentage.value) / 10 ** Number(discountPercentage.scale)) * 100 - : 0} - %): - - - -{formatCurrency(discountAmount, 4, currency_code, language_code)} - -
- )} - - {/* Base imponible */} -
- - {t("components.hover_card_totals_summary.fields.taxable_amount")}: - - - {formatCurrency(taxableBase, 4, currency_code, language_code)} - -
- - {/* Total final */} -
- {t("components.hover_card_totals_summary.fields.total_amount")}: - - {formatCurrency(total, 4, currency_code, language_code)} - -
-
- ); - - return ( - <> - {/* Variante móvil (Dialog) */} -
- - {children} - - - {t("components.hover_card_totals_summary.label")} - - - - -
- - {/* Variante escritorio (HoverCard) */} -
- - {children} - - - - -
- - ); -}; diff --git a/modules/customer-invoices/src/web/shared/ui/components/editor/items/index.ts b/modules/customer-invoices/src/web/shared/ui/components/editor/items/index.ts deleted file mode 100644 index a982297c..00000000 --- a/modules/customer-invoices/src/web/shared/ui/components/editor/items/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./percentage-input-field"; diff --git a/modules/customer-invoices/src/web/shared/ui/components/editor/items/item-row.tsx b/modules/customer-invoices/src/web/shared/ui/components/editor/items/item-row.tsx deleted file mode 100644 index 1fe5881a..00000000 --- a/modules/customer-invoices/src/web/shared/ui/components/editor/items/item-row.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import { - Button, - Checkbox, - TableCell, - TableRow, - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@repo/shadcn-ui/components"; -import { cn } from "@repo/shadcn-ui/lib/utils"; -import { ArrowDownIcon, ArrowUpIcon, CopyIcon, Trash2Icon } from "lucide-react"; -import { type Control, Controller, type FieldValues } from "react-hook-form"; - -import { useTranslation } from "../../../../../i18n"; -import { useInvoiceContext } from "../../../../../proformas/pages/update/context"; -import { ProformaTaxesMultiSelect } from "../../proforma-taxes-multi-select"; - -import { AmountInputField } from "./amount-input-field"; -import { HoverCardTotalsSummary } from "./hover-card-total-summary"; -import { PercentageInputField } from "./percentage-input-field"; -import { QuantityInputField } from "./quantity-input-field"; - -export type ItemRowProps = { - control: Control; - rowIndex: number; - isSelected: boolean; - isFirst: boolean; - isLast: boolean; - readOnly: boolean; - onToggleSelect: () => void; - onDuplicate: () => void; - onMoveUp: () => void; - onMoveDown: () => void; - onRemove: () => void; -}; - -export const ItemRow = ({ - control, - rowIndex, - isSelected, - isFirst, - isLast, - readOnly, - onToggleSelect, - onDuplicate, - onMoveUp, - onMoveDown, - onRemove, -}: ItemRowProps) => { - const { t } = useTranslation(); - const { currency_code, language_code } = useInvoiceContext(); - - return ( - - {/* selección */} - -
- -
-
- - {/* # */} - - - {rowIndex + 1} - - - - {/* description */} - - ( -