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 e274f083..eeedaf4e 100644 --- a/modules/core/src/api/infrastructure/sequelize/sequelize-error-translator.ts +++ b/modules/core/src/api/infrastructure/sequelize/sequelize-error-translator.ts @@ -56,7 +56,7 @@ export function translateSequelizeError(err: unknown): Error { return DomainValidationError.invalidFormat(d.path, d.message, { cause: err }); } - return new ValidationErrorCollection(details, { cause: err }); + return new ValidationErrorCollection("Invalid data provided", details, { cause: err }); } // 4) Conectividad / indisponibilidad (transitorio) diff --git a/modules/core/src/common/dto/base.schemas.ts b/modules/core/src/common/dto/base.schemas.ts index a82d22a8..19bf89d5 100644 --- a/modules/core/src/common/dto/base.schemas.ts +++ b/modules/core/src/common/dto/base.schemas.ts @@ -10,7 +10,7 @@ import { z } from "zod/v4"; export const NumericStringSchema = z .string() - .regex(/^\d$/, { message: "Must be empty or contain only digits (0-9)." }); + .regex(/^\d*$/, { message: "Must be empty or contain only digits (0-9)." }); // Cantidad de dinero (base): solo para la cantidad y la escala, sin moneda export const AmountBaseSchema = z.object({ diff --git a/modules/core/src/common/dto/list-view.response.dto.ts b/modules/core/src/common/dto/list-view.response.dto.ts index 8d92c0db..7bcc4e7a 100644 --- a/modules/core/src/common/dto/list-view.response.dto.ts +++ b/modules/core/src/common/dto/list-view.response.dto.ts @@ -7,12 +7,17 @@ import { MetadataSchema } from "./metadata.dto"; * @param itemSchema Esquema Zod del elemento T * @returns Zod schema para ListViewDTO */ -export const createListViewResponseSchema = (itemSchema: T) => - z.object({ - page: z.number().int().min(1, "Page must be a positive integer"), - per_page: z.number().int().min(1, "Items per page must be a positive integer"), - total_pages: z.number().int().min(0, "Total pages must be a non-negative integer"), - total_items: z.number().int().min(0, "Total items must be a non-negative integer"), +export const PaginationSchema = z.object({ + page: z.number().int().min(1, "Page must be a positive integer"), + per_page: z.number().int().min(1, "Items per page must be a positive integer"), + total_pages: z.number().int().min(0, "Total pages must be a non-negative integer"), + total_items: z.number().int().min(0, "Total items must be a non-negative integer"), +}); + +export type Pagination = z.infer; + +export const createPaginatedListSchema = (itemSchema: T) => + PaginationSchema.extend({ items: z.array(itemSchema), metadata: MetadataSchema.optional(), }); diff --git a/modules/core/src/common/helpers/money-dto-helper.ts b/modules/core/src/common/helpers/money-dto-helper.ts index 1d4dddc2..4451e370 100644 --- a/modules/core/src/common/helpers/money-dto-helper.ts +++ b/modules/core/src/common/helpers/money-dto-helper.ts @@ -46,7 +46,7 @@ const fromNumericString = (amount?: string, currency: string = "EUR", scale = 2) if (!amount || amount?.trim?.() === "") { return { value: "", - scale: "", + scale: String(scale), currency_code: currency, }; } diff --git a/modules/core/src/common/helpers/percentage-dto-helpers.ts b/modules/core/src/common/helpers/percentage-dto-helpers.ts index 0a499633..7f02028b 100644 --- a/modules/core/src/common/helpers/percentage-dto-helpers.ts +++ b/modules/core/src/common/helpers/percentage-dto-helpers.ts @@ -42,7 +42,7 @@ const fromNumericString = (amount?: string, scale = 2): PercentageDTO => { if (!amount || amount?.trim?.() === "") { return { value: "", - scale: "", + scale: String(scale), }; } return { diff --git a/modules/core/src/common/helpers/quantity-dto-helpers.ts b/modules/core/src/common/helpers/quantity-dto-helpers.ts index 9577ef23..92470fb9 100644 --- a/modules/core/src/common/helpers/quantity-dto-helpers.ts +++ b/modules/core/src/common/helpers/quantity-dto-helpers.ts @@ -42,7 +42,7 @@ const fromNumericString = (amount?: string, scale = 2): QuantityDTO => { if (!amount || amount?.trim?.() === "") { return { value: "", - scale: "", + scale: String(scale), }; } return { diff --git a/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts b/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts index 09c92f8e..cbb1ce2a 100644 --- a/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts +++ b/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts @@ -41,11 +41,11 @@ export const createAxiosDataSource = (client: AxiosInstance): IDataSource => { return { getBaseUrl: () => (client as AxiosInstance).getUri(), - getList: async (resource: string, params?: Record): Promise => { + getList: async (resource: string, params?: Record): Promise => { const { signal, ...rest } = params as any; // en 'rest' puede venir el "criteria". const res = await (client as AxiosInstance).get(resource, { signal, params: rest }); - return res.data; + return res.data; }, getOne: async (resource: string, id: string | number, params?: Record) => { 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 9f2c5fa6..4b8013f1 100644 --- a/modules/core/src/web/lib/data-source/datasource.interface.ts +++ b/modules/core/src/web/lib/data-source/datasource.interface.ts @@ -14,7 +14,7 @@ export interface ICustomParams { export interface IDataSource { getBaseUrl(): string; - getList(resource: string, params?: Record): Promise; + getList(resource: string, params?: Record): Promise; getOne(resource: string, id: string | number, params?: Record): Promise; getMany(resource: string, ids: Array): Promise; createOne(resource: string, data: Partial, params?: Record): Promise; diff --git a/modules/customer-invoices/src/api/application/helpers/map-dto-to-customer-invoice-items-props.ts b/modules/customer-invoices/src/api/application/helpers/map-dto-to-customer-invoice-items-props.ts index e37fbef2..7792a032 100644 --- a/modules/customer-invoices/src/api/application/helpers/map-dto-to-customer-invoice-items-props.ts +++ b/modules/customer-invoices/src/api/application/helpers/map-dto-to-customer-invoice-items-props.ts @@ -2,12 +2,14 @@ import { ValidationErrorCollection, ValidationErrorDetail, extractOrPushError, + maybeFromNullableVO, } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { CreateCustomerInvoiceRequestDTO } from "../../../common"; import { CustomerInvoiceItem, CustomerInvoiceItemDescription, + CustomerInvoiceItemProps, ItemAmount, ItemDiscount, ItemQuantity, @@ -24,44 +26,40 @@ export function mapDTOToCustomerInvoiceItemsProps( const path = (field: string) => `items[${index}].${field}`; const description = extractOrPushError( - CustomerInvoiceItemDescription.create(item.description), + maybeFromNullableVO(item.description, (value) => + CustomerInvoiceItemDescription.create(value) + ), path("description"), errors ); const quantity = extractOrPushError( - ItemQuantity.create({ - value: Number(item.quantity), - }), + maybeFromNullableVO(item.quantity, (value) => ItemQuantity.create({ value })), path("quantity"), errors ); - const unitPrice = extractOrPushError( - ItemAmount.create({ - value: item.unitPrice.amount, - scale: item.unitPrice.scale, - currency_code: item.unitPrice.currency, - }), - path("unit_price"), + const unitAmount = extractOrPushError( + maybeFromNullableVO(item.unit_amount, (value) => ItemAmount.create({ value })), + path("unit_amount"), errors ); - const discount = extractOrPushError( - ItemDiscount.create({ - value: item.discount.amount, - scale: item.discount.scale, - }), - path("discount"), + const discountPercentage = extractOrPushError( + maybeFromNullableVO(item.discount_percentage, (value) => ItemDiscount.create({ value })), + path("discount_percentage"), errors ); if (errors.length === 0) { - const itemProps = { + const itemProps: CustomerInvoiceItemProps = { description: description, quantity: quantity, - unitPrice: unitPrice, - discount: discount, + unitAmount: unitAmount, + discountPercentage: discountPercentage, + //currencyCode, + //languageCode, + //taxes: }; if (hasNoUndefinedFields(itemProps)) { @@ -77,7 +75,7 @@ export function mapDTOToCustomerInvoiceItemsProps( } if (errors.length > 0) { - return Result.fail(new ValidationErrorCollection(errors)); + return Result.fail(new ValidationErrorCollection("Invoice items dto mapping failed", errors)); } }); diff --git a/modules/customer-invoices/src/api/application/helpers/map-dto-to-customer-invoice-props.ts b/modules/customer-invoices/src/api/application/helpers/map-dto-to-customer-invoice-props.ts index 02c561c8..24f1deb8 100644 --- a/modules/customer-invoices/src/api/application/helpers/map-dto-to-customer-invoice-props.ts +++ b/modules/customer-invoices/src/api/application/helpers/map-dto-to-customer-invoice-props.ts @@ -66,7 +66,7 @@ export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceRequestDT } if (errors.length > 0) { - return Result.fail(new ValidationErrorCollection("Customer dto mapping failed", errors)); + return Result.fail(new ValidationErrorCollection("Invoice dto mapping failed", errors)); } const invoiceProps: CustomerInvoiceProps = { 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/list-customer-invoices.presenter.ts index ec274f77..54034408 100644 --- a/modules/customer-invoices/src/api/application/presenters/queries/list-customer-invoices.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/queries/list-customer-invoices.presenter.ts @@ -12,6 +12,7 @@ export class ListCustomerInvoicesPresenter extends Presenter { const invoiceDTO: ArrayElement = { id: invoice.id.toString(), company_id: invoice.companyId.toString(), + is_proforma: invoice.isProforma, customer_id: invoice.customerId.toString(), invoice_number: toEmptyString(invoice.invoiceNumber, (value) => value.toString()), @@ -20,6 +21,8 @@ export class ListCustomerInvoicesPresenter extends Presenter { invoice_date: invoice.invoiceDate.toDateString(), operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()), + reference: toEmptyString(invoice.reference, (value) => value.toString()), + description: toEmptyString(invoice.description, (value) => value.toString()), recipient: { customer_id: invoice.customerId.toString(), @@ -32,6 +35,7 @@ export class ListCustomerInvoicesPresenter extends Presenter { taxes: invoice.taxes, subtotal_amount: invoice.subtotalAmount.toObjectString(), + discount_percentage: invoice.discountPercentage.toObjectString(), discount_amount: invoice.discountAmount.toObjectString(), taxable_amount: invoice.taxableAmount.toObjectString(), taxes_amount: invoice.taxesAmount.toObjectString(), diff --git a/modules/customer-invoices/src/api/application/use-cases/create/map-dto-to-create-customer-invoice-props.ts b/modules/customer-invoices/src/api/application/use-cases/create/map-dto-to-create-customer-invoice-props.ts index 682a32da..5776e459 100644 --- a/modules/customer-invoices/src/api/application/use-cases/create/map-dto-to-create-customer-invoice-props.ts +++ b/modules/customer-invoices/src/api/application/use-cases/create/map-dto-to-create-customer-invoice-props.ts @@ -72,7 +72,7 @@ export class CreateCustomerInvoicePropsMapper { ); const invoiceNumber = extractOrPushError( - CustomerInvoiceNumber.create(dto.invoice_number), + maybeFromNullableVO(dto.invoice_number, (value) => CustomerInvoiceNumber.create(value)), "invoice_number", this.errors ); diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts index 2a6e80ef..1e3d0fe4 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts @@ -148,7 +148,7 @@ export class CustomerInvoiceItemDomainMapper if (createResult.isFailure) { return Result.fail( - new ValidationErrorCollection([ + new ValidationErrorCollection("Invoice item entity creation failed", [ { path: `items[${index}]`, message: createResult.error.message }, ]) ); diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts index 838b8ce0..d636d268 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts @@ -235,7 +235,9 @@ export class CustomerInvoiceDomainMapper // 5) Si hubo errores de mapeo, devolvemos colección de validación if (errors.length > 0) { - return Result.fail(new ValidationErrorCollection(errors)); + return Result.fail( + new ValidationErrorCollection("Customer invoice mapping failed [mapToDomain]", errors) + ); } // 6) Construcción del agregado (Dominio) @@ -279,7 +281,9 @@ export class CustomerInvoiceDomainMapper if (createResult.isFailure) { return Result.fail( - new ValidationErrorCollection([{ path: "invoice", message: createResult.error.message }]) + new ValidationErrorCollection("Customer invoice entity creation failed", [ + { path: "invoice", message: createResult.error.message }, + ]) ); } @@ -338,7 +342,9 @@ export class CustomerInvoiceDomainMapper // 7) Si hubo errores de mapeo, devolvemos colección de validación if (errors.length > 0) { - return Result.fail(new ValidationErrorCollection(errors)); + return Result.fail( + new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors) + ); } const invoiceValues: CustomerInvoiceCreationAttributes = { diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-recipient.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-recipient.mapper.ts index a9c534b5..411b9c63 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-recipient.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-recipient.mapper.ts @@ -106,7 +106,9 @@ export class InvoiceRecipientDomainMapper { if (createResult.isFailure) { return Result.fail( - new ValidationErrorCollection([{ path: "recipient", message: createResult.error.message }]) + new ValidationErrorCollection("Invoice recipient entity creation failed", [ + { path: "recipient", message: createResult.error.message }, + ]) ); } diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/item-taxes.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/item-taxes.mapper.ts index 59033b0f..28621515 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/item-taxes.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/domain/item-taxes.mapper.ts @@ -68,7 +68,7 @@ export class ItemTaxesDomainMapper const createResult = Tax.create(tax!); if (createResult.isFailure) { return Result.fail( - new ValidationErrorCollection([ + new ValidationErrorCollection("Invoice item tax creation failed", [ { path: `taxes[${index}]`, message: createResult.error.message }, ]) ); 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 971e034b..0cc1f24a 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 @@ -38,6 +38,7 @@ export type CustomerInvoiceListDTO = { invoiceDate: UtcDate; operationDate: Maybe; + reference: Maybe; description: Maybe; customerId: UniqueID; diff --git a/modules/customer-invoices/src/common/dto/request/update-customer-invoice-by-id.request.dto.ts b/modules/customer-invoices/src/common/dto/request/update-customer-invoice-by-id.request.dto.ts index 0f363b2d..01781b65 100644 --- a/modules/customer-invoices/src/common/dto/request/update-customer-invoice-by-id.request.dto.ts +++ b/modules/customer-invoices/src/common/dto/request/update-customer-invoice-by-id.request.dto.ts @@ -20,19 +20,22 @@ export const UpdateCustomerInvoiceByIdRequestSchema = z.object({ language_code: z.string().optional(), currency_code: z.string().optional(), - items: z.array( - z.object({ - is_non_valued: z.string().optional(), + items: z + .array( + z.object({ + is_non_valued: z.string().optional(), - description: z.string().optional(), - quantity: QuantitySchema.optional(), - unit_amount: MoneySchema.optional(), + description: z.string().optional(), + quantity: QuantitySchema.optional(), + unit_amount: MoneySchema.optional(), - discount_percentage: PercentageSchema.optional(), + discount_percentage: PercentageSchema.optional(), - tax_codes: z.array(z.string()).default([]), - }) - ), + tax_codes: z.array(z.string()).default([]), + }) + ) + .optional() + .default([]), }); export type UpdateCustomerInvoiceByIdRequestDTO = Partial< diff --git a/modules/customer-invoices/src/common/dto/response/list-customer-invoices.response.dto.ts b/modules/customer-invoices/src/common/dto/response/list-customer-invoices.response.dto.ts index 64db0726..031e26e5 100644 --- a/modules/customer-invoices/src/common/dto/response/list-customer-invoices.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/list-customer-invoices.response.dto.ts @@ -1,10 +1,17 @@ -import { MetadataSchema, MoneySchema, createListViewResponseSchema } from "@erp/core"; +import { + MetadataSchema, + MoneySchema, + PercentageSchema, + createPaginatedListSchema, +} from "@erp/core"; import { z } from "zod/v4"; -export const ListCustomerInvoicesResponseSchema = createListViewResponseSchema( +export const ListCustomerInvoicesResponseSchema = createPaginatedListSchema( z.object({ id: z.uuid(), company_id: z.uuid(), + is_proforma: z.boolean(), + customer_id: z.string(), invoice_number: z.string(), @@ -17,7 +24,10 @@ export const ListCustomerInvoicesResponseSchema = createListViewResponseSchema( language_code: z.string(), currency_code: z.string(), - recipient: { + reference: z.string(), + description: z.string(), + + recipient: z.object({ tin: z.string(), name: z.string(), street: z.string(), @@ -26,11 +36,12 @@ export const ListCustomerInvoicesResponseSchema = createListViewResponseSchema( postal_code: z.string(), province: z.string(), country: z.string(), - }, + }), taxes: z.string(), subtotal_amount: MoneySchema, + discount_percentage: PercentageSchema, discount_amount: MoneySchema, taxable_amount: MoneySchema, taxes_amount: MoneySchema, diff --git a/modules/customer-invoices/src/common/locales/en.json b/modules/customer-invoices/src/common/locales/en.json index 30fa4517..a4af1d77 100644 --- a/modules/customer-invoices/src/common/locales/en.json +++ b/modules/customer-invoices/src/common/locales/en.json @@ -36,12 +36,15 @@ "invoice_number": "Inv. number", "series": "Serie", "status": "Status", - "invoice_date": "Date", - "recipient_tin": "Customer TIN", + "invoice_date": "Invoice date", + "operation_date": "Operation date", + "recipient_tin": "TIN", "recipient_name": "Customer name", - "recipient_city": "Customer city", - "recipient_province": "Customer province", - "recipient_postal_code": "Customer postal code", + "recipient_street": "Street", + "recipient_city": "City", + "recipient_province": "Province", + "recipient_postal_code": "Postal code", + "recipient_country": "Country", "total_amount": "Total price" } }, diff --git a/modules/customer-invoices/src/common/locales/es.json b/modules/customer-invoices/src/common/locales/es.json index 17dd7299..4c4b5fd6 100644 --- a/modules/customer-invoices/src/common/locales/es.json +++ b/modules/customer-invoices/src/common/locales/es.json @@ -35,13 +35,16 @@ "invoice_number": "Nº factura", "series": "Serie", "status": "Estado", - "invoice_date": "Fecha", - "recipient_tin": "NIF cliente", - "recipient_name": "Nombre cliente", - "recipient_city": "Ciudad cliente", - "recipient_province": "Provincia cliente", - "recipient_postal_code": "Código postal cliente", - "total_amount": "Precio total" + "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" } }, "create": { diff --git a/modules/customer-invoices/src/web/components/customer-invoice-status-badge.tsx b/modules/customer-invoices/src/web/components/customer-invoice-status-badge.tsx index ccd2bb94..b34647d1 100644 --- a/modules/customer-invoices/src/web/components/customer-invoice-status-badge.tsx +++ b/modules/customer-invoices/src/web/components/customer-invoice-status-badge.tsx @@ -3,44 +3,46 @@ import { cn } from "@repo/shadcn-ui/lib/utils"; import { forwardRef } from "react"; import { useTranslation } from "../i18n"; -export type CustomerInvoiceStatus = "draft" | "issued" | "sent" | "received" | "rejected"; +export type CustomerInvoiceStatus = "draft" | "sent" | "approved" | "rejected" | "issued"; export type CustomerInvoiceStatusBadgeProps = { - status: string; // permitir cualquier valor + status: string | CustomerInvoiceStatus; // permitir cualquier valor + dotVisible?: boolean; className?: string; }; const statusColorConfig: Record = { draft: { badge: - "bg-gray-600/10 dark:bg-gray-600/20 hover:bg-gray-600/10 text-gray-500 border-gray-600/60", + "bg-gray-500/10 dark:bg-gray-500/20 hover:bg-gray-500/10 text-gray-600 border-gray-400/60", dot: "bg-gray-500", }, - issued: { - badge: - "bg-amber-600/10 dark:bg-amber-600/20 hover:bg-amber-600/10 text-amber-500 border-amber-600/60", - dot: "bg-amber-500", - }, sent: { badge: - "bg-cyan-600/10 dark:bg-cyan-600/20 hover:bg-cyan-600/10 text-cyan-500 border-cyan-600/60 shadow-none rounded-full", - dot: "bg-cyan-500", + "bg-amber-500/10 dark:bg-amber-500/20 hover:bg-amber-500/10 text-amber-500 border-amber-600/60", + dot: "bg-amber-500", }, - received: { + approved: { badge: - "bg-emerald-600/10 dark:bg-emerald-600/20 hover:bg-emerald-600/10 text-emerald-500 border-emerald-600/60", + "bg-emerald-500/10 dark:bg-emerald-500/20 hover:bg-emerald-500/10 text-emerald-500 border-emerald-600/60", dot: "bg-emerald-500", }, rejected: { - badge: "bg-red-600/10 dark:bg-red-600/20 hover:bg-red-600/10 text-red-500 border-red-600/60", + badge: + "bg-red-500/10 dark:bg-red-500/20 hover:bg-red-500/10 text-red-500 border-red-600/60", dot: "bg-red-500", }, + issued: { + badge: + "bg-blue-600/10 dark:bg-blue-600/20 hover:bg-blue-600/10 text-blue-500 border-blue-600/60", + dot: "bg-blue-500", + }, }; export const CustomerInvoiceStatusBadge = forwardRef< HTMLDivElement, CustomerInvoiceStatusBadgeProps ->(({ status, className, ...props }, ref) => { +>(({ status, dotVisible, className, ...props }, ref) => { const { t } = useTranslation(); const normalizedStatus = status.toLowerCase() as CustomerInvoiceStatus; const config = statusColorConfig[normalizedStatus]; @@ -56,8 +58,8 @@ export const CustomerInvoiceStatusBadge = forwardRef< return ( -
- {t(`catalog.status.${status}`)} + {dotVisible &&
} + {t(`catalog.status.${normalizedStatus}`, { defaultValue: status })} ); }); diff --git a/modules/customer-invoices/src/web/components/customer-invoices-layout.tsx b/modules/customer-invoices/src/web/components/customer-invoices-layout.tsx index 786cc726..574f42d1 100644 --- a/modules/customer-invoices/src/web/components/customer-invoices-layout.tsx +++ b/modules/customer-invoices/src/web/components/customer-invoices-layout.tsx @@ -1,5 +1,5 @@ import { PropsWithChildren } from "react"; -export const CustomerInvoicesLayout = ({ children }: PropsWithChildren) => { +export const InvoicesLayout = ({ children }: PropsWithChildren) => { return
{children}
; }; diff --git a/modules/customer-invoices/src/web/components/customer-invoices-list-grid.tsx b/modules/customer-invoices/src/web/components/customer-invoices-list-grid.tsx deleted file mode 100644 index 8ed5ebd9..00000000 --- a/modules/customer-invoices/src/web/components/customer-invoices-list-grid.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import { AG_GRID_LOCALE_ES } from "@ag-grid-community/locale"; -import type { CellKeyDownEvent, RowClickedEvent, ValueFormatterParams } from "ag-grid-community"; -import { - ColDef, - GridOptions, - SizeColumnsToContentStrategy, - SizeColumnsToFitGridStrategy, - SizeColumnsToFitProvidedWidthStrategy, -} from "ag-grid-community"; -import { useCallback, useMemo, useState } from "react"; - - -import { formatDate } from "@erp/core/client"; -import { ErrorOverlay } from "@repo/rdx-ui/components"; -import { Button } from "@repo/shadcn-ui/components"; -import { AgGridReact } from "ag-grid-react"; -import { ChevronRightIcon } from "lucide-react"; -import { useNavigate } from "react-router-dom"; -import { useCustomerInvoicesQuery } from "../hooks"; -import { useTranslation } from "../i18n"; -import { CustomerInvoiceStatusBadge } from "./customer-invoice-status-badge"; - -// Create new GridExample component -export const CustomerInvoicesListGrid = () => { - const { t } = useTranslation(); - const navigate = useNavigate(); - //const { formatCurrency } = useMoney(); - - const { - data: invoices, - isLoading, - isError, - error, - } = useCustomerInvoicesQuery({ - pagination: { pageSize: 999 }, - }); - - // Definición de columnas - const [colDefs] = useState([ - { - field: "status", - headerName: t("pages.list.grid_columns.status"), - cellRenderer: (params: ValueFormatterParams) => ( - - ), - minWidth: 120, - }, - { - field: "invoice_number", - headerName: t("pages.list.grid_columns.invoice_number"), - cellClass: "tabular-nums", - minWidth: 130, - }, - { - field: "series", - headerName: t("pages.list.grid_columns.series"), - cellClass: "tabular-nums", - minWidth: 80, - }, - { - field: "invoice_date", - headerName: t("pages.list.grid_columns.invoice_date"), - valueFormatter: (p: ValueFormatterParams) => formatDate(p.value), - cellClass: "tabular-nums", - minWidth: 130, - }, - { - field: "recipient.tin", - headerName: t("pages.list.grid_columns.recipient_tin"), - cellClass: "tabular-nums", - minWidth: 130, - }, - { - field: "recipient.name", - headerName: t("pages.list.grid_columns.recipient_name"), - minWidth: 200, - }, - { - field: "recipient.city", - headerName: t("pages.list.grid_columns.recipient_city"), - minWidth: 130, - }, - { - field: "recipient.province", - headerName: t("pages.list.grid_columns.recipient_province"), - minWidth: 130, - }, - { - field: "recipient.postal_code", - headerName: t("pages.list.grid_columns.recipient_postal_code"), - minWidth: 100, - }, - { - field: "taxable_amount", - headerName: t("pages.list.grid_columns.taxable_amount"), - type: "rightAligned", - /*valueFormatter: (params: ValueFormatterParams) => { - const raw: MoneyDTO | null = params.value; - return raw ? formatCurrency(raw) : "—"; - },*/ - cellClass: "tabular-nums", - minWidth: 130, - }, - { - field: "taxes_amount", - headerName: t("pages.list.grid_columns.taxes_amount"), - type: "rightAligned", - /*valueFormatter: (params: ValueFormatterParams) => { - const raw: MoneyDTO | null = params.value; - return raw ? formatCurrency(raw) : "—"; - },*/ - cellClass: "tabular-nums", - minWidth: 130, - }, - { - field: "total_amount", - headerName: t("pages.list.grid_columns.total_amount"), - type: "rightAligned", - /*valueFormatter: (params: ValueFormatterParams) => { - const raw: MoneyDTO | null = params.value; - return raw ? formatCurrency(raw) : "—"; - },*/ - cellClass: "tabular-nums font-semibold", - minWidth: 140, - }, - { - colId: "actions", - headerName: t("pages.list.grid_columns.actions", "Acciones"), - cellRenderer: (params: ValueFormatterParams) => { - const id = params.data?.id; - if (!id) return null; - return ( - - ); - }, - minWidth: 80, - maxWidth: 80, - pinned: "right", - }, - ]); - - // Navegación accesible (click o teclado) - const goToRow = useCallback( - (id: string, newTab = false) => { - const url = `/customer-invoices/${id}/edit`; - newTab - ? window.open(url, "_blank", "noopener,noreferrer") - : navigate(url); - }, - [navigate] - ); - - const onRowClicked = useCallback( - (e: RowClickedEvent) => { - if (!e.data) return; - const newTab = - e.event instanceof MouseEvent && (e.event.metaKey || e.event.ctrlKey); - goToRow(e.data.id, newTab); - }, - [goToRow] - ); - - const onCellKeyDown = useCallback( - (e: CellKeyDownEvent) => { - if (!e.data) return; - - const ev = e.event; - if (!ev || !(ev instanceof KeyboardEvent)) return; - - const key = ev.key; - if (key === "Enter" || key === " ") { - ev.preventDefault(); - goToRow(e.data.id); - } - if ((ev.ctrlKey || ev.metaKey) && key === "Enter") { - ev.preventDefault(); - goToRow(e.data.id, true); - } - }, - [goToRow] - ); - - // Estrategia de autoajuste de columnas - const autoSizeStrategy = useMemo< - | SizeColumnsToFitGridStrategy - | SizeColumnsToFitProvidedWidthStrategy - | SizeColumnsToContentStrategy - >( - () => ({ - type: "fitGridWidth", - defaultMinWidth: 100, - columnLimits: [{ colId: "actions", minWidth: 80, maxWidth: 80 }], - }), - [] - ); - - // Config general de AG Grid - const gridOptions: GridOptions = useMemo( - () => ({ - columnDefs: colDefs, - autoSizeStrategy, - defaultColDef: { - editable: false, - flex: 1, - filter: false, - sortable: false, - resizable: true, - }, - pagination: true, - paginationPageSize: 20, - paginationPageSizeSelector: [10, 20, 30, 50], - localeText: AG_GRID_LOCALE_ES, - suppressRowClickSelection: true, - getRowClass: () => "clickable-row", - onCellKeyDown, - onRowClicked, - getRowId: (p) => p.data.id, - }), - [autoSizeStrategy, colDefs, onCellKeyDown, onRowClicked] - ); - - // Error al cargar - if (isError) { - return ( - - ); - } - - // Render principal - return ( -
- -
- ); -}; \ No newline at end of file diff --git a/modules/customer-invoices/src/web/components/editor/items/items-editor.tsx b/modules/customer-invoices/src/web/components/editor/items/items-editor.tsx index b3a065da..59f084f2 100644 --- a/modules/customer-invoices/src/web/components/editor/items/items-editor.tsx +++ b/modules/customer-invoices/src/web/components/editor/items/items-editor.tsx @@ -26,8 +26,6 @@ export const ItemsEditor = () => { name: "items", }); - console.log(fields); - const baseColumns = useWithRowSelection(useItemsColumns(), true); const columns = useMemo( () => [...baseColumns, debugIdCol], diff --git a/modules/customer-invoices/src/web/components/index.tsx b/modules/customer-invoices/src/web/components/index.tsx index b58f5cf1..d16d34bd 100644 --- a/modules/customer-invoices/src/web/components/index.tsx +++ b/modules/customer-invoices/src/web/components/index.tsx @@ -1,10 +1,11 @@ +export * from "../pages/list/invoices-list-grid"; export * from "./customer-invoice-editor-skeleton"; export * from "./customer-invoice-prices-card"; export * from "./customer-invoice-status-badge"; export * from "./customer-invoice-taxes-multi-select"; export * from "./customer-invoices-layout"; -export * from "./customer-invoices-list-grid"; export * from "./editor"; export * from "./editor/invoice-tax-summary"; export * from "./editor/invoice-totals"; export * from "./page-header"; + diff --git a/modules/customer-invoices/src/web/customer-invoice-routes.tsx b/modules/customer-invoices/src/web/customer-invoice-routes.tsx index c8288d64..e0cf5071 100644 --- a/modules/customer-invoices/src/web/customer-invoice-routes.tsx +++ b/modules/customer-invoices/src/web/customer-invoice-routes.tsx @@ -3,18 +3,18 @@ import { lazy } from "react"; import { Outlet, RouteObject } from "react-router-dom"; // Lazy load components -const CustomerInvoicesLayout = lazy(() => - import("./components").then((m) => ({ default: m.CustomerInvoicesLayout })) +const InvoicesLayout = lazy(() => + import("./components").then((m) => ({ default: m.InvoicesLayout })) ); -const CustomerInvoicesList = lazy(() => - import("./pages").then((m) => ({ default: m.CustomerInvoicesList })) +const InvoiceListPage = lazy(() => + import("./pages").then((m) => ({ default: m.InvoiceListPage })) ); const CustomerInvoiceAdd = lazy(() => import("./pages").then((m) => ({ default: m.CustomerInvoiceCreate })) ); -const CustomerInvoiceUpdate = lazy(() => +const InvoiceUpdatePage = lazy(() => import("./pages").then((m) => ({ default: m.InvoiceUpdatePage })) ); @@ -23,15 +23,15 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] { path: "customer-invoices", element: ( - + - + ), children: [ - { path: "", index: true, element: }, // index - { path: "list", element: }, + { path: "", index: true, element: }, // index + { path: "list", element: }, { path: "create", element: }, - { path: ":id/edit", element: }, + { path: ":id/edit", element: }, // /*{ path: "create", element: }, diff --git a/modules/customer-invoices/src/web/hooks/use-customer-invoices-query.tsx b/modules/customer-invoices/src/web/hooks/use-customer-invoices-query.tsx index 2524d211..eb48902d 100644 --- a/modules/customer-invoices/src/web/hooks/use-customer-invoices-query.tsx +++ b/modules/customer-invoices/src/web/hooks/use-customer-invoices-query.tsx @@ -1,22 +1,30 @@ -import { useDataSource, useQueryKey } from "@erp/core/hooks"; -import { useQuery } from "@tanstack/react-query"; -import { ListCustomerInvoicesResponseDTO } from "../../common/dto"; +import { CriteriaDTO } from '@erp/core'; +import { useDataSource } from "@erp/core/hooks"; +import { DefaultError, QueryKey, useQuery } from "@tanstack/react-query"; +import { CustomerInvoicesPage } from '../schemas'; + +export const CUSTOMER_INVOICES_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => + ["customer_invoices", criteria] as const; + +type InvoicesQueryOptions = { + enabled?: boolean; + criteria?: CriteriaDTO +}; // Obtener todas las facturas -export const useCustomerInvoicesQuery = (params?: any) => { +export const useInvoicesQuery = (options?: InvoicesQueryOptions) => { const dataSource = useDataSource(); - const keys = useQueryKey(); + const enabled = options?.enabled ?? true; + const criteria = options?.criteria ?? {}; - return useQuery({ - queryKey: keys().data().resource("customer-invoices").action("list").params(params).get(), - queryFn: async (context) => { - const { signal } = context; - const invoices = await dataSource.getList("customer-invoices", { + return useQuery({ + queryKey: CUSTOMER_INVOICES_QUERY_KEY(criteria), + queryFn: async ({ signal }) => { + return await dataSource.getList("customer-invoices", { + params: criteria, signal, - ...params, }); - - return invoices as ListCustomerInvoicesResponseDTO; }, + enabled }); }; 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 0e3a7c8d..59f718df 100644 --- a/modules/customer-invoices/src/web/hooks/use-invoice-query.ts +++ b/modules/customer-invoices/src/web/hooks/use-invoice-query.ts @@ -5,11 +5,11 @@ import { CustomerInvoice } from "../schemas"; export const CUSTOMER_INVOICE_QUERY_KEY = (id: string): QueryKey => ["customer_invoice", id] as const; -type CustomerInvoiceQueryOptions = { +type InvoiceQueryOptions = { enabled?: boolean; }; -export function useInvoiceQuery(invoiceId?: string, options?: CustomerInvoiceQueryOptions) { +export const useInvoiceQuery = (invoiceId?: string, options?: InvoiceQueryOptions) => { const dataSource = useDataSource(); const enabled = (options?.enabled ?? true) && !!invoiceId; @@ -26,7 +26,7 @@ export function useInvoiceQuery(invoiceId?: string, options?: CustomerInvoiceQue }, enabled, }); -} +}; /* export function useQuery< diff --git a/modules/customer-invoices/src/web/pages/customer-invoices-list.tsx b/modules/customer-invoices/src/web/pages/customer-invoices-list.tsx deleted file mode 100644 index 06d6d97a..00000000 --- a/modules/customer-invoices/src/web/pages/customer-invoices-list.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { AppBreadcrumb, AppContent } from "@repo/rdx-ui/components"; -import { Button } from "@repo/shadcn-ui/components"; -import { PlusIcon } from "lucide-react"; -import { useNavigate } from "react-router-dom"; -import { CustomerInvoicesListGrid } from "../components"; -import { useTranslation } from "../i18n"; - -export const CustomerInvoicesList = () => { - const { t } = useTranslation(); - const navigate = useNavigate(); - - return ( - <> - - -
-
-

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

-

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

-
-
- -
-
-
- -
-
- - ); -}; - -/* - return ( - <> -
-
-

- {t('customerInvoices.list.title' /> -

-

- {t('CustomerInvoices.list.subtitle' /> -

-
-
- -
-
- - -
-
- - {CustomerInvoiceStatuses.map((s) => ( - - {s.label} - - ))} - -
- - -
-
-
- {CustomerInvoiceStatuses.map((s) => ( - - - - ))} -
- - ); -}; -*/ diff --git a/modules/customer-invoices/src/web/pages/index.ts b/modules/customer-invoices/src/web/pages/index.ts index 9863739a..d73064af 100644 --- a/modules/customer-invoices/src/web/pages/index.ts +++ b/modules/customer-invoices/src/web/pages/index.ts @@ -1,3 +1,3 @@ export * from "./create"; -export * from "./customer-invoices-list"; +export * from "./list"; export * from "./update"; diff --git a/modules/customer-invoices/src/web/pages/list/index.ts b/modules/customer-invoices/src/web/pages/list/index.ts new file mode 100644 index 00000000..6271cb91 --- /dev/null +++ b/modules/customer-invoices/src/web/pages/list/index.ts @@ -0,0 +1 @@ +export * from "./invoices-list-page"; 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 new file mode 100644 index 00000000..73c5e6a0 --- /dev/null +++ b/modules/customer-invoices/src/web/pages/list/invoices-list-grid.tsx @@ -0,0 +1,110 @@ +import type { CellKeyDownEvent, RowClickedEvent } from "ag-grid-community"; +import { useCallback, useState } from "react"; + + +import { DataTable } from '@repo/rdx-ui/components'; +import { Button, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@repo/shadcn-ui/components'; +import { FileDownIcon, FilterIcon, SearchIcon } from 'lucide-react'; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "../../i18n"; +import { CustomerInvoicesPage } from '../../schemas'; +import { useInvoicesListColumns } from './use-invoices-list-columns'; + +export type InvoiceUpdateCompProps = { + invoicesPage: CustomerInvoicesPage; + loading?: boolean; +} + + +// Create new GridExample component +export const InvoicesListGrid = ({ invoicesPage, loading }: InvoiceUpdateCompProps) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + const [searchTerm, setSearchTerm] = useState(""); + const [statusFilter, setStatusFilter] = useState("todas"); + + const columns = useInvoicesListColumns(); + + const { items, page, total_pages, total_items } = invoicesPage; + + + // Navegación accesible (click o teclado) + const goToRow = useCallback( + (id: string, newTab = false) => { + const url = `/customer-invoices/${id}/edit`; + newTab + ? window.open(url, "_blank", "noopener,noreferrer") + : navigate(url); + }, + [navigate] + ); + + const onRowClicked = useCallback( + (e: RowClickedEvent) => { + if (!e.data) return; + const newTab = + e.event instanceof MouseEvent && (e.event.metaKey || e.event.ctrlKey); + goToRow(e.data.id, newTab); + }, + [goToRow] + ); + + const onCellKeyDown = useCallback( + (e: CellKeyDownEvent) => { + if (!e.data) return; + + const ev = e.event; + if (!ev || !(ev instanceof KeyboardEvent)) return; + + const key = ev.key; + if (key === "Enter" || key === " ") { + ev.preventDefault(); + goToRow(e.data.id); + } + if ((ev.ctrlKey || ev.metaKey) && key === "Enter") { + ev.preventDefault(); + goToRow(e.data.id, true); + } + }, + [goToRow] + ); + + + // Render principal + return ( + <> + {/* Filters and Actions */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-10 " + /> +
+ + +
+
+ +
+ + ); +}; \ No newline at end of file diff --git a/modules/customer-invoices/src/web/pages/list/invoices-list-page.tsx b/modules/customer-invoices/src/web/pages/list/invoices-list-page.tsx new file mode 100644 index 00000000..f75cc4d0 --- /dev/null +++ b/modules/customer-invoices/src/web/pages/list/invoices-list-page.tsx @@ -0,0 +1,100 @@ +import { ErrorAlert } from '@erp/customers/components'; +import { AppBreadcrumb, AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components"; +import { Button } from "@repo/shadcn-ui/components"; +import { FilePenIcon, PlusIcon } from "lucide-react"; +import { useMemo } from 'react'; +import { useNavigate } from "react-router-dom"; +import { InvoicesListGrid, PageHeader } from '../../components'; +import { useInvoicesQuery } from '../../hooks'; +import { useTranslation } from "../../i18n"; +import { invoiceResumeDtoToFormAdapter } from '../../schemas/invoice-resume-dto.adapter'; + +export const InvoiceListPage = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + const { + data, + isLoading, + isError, + error, + } = useInvoicesQuery({ + criteria: { + pageSize: 999, + } + }); + + const invoicesPageData = useMemo(() => { + if (!data) return undefined; + return { + ...data, + items: invoiceResumeDtoToFormAdapter.fromDto(data.items) + } + }, [data]); + + + if (isError || !invoicesPageData) { + return ( + + + + + ); + } + + return ( + <> + + + } + rightSlot={ + <>} + + + /> + {/* Header */} +
+
+

+ Facturas +

+

Gestiona y consulta todas tus facturas de cliente

+
+ + + +
+ +
+ +
+
+

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

+

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

+
+
+ +
+
+
+ +
+
+ + ); +}; diff --git a/modules/customer-invoices/src/web/pages/list/use-invoices-list-columns.tsx b/modules/customer-invoices/src/web/pages/list/use-invoices-list-columns.tsx new file mode 100644 index 00000000..eea9e1ea --- /dev/null +++ b/modules/customer-invoices/src/web/pages/list/use-invoices-list-columns.tsx @@ -0,0 +1,120 @@ +import { formatDate } from '@erp/core/client'; +import { DataTableColumnHeader } from '@repo/rdx-ui/components'; +import type { ColumnDef } from "@tanstack/react-table"; +import * as React from "react"; +import { CustomerInvoiceStatusBadge } from '../../components'; +import { useTranslation } from '../../i18n'; +import { InvoicesPageFormData } from '../../schemas/invoice-resume.form.schema'; + + + +export function useInvoicesListColumns(): ColumnDef[] { + //const { t, readOnly, currency_code, language_code } = useInvoiceContext(); + const { t } = useTranslation(); + + // Atención: Memoizar siempre para evitar reconstrucciones y resets de estado de tabla + return React.useMemo[]>(() => [ + { + accessorKey: "invoice_number", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.getValue('invoice_number')} +
+ ), + enableHiding: false, + enableSorting: false, + size: 32, + }, { + accessorKey: "status", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + enableSorting: false, + size: 32, + }, { + accessorKey: "series", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.getValue('series')} +
+ + ), + enableSorting: false, + size: 32, + }, { + accessorKey: "invoice_date", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {formatDate(row.getValue('invoice_date'))} +
+ + ), + enableSorting: false, + size: 32, + }, { + accessorKey: "operation_date", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {formatDate(row.getValue('operation_date'))} +
+ + ), + enableSorting: false, + size: 32, + }, { + id: "recipient_tin", + accessorKey: "recipient.tin", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.getValue('recipient_tin')} +
+ ), + enableSorting: false, + size: 32, + }, { + accessorKey: "recipient.name", + id: "recipient_name", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.getValue('recipient_name')} +
+ ), + enableSorting: false, + size: 32, + }, { + accessorKey: "total_amount_fmt", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.getValue('total_amount_fmt')} +
+ ), + enableSorting: false, + size: 32, + } + + ], [t]); +} diff --git a/modules/customer-invoices/src/web/pages/update/invoice-update-comp.tsx b/modules/customer-invoices/src/web/pages/update/invoice-update-comp.tsx index 6f422ce1..6fd7324f 100644 --- a/modules/customer-invoices/src/web/pages/update/invoice-update-comp.tsx +++ b/modules/customer-invoices/src/web/pages/update/invoice-update-comp.tsx @@ -60,8 +60,10 @@ export const InvoiceUpdateComp = ({ }); const handleSubmit = (formData: InvoiceFormData) => { + const dto = invoiceDtoToFormAdapter.toDto(formData, context) + console.log("dto => ", dto); mutate( - { id: invoice_id, data: formData }, + { id: invoice_id, data: dto as Partial }, { onSuccess: () => showSuccessToast(t("pages.update.successTitle")), onError: (e) => showErrorToast(t("pages.update.errorTitle"), e.message), diff --git a/modules/customer-invoices/src/web/pages/update/invoice-update-page.tsx b/modules/customer-invoices/src/web/pages/update/invoice-update-page.tsx index f0f2e888..3fa4697b 100644 --- a/modules/customer-invoices/src/web/pages/update/invoice-update-page.tsx +++ b/modules/customer-invoices/src/web/pages/update/invoice-update-page.tsx @@ -19,11 +19,7 @@ export const InvoiceUpdatePage = () => { const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []); const invoiceQuery = useInvoiceQuery(invoice_id, { enabled: !!invoice_id }); - const { data: invoiceData, isLoading, isError, error, - - } = invoiceQuery; - - console.log("InvoiceUpdatePage"); + const { data: invoiceData, isLoading, isError, error } = invoiceQuery; if (isLoading) { return ; diff --git a/modules/customer-invoices/src/web/schemas/index.ts b/modules/customer-invoices/src/web/schemas/index.ts index d37fdaf1..ded0809d 100644 --- a/modules/customer-invoices/src/web/schemas/index.ts +++ b/modules/customer-invoices/src/web/schemas/index.ts @@ -1,3 +1,5 @@ -export * from "./customer-invoices.api.schema"; export * from "./invoice-dto.adapter"; +export * from "./invoice-resume-dto.adapter"; +export * from "./invoice-resume.form.schema"; export * from "./invoice.form.schema"; +export * from "./invoices.api.schema"; diff --git a/modules/customer-invoices/src/web/schemas/invoice-resume-dto.adapter.ts b/modules/customer-invoices/src/web/schemas/invoice-resume-dto.adapter.ts new file mode 100644 index 00000000..cf98ba52 --- /dev/null +++ b/modules/customer-invoices/src/web/schemas/invoice-resume-dto.adapter.ts @@ -0,0 +1,62 @@ +import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core"; +import { InvoiceSummaryFormData } from "./invoice-resume.form.schema"; +import { CustomerInvoiceSummary } from "./invoices.api.schema"; + +/** + * Convierte el DTO completo de API a datos numéricos para el formulario. + */ +export const invoiceResumeDtoToFormAdapter = { + fromDto(dtos: CustomerInvoiceSummary[], context?: any) { + return dtos.map( + (dto) => + ({ + ...dto, + + subtotal_amount: MoneyDTOHelper.toNumber(dto.subtotal_amount), + subtotal_amount_fmt: formatCurrency( + MoneyDTOHelper.toNumber(dto.subtotal_amount), + Number(dto.total_amount.scale || 2), + dto.currency_code, + dto.language_code + ), + + discount_percentage: PercentageDTOHelper.toNumber(dto.discount_percentage), + discount_percentage_fmt: PercentageDTOHelper.toNumericString(dto.discount_percentage), + + discount_amount: MoneyDTOHelper.toNumber(dto.discount_amount), + discount_amount_fmt: formatCurrency( + MoneyDTOHelper.toNumber(dto.discount_amount), + Number(dto.total_amount.scale || 2), + dto.currency_code, + dto.language_code + ), + + taxable_amount: MoneyDTOHelper.toNumber(dto.taxable_amount), + taxable_amount_fmt: formatCurrency( + MoneyDTOHelper.toNumber(dto.taxable_amount), + Number(dto.total_amount.scale || 2), + dto.currency_code, + dto.language_code + ), + + taxes_amount: MoneyDTOHelper.toNumber(dto.taxes_amount), + taxes_amount_fmt: formatCurrency( + MoneyDTOHelper.toNumber(dto.taxes_amount), + Number(dto.total_amount.scale || 2), + dto.currency_code, + dto.language_code + ), + + total_amount: MoneyDTOHelper.toNumber(dto.total_amount), + total_amount_fmt: formatCurrency( + MoneyDTOHelper.toNumber(dto.total_amount), + Number(dto.total_amount.scale || 2), + dto.currency_code, + dto.language_code + ), + + //taxes: dto.taxes, + }) as unknown as InvoiceSummaryFormData + ); + }, +}; diff --git a/modules/customer-invoices/src/web/schemas/invoice-resume.form.schema.ts b/modules/customer-invoices/src/web/schemas/invoice-resume.form.schema.ts new file mode 100644 index 00000000..0a131831 --- /dev/null +++ b/modules/customer-invoices/src/web/schemas/invoice-resume.form.schema.ts @@ -0,0 +1,25 @@ +import { CustomerInvoiceSummary, CustomerInvoicesPage } from "./invoices.api.schema"; + +export type InvoiceSummaryFormData = CustomerInvoiceSummary & { + 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 InvoicesPageFormData = CustomerInvoicesPage & { + items: InvoiceSummaryFormData[]; +}; diff --git a/modules/customer-invoices/src/web/schemas/customer-invoices.api.schema.ts b/modules/customer-invoices/src/web/schemas/invoices.api.schema.ts similarity index 77% rename from modules/customer-invoices/src/web/schemas/customer-invoices.api.schema.ts rename to modules/customer-invoices/src/web/schemas/invoices.api.schema.ts index 4cc739de..79c98186 100644 --- a/modules/customer-invoices/src/web/schemas/customer-invoices.api.schema.ts +++ b/modules/customer-invoices/src/web/schemas/invoices.api.schema.ts @@ -1,14 +1,14 @@ import { z } from "zod/v4"; +import { PaginationSchema } from "@erp/core"; import { ArrayElement } from "@repo/rdx-utils"; import { CreateCustomerInvoiceRequestSchema, GetCustomerInvoiceByIdResponseSchema, - ListCustomerInvoicesResponseDTO, + ListCustomerInvoicesResponseSchema, UpdateCustomerInvoiceByIdRequestSchema, } from "../../common"; -// Esquemas (Zod) provenientes del servidor export const CustomerInvoiceSchema = GetCustomerInvoiceByIdResponseSchema.omit({ metadata: true, }); @@ -23,7 +23,12 @@ export type CustomerInvoiceCreateInput = z.infer; // Cuerpo para actualizar // Resultado de consulta con criteria (paginado, etc.) -export type CustomerInvoicesPage = ListCustomerInvoicesResponseDTO; +export const CustomerInvoicesPageSchema = ListCustomerInvoicesResponseSchema.omit({ + metadata: true, +}); + +export type PaginatedResponse = z.infer; +export type CustomerInvoicesPage = z.infer; // Ítem simplificado dentro del listado (no toda la entidad) export type CustomerInvoiceSummary = Omit, "metadata">; diff --git a/modules/customers/src/api/infrastructure/mappers/domain/customer.mapper.ts b/modules/customers/src/api/infrastructure/mappers/domain/customer.mapper.ts index e3adbaf6..9a91c71a 100644 --- a/modules/customers/src/api/infrastructure/mappers/domain/customer.mapper.ts +++ b/modules/customers/src/api/infrastructure/mappers/domain/customer.mapper.ts @@ -197,7 +197,7 @@ export class CustomerDomainMapper // Si hubo errores de mapeo, devolvemos colección de validación if (errors.length > 0) { - return Result.fail(new ValidationErrorCollection(errors)); + return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors)); } const customerProps: CustomerProps = { diff --git a/modules/customers/src/common/dto/response/list-customers.response.dto.ts b/modules/customers/src/common/dto/response/list-customers.response.dto.ts index c890082c..681889e2 100644 --- a/modules/customers/src/common/dto/response/list-customers.response.dto.ts +++ b/modules/customers/src/common/dto/response/list-customers.response.dto.ts @@ -1,7 +1,7 @@ -import { MetadataSchema, createListViewResponseSchema } from "@erp/core"; +import { MetadataSchema, createPaginatedListSchema } from "@erp/core"; import { z } from "zod/v4"; -export const ListCustomersResponseSchema = createListViewResponseSchema( +export const ListCustomersResponseSchema = createPaginatedListSchema( z.object({ id: z.uuid(), company_id: z.uuid(), diff --git a/packages/rdx-ddd/src/errors/validation-error-collection.ts b/packages/rdx-ddd/src/errors/validation-error-collection.ts index d0ef914b..0e09fd6d 100644 --- a/packages/rdx-ddd/src/errors/validation-error-collection.ts +++ b/packages/rdx-ddd/src/errors/validation-error-collection.ts @@ -11,7 +11,7 @@ * { path: "lines[1].unitPrice.amount", message: "Amount must be a positive number" }, * { path: "lines[1].unitPrice.scale", message: "Scale must be a non-negative integer" }, * ]; - * const validationError = new ValidationErrorCollection(errors); + * const validationError = new ValidationErrorCollection(message, errors); * */ @@ -36,7 +36,7 @@ export interface ValidationErrorDetail { * { path: "lines[1].unitPrice.amount", message: "Amount must be positive" }, * { path: "lines[1].unitPrice.scale", message: "Scale must be non-negative" }, * ]; - * throw new ValidationErrorCollection(errors); + * throw new ValidationErrorCollection(message, errors); */ export class ValidationErrorCollection extends DomainError { public readonly kind = "VALIDATION" as const; @@ -44,10 +44,11 @@ export class ValidationErrorCollection extends DomainError { public readonly details: ValidationErrorDetail[]; constructor( + message: string, details: ValidationErrorDetail[], options?: ErrorOptions & { metadata?: Record } ) { - super("Multiple validation errors", "MULTIPLE_VALIDATION_ERRORS", { + super(message, "MULTIPLE_VALIDATION_ERRORS", { ...options, metadata: { ...(options?.metadata ?? {}), errors: details }, }); @@ -60,6 +61,7 @@ export class ValidationErrorCollection extends DomainError { /** Crear a partir de varios DomainValidationError */ static fromErrors( + message: string, errors: DomainValidationError[], options?: ErrorOptions & { metadata?: Record } ): ValidationErrorCollection { @@ -69,7 +71,7 @@ export class ValidationErrorCollection extends DomainError { value: (e as any).cause, // opcional: valor que provocó el error })); - return new ValidationErrorCollection(details, options); + return new ValidationErrorCollection(message, details, options); } /** Serialización para Problem+JSON / logs */ diff --git a/packages/rdx-ddd/src/helpers/extract-or-push-error.ts b/packages/rdx-ddd/src/helpers/extract-or-push-error.ts index cce96696..7a7d0f9b 100644 --- a/packages/rdx-ddd/src/helpers/extract-or-push-error.ts +++ b/packages/rdx-ddd/src/helpers/extract-or-push-error.ts @@ -39,7 +39,8 @@ export function extractOrPushError( if (isValidationErrorCollection(error)) { // Copiar todos los detalles, rellenando path si falta - error.details.forEach((detail) => { + console.log(error); + error.details?.forEach((detail) => { errors.push({ ...detail, path: detail.path ?? path, diff --git a/packages/rdx-ui/src/components/datatable/data-table-column-header.tsx b/packages/rdx-ui/src/components/datatable/data-table-column-header.tsx index 6baec231..668570e3 100644 --- a/packages/rdx-ui/src/components/datatable/data-table-column-header.tsx +++ b/packages/rdx-ui/src/components/datatable/data-table-column-header.tsx @@ -26,7 +26,7 @@ export function DataTableColumnHeader({ const { t } = useTranslation(); if (!column.getCanSort()) { - return
{title}
+ return
{title}
} return ( diff --git a/packages/rdx-ui/src/components/datatable/data-table-pagination.tsx b/packages/rdx-ui/src/components/datatable/data-table-pagination.tsx index 4c0d3ac4..3cde5d36 100644 --- a/packages/rdx-ui/src/components/datatable/data-table-pagination.tsx +++ b/packages/rdx-ui/src/components/datatable/data-table-pagination.tsx @@ -1,104 +1,157 @@ -import { Table } from "@tanstack/react-table" +import { Table } from "@tanstack/react-table"; import { - ChevronLeft, - ChevronRight, - ChevronsLeft, - ChevronsRight, -} from "lucide-react" + ChevronLeftIcon, + ChevronRightIcon, + ChevronsLeftIcon, + ChevronsRightIcon +} from "lucide-react"; import { - Button, Select, + Pagination, PaginationContent, + PaginationItem, PaginationLink, + Select, SelectContent, SelectItem, SelectTrigger, SelectValue -} from '@repo/shadcn-ui/components' +} from '@repo/shadcn-ui/components'; +import { cn } from '@repo/shadcn-ui/lib/utils'; +import { useTranslation } from '../../locales/i18n.ts'; +import { DataTableMeta } from './data-table.tsx'; interface DataTablePaginationProps { - table: Table + table: Table; + className?: string; } -export function DataTablePagination({ - table, -}: DataTablePaginationProps) { +export function DataTablePagination({ table, className }: DataTablePaginationProps) { + const { t } = useTranslation(); + + const { pageIndex, pageSize } = table.getState().pagination; + const pageCount = table.getPageCount() || 1; + const totalRows = (table.options.meta as DataTableMeta)?.totalItems ?? table.getFilteredRowModel().rows.length; + const hasSelected = table.getFilteredSelectedRowModel().rows.length > 0; + + // Calcula rango visible (inicio-fin) + const start = totalRows === 0 ? 0 : pageIndex * pageSize + 1; + const end = Math.min((pageIndex + 1) * pageSize, totalRows); + return ( -
-
- {table.getFilteredSelectedRowModel().rows.length} of{" "} - {table.getFilteredRowModel().rows.length} row(s) selected. -
-
-
-

Rows per page

+
+ {/* Información izquierda */} +
+ {/* Rango visible */} + + {t("components.datatable.pagination.showing_range", { + start, + end, + total: totalRows, + })} + + + {/* Selección de filas */} + {hasSelected && ( + + {t("components.datatable.pagination.rows_selected", { + count: table.getFilteredSelectedRowModel().rows.length, + total: table.getFilteredRowModel().rows.length, + })} + + )} + +
+ + {t("components.datatable.pagination.rows_per_page")} +
-
- Page {table.getState().pagination.pageIndex + 1} of{" "} - {table.getPageCount()} -
-
- - - - -
+
+ + {/* Controles derecha */} +
+ + + {/* Primera página */} + + table.setPageIndex(0)} + isActive={!table.getCanPreviousPage()} + size="sm" + className="px-2.5" + > + + + + + {/* Anterior */} + + table.previousPage()} + isActive={!table.getCanPreviousPage()} + size="sm" + className="px-2.5" + > + + + + + + {t("components.datatable.pagination.page_of", { + page: pageIndex + 1, + of: pageCount || 1, + })} + + + {/* Siguiente */} + + table.nextPage()} + isActive={!table.getCanNextPage()} + size="sm" + className="px-2.5" + > + + + + + {/* Última página */} + + table.setPageIndex(pageCount - 1)} + isActive={!table.getCanNextPage()} + size="sm" + className="px-2.5" + > + + + + +
- ) -} + ); +} \ No newline at end of file diff --git a/packages/rdx-ui/src/components/datatable/data-table-toolbar.tsx b/packages/rdx-ui/src/components/datatable/data-table-toolbar.tsx index 9f17a833..01262c4b 100644 --- a/packages/rdx-ui/src/components/datatable/data-table-toolbar.tsx +++ b/packages/rdx-ui/src/components/datatable/data-table-toolbar.tsx @@ -2,61 +2,79 @@ import { Button, Separator, Tooltip, TooltipContent, TooltipTrigger } from '@rep import { cn } from '@repo/shadcn-ui/lib/utils' import { Table } from "@tanstack/react-table" import { ArrowDownIcon, ArrowUpIcon, CopyPlusIcon, PlusIcon, ScanIcon, TrashIcon } from 'lucide-react' -import { useCallback, useMemo } from 'react' +import React from 'react' import { useTranslation } from "../../locales/i18n.ts" import { DataTableViewOptions } from './data-table-view-options.tsx' import { DataTableMeta } from './data-table.tsx' + interface DataTableToolbarProps { - table: Table - showViewOptions?: boolean + table: Table; + showViewOptions?: boolean; + className?: string; } export function DataTableToolbar({ table, showViewOptions = true, + className }: DataTableToolbarProps) { + const { t } = useTranslation(); + const meta = table.options.meta as DataTableMeta | undefined; - const { t } = useTranslation() - const meta = table.options.meta as DataTableMeta - | undefined + const readOnly = meta?.readOnly ?? false; - const rowSelection = table.getSelectedRowModel().rows; - const selectedCount = rowSelection.length; + // Modelos y conteos + const allRows = table.getFilteredRowModel().rows; + const selectedRows = table.getSelectedRowModel().rows; + const totalCount = allRows.length; + const selectedCount = selectedRows.length; const hasSelection = selectedCount > 0; - const selectedRowIndexes = useMemo(() => rowSelection.map((row) => row.index), [rowSelection]); + // Índices seleccionados (memoizado) + const selectedIndexes = React.useMemo( + () => selectedRows.map((r) => r.index), + [selectedRows] + ); - const handleAdd = useCallback(() => meta?.tableOps?.onAdd?.(table), [meta]) - const handleDuplicateSelected = useCallback( - () => meta?.bulkOps?.duplicateSelected?.(selectedRowIndexes, table), - [meta, selectedRowIndexes] - ) - const handleMoveSelectedUp = useCallback( - () => meta?.bulkOps?.moveSelectedUp?.(selectedRowIndexes, table), - [meta, selectedRowIndexes] - ) - const handleMoveSelectedDown = useCallback( - () => meta?.bulkOps?.moveSelectedDown?.(selectedRowIndexes, table), - [meta, selectedRowIndexes] - ) - const handleRemoveSelected = useCallback( - () => meta?.bulkOps?.removeSelected?.(selectedRowIndexes, table), - [meta, selectedRowIndexes] - ) + const handleAdd = React.useCallback(() => { + if (!readOnly) meta?.tableOps?.onAdd?.(table); + }, [meta, table, readOnly]); + const handleDuplicateSelected = React.useCallback(() => { + if (!readOnly) meta?.bulkOps?.duplicateSelected?.(selectedIndexes, table); + }, [meta, selectedIndexes, table, readOnly]); + + const handleMoveSelectedUp = React.useCallback(() => { + if (!readOnly) meta?.bulkOps?.moveSelectedUp?.(selectedIndexes, table); + }, [meta, selectedIndexes, table, readOnly]); + + const handleMoveSelectedDown = React.useCallback(() => { + if (!readOnly) meta?.bulkOps?.moveSelectedDown?.(selectedIndexes, table); + }, [meta, selectedIndexes, table, readOnly]); + + const handleRemoveSelected = React.useCallback(() => { + if (!readOnly) meta?.bulkOps?.removeSelected?.(selectedIndexes, table); + }, [meta, selectedIndexes, table, readOnly]); + + const handleClearSelection = React.useCallback(() => { + table.resetRowSelection(); + }, [table]); + + // Render principal return (
- {/* IZQUIERDA: acciones globales y sobre selección */} -
- {meta?.tableOps?.onAdd && ( + {/* IZQUIERDA: acciones + contador */} +
+ {/* Botón añadir */} + {!readOnly && meta?.tableOps?.onAdd && ( )} + {/* Acciones sobre selección */} {hasSelection && ( <> - {meta?.bulkOps?.duplicateSelected && ( + {!readOnly && meta?.bulkOps?.duplicateSelected && ( )} - {meta?.bulkOps?.moveSelectedUp && ( + {!readOnly && meta?.bulkOps?.moveSelectedUp && ( - - {t("components.datatable.actions.move_up")} + + {t("components.datatable.actions.move_up")} + - )} - {meta?.bulkOps?.moveSelectedDown && ( + {!readOnly && meta?.bulkOps?.moveSelectedDown && ( - - - {t("components.datatable.actions.move_down")} + + {t("components.datatable.actions.move_down")} + - )} - {meta?.bulkOps?.removeSelected && ( + {!readOnly && meta?.bulkOps?.removeSelected && ( <> - - + - Quita la selección + + {t("components.datatable.actions.clear_selection")} + - )} + + {/* Contador de selección */} +
+ {hasSelection + ? t("components.datatable.selection_summary", { + count: selectedCount, + total: totalCount, + }) + : t("components.datatable.selection_none", { total: totalCount })} +
- {/* DERECHA: opciones de vista / filtros */} + {/* DERECHA: opciones de vista */}
- {showViewOptions && } + {showViewOptions && !readOnly && }
- ) + ); } + +export const MemoizedDataTableToolbar = React.memo(DataTableToolbar) as typeof DataTableToolbar; \ No newline at end of file diff --git a/packages/rdx-ui/src/components/datatable/data-table.tsx b/packages/rdx-ui/src/components/datatable/data-table.tsx index cf912374..181850b0 100644 --- a/packages/rdx-ui/src/components/datatable/data-table.tsx +++ b/packages/rdx-ui/src/components/datatable/data-table.tsx @@ -61,6 +61,9 @@ export type DataTableBulkRowOps = { }; export type DataTableMeta = TableMeta & { + totalItems?: number; // para paginación server-side + readOnly?: boolean; + tableOps?: DataTableOps rowOps?: DataTableRowOps bulkOps?: DataTableBulkRowOps @@ -78,52 +81,99 @@ export interface DataTableProps { enableRowSelection?: boolean EditorComponent?: React.ComponentType<{ row: TData; index: number; onClose: () => void }> - getRowId?: (row: Row, index: number) => string; + getRowId?: (originalRow: TData, index: number, parent?: Row) => string; + + // Soporte para paginación server-side + manualPagination?: boolean; + pageIndex?: number; // 0-based + totalItems?: number; + onPageChange?: (pageIndex: number) => void; + onPageSizeChange?: (pageSize: number) => void; + + // Acción al hacer click en una fila + onRowClick?: (row: TData, index: number, event: React.MouseEvent) => void; } export function DataTable({ columns, data, meta, + readOnly = false, enablePagination = true, pageSize = 25, enableRowSelection = false, EditorComponent, + + getRowId, + + manualPagination, + pageIndex = 0, + totalItems, + onPageChange, + onPageSizeChange, + + onRowClick, }: DataTableProps) { const { t } = useTranslation(); - const [rowSelection, setRowSelection] = React.useState({}) - const [sorting, setSorting] = React.useState([]) - const [columnVisibility, setColumnVisibility] = React.useState({}) - const [columnFilters, setColumnFilters] = React.useState([]) - const [colSizes, setColSizes] = React.useState({}) - const [editIndex, setEditIndex] = React.useState(null) + const [rowSelection, setRowSelection] = React.useState({}); + const [sorting, setSorting] = React.useState([]); + const [columnVisibility, setColumnVisibility] = React.useState({}); + const [columnFilters, setColumnFilters] = React.useState([]); + const [colSizes, setColSizes] = React.useState({}); + const [editIndex, setEditIndex] = React.useState(null); + + // Configuración TanStack const table = useReactTable({ data, columns, columnResizeMode: "onChange", onColumnSizingChange: setColSizes, - getRowId: (row: any, i) => row.id ?? String(i), - meta, + meta: { ...meta, totalItems, readOnly }, + + getRowId: + getRowId ?? + ((originalRow: TData, i: number) => + typeof (originalRow as any).id !== "undefined" + ? String((originalRow as any).id) + : String(i)), + state: { columnSizing: colSizes, sorting, columnVisibility, rowSelection, columnFilters, + pagination: { + pageIndex, + pageSize, + }, }, - initialState: { - pagination: { pageSize }, + + manualPagination, + pageCount: manualPagination + ? Math.ceil((totalItems ?? data.length) / (pageSize ?? 25)) + : undefined, + + onPaginationChange: (updater) => { + const next = + typeof updater === "function" + ? updater({ pageIndex, pageSize }) + : updater; + if (next.pageIndex !== undefined) onPageChange?.(next.pageIndex); + if (next.pageSize !== undefined) onPageSizeChange?.(next.pageSize); }, + enableRowSelection, onRowSelectionChange: setRowSelection, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, onColumnVisibilityChange: setColumnVisibility, + getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), + getPaginationRowModel: manualPagination ? undefined : getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFacetedRowModel: getFacetedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), @@ -131,17 +181,19 @@ export function DataTable({ const handleCloseEditor = React.useCallback(() => setEditIndex(null), []) + // Render principal return ( -
- +
+
- + {/* CABECERA */} + {table.getHeaderGroups().map((hg) => ( {hg.headers.map((h) => { - const w = h.getSize(); // px + const w = h.getSize(); const minW = h.column.columnDef.minSize; const maxW = h.column.columnDef.maxSize; return ( @@ -154,7 +206,11 @@ export function DataTable({ maxWidth: typeof maxW === "number" ? `${maxW}px` : undefined, }} > - {h.isPlaceholder ? null : flexRender(h.column.columnDef.header, h.getContext())} +
+ {h.isPlaceholder + ? null + : flexRender(h.column.columnDef.header, h.getContext())} +
); })} @@ -162,10 +218,22 @@ export function DataTable({ ))}
+ {/* CUERPO */} {table.getRowModel().rows.length ? ( - table.getRowModel().rows.map((row, i) => ( - + table.getRowModel().rows.map((row, rowIndex) => ( + { + if (e.key === "Enter" || e.key === " ") onRowClick?.(row.original, rowIndex, e as any); + }} + onDoubleClick={ + !readOnly && !onRowClick ? () => setEditIndex(rowIndex) : undefined + } > {row.getVisibleCells().map((cell) => { const w = cell.column.getSize(); const minW = cell.column.columnDef.minSize; @@ -188,23 +256,29 @@ export function DataTable({ )) ) : ( - + {t("components.datatable.empty")} )} - + {/* Paginación */} + {enablePagination && ( + + + + + + + ) + } -
- {enablePagination && } + + {/* Editor modal */} {EditorComponent && editIndex !== null && ( diff --git a/packages/rdx-ui/src/locales/en.json b/packages/rdx-ui/src/locales/en.json index 211e7ec5..00f9458a 100644 --- a/packages/rdx-ui/src/locales/en.json +++ b/packages/rdx-ui/src/locales/en.json @@ -12,11 +12,27 @@ "desc": "Desc", "hide": "Hide", "empty": "No results found", + "selection_summary": "{{count}} selected rows of {{total}}", + "selection_none": "Total: {{total}} rows", "columns": { "actions": "Actions" }, "actions": { - "add": "Add new line" + "add": "Add new line", + "duplicate": "Duplicate", + "remove": "Remove", + "move_up": "Move up", + "move_down": "Move down" + }, + "pagination": { + "goto_first_page": "Go to first page", + "goto_previus_page": "Go to previus_page", + "goto_next_page": "Go to next page", + "goto_last_page": "Go to last page", + "page_of": "Page {{page}} of {{of}}", + "rows_per_page": "Rows per page", + "showing_range": "Showing {{start}}–{{end}} of {{total}} records", + "rows_selected": "{{count}} of {{total}} selected rows" } }, "loading_indicator": { diff --git a/packages/rdx-ui/src/locales/es.json b/packages/rdx-ui/src/locales/es.json index 09f5b766..f47043a9 100644 --- a/packages/rdx-ui/src/locales/es.json +++ b/packages/rdx-ui/src/locales/es.json @@ -15,6 +15,8 @@ "desc": "Desc", "hide": "Ocultar", "empty": "No hay resultados", + "selection_summary": "{{count}} filas seleccionadas de {{total}}", + "selection_none": "Total: {{total}} filas", "columns": { "actions": "Acciones" }, @@ -24,6 +26,16 @@ "remove": "Eliminar", "move_up": "Subir", "move_down": "Bajar" + }, + "pagination": { + "goto_first_page": "Ir a la primera página", + "goto_previus_page": "Ir a la página anterior", + "goto_next_page": "Ir a la página siguiente", + "goto_last_page": "Ir a la última página", + "page_of": "Página {{page}} de {{of}}", + "rows_per_page": "Filas por página", + "showing_range": "Mostrando {{start}}–{{end}} de {{total}} registros", + "rows_selected": "{{count}} de {{total}} filas seleccionadas" } }, "loading_indicator": { diff --git a/packages/rdx-ui/src/styles/globals.css b/packages/rdx-ui/src/styles/globals.css index d17815d5..1b63eab9 100644 --- a/packages/rdx-ui/src/styles/globals.css +++ b/packages/rdx-ui/src/styles/globals.css @@ -1 +1,43 @@ @source "../components"; + +@layer components { + /** + * Convención: .brand-[surface/text]-[escala]-[dirección] + * Requiere dark mode activo en Tailwind (class o media). + */ + + /* Fondo suave diagonal */ + .brand-surface-50-br { + @apply bg-gradient-to-br from-blue-50 via-violet-50 to-purple-50 + dark:from-blue-950 dark:via-violet-950 dark:to-purple-950; + } + + .brand-surface-100-br { + @apply bg-gradient-to-br from-blue-100 via-violet-100 to-purple-100 + dark:from-blue-900 dark:via-violet-900 dark:to-purple-900; + } + + /* Fondo suave horizontal */ + .brand-surface-50-x { + @apply bg-gradient-to-r from-blue-50 to-violet-50 + hover:from-blue-50 hover:to-violet-50 + dark:from-blue-900 dark:to-violet-900; + } + + .brand-surface-100-x { + @apply bg-gradient-to-r from-blue-100 to-violet-100 + hover:from-blue-100 hover:to-violet-100 + dark:from-blue-900 dark:to-violet-900; + } + + .brand-surface-200-x { + @apply bg-gradient-to-r from-blue-200 to-violet-200 + dark:from-blue-800 dark:to-violet-800; + } + + /* Gradiente para texto (intenso) */ + .brand-text-strong-x { + @apply bg-gradient-to-r from-blue-600 to-violet-600 bg-clip-text text-transparent antialiased + dark:from-blue-400 dark:to-violet-400; + } +} diff --git a/packages/shadcn-ui/src/components/pagination.tsx b/packages/shadcn-ui/src/components/pagination.tsx index aa23870c..93b2f9d7 100644 --- a/packages/shadcn-ui/src/components/pagination.tsx +++ b/packages/shadcn-ui/src/components/pagination.tsx @@ -1,17 +1,16 @@ -import * as React from "react" import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon, } from "lucide-react" +import * as React from "react" -import { cn } from "@repo/shadcn-ui/lib/utils" import { Button, buttonVariants } from "@repo/shadcn-ui/components/button" +import { cn } from "@repo/shadcn-ui/lib/utils" function Pagination({ className, ...props }: React.ComponentProps<"nav">) { return (