From 99c161cb31737556385fc819677c3d399a86a072 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 13 Nov 2025 12:49:36 +0100 Subject: [PATCH] . --- modules/core/src/common/locales/en.json | 6 + modules/core/src/common/locales/es.json | 6 + .../components/form/simple-search-input.tsx | 49 +-- .../lib/data-source/datasource.interface.ts | 4 +- .../customer-invoice-application.service.ts | 28 +- .../proformas/list-proformas.use-case.ts | 2 +- .../customer-invoice-repository.interface.ts | 8 +- .../express/issue-invoices.routes.ts | 6 +- .../queries/customer-invoice.list.mapper.ts | 19 +- .../sequelize/customer-invoice.repository.ts | 23 +- .../src/common/locales/en.json | 118 +++---- .../src/common/locales/es.json | 112 ++++--- .../src/web/customer-invoice-routes.tsx | 10 +- .../use-create-customer-invoice-mutation.ts | 16 +- .../src/web/hooks/use-invoice-query.ts | 9 +- .../use-update-customer-invoice-mutation.ts | 15 +- .../src/web/issue-invoices/hooks/index.ts | 2 + .../hooks/use-issue-invoice-query.ts | 49 +++ .../hooks/use-issue-invoices-query.ts | 41 +++ .../src/web/issue-invoices/index.ts | 1 + .../issue-invoice-resume-dto.adapter.ts | 71 ++++ .../issue-invoice-resume.form.schema.ts | 25 ++ .../issue-invoice.api.schema.ts | 23 ++ modules/customer-invoices/src/web/manifest.ts | 3 +- .../create/create-customer-invoice-page.tsx | 20 +- .../pages/create/customer-invoice.schema.ts | 5 +- .../src/web/pages/list/invoices-list-grid.tsx | 61 ++-- .../src/web/pages/list/invoices-list-page.tsx | 51 ++- .../pages/list/use-invoices-list-columns.tsx | 233 ------------- .../web/pages/update/invoice-update-comp.tsx | 199 +++++------ .../src/web/proformas/hooks/index.ts | 1 + .../web/proformas/hooks/use-proforma-query.ts | 49 +++ .../proformas/hooks/use-proformas-query.ts | 59 ++-- .../pages/list/proforma-list-page.tsx | 49 +-- .../proformas/pages/list/proformas-grid.tsx | 196 +++++++++++ .../pages/list/use-proformas-grid-columns.tsx | 314 +++++++++++++++++ .../proformas/proforma-resume-dto.adapter.ts | 68 ++++ .../proformas/proforma-resume.form.schema.ts | 25 ++ .../src/web/proformas/proforma.api.schema.ts | 31 ++ .../src/web/schemas/invoices.api.schema.ts | 30 +- .../web/components/customer-status-badge.tsx | 68 ++-- modules/customers/src/web/components/index.ts | 1 + .../pages/list/use-customers-list-columns.tsx | 316 +++++++++--------- .../src/criteria-to-sequelize-converter.ts | 18 +- packages/rdx-criteria/src/utils.ts | 2 +- .../src/components/layout/app-sidebar.tsx | 61 ++-- packages/shadcn-ui/src/styles/globals.css | 2 +- 47 files changed, 1613 insertions(+), 892 deletions(-) create mode 100644 modules/customer-invoices/src/web/issue-invoices/hooks/index.ts create mode 100644 modules/customer-invoices/src/web/issue-invoices/hooks/use-issue-invoice-query.ts create mode 100644 modules/customer-invoices/src/web/issue-invoices/hooks/use-issue-invoices-query.ts create mode 100644 modules/customer-invoices/src/web/issue-invoices/index.ts create mode 100644 modules/customer-invoices/src/web/issue-invoices/issue-invoice-resume-dto.adapter.ts create mode 100644 modules/customer-invoices/src/web/issue-invoices/issue-invoice-resume.form.schema.ts create mode 100644 modules/customer-invoices/src/web/issue-invoices/issue-invoice.api.schema.ts delete mode 100644 modules/customer-invoices/src/web/pages/list/use-invoices-list-columns.tsx create mode 100644 modules/customer-invoices/src/web/proformas/hooks/use-proforma-query.ts create mode 100644 modules/customer-invoices/src/web/proformas/pages/list/proformas-grid.tsx create mode 100644 modules/customer-invoices/src/web/proformas/pages/list/use-proformas-grid-columns.tsx create mode 100644 modules/customer-invoices/src/web/proformas/proforma-resume-dto.adapter.ts create mode 100644 modules/customer-invoices/src/web/proformas/proforma-resume.form.schema.ts create mode 100644 modules/customer-invoices/src/web/proformas/proforma.api.schema.ts diff --git a/modules/core/src/common/locales/en.json b/modules/core/src/common/locales/en.json index 2737b410..757108a3 100644 --- a/modules/core/src/common/locales/en.json +++ b/modules/core/src/common/locales/en.json @@ -11,6 +11,12 @@ "placeholder": "Select taxes", "description": "Select the taxes to apply to the invoice items", "invalid_tax_selection": "Invalid tax selection. Please select a valid tax." + }, + "simple_search_input": { + "search_button": "Search", + "loading": "Loading", + "clear_search": "Clear search", + "search_placeholder": "Search..." } }, "hooks": { diff --git a/modules/core/src/common/locales/es.json b/modules/core/src/common/locales/es.json index 0f6be616..0584a86a 100644 --- a/modules/core/src/common/locales/es.json +++ b/modules/core/src/common/locales/es.json @@ -10,6 +10,12 @@ "placeholder": "Seleccionar impuestos", "description": "Seleccionar los impuestos a aplicar a los artículos de la factura", "invalid_tax_selection": "Selección de impuestos no válida. Por favor, seleccione un impuesto válido." + }, + "simple_search_input": { + "search_button": "Buscar", + "loading": "Buscando", + "clear_search": "Limpiar búsquedaClear search", + "search_placeholder": "Escribe aquí para buscar..." } }, "hooks": { diff --git a/modules/core/src/web/components/form/simple-search-input.tsx b/modules/core/src/web/components/form/simple-search-input.tsx index 872d00e1..1d7007ce 100644 --- a/modules/core/src/web/components/form/simple-search-input.tsx +++ b/modules/core/src/web/components/form/simple-search-input.tsx @@ -8,7 +8,8 @@ import { import { Spinner } from "@repo/shadcn-ui/components/spinner"; import { SearchIcon, XIcon } from "lucide-react"; import { useEffect, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; + +import { useTranslation } from "../../i18n"; type SimpleSearchInputProps = { onSearchChange: (value: string) => void; @@ -103,44 +104,48 @@ export const SimpleSearchInput = ({ }; return ( -
- +
+ history.length > 0 && setOpen(true)} + onKeyDown={handleKeyDown} + placeholder={t("components.simple_search_input.search_placeholder", "Search...")} + ref={inputRef} + spellCheck={false} + value={searchValue} /> - - {loading && } - {!searchValue && !loading && ( + + {loading && ( + + )} + {!(searchValue || loading) && ( onSearchChange(searchValue)} + variant="secondary" > - {t("common.search", "Search")} + {t("components.simple_search_input.search_button", "Search")} )} {searchValue && !loading && ( - - {t("common.clear", "Clear")} + + + {t("components.simple_search_input.clear_search", "Clear")} + )} diff --git a/modules/core/src/web/lib/data-source/datasource.interface.ts b/modules/core/src/web/lib/data-source/datasource.interface.ts index 4b8013f1..aeb6a3f8 100644 --- a/modules/core/src/web/lib/data-source/datasource.interface.ts +++ b/modules/core/src/web/lib/data-source/datasource.interface.ts @@ -1,4 +1,4 @@ -import { ResponseType } from "axios"; +import type { ResponseType } from "axios"; export interface ICustomParams { url?: string; @@ -7,7 +7,7 @@ export interface ICustomParams { signal?: AbortSignal; responseType?: ResponseType; headers?: { - [key: string]: any; + [key: string]: unknown; }; [key: string]: unknown; } 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 4c972141..df04399a 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 @@ -175,6 +175,26 @@ export class CustomerInvoiceApplicationService { }); } + /** + * Obtiene una colección de proformas que cumplen con los filtros definidos en un objeto Criteria. + * + * @param companyId - Identificador de la empresa a la que pertenece la factura. + * @param criteria - Objeto con condiciones de filtro, paginación y orden. + * @param transaction - Transacción activa para la operación. + * @returns Result, Error> - Colección de proformas o error. + */ + async findProformasByCriteriaInCompany( + companyId: UniqueID, + criteria: Criteria, + transaction?: Transaction + ): Promise, Error>> { + return this.repository.findByCriteriaInCompany(companyId, criteria, transaction, { + where: { + is_proforma: true, + }, + }); + } + /** * Obtiene una colección de facturas que cumplen con los filtros definidos en un objeto Criteria. * @@ -183,12 +203,16 @@ export class CustomerInvoiceApplicationService { * @param transaction - Transacción activa para la operación. * @returns Result, Error> - Colección de facturas o error. */ - async findInvoiceByCriteriaInCompany( + async findIssueInvoiceByCriteriaInCompany( companyId: UniqueID, criteria: Criteria, transaction?: Transaction ): Promise, Error>> { - return this.repository.findByCriteriaInCompany(companyId, criteria, transaction); + return this.repository.findByCriteriaInCompany(companyId, criteria, transaction, { + where: { + is_proforma: false, + }, + }); } /** 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 58c332fd..600bf4df 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 @@ -31,7 +31,7 @@ export class ListProformasUseCase { return this.transactionManager.complete(async (transaction: Transaction) => { try { - const result = await this.service.findInvoiceByCriteriaInCompany( + const result = await this.service.findProformasByCriteriaInCompany( companyId, criteria, transaction 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 8462f5f1..e04b147d 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,7 +41,7 @@ export interface ICustomerInvoiceRepository { ): Promise>; /** - * Recupera una factura por su ID y companyId. + * Recupera una factura/proforma por su ID y companyId. * Devuelve un `NotFoundError` si no se encuentra. */ getByIdInCompany( @@ -53,13 +53,14 @@ export interface ICustomerInvoiceRepository { /** * - * Consulta facturas dentro de una empresa usando un + * Consulta facturas/proformas 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 @@ -67,7 +68,8 @@ export interface ICustomerInvoiceRepository { findByCriteriaInCompany( companyId: UniqueID, criteria: Criteria, - transaction: unknown + transaction: unknown, + options: unknown ): Promise, Error>>; /** diff --git a/modules/customer-invoices/src/api/infrastructure/express/issue-invoices.routes.ts b/modules/customer-invoices/src/api/infrastructure/express/issue-invoices.routes.ts index c49f8184..9cb369c8 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/issue-invoices.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/issue-invoices.routes.ts @@ -51,7 +51,7 @@ export const issueInvoicesRouter = (params: ModuleParams) => { //checkTabContext, validateRequest(ListIssueInvoicesRequestSchema, "params"), async (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.list(); + const useCase = deps.useCases.list_proformas(); const controller = new ListProformasController(useCase /*, deps.presenters.list */); return controller.execute(req, res, next); } @@ -62,7 +62,7 @@ export const issueInvoicesRouter = (params: ModuleParams) => { //checkTabContext, validateRequest(GetIssueInvoiceByIdRequestSchema, "params"), (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.get(); + const useCase = deps.useCases.get_proforma(); const controller = new GetProformaController(useCase); return controller.execute(req, res, next); } @@ -73,7 +73,7 @@ export const issueInvoicesRouter = (params: ModuleParams) => { //checkTabContext, validateRequest(ReportIssueInvoiceByIdRequestSchema, "params"), (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.report(); + const useCase = deps.useCases.report_proforma(); const controller = new ReportProformaController(useCase); return controller.execute(req, res, next); } 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 9780d7e1..b72d2e65 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 @@ -1,25 +1,30 @@ -import { ISequelizeQueryMapper, MapperParamsType, SequelizeQueryMapper } from "@erp/core/api"; +import { + type ISequelizeQueryMapper, + type MapperParamsType, + SequelizeQueryMapper, +} from "@erp/core/api"; import { CurrencyCode, - extractOrPushError, LanguageCode, - maybeFromNullableVO, Percentage, UniqueID, UtcDate, ValidationErrorCollection, - ValidationErrorDetail, + type ValidationErrorDetail, + extractOrPushError, + maybeFromNullableVO, } from "@repo/rdx-ddd"; +import { type Maybe, Result } from "@repo/rdx-utils"; -import { Maybe, Result } from "@repo/rdx-utils"; import { CustomerInvoiceNumber, CustomerInvoiceSerie, CustomerInvoiceStatus, InvoiceAmount, - InvoiceRecipient, + type InvoiceRecipient, } from "../../../domain"; -import { CustomerInvoiceModel } from "../../sequelize"; +import type { CustomerInvoiceModel } from "../../sequelize"; + import { InvoiceRecipientListMapper } from "./invoice-recipient.list.mapper"; export type CustomerInvoiceListDTO = { 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 9c4421ab..a3c1bdb9 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 @@ -7,7 +7,7 @@ import { import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; import type { UniqueID } from "@repo/rdx-ddd"; import { type Collection, Result } from "@repo/rdx-utils"; -import type { FindOptions, InferAttributes, Transaction } from "sequelize"; +import type { FindOptions, InferAttributes, OrderItem, Transaction } from "sequelize"; import type { CustomerInvoice, @@ -293,7 +293,8 @@ export class CustomerInvoiceRepository public async findByCriteriaInCompany( companyId: UniqueID, criteria: Criteria, - transaction: Transaction + transaction: Transaction, + options: FindOptions> = {} ): Promise, Error>> { const { CustomerModel } = this._database.models; @@ -315,13 +316,30 @@ export class CustomerInvoiceRepository 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, company_id: companyId.toString(), deleted_at: null, + ...(options.where ?? {}), }; + query.order = [...(query.order as OrderItem[]), ...normalizedOrder]; + query.include = [ + ...normalizedInclude, { model: CustomerModel, as: "current_customer", @@ -338,7 +356,6 @@ export class CustomerInvoiceRepository "country", ], }, - { model: CustomerInvoiceTaxModel, as: "taxes", diff --git a/modules/customer-invoices/src/common/locales/en.json b/modules/customer-invoices/src/common/locales/en.json index 62b122a3..0807031e 100644 --- a/modules/customer-invoices/src/common/locales/en.json +++ b/modules/customer-invoices/src/common/locales/en.json @@ -31,61 +31,63 @@ } }, "pages": { - "title": "Customer invoices", - "description": "Manage your customer invoices", - "list": { - "title": "Customer invoices", - "description": "List all customer invoices", - "grid_columns": { - "invoice_number": "Inv. number", - "series": "Serie", - "status": "Status", - "invoice_date": "Invoice date", - "operation_date": "Operation date", - "recipient_tin": "TIN", - "recipient_name": "Customer name", - "recipient_street": "Street", - "recipient_city": "City", - "recipient_province": "Province", - "recipient_postal_code": "Postal code", - "recipient_country": "Country", - "total_amount": "Total price" + "proformas": { + "title": "Proformas", + "description": "Manage your customer proformas", + "list": { + "title": "Customer proformas", + "description": "List all customer proformas", + "grid_columns": { + "invoice_number": "Inv. number", + "series": "Serie", + "status": "Status", + "invoice_date": "Proforma date", + "operation_date": "Operation date", + "recipient_tin": "TIN", + "recipient_name": "Customer name", + "recipient_street": "Street", + "recipient_city": "City", + "recipient_province": "Province", + "recipient_postal_code": "Postal code", + "recipient_country": "Country", + "total_amount": "Total price" + } + }, + "create": { + "title": "New customer proforma", + "description": "Create a new customer proforma", + "back_to_list": "Back to the list" + }, + "edit": { + "title": "Edit customer proforma", + "description": "Edit the selected customer proforma" + }, + "delete": { + "title": "Delete customer proforma", + "description": "Delete the selected customer proforma" + }, + "view": { + "title": "View customer proforma", + "description": "View the details of the selected customer proforma" } - }, - "create": { - "title": "New customer invoice", - "description": "Create a new customer invoice", - "back_to_list": "Back to the list" - }, - "edit": { - "title": "Edit customer invoice", - "description": "Edit the selected customer invoice" - }, - "delete": { - "title": "Delete customer invoice", - "description": "Delete the selected customer invoice" - }, - "view": { - "title": "View customer invoice", - "description": "View the details of the selected customer invoice" } }, "form_groups": { "customer": { "title": "Customer", - "description": "Select the customer for this invoice." + "description": "Select the customer for this proforma." }, "items": { - "title": "Invoice details", + "title": "Proforma details", "description": "" }, "basic_info": { - "title": "Invoice information", - "description": "Basic invoice information" + "title": "Proforma information", + "description": "Basic proforma information" }, "totals": { - "title": "Invoice totals", - "description": "Breakdown of invoice amounts with discounts and taxes." + "title": "Proforma totals", + "description": "Breakdown of proforma amounts with discounts and taxes." }, "tax_resume": { "title": "Resumen de impuestos", @@ -93,7 +95,7 @@ }, "preferences": { "title": "Preferences", - "description": "Additional invoice settings" + "description": "Additional proforma settings" } }, "form_fields": { @@ -103,39 +105,39 @@ "description": "" }, "invoice_number": { - "label": "Invoice number", + "label": "Proforma number", "placeholder": "", "description": "" }, "invoice_date": { - "label": "Invoice date", + "label": "Proforma date", "placeholder": "Select a date", - "description": "Invoice date" + "description": "Proforma date" }, "series": { "label": "Serie", "placeholder": "", - "description": "Invoice serie" + "description": "Proforma serie" }, "operation_date": { "label": "Operation date", "placeholder": "Select a date", - "description": "Invoice operation date" + "description": "Proforma operation date" }, "reference": { "label": "Reference", - "placeholder": "Reference of the invoice", - "description": "Reference of the invoice" + "placeholder": "Reference of the proforma", + "description": "Reference of the proforma" }, "description": { "label": "Description", - "placeholder": "Description of the invoice", - "description": "General description of the invoice" + "placeholder": "Description of the proforma", + "description": "General description of the proforma" }, "subtotal_amount": { "label": "Subtotal", "placeholder": "", - "desc": "Invoice subtotal" + "desc": "Proforma subtotal" }, "discount": { "label": "Discount (%)", @@ -150,12 +152,12 @@ "total_amount": { "label": "Total price", "placeholder": "", - "desc": "Invoice total price" + "desc": "Proforma total price" }, "notes": { "label": "Notes", - "placeholder": "Additional notes about the invoice", - "description": "Additional notes that can be included in the invoice" + "placeholder": "Additional notes about the proforma", + "description": "Additional notes that can be included in the proforma" }, "item": { "quantity": { @@ -201,7 +203,7 @@ "total_amount": { "label": "Total amount", "placeholder": "", - "description": "Invoice line total" + "description": "Proforma line total" } } }, @@ -212,7 +214,7 @@ "customer_invoice_taxes_multi_select": { "label": "Taxes", "placeholder": "Select taxes", - "description": "Select the taxes to apply to the invoice items", + "description": "Select the taxes to apply to the proforma items", "invalid_tax_selection": "Invalid tax selection. Please select a valid tax." }, "hover_card_totals_summary": { diff --git a/modules/customer-invoices/src/common/locales/es.json b/modules/customer-invoices/src/common/locales/es.json index d7fa847f..5d4b3487 100644 --- a/modules/customer-invoices/src/common/locales/es.json +++ b/modules/customer-invoices/src/common/locales/es.json @@ -30,78 +30,80 @@ } }, "pages": { - "title": "Facturas de clientes", - "description": "Gestiona tus facturas de clientes", - "list": { - "title": "Facturas de clientes", - "description": "Lista todas las facturas de clientes", - "grid_columns": { - "invoice_number": "Nº factura", - "series": "Serie", - "status": "Estado", - "invoice_date": "Fecha de factura", - "operation_date": "Fecha de operación", - "recipient_tin": "NIF/CIF", - "recipient_name": "Cliente", - "recipient_street": "Dirección", - "recipient_city": "Ciudad", - "recipient_province": "Provincia", - "recipient_postal_code": "Código postal", - "recipient_country": "País", - "total_amount": "Importe total" + "proformas": { + "title": "Proformas", + "description": "Gestiona tus proformas", + "list": { + "title": "Proformas", + "description": "Lista todas las proformas", + "grid_columns": { + "invoice_number": "Nº proforma", + "series": "Serie", + "status": "Estado", + "invoice_date": "Fecha de proforma", + "operation_date": "Fecha de operación", + "recipient_tin": "NIF/CIF", + "recipient_name": "Cliente", + "recipient_street": "Dirección", + "recipient_city": "Ciudad", + "recipient_province": "Provincia", + "recipient_postal_code": "Código postal", + "recipient_country": "País", + "total_amount": "Importe total" + } + }, + "create": { + "title": "Nueva proforma", + "description": "Crear una nueva proforma", + "back_to_list": "Volver al listado" + }, + "edit": { + "title": "Editar proforma", + "description": "Editar la proforma seleccionada" + }, + "delete": { + "title": "Eliminar proforma", + "description": "Eliminar la proforma seleccionada" + }, + "view": { + "title": "Ver proforma", + "description": "Ver los detalles de la proforma seleccionada" } - }, - "create": { - "title": "Nueva factura de cliente", - "description": "Crear una nueva factura de cliente", - "back_to_list": "Volver al listado" - }, - "edit": { - "title": "Editar factura de cliente", - "description": "Editar la factura de cliente seleccionada" - }, - "delete": { - "title": "Eliminar factura de cliente", - "description": "Eliminar la factura de cliente seleccionada" - }, - "view": { - "title": "Ver factura de cliente", - "description": "Ver los detalles de la factura de cliente seleccionada" } }, "form_groups": { "customer": { "title": "Cliente", - "description": "Selecciona el cliente para esta factura" + "description": "Selecciona el cliente para esta proforma" }, "items": { - "title": "Detalles de la factura", + "title": "Detalles de la proforma", "description": "" }, "basic_info": { - "title": "Información de la factura", - "description": "Información básica de la factura" + "title": "Información de la proforma", + "description": "Información básica de la proforma" }, "totals": { - "title": "Totales de la factura", - "description": "Desglose de los importes de la factura con descuentos e impuestos." + "title": "Totales de la proforma", + "description": "Desglose de los importes de la proforma con descuentos e impuestos." }, "preferences": { "title": "Preferencias", - "description": "Configuraciones adicionales de la factura" + "description": "Configuraciones adicionales de la proforma" } }, "form_fields": { "invoice_number": { - "label": "Número de factura", + "label": "Número de proforma", "placeholder": "", "description": "" }, "invoice_date": { "label": "Fecha", "placeholder": "Selecciona una fecha", - "description": "Fecha de emisión de la factura" + "description": "Fecha de emisión de la proforma" }, "series": { "label": "Serie", @@ -111,23 +113,23 @@ "operation_date": { "label": "Fecha de operación", "placeholder": "Selecciona una fecha", - "description": "Fecha de la operación de la factura" + "description": "Fecha de la operación de la proforma" }, "reference": { "label": "Referencia", - "placeholder": "Referencia de la factura", - "description": "Referencia de la factura" + "placeholder": "Referencia de la proforma", + "description": "Referencia de la proforma" }, "description": { "label": "Descripción", - "placeholder": "Descripción de la factura", - "description": "Descripción general de la factura" + "placeholder": "Descripción de la proforma", + "description": "Descripción general de la proforma" }, "subtotal_amount": { "label": "Subtotal", "placeholder": "", - "desc": "Subtotal de la factura" + "desc": "Subtotal de la proforma" }, "discount": { "label": "Descuento (%)", @@ -142,12 +144,12 @@ "total_amount": { "label": "Precio total", "placeholder": "", - "desc": "Precio total de la factura" + "desc": "Precio total de la proforma" }, "notes": { "label": "Notas", - "placeholder": "Notas adicionales sobre la factura", - "description": "Notas adicionales que se pueden incluir en la factura" + "placeholder": "Notas adicionales sobre la proforma", + "description": "Notas adicionales que se pueden incluir en la proforma" }, "item": { "quantity": { @@ -204,7 +206,7 @@ "customer_invoice_taxes_multi_select": { "label": "Impuestos", "placeholder": "Selecciona impuestos", - "description": "Selecciona los impuestos a aplicar a los artículos de la factura", + "description": "Selecciona los impuestos a aplicar a los artículos de la proforma", "invalid_tax_selection": "Selección de impuestos no válida. Por favor, selecciona un impuesto válido." }, "hover_card_totals_summary": { diff --git a/modules/customer-invoices/src/web/customer-invoice-routes.tsx b/modules/customer-invoices/src/web/customer-invoice-routes.tsx index 4825bfb7..56f26413 100644 --- a/modules/customer-invoices/src/web/customer-invoice-routes.tsx +++ b/modules/customer-invoices/src/web/customer-invoice-routes.tsx @@ -8,7 +8,7 @@ const InvoicesLayout = lazy(() => ); const ProformaListPage = lazy(() => - import("./pages").then((m) => ({ default: m.InvoiceListPage })) + import("./pages").then((m) => ({ default: m.ProformaListPage })) ); const CustomerInvoiceAdd = lazy(() => @@ -34,7 +34,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] { path: ":id/edit", element: }, ], }, - { + /*{ path: "customer-invoices", element: ( @@ -45,7 +45,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] //{ path: "", index: true, element: }, // index //{ path: "list", element: }, // - /*{ path: "create", element: }, + { path: "create", element: }, { path: ":id", element: }, { path: ":id/edit", element: }, { path: ":id/delete", element: }, @@ -54,8 +54,8 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] { path: ":id/email", element: }, { path: ":id/download", element: }, { path: ":id/duplicate", element: }, - { path: ":id/preview", element: },*/ + { path: ":id/preview", element: }, ], - }, + },*/ ]; }; diff --git a/modules/customer-invoices/src/web/hooks/use-create-customer-invoice-mutation.ts b/modules/customer-invoices/src/web/hooks/use-create-customer-invoice-mutation.ts index 2e540f3a..3ccb59dc 100644 --- a/modules/customer-invoices/src/web/hooks/use-create-customer-invoice-mutation.ts +++ b/modules/customer-invoices/src/web/hooks/use-create-customer-invoice-mutation.ts @@ -1,19 +1,21 @@ import { useDataSource } from "@erp/core/hooks"; import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd"; -import { DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; -import { CreateCustomerInvoiceRequestSchema } from "../../common"; -import { CustomerInvoice, InvoiceFormData } from "../schemas"; +import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; + +import { CreateProformaRequestSchema } from "../../common"; +import type { Proforma } from "../proformas/proforma.api.schema"; +import type { InvoiceFormData } from "../schemas"; type CreateCustomerInvoicePayload = { data: InvoiceFormData; }; -export const useCreateCustomerInvoiceMutation = () => { +export const useCreateProforma = () => { const queryClient = useQueryClient(); const dataSource = useDataSource(); - const schema = CreateCustomerInvoiceRequestSchema; + const schema = CreateProformaRequestSchema; - return useMutation({ + return useMutation({ mutationKey: ["customer-invoice:create"], mutationFn: async (payload) => { @@ -37,7 +39,7 @@ export const useCreateCustomerInvoiceMutation = () => { } const created = await dataSource.createOne("customer-invoices", newInvoiceData); - return created as CustomerInvoice; + return created; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["customer-invoices"] }); diff --git a/modules/customer-invoices/src/web/hooks/use-invoice-query.ts b/modules/customer-invoices/src/web/hooks/use-invoice-query.ts index 59f718df..11174a74 100644 --- a/modules/customer-invoices/src/web/hooks/use-invoice-query.ts +++ b/modules/customer-invoices/src/web/hooks/use-invoice-query.ts @@ -1,6 +1,7 @@ import { useDataSource } from "@erp/core/hooks"; -import { DefaultError, type QueryKey, useQuery } from "@tanstack/react-query"; -import { CustomerInvoice } from "../schemas"; +import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query"; + +import type { Proforma } from "../schemas"; export const CUSTOMER_INVOICE_QUERY_KEY = (id: string): QueryKey => ["customer_invoice", id] as const; @@ -13,14 +14,14 @@ export const useInvoiceQuery = (invoiceId?: string, options?: InvoiceQueryOption const dataSource = useDataSource(); const enabled = (options?.enabled ?? true) && !!invoiceId; - return useQuery({ + return useQuery({ queryKey: CUSTOMER_INVOICE_QUERY_KEY(invoiceId ?? "unknown"), queryFn: async (context) => { const { signal } = context; if (!invoiceId) { if (!invoiceId) throw new Error("invoiceId is required"); } - return await dataSource.getOne("customer-invoices", invoiceId, { + return await dataSource.getOne("customer-invoices", invoiceId, { signal, }); }, diff --git a/modules/customer-invoices/src/web/hooks/use-update-customer-invoice-mutation.ts b/modules/customer-invoices/src/web/hooks/use-update-customer-invoice-mutation.ts index 96d2a35a..9bb69d06 100644 --- a/modules/customer-invoices/src/web/hooks/use-update-customer-invoice-mutation.ts +++ b/modules/customer-invoices/src/web/hooks/use-update-customer-invoice-mutation.ts @@ -1,11 +1,10 @@ import { useDataSource } from "@erp/core/hooks"; import { ValidationErrorCollection } from "@repo/rdx-ddd"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { - UpdateCustomerInvoiceByIdRequestDTO, - UpdateCustomerInvoiceByIdRequestSchema, -} from "../../common"; -import { InvoiceFormData } from "../schemas"; + +import { type UpdateProformaByIdRequestDTO, UpdateProformaByIdRequestSchema } from "../../common"; +import type { InvoiceFormData } from "../schemas"; + import { CUSTOMER_INVOICE_QUERY_KEY } from "./use-invoice-query"; export const CUSTOMER_INVOICES_LIST_KEY = ["customer-invoices"] as const; @@ -17,10 +16,10 @@ type UpdateCustomerInvoicePayload = { data: Partial; }; -export function useUpdateCustomerInvoice() { +export function useUpdateProforma() { const queryClient = useQueryClient(); const dataSource = useDataSource(); - const schema = UpdateCustomerInvoiceByIdRequestSchema; + const schema = UpdateProformaByIdRequestSchema; return useMutation< InvoiceFormData, @@ -59,7 +58,7 @@ export function useUpdateCustomerInvoice() { const { id: invoiceId } = variables; // Refresca inmediatamente el detalle - queryClient.setQueryData( + queryClient.setQueryData( CUSTOMER_INVOICE_QUERY_KEY(invoiceId), updated ); diff --git a/modules/customer-invoices/src/web/issue-invoices/hooks/index.ts b/modules/customer-invoices/src/web/issue-invoices/hooks/index.ts new file mode 100644 index 00000000..a38a2a65 --- /dev/null +++ b/modules/customer-invoices/src/web/issue-invoices/hooks/index.ts @@ -0,0 +1,2 @@ +export * from "./use-issue-invoice-query"; +export * from "./use-issue-invoices-query"; diff --git a/modules/customer-invoices/src/web/issue-invoices/hooks/use-issue-invoice-query.ts b/modules/customer-invoices/src/web/issue-invoices/hooks/use-issue-invoice-query.ts new file mode 100644 index 00000000..9d6e112d --- /dev/null +++ b/modules/customer-invoices/src/web/issue-invoices/hooks/use-issue-invoice-query.ts @@ -0,0 +1,49 @@ +import { useDataSource } from "@erp/core/hooks"; +import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query"; + +import type { IssueInvoice } from "../issue-invoice.api.schema"; + +export const ISSUE_INVOICE_QUERY_KEY = (id: string): QueryKey => ["issue-invoice", id] as const; + +type InvoiceQueryOptions = { + enabled?: boolean; +}; + +export const useIssueInvoiceQuery = (issueInvoiceId?: string, options?: InvoiceQueryOptions) => { + const dataSource = useDataSource(); + const enabled = (options?.enabled ?? true) && !!issueInvoiceId; + + return useQuery({ + queryKey: ISSUE_INVOICE_QUERY_KEY(issueInvoiceId ?? "unknown"), + queryFn: async (context) => { + const { signal } = context; + if (!issueInvoiceId) { + if (!issueInvoiceId) throw new Error("issueInvoiceId is required"); + } + return await dataSource.getOne("issue-invoices", issueInvoiceId, { + signal, + }); + }, + enabled, + }); +}; + +/* + export function useQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey + > + + TQueryFnData: the type returned from the queryFn. + TError: the type of Errors to expect from the queryFn. + TData: the type our data property will eventually have. + Only relevant if you use the select option, + because then the data property can be different + from what the queryFn returns. + Otherwise, it will default to whatever the queryFn returns. + TQueryKey: the type of our queryKey, only relevant + if you use the queryKey that is passed to your queryFn. + +*/ diff --git a/modules/customer-invoices/src/web/issue-invoices/hooks/use-issue-invoices-query.ts b/modules/customer-invoices/src/web/issue-invoices/hooks/use-issue-invoices-query.ts new file mode 100644 index 00000000..a368e01c --- /dev/null +++ b/modules/customer-invoices/src/web/issue-invoices/hooks/use-issue-invoices-query.ts @@ -0,0 +1,41 @@ +import type { CriteriaDTO } from "@erp/core"; +import { useDataSource } from "@erp/core/hooks"; +import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query"; + +import type { IssueInvoiceSummaryPage } from "../issue-invoice.api.schema"; + +export const ISSUE_INVOICES_QUERY_KEY = (criteria: CriteriaDTO): QueryKey => [ + "issue-invoice", + { + pageNumber: criteria.pageNumber ?? 0, + pageSize: criteria.pageSize ?? 10, + q: criteria.q ?? "", + filters: criteria.filters ?? [], + orderBy: criteria.orderBy ?? "", + order: criteria.order ?? "", + }, +]; + +type IssueInvoicesQueryOptions = { + enabled?: boolean; + criteria?: CriteriaDTO; +}; + +// Obtener todas las facturas +export const useIssueInvoicesQuery = (options?: IssueInvoicesQueryOptions) => { + const dataSource = useDataSource(); + const enabled = options?.enabled ?? true; + const criteria = options?.criteria ?? {}; + + return useQuery({ + queryKey: ISSUE_INVOICES_QUERY_KEY(criteria), + queryFn: async ({ signal }) => { + return await dataSource.getList("issue-invoices", { + signal, + ...criteria, + }); + }, + enabled, + placeholderData: (previousData, previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`) + }); +}; diff --git a/modules/customer-invoices/src/web/issue-invoices/index.ts b/modules/customer-invoices/src/web/issue-invoices/index.ts new file mode 100644 index 00000000..007f69d0 --- /dev/null +++ b/modules/customer-invoices/src/web/issue-invoices/index.ts @@ -0,0 +1 @@ +export * from "./hooks"; diff --git a/modules/customer-invoices/src/web/issue-invoices/issue-invoice-resume-dto.adapter.ts b/modules/customer-invoices/src/web/issue-invoices/issue-invoice-resume-dto.adapter.ts new file mode 100644 index 00000000..68520563 --- /dev/null +++ b/modules/customer-invoices/src/web/issue-invoices/issue-invoice-resume-dto.adapter.ts @@ -0,0 +1,71 @@ +import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core"; + +import type { IssueInvoiceSummaryPage } from "./issue-invoice.api.schema"; +import type { + IssueInvoiceSummaryData, + IssueInvoiceSummaryPageData, +} from "./issue-invoice-resume.form.schema"; + +/** + * Convierte el DTO completo de API a datos numéricos para el formulario. + */ +export const IssueInvoiceResumeDtoAdapter = { + fromDto(pageDto: IssueInvoiceSummaryPage, context?: unknown): IssueInvoiceSummaryPageData { + return { + ...pageDto, + items: pageDto.items.map( + (summaryDto) => + ({ + ...summaryDto, + + subtotal_amount: MoneyDTOHelper.toNumber(summaryDto.subtotal_amount), + subtotal_amount_fmt: formatCurrency( + MoneyDTOHelper.toNumber(summaryDto.subtotal_amount), + Number(summaryDto.total_amount.scale || 2), + summaryDto.currency_code, + summaryDto.language_code + ), + + discount_percentage: PercentageDTOHelper.toNumber(summaryDto.discount_percentage), + discount_percentage_fmt: PercentageDTOHelper.toNumericString( + summaryDto.discount_percentage + ), + + discount_amount: MoneyDTOHelper.toNumber(summaryDto.discount_amount), + discount_amount_fmt: formatCurrency( + MoneyDTOHelper.toNumber(summaryDto.discount_amount), + Number(summaryDto.total_amount.scale || 2), + summaryDto.currency_code, + summaryDto.language_code + ), + + taxable_amount: MoneyDTOHelper.toNumber(summaryDto.taxable_amount), + taxable_amount_fmt: formatCurrency( + MoneyDTOHelper.toNumber(summaryDto.taxable_amount), + Number(summaryDto.total_amount.scale || 2), + summaryDto.currency_code, + summaryDto.language_code + ), + + taxes_amount: MoneyDTOHelper.toNumber(summaryDto.taxes_amount), + taxes_amount_fmt: formatCurrency( + MoneyDTOHelper.toNumber(summaryDto.taxes_amount), + Number(summaryDto.total_amount.scale || 2), + summaryDto.currency_code, + summaryDto.language_code + ), + + total_amount: MoneyDTOHelper.toNumber(summaryDto.total_amount), + total_amount_fmt: formatCurrency( + MoneyDTOHelper.toNumber(summaryDto.total_amount), + Number(summaryDto.total_amount.scale || 2), + summaryDto.currency_code, + summaryDto.language_code + ), + + //taxes: dto.taxes, + }) as unknown as IssueInvoiceSummaryData + ), + }; + }, +}; diff --git a/modules/customer-invoices/src/web/issue-invoices/issue-invoice-resume.form.schema.ts b/modules/customer-invoices/src/web/issue-invoices/issue-invoice-resume.form.schema.ts new file mode 100644 index 00000000..8cd0f94e --- /dev/null +++ b/modules/customer-invoices/src/web/issue-invoices/issue-invoice-resume.form.schema.ts @@ -0,0 +1,25 @@ +import type { IssueInvoiceSummary, IssueInvoiceSummaryPage } from "./issue-invoice.api.schema"; + +export type IssueInvoiceSummaryData = IssueInvoiceSummary & { + subtotal_amount_fmt: string; + subtotal_amount: number; + + discount_percentage_fmt: string; + discount_percentage: number; + + discount_amount_fmt: string; + discount_amount: number; + + taxable_amount_fmt: string; + taxable_amount: number; + + taxes_amoun_fmt: string; + taxes_amount: number; + + total_amount_fmt: string; + total_amount: number; +}; + +export type IssueInvoiceSummaryPageData = IssueInvoiceSummaryPage & { + items: IssueInvoiceSummary[]; +}; diff --git a/modules/customer-invoices/src/web/issue-invoices/issue-invoice.api.schema.ts b/modules/customer-invoices/src/web/issue-invoices/issue-invoice.api.schema.ts new file mode 100644 index 00000000..cd90b687 --- /dev/null +++ b/modules/customer-invoices/src/web/issue-invoices/issue-invoice.api.schema.ts @@ -0,0 +1,23 @@ +import { + GetIssueInvoiceByIdResponseSchema, + ListIssueInvoicesResponseSchema, +} from "@erp/customer-invoices/common"; +import type { ArrayElement } from "@repo/rdx-utils"; +import type { z } from "zod/v4"; + +// IssueInvoices +export const IssueInvoiceSchema = GetIssueInvoiceByIdResponseSchema.omit({ + metadata: true, +}); + +export type IssueInvoice = z.infer; +export type IssueInvoiceRecipient = IssueInvoice["recipient"]; +export type IssueInvoiceItem = ArrayElement; + +// Resultado de consulta con criteria (paginado, etc.) +export const IssueInvoiceSummaryPageSchema = ListIssueInvoicesResponseSchema.omit({ + metadata: true, +}); + +export type IssueInvoiceSummaryPage = z.infer; +export type IssueInvoiceSummary = Omit, "metadata">; diff --git a/modules/customer-invoices/src/web/manifest.ts b/modules/customer-invoices/src/web/manifest.ts index 173447bb..620945cf 100644 --- a/modules/customer-invoices/src/web/manifest.ts +++ b/modules/customer-invoices/src/web/manifest.ts @@ -1,4 +1,5 @@ -import { IModuleClient, ModuleClientParams } from "@erp/core/client"; +import type { IModuleClient, ModuleClientParams } from "@erp/core/client"; + import { CustomerInvoiceRoutes } from "./customer-invoice-routes"; export const MODULE_NAME = "CustomerInvoices"; diff --git a/modules/customer-invoices/src/web/pages/create/create-customer-invoice-page.tsx b/modules/customer-invoices/src/web/pages/create/create-customer-invoice-page.tsx index f3d6ac36..60060320 100644 --- a/modules/customer-invoices/src/web/pages/create/create-customer-invoice-page.tsx +++ b/modules/customer-invoices/src/web/pages/create/create-customer-invoice-page.tsx @@ -1,15 +1,17 @@ import { AppContent } from "@repo/rdx-ui/components"; import { Button } from "@repo/shadcn-ui/components"; import { useNavigate } from "react-router-dom"; -import { useCreateCustomerInvoiceMutation } from "../../hooks"; + +import { useCreateProforma } from "../../hooks"; import { useTranslation } from "../../i18n"; + import { CreateCustomerInvoiceEditForm } from "./create-customer-invoice-edit-form"; export const CustomerInvoiceCreate = () => { const { t } = useTranslation(); const navigate = useNavigate(); - const { mutate, isPending, isError, error } = useCreateCustomerInvoiceMutation(); + const { mutate, isPending, isError, error } = useCreateProforma(); const handleSubmit = (data: any) => { // Handle form submission logic here @@ -51,19 +53,19 @@ export const CustomerInvoiceCreate = () => { return ( <> -
+
-

{t("pages.create.title")}

-

{t("pages.create.description")}

+

{t("pages.create.title")}

+

{t("pages.create.description")}

-
-
-
- +
+
diff --git a/modules/customer-invoices/src/web/pages/create/customer-invoice.schema.ts b/modules/customer-invoices/src/web/pages/create/customer-invoice.schema.ts index f1979b86..3c9ed456 100644 --- a/modules/customer-invoices/src/web/pages/create/customer-invoice.schema.ts +++ b/modules/customer-invoices/src/web/pages/create/customer-invoice.schema.ts @@ -1,7 +1,8 @@ import { z } from "zod/v4"; -import { CreateCustomerInvoiceRequestSchema } from "../../../common/dto"; -export const CustomerInvoiceItemDataFormSchema = CreateCustomerInvoiceRequestSchema.extend({ +import { CreateProformaRequestSchema } from "../../../common/dto"; + +export const CustomerInvoiceItemDataFormSchema = CreateProformaRequestSchema.extend({ subtotal_price: z.object({ amount: z.number().nullable(), scale: z.number(), diff --git a/modules/customer-invoices/src/web/pages/list/invoices-list-grid.tsx b/modules/customer-invoices/src/web/pages/list/invoices-list-grid.tsx index e20e60b3..be1c8d71 100644 --- a/modules/customer-invoices/src/web/pages/list/invoices-list-grid.tsx +++ b/modules/customer-invoices/src/web/pages/list/invoices-list-grid.tsx @@ -1,14 +1,12 @@ -import type { CellKeyDownEvent, RowClickedEvent } from "ag-grid-community"; +import { SimpleSearchInput } from "@erp/core/components"; +import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components"; import { useCallback, useState } from "react"; - - -import { SimpleSearchInput } from '@erp/core/components'; -import { DataTable, SkeletonDataTable } from '@repo/rdx-ui/components'; import { useNavigate } from "react-router-dom"; -import { usePinnedPreviewSheet } from '../../hooks'; + +import { usePinnedPreviewSheet } from "../../hooks"; import { useTranslation } from "../../i18n"; -import { InvoiceSummaryFormData, InvoicesPageFormData } from '../../schemas'; -import { useInvoicesListColumns } from './use-invoices-list-columns'; +import { useProformasGridColumns } from "../../proformas/pages/list/use-proformas-grid-columns"; +import type { InvoiceSummaryFormData, InvoicesPageFormData } from "../../schemas"; export type InvoiceUpdateCompProps = { invoicesPage: InvoicesPageFormData; @@ -22,9 +20,12 @@ export type InvoiceUpdateCompProps = { searchValue: string; onSearchChange: (value: string) => void; - onRowClick?: (row: InvoiceSummaryFormData, index: number, event: React.MouseEvent) => void; -} - + onRowClick?: ( + row: InvoiceSummaryFormData, + index: number, + event: React.MouseEvent + ) => void; +}; // Create new GridExample component export const InvoicesListGrid = ({ @@ -34,8 +35,9 @@ export const InvoicesListGrid = ({ pageSize, onPageChange, onPageSizeChange, - searchValue, onSearchChange, - onRowClick + searchValue, + onSearchChange, + onRowClick, }: InvoiceUpdateCompProps) => { const { t } = useTranslation(); const navigate = useNavigate(); @@ -49,7 +51,7 @@ export const InvoicesListGrid = ({ const [statusFilter, setStatusFilter] = useState("todas"); - const columns = useInvoicesListColumns({ + const columns = useProformasGridColumns({ onEdit: (invoice) => navigate(`/customer-invoices/${invoice.id}/edit`), onDuplicate: (invoice) => null, //duplicateInvoice(inv.id), onDownloadPdf: (invoice) => null, //downloadInvoicePdf(inv.id), @@ -61,9 +63,7 @@ export const InvoicesListGrid = ({ const goToRow = useCallback( (id: string, newTab = false) => { const url = `/customer-invoices/${id}/edit`; - newTab - ? window.open(url, "_blank", "noopener,noreferrer") - : navigate(url); + newTab ? window.open(url, "_blank", "noopener,noreferrer") : navigate(url); }, [navigate] ); @@ -71,8 +71,7 @@ export const InvoicesListGrid = ({ const onRowClicked = useCallback( (e: RowClickedEvent) => { if (!e.data) return; - const newTab = - e.event instanceof MouseEvent && (e.event.metaKey || e.event.ctrlKey); + const newTab = e.event instanceof MouseEvent && (e.event.metaKey || e.event.ctrlKey); goToRow(e.data.id, newTab); }, [goToRow] @@ -83,7 +82,7 @@ export const InvoicesListGrid = ({ if (!e.data) return; const ev = e.event; - if (!ev || !(ev instanceof KeyboardEvent)) return; + if (!(ev && ev instanceof KeyboardEvent)) return; const key = ev.key; if (key === "Enter" || key === " ") { @@ -98,11 +97,13 @@ export const InvoicesListGrid = ({ [goToRow] ); - const handleRowClick = useCallback( (invoice: InvoiceSummaryFormData, _i: number, e: React.MouseEvent) => { const url = `/customer-invoices/${invoice.id}/edit`; - if (e.metaKey || e.ctrlKey) { window.open(url, "_blank", "noopener,noreferrer"); return; } + if (e.metaKey || e.ctrlKey) { + window.open(url, "_blank", "noopener,noreferrer"); + return; + } preview.open(invoice); }, [preview] @@ -113,9 +114,9 @@ export const InvoicesListGrid = ({
); @@ -126,7 +127,7 @@ export const InvoicesListGrid = ({
{/* Barra de filtros */}
- + {/* + + + + + + Todas + Borradores + Enviadas + Aprobadas + Rechazadas + Emitidas + + + +
+
+
+ +
+ + {/* + {({ item, isPinned, close, togglePin }) => ( + + )} + */} +
+
+ ); +}; diff --git a/modules/customer-invoices/src/web/proformas/pages/list/use-proformas-grid-columns.tsx b/modules/customer-invoices/src/web/proformas/pages/list/use-proformas-grid-columns.tsx new file mode 100644 index 00000000..2e0935f9 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/pages/list/use-proformas-grid-columns.tsx @@ -0,0 +1,314 @@ +import { formatDate } from "@erp/core/client"; +import { DataTableColumnHeader } from "@repo/rdx-ui/components"; +import { + Avatar, + AvatarFallback, + Badge, + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@repo/shadcn-ui/components"; +import type { ColumnDef } from "@tanstack/react-table"; +import { + Building2Icon, + CopyIcon, + DownloadIcon, + EditIcon, + MailIcon, + MoreVerticalIcon, + Trash2Icon, + User2Icon, +} from "lucide-react"; +import * as React from "react"; + +import { CustomerInvoiceStatusBadge } from "../../../components"; +import { useTranslation } from "../../../i18n"; +import type { InvoiceSummaryFormData } from "../../../schemas"; + +type GridActionHandlers = { + onEdit?: (invoice: InvoiceSummaryFormData) => void; + onDuplicate?: (invoice: InvoiceSummaryFormData) => void; + onDownloadPdf?: (invoice: InvoiceSummaryFormData) => void; + onSendEmail?: (invoice: InvoiceSummaryFormData) => void; + onDelete?: (invoice: InvoiceSummaryFormData) => void; +}; + +function initials(name: string) { + const parts = name.trim().split(/\s+/).slice(0, 2); + return parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "?"; +} + +const KindBadge = ({ isCompany }: { isCompany: boolean }) => ( + + {isCompany ? : } + {isCompany ? "Company" : "Person"} + +); + +const Soft = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +export function useProformasGridColumns( + actionHandlers: GridActionHandlers = {} +): ColumnDef[] { + const { t } = useTranslation(); + const { onEdit, onDuplicate, onDownloadPdf, onSendEmail, onDelete } = actionHandlers; + + return React.useMemo[]>( + () => [ + // Nº + { + accessorKey: "invoice_number", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
{row.original.invoice_number}
+ ), + enableHiding: false, + enableSorting: false, + size: 160, + minSize: 120, + }, + // Estado + { + accessorKey: "status", + header: ({ column }) => ( + + ), + cell: ({ row }) => , + enableSorting: false, + size: 140, + minSize: 120, + }, + { + id: "recipient", + header: ({ column }) => ( + + ), + accessorFn: (row) => row.recipient.name, // para ordenar/buscar por nombre + enableHiding: false, + size: 140, + minSize: 120, + cell: ({ row }) => { + const c = row.original.recipient; + const isCompany = String(c.is_company).toLowerCase() === "true"; + return ( +
+ + {initials(c.name)} + +
+
+ {c.name} + {c.trade_name && ({c.trade_name})} +
+
+ {c.tin && {c.tin}} + +
+
+
+ ); + }, + }, + // Serie + { + accessorKey: "series", + header: ({ column }) => ( + + ), + cell: ({ row }) =>
{row.original.series}
, + enableSorting: false, + size: 120, + minSize: 100, + }, + // Referencia + { + accessorKey: "reference", + header: ({ column }) => ( + + ), + cell: ({ row }) =>
{row.original.reference}
, + enableSorting: false, + size: 120, + minSize: 100, + }, + + // Fecha factura + { + accessorKey: "invoice_date", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {formatDate(row.original.invoice_date)} +
+ ), + enableSorting: false, + size: 140, + minSize: 120, + }, + // Fecha operación + { + accessorKey: "operation_date", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {formatDate(row.original.operation_date)} +
+ ), + enableSorting: false, + size: 140, + minSize: 120, + }, + + // Total + { + accessorKey: "total_amount_fmt", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.original.total_amount_fmt} +
+ ), + enableSorting: false, + size: 140, + minSize: 120, + }, + + // ───────────────────────────── + // Acciones + // ───────────────────────────── + { + id: "actions", + header: () => {t("common.actions")}, + enableSorting: false, + enableHiding: false, + size: 110, + minSize: 96, + cell: ({ row }) => { + const invoice = row.original; + const stop = (e: React.MouseEvent | React.KeyboardEvent) => e.stopPropagation(); + + return ( +
+ {/* Editar (acción primaria) */} + + + + + {t("common.edit")} + + + {/* Menú demás acciones */} + + + + + + onDuplicate?.(invoice)} + > + + {t("common.duplicate")} + + + onDownloadPdf?.(invoice)} + > + + {t("common.download_pdf")} + + onSendEmail?.(invoice)} + > + + {t("common.send_email")} + {" "} + + onDelete?.(invoice)} + > + + {t("common.delete")} + + + +
+ ); + }, + }, + ], + [t, onEdit, onDuplicate, onDownloadPdf, onSendEmail, onDelete] + ); +} diff --git a/modules/customer-invoices/src/web/proformas/proforma-resume-dto.adapter.ts b/modules/customer-invoices/src/web/proformas/proforma-resume-dto.adapter.ts new file mode 100644 index 00000000..21cac9eb --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/proforma-resume-dto.adapter.ts @@ -0,0 +1,68 @@ +import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core"; + +import type { ProformaSummaryPage } from "./proforma.api.schema"; +import type { ProformaSummaryData, ProformaSummaryPageData } from "./proforma-resume.form.schema"; + +/** + * Convierte el DTO completo de API a datos numéricos para el formulario. + */ +export const ProformaResumeDtoAdapter = { + fromDto(pageDto: ProformaSummaryPage, context?: unknown): ProformaSummaryPageData { + return { + ...pageDto, + items: pageDto.items.map( + (summaryDto) => + ({ + ...summaryDto, + + subtotal_amount: MoneyDTOHelper.toNumber(summaryDto.subtotal_amount), + subtotal_amount_fmt: formatCurrency( + MoneyDTOHelper.toNumber(summaryDto.subtotal_amount), + Number(summaryDto.total_amount.scale || 2), + summaryDto.currency_code, + summaryDto.language_code + ), + + discount_percentage: PercentageDTOHelper.toNumber(summaryDto.discount_percentage), + discount_percentage_fmt: PercentageDTOHelper.toNumericString( + summaryDto.discount_percentage + ), + + discount_amount: MoneyDTOHelper.toNumber(summaryDto.discount_amount), + discount_amount_fmt: formatCurrency( + MoneyDTOHelper.toNumber(summaryDto.discount_amount), + Number(summaryDto.total_amount.scale || 2), + summaryDto.currency_code, + summaryDto.language_code + ), + + taxable_amount: MoneyDTOHelper.toNumber(summaryDto.taxable_amount), + taxable_amount_fmt: formatCurrency( + MoneyDTOHelper.toNumber(summaryDto.taxable_amount), + Number(summaryDto.total_amount.scale || 2), + summaryDto.currency_code, + summaryDto.language_code + ), + + taxes_amount: MoneyDTOHelper.toNumber(summaryDto.taxes_amount), + taxes_amount_fmt: formatCurrency( + MoneyDTOHelper.toNumber(summaryDto.taxes_amount), + Number(summaryDto.total_amount.scale || 2), + summaryDto.currency_code, + summaryDto.language_code + ), + + total_amount: MoneyDTOHelper.toNumber(summaryDto.total_amount), + total_amount_fmt: formatCurrency( + MoneyDTOHelper.toNumber(summaryDto.total_amount), + Number(summaryDto.total_amount.scale || 2), + summaryDto.currency_code, + summaryDto.language_code + ), + + //taxes: dto.taxes, + }) as unknown as ProformaSummaryData + ), + }; + }, +}; diff --git a/modules/customer-invoices/src/web/proformas/proforma-resume.form.schema.ts b/modules/customer-invoices/src/web/proformas/proforma-resume.form.schema.ts new file mode 100644 index 00000000..cea670dc --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/proforma-resume.form.schema.ts @@ -0,0 +1,25 @@ +import type { ProformaSummary, ProformaSummaryPage } from "./proforma.api.schema"; + +export type ProformaSummaryData = ProformaSummary & { + subtotal_amount_fmt: string; + subtotal_amount: number; + + discount_percentage_fmt: string; + discount_percentage: number; + + discount_amount_fmt: string; + discount_amount: number; + + taxable_amount_fmt: string; + taxable_amount: number; + + taxes_amoun_fmt: string; + taxes_amount: number; + + total_amount_fmt: string; + total_amount: number; +}; + +export type ProformaSummaryPageData = ProformaSummaryPage & { + items: ProformaSummary[]; +}; diff --git a/modules/customer-invoices/src/web/proformas/proforma.api.schema.ts b/modules/customer-invoices/src/web/proformas/proforma.api.schema.ts new file mode 100644 index 00000000..9170d026 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/proforma.api.schema.ts @@ -0,0 +1,31 @@ +import { + CreateProformaRequestSchema, + GetProformaByIdResponseSchema, + ListProformasResponseSchema, + UpdateProformaByIdRequestSchema, +} from "@erp/customer-invoices/common"; +import type { ArrayElement } from "@repo/rdx-utils"; +import type { z } from "zod/v4"; + +// Proformas +export const ProformaSchema = GetProformaByIdResponseSchema.omit({ + metadata: true, +}); + +export type Proforma = z.infer; +export type ProformaRecipient = Proforma["recipient"]; +export type ProformaItem = ArrayElement; + +export const CreateProformaSchema = CreateProformaRequestSchema; +export const UpdateProformaSchema = UpdateProformaByIdRequestSchema; + +export type CreateProformaInput = z.infer; // Cuerpo para crear +export type UpdateProformaInput = z.infer; // Cuerpo para actualizar + +// Resultado de consulta con criteria (paginado, etc.) +export const ProformaSummaryPageSchema = ListProformasResponseSchema.omit({ + metadata: true, +}); + +export type ProformaSummaryPage = z.infer; +export type ProformaSummary = Omit, "metadata">; diff --git a/modules/customer-invoices/src/web/schemas/invoices.api.schema.ts b/modules/customer-invoices/src/web/schemas/invoices.api.schema.ts index 697cbab7..bbd4b34a 100644 --- a/modules/customer-invoices/src/web/schemas/invoices.api.schema.ts +++ b/modules/customer-invoices/src/web/schemas/invoices.api.schema.ts @@ -1,35 +1,23 @@ -import type { PaginationSchema } from "@erp/core"; import type { ArrayElement } from "@repo/rdx-utils"; import type { z } from "zod/v4"; -import { - CreateCustomerInvoiceRequestSchema, - GetIssueInvoiceByIdResponseSchema, - ListIssueInvoicesResponseSchema, - UpdateCustomerInvoiceByIdRequestSchema, -} from "../../common"; +import { GetIssueInvoiceByIdResponseSchema, ListIssueInvoicesResponseSchema } from "../../common"; -export const CustomerInvoiceSchema = GetIssueInvoiceByIdResponseSchema.omit({ +export const IssueInvoiceSchema = GetIssueInvoiceByIdResponseSchema.omit({ metadata: true, }); -export const CustomerInvoiceCreateSchema = CreateCustomerInvoiceRequestSchema; -export const CustomerInvoiceUpdateSchema = UpdateCustomerInvoiceByIdRequestSchema; -// Tipos (derivados de Zod o DTOs del backend) -export type CustomerInvoice = z.infer; -export type CustomerInvoiceRecipient = CustomerInvoice["recipient"]; -export type CustomerInvoiceItem = ArrayElement; - -export type CustomerInvoiceCreateInput = z.infer; // Cuerpo para crear -export type CustomerInvoiceUpdateInput = z.infer; // Cuerpo para actualizar +export type IssueInvoice = z.infer; +export type IssueInvoiceRecipient = IssueInvoice["recipient"]; +export type IssueInvoiceItem = ArrayElement; // Resultado de consulta con criteria (paginado, etc.) -export const CustomerInvoicesPageSchema = ListIssueInvoicesResponseSchema.omit({ +export const IssueInvoicesPageSchema = ListIssueInvoicesResponseSchema.omit({ metadata: true, }); -export type PaginatedResponse = z.infer; -export type CustomerInvoicesPage = z.infer; +//export type PaginatedResponse = z.infer; +//export type CustomerInvoicesPage = z.infer; // Ítem simplificado dentro del listado (no toda la entidad) -export type CustomerInvoiceSummary = Omit, "metadata">; +//export type CustomerInvoiceSummary = Omit, "metadata">; diff --git a/modules/customers/src/web/components/customer-status-badge.tsx b/modules/customers/src/web/components/customer-status-badge.tsx index 98f45737..64e217f2 100644 --- a/modules/customers/src/web/components/customer-status-badge.tsx +++ b/modules/customers/src/web/components/customer-status-badge.tsx @@ -1,51 +1,43 @@ -import { Badge } from "@repo/shadcn-ui/components"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components"; import { cn } from "@repo/shadcn-ui/lib/utils"; -import { forwardRef } from "react"; -import { useTranslation } from "../i18n"; +import React from "react"; -export type CustomerStatus = "active" | "inactive"; +export type CustomerStatus = "active" | "inactive" | "error"; export type CustomerStatusBadgeProps = { status: string; // permitir cualquier valor className?: string; }; -const statusColorConfig: Record = { - inactive: { - badge: - "bg-gray-600/10 dark:bg-gray-600/20 hover:bg-gray-600/10 text-gray-500 border-gray-600/60", - dot: "bg-gray-500", - }, - active: { - badge: - "bg-emerald-600/10 dark:bg-emerald-600/20 hover:bg-emerald-600/10 text-emerald-500 border-emerald-600/60", - dot: "bg-emerald-500", - }, +const statusColorConfig: Record = { + inactive: "text-gray-400 bg-gray-100 dark:text-gray-500 dark:bg-gray-100/10", + active: "text-green-500 bg-green-500/10 dark:text-green-400 dark:bg-green-400/10", + error: "text-rose-500 bg-rose-500/10 dark:text-rose-400 dark:bg-rose-400/10", }; -export const CustomerStatusBadge = forwardRef( - ({ status, className, ...props }, ref) => { - const { t } = useTranslation(); - const normalizedStatus = status.toLowerCase() as CustomerStatus; - const config = statusColorConfig[normalizedStatus]; - const commonClassName = - "transition-colors duration-200 cursor-pointer shadow-none rounded-full"; +export const CustomerStatusBadge = ({ status }: { status: string }) => { + // Map visual simple; ajustar a tu catálogo real + const statusClass = React.useMemo( + () => + status.toLowerCase() === "active" ? statusColorConfig.active : statusColorConfig.inactive, + [status] + ); + const contentTxt = React.useMemo( + () => + status.toLowerCase() === "active" ? "El cliente está activo" : "El cliente está inactivo", + [status] + ); - if (!config) { - return ( - - {status} - - ); - } - - return ( - -
- {t(`catalog.status.${status}`)} - - ); - } -); + return ( + + +
+
+
+ + {contentTxt} + + ); +}; CustomerStatusBadge.displayName = "CustomerStatusBadge"; diff --git a/modules/customers/src/web/components/index.ts b/modules/customers/src/web/components/index.ts index 7d0fe3aa..dae680dc 100644 --- a/modules/customers/src/web/components/index.ts +++ b/modules/customers/src/web/components/index.ts @@ -1,5 +1,6 @@ export * from "./client-selector-modal"; export * from "./customer-modal-selector"; +export * from "./customer-status-badge"; export * from "./customers-layout"; export * from "./editor"; export * from "./error-alert"; diff --git a/modules/customers/src/web/pages/list/use-customers-list-columns.tsx b/modules/customers/src/web/pages/list/use-customers-list-columns.tsx index 8774c711..4b88fa3e 100644 --- a/modules/customers/src/web/pages/list/use-customers-list-columns.tsx +++ b/modules/customers/src/web/pages/list/use-customers-list-columns.tsx @@ -1,21 +1,30 @@ -import { DataTableColumnHeader } from '@repo/rdx-ui/components'; +import { DataTableColumnHeader } from "@repo/rdx-ui/components"; import { Avatar, AvatarFallback, Badge, Button, - DropdownMenu, DropdownMenuContent, - DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, - Tooltip, - TooltipContent, - TooltipTrigger -} from '@repo/shadcn-ui/components'; -import { cn } from '@repo/shadcn-ui/lib/utils'; + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@repo/shadcn-ui/components"; import type { ColumnDef } from "@tanstack/react-table"; -import { Building2Icon, GlobeIcon, MailIcon, MoreHorizontalIcon, PencilIcon, PhoneIcon, User2Icon } from 'lucide-react'; +import { + Building2Icon, + MailIcon, + MoreHorizontalIcon, + PencilIcon, + PhoneIcon, + User2Icon, +} from "lucide-react"; import * as React from "react"; -import { useTranslation } from '../../i18n'; -import { CustomerSummaryFormData } from '../../schemas'; + +import { CustomerStatusBadge } from "../../components"; +import { useTranslation } from "../../i18n"; +import type { CustomerSummaryFormData } from "../../schemas"; type CustomerActionHandlers = { onEdit?: (customer: CustomerSummaryFormData) => void; @@ -27,33 +36,8 @@ function shortId(id: string) { return id ? `${id.slice(0, 4)}_${id.slice(-4)}` : "-"; } -const statuses = { - inactive: 'text-gray-400 bg-gray-100 dark:text-gray-500 dark:bg-gray-100/10', - active: 'text-green-500 bg-green-500/10 dark:text-green-400 dark:bg-green-400/10', - error: 'text-rose-500 bg-rose-500/10 dark:text-rose-400 dark:bg-rose-400/10', -} - -// ---- Helpers UI ---- -const StatusBadge = ({ status }: { status: string }) => { - // Map visual simple; ajustar a tu catálogo real - const statusClass = React.useMemo(() => status.toLowerCase() === 'active' ? statuses.active : statuses.inactive, [status]); - const contentTxt = React.useMemo(() => status.toLowerCase() === 'active' ? 'El cliente está activo' : 'El cliente está inactivo', [status]); - - return ( - - -
-
-
- - {contentTxt} - - - ) -}; - const KindBadge = ({ isCompany }: { isCompany: boolean }) => ( - + {isCompany ? : } {isCompany ? "Company" : "Person"} @@ -65,18 +49,20 @@ const Soft = ({ children }: { children: React.ReactNode }) => ( const ContactCell = ({ customer }: { customer: CustomerSummaryFormData }) => (
- {customer.email_primary && ( -
- - + )} {customer.email_secondary && ( -
{customer.email_secondary}
+
+ + {customer.email_secondary} +
)}
@@ -84,16 +70,9 @@ const ContactCell = ({ customer }: { customer: CustomerSummaryFormData }) => ( {customer.phone_primary || customer.mobile_primary || -} {customer.phone_secondary && • {customer.phone_secondary}} {customer.mobile_secondary && • {customer.mobile_secondary}} - {false && customer.fax && • fax {customer.fax}} + {false}
- {false && customer.website && ( - - )} + {false}
); @@ -111,7 +90,7 @@ const AddressCell = ({ c }: { c: CustomerSummaryFormData }) => { function initials(name: string) { const parts = name.trim().split(/\s+/).slice(0, 2); - return parts.map(p => p[0]?.toUpperCase() ?? "").join("") || "?"; + return parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "?"; } function safeHttp(url: string) { @@ -120,127 +99,140 @@ function safeHttp(url: string) { return `https://${url}`; } - export function useCustomersListColumns( handlers: CustomerActionHandlers = {} ): ColumnDef[] { const { t } = useTranslation(); - const { - onEdit, onView, onDelete, - } = handlers; + const { onEdit, onView, onDelete } = handlers; - return React.useMemo[]>(() => [ - // Identidad + estado + metadatos (columna compuesta) - { - id: "customer", - header: ({ column }) => ( - - ), - accessorFn: (row) => row.name, // para ordenar/buscar por nombre - enableHiding: false, - size: 140, - minSize: 120, - cell: ({ row }) => { - const c = row.original; - const isCompany = String(c.is_company).toLowerCase() === "true"; - return ( -
- - {initials(c.name)} - -
-
- {c.name} - {c.trade_name && ({c.trade_name})} -
-
- {c.tin && {c.tin}} - + return React.useMemo[]>( + () => [ + // Identidad + estado + metadatos (columna compuesta) + { + id: "customer", + header: ({ column }) => ( + + ), + accessorFn: (row) => row.name, // para ordenar/buscar por nombre + enableHiding: false, + size: 140, + minSize: 120, + cell: ({ row }) => { + const c = row.original; + const isCompany = String(c.is_company).toLowerCase() === "true"; + return ( +
+ + {initials(c.name)} + +
+
+ {" "} + {c.name} + {c.trade_name && ({c.trade_name})} +
+
+ {c.tin && {c.tin}} + +
-
- ); + ); + }, }, - }, - // Contacto (emails, teléfonos, web) - { - id: "contact", - header: ({ column }) => ( - - ), - accessorFn: (r) => `${r.email_primary} ${r.phone_primary} ${r.mobile_primary} ${r.website}`, - size: 140, - minSize: 120, - cell: ({ row }) => , - }, + // Contacto (emails, teléfonos, web) + { + id: "contact", + header: ({ column }) => ( + + ), + accessorFn: (r) => `${r.email_primary} ${r.phone_primary} ${r.mobile_primary} ${r.website}`, + size: 140, + minSize: 120, + cell: ({ row }) => , + }, - // Dirección (múltiples campos en bloque) - { - id: "address", - header: t("pages.list.grid_columns.address"), - accessorFn: (r) => - `${r.street} ${r.street2} ${r.city} ${r.postal_code} ${r.province} ${r.country}`, - size: 140, - minSize: 120, - cell: ({ row }) => , - }, + // Dirección (múltiples campos en bloque) + { + id: "address", + header: t("pages.list.grid_columns.address"), + accessorFn: (r) => + `${r.street} ${r.street2} ${r.city} ${r.postal_code} ${r.province} ${r.country}`, + size: 140, + minSize: 120, + cell: ({ row }) => , + }, - // Acciones - { - id: "actions", - header: ({ column }) => ( - - ), - size: 64, - minSize: 64, - enableSorting: false, - enableHiding: false, - cell: ({ row }) => { - const customer = row.original; - const { website, email_primary } = customer; - return ( -
-
- - - - - - - Actions - - onView?.(customer)}>Open - onEdit?.(customer)}>Edit - - window.open(safeHttp(website), "_blank")}> - Visit website - - navigator.clipboard.writeText(email_primary)}> - Copy email - - - onDelete?.(customer)} - > - Delete - - - + // Acciones + { + id: "actions", + header: ({ column }) => ( + + ), + size: 64, + minSize: 64, + enableSorting: false, + enableHiding: false, + cell: ({ row }) => { + const customer = row.original; + const { website, email_primary } = customer; + return ( +
+
+ + + + + + + Actions + + onView?.(customer)}>Open + onEdit?.(customer)}>Edit + + window.open(safeHttp(website), "_blank")}> + Visit website + + navigator.clipboard.writeText(email_primary)}> + Copy email + + + onDelete?.(customer)} + > + Delete + + + +
-
- ); + ); + }, }, - }, - ], [t, onEdit, onView, onDelete]); + ], + [t, onEdit, onView, onDelete] + ); } diff --git a/packages/rdx-criteria/src/criteria-to-sequelize-converter.ts b/packages/rdx-criteria/src/criteria-to-sequelize-converter.ts index b02303c8..f5d09496 100644 --- a/packages/rdx-criteria/src/criteria-to-sequelize-converter.ts +++ b/packages/rdx-criteria/src/criteria-to-sequelize-converter.ts @@ -17,7 +17,23 @@ export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter { this.applyOrder(options, criteria, mappings, params); this.applyPagination(options, criteria); - return options; + const normalizedOrder = Array.isArray(options.order) + ? options.order + : options.order + ? [options.order] + : []; + + const normalizedInclude = Array.isArray(options.include) + ? options.include + : options.include + ? [options.include] + : []; + + return { + ...options, + order: normalizedOrder, + include: normalizedInclude, + }; } /** Filtros simples (sin anidaciones complejas) */ diff --git a/packages/rdx-criteria/src/utils.ts b/packages/rdx-criteria/src/utils.ts index ced92e0a..72efe76d 100644 --- a/packages/rdx-criteria/src/utils.ts +++ b/packages/rdx-criteria/src/utils.ts @@ -1,4 +1,4 @@ -import { FindOptions } from "sequelize"; +import type { FindOptions } from "sequelize"; // orderItem puede ser: ['campo', 'ASC'|'DESC'] // o [Sequelize.literal('score'), 'DESC'] diff --git a/packages/rdx-ui/src/components/layout/app-sidebar.tsx b/packages/rdx-ui/src/components/layout/app-sidebar.tsx index 0bbf8b7a..3d366098 100644 --- a/packages/rdx-ui/src/components/layout/app-sidebar.tsx +++ b/packages/rdx-ui/src/components/layout/app-sidebar.tsx @@ -1,31 +1,3 @@ -import { - AudioWaveform, - Command, - FileCheckIcon, - Frame, - GalleryVerticalEnd, - HomeIcon, - MapIcon, - PieChart, -} from "lucide-react"; - -import { - BarChartIcon, - CameraIcon, - ClipboardListIcon, - DatabaseIcon, - FileCodeIcon, - FileIcon, - FileTextIcon, - FolderIcon, - HelpCircleIcon, - LayoutDashboardIcon, - ListIcon, - SettingsIcon, - UsersIcon, -} from "lucide-react"; -import * as React from "react"; - import { Sidebar, SidebarContent, @@ -33,6 +5,31 @@ import { SidebarHeader, SidebarRail, } from "@repo/shadcn-ui/components"; +import { + AudioWaveform, + BarChartIcon, + CameraIcon, + ClipboardListIcon, + Command, + DatabaseIcon, + FileCheckIcon, + FileCodeIcon, + FileIcon, + FileTextIcon, + FolderIcon, + Frame, + GalleryVerticalEnd, + HelpCircleIcon, + HomeIcon, + LayoutDashboardIcon, + ListIcon, + MapIcon, + PieChart, + SettingsIcon, + UsersIcon, +} from "lucide-react"; +import type * as React from "react"; + import { NavMain } from "./nav-main.tsx"; import { NavSecondary } from "./nav-secondary.tsx"; import { NavUser } from "./nav-user.tsx"; @@ -203,7 +200,7 @@ const data2 = { items: [ { title: "Listado de proformas", - url: "/customer-proforma", + url: "/proformas", }, { title: "Enviar a Veri*Factu", @@ -243,8 +240,8 @@ const data2 = { export function AppSidebar({ ...props }: React.ComponentProps) { return ( - - + + @@ -252,7 +249,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) { - + diff --git a/packages/shadcn-ui/src/styles/globals.css b/packages/shadcn-ui/src/styles/globals.css index 4f37eb2d..1a70f7b0 100644 --- a/packages/shadcn-ui/src/styles/globals.css +++ b/packages/shadcn-ui/src/styles/globals.css @@ -65,7 +65,7 @@ --shadow-xl: 1px 1px 6px 0px hsl(0 0% 0% / 0.1), 1px 8px 10px -1px hsl(0 0% 0% / 0.1); --shadow-2xl: 1px 1px 6px 0px hsl(0 0% 0% / 0.25); --tracking-normal: 0rem; - --spacing: 0.20rem; + --spacing: 0.22rem; } .dark {