diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts index 457f49e1..fb9ca6f9 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts @@ -220,11 +220,19 @@ export class IssuedInvoiceRepository const query = criteriaConverter.convert(criteria, { searchableFields: ["invoice_number", "reference", "description"], mappings: { - invoice_number: "CustomerInvoiceModel.invoice_number", - reference: "CustomerInvoiceModel.reference", - description: "CustomerInvoiceModel.description", + invoice_date: "invoice_date", + invoice_number: "invoice_number", + reference: "reference", + description: "description", + recipient_name: "current_customer.name", }, - allowedFields: ["invoice_date", "id", "created_at"], + sortableFields: [ + "current_customer.name", + "invoice_number", + "invoice_date", + "id", + "created_at", + ], enableFullText: true, database: this.database, strictMode: true, // fuerza error si ORDER BY no permitido diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts index 6b1932e9..20abc09b 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts @@ -350,11 +350,19 @@ export class ProformaRepository const query = criteriaConverter.convert(criteria, { searchableFields: ["invoice_number", "reference", "description"], mappings: { - invoice_number: "CustomerInvoiceModel.invoice_number", - reference: "CustomerInvoiceModel.reference", - description: "CustomerInvoiceModel.description", + invoice_date: "invoice_date", + invoice_number: "invoice_number", + reference: "reference", + description: "description", + recipient_name: "current_customer.name", }, - allowedFields: ["invoice_date", "id", "created_at"], + sortableFields: [ + "current_customer.name", + "invoice_number", + "invoice_date", + "id", + "created_at", + ], enableFullText: true, database: this.database, strictMode: true, // fuerza error si ORDER BY no permitido diff --git a/modules/customer-invoices/src/common/locales/en.json b/modules/customer-invoices/src/common/locales/en.json index 019a6157..42028f1d 100644 --- a/modules/customer-invoices/src/common/locales/en.json +++ b/modules/customer-invoices/src/common/locales/en.json @@ -150,12 +150,12 @@ "list": { "title": "Customer proformas", "description": "List all customer proformas", - "grid_columns": { - "invoice_number": "#", + "columns": { + "invoice_number": "Num.", "series": "Serie", "reference": "Reference", "status": "Status", - "invoice_date": "Proforma date", + "invoice_date": "Date", "operation_date": "Operation date", "recipient": "Customer", "recipient_tin": "TIN", diff --git a/modules/customer-invoices/src/common/locales/es.json b/modules/customer-invoices/src/common/locales/es.json index 5f1d2c43..df6324cf 100644 --- a/modules/customer-invoices/src/common/locales/es.json +++ b/modules/customer-invoices/src/common/locales/es.json @@ -151,12 +151,12 @@ "list": { "title": "Proformas", "description": "Lista todas las proformas", - "grid_columns": { - "invoice_number": "#", + "columns": { + "invoice_number": "Num.", "series": "Serie", "reference": "Reference", "status": "Estado", - "invoice_date": "Fecha de proforma", + "invoice_date": "Fecha", "operation_date": "Fecha de operación", "recipient": "Cliente", "recipient_tin": "NIF/CIF", diff --git a/modules/customer-invoices/src/web/proformas/list/controllers/use-list-proformas.controller.ts b/modules/customer-invoices/src/web/proformas/list/controllers/use-list-proformas.controller.ts index 955f9f32..1687bb70 100644 --- a/modules/customer-invoices/src/web/proformas/list/controllers/use-list-proformas.controller.ts +++ b/modules/customer-invoices/src/web/proformas/list/controllers/use-list-proformas.controller.ts @@ -1,5 +1,10 @@ import { useDebounce } from "@erp/core/hooks"; -import { useDataTablePreferences } from "@repo/rdx-ui/components"; +import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria"; +import { + type DataTableSort, + type DataTableSortDirection, + useDataTablePreferences, +} from "@repo/rdx-ui/components"; import { NumberHelper } from "@repo/rdx-utils"; import { useCallback, useMemo, useState } from "react"; import { useSearchParams } from "react-router-dom"; @@ -13,14 +18,41 @@ import { type ProformaListStatusFilter = "all" | ProformaStatus; +// Datos por defecto mientras se carga la consulta o en caso de error. const EMPTY_PROFORMAS_LIST: ProformaList = { items: [], - page: 0, - perPage: 5, + page: INITIAL_PAGE_INDEX, + perPage: INITIAL_PAGE_SIZE, totalPages: 0, totalItems: 0, }; +// Campos que se permiten ordenar para la lista de proformas (consulta). +type ProformaListSortField = "invoiceNumber" | "recipientName" | "invoiceDate"; + +type ProformaListApiSortField = "invoice_number" | "recipient_name" | "invoice_date"; + +const PROFORMA_LIST_SORT_FIELDS: Record = { + invoiceNumber: "invoice_number", + recipientName: "recipient_name", + invoiceDate: "invoice_date", +}; + +const DEFAULT_API_SORT_FIELD: ProformaListApiSortField = "invoice_date"; + +const DEFAULT_SORT = { + field: "invoiceDate", + direction: "desc", +} satisfies DataTableSort; + +const isProformaListSortField = (value: string | null): value is ProformaListSortField => { + return value === "invoiceNumber" || value === "recipientName" || value === "invoiceDate"; +}; + +const isSortDirection = (value: string | null): value is DataTableSortDirection => { + return value === "asc" || value === "desc"; +}; + export const useListProformasController = () => { const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); @@ -28,7 +60,7 @@ export const useListProformasController = () => { const tablePreferences = useDataTablePreferences({ storageKey: "proformas:list:grid", - defaultPageSize: 5, + defaultPageSize: EMPTY_PROFORMAS_LIST.perPage, defaultColumnVisibility: { reference: true, recipientName: true, @@ -36,12 +68,17 @@ export const useListProformasController = () => { totalAmountFmt: true, invoiceDate: true, }, + defaultSort: DEFAULT_SORT, }); const { pageSize: preferencesPageSize, setPageSize: setPageSizePreference } = tablePreferences; + const { sort: preferencesSorting, setSort: setSortPreference } = tablePreferences; // Parse page from URL (1-based) or default to 1 - const urlPage = NumberHelper.parsePositiveInteger(searchParams.get("page"), 1); + const urlPage = NumberHelper.parsePositiveInteger( + searchParams.get("page"), + INITIAL_PAGE_INDEX + 1 + ); const pageIndex = urlPage - 1; // Convert to 0-based for internal use // Parse pageSize from URL or use preferences @@ -52,17 +89,37 @@ export const useListProformasController = () => { const debouncedSearch = useDebounce(search, 300); + // Criterios de ordenamiento + const urlSortFieldValue = searchParams.get("sortField"); + const urlSortDirectionValue = searchParams.get("sortDirection"); + + const urlSort = + isProformaListSortField(urlSortFieldValue) && isSortDirection(urlSortDirectionValue) + ? { + field: urlSortFieldValue, + direction: urlSortDirectionValue, + } + : undefined; + + const sort = urlSort ?? preferencesSorting ?? DEFAULT_SORT; + + const orderBy = + PROFORMA_LIST_SORT_FIELDS[sort.field as ProformaListSortField] ?? DEFAULT_API_SORT_FIELD; + + const order = sort.direction; + + // Construir criterios de consulta const criteria = useMemo>( () => ({ q: debouncedSearch || "", pageNumber: pageIndex, pageSize, - order: "desc", - orderBy: "invoice_date", + orderBy, + order, filters: statusFilter === "all" ? [] : [{ field: "status", operator: "eq", value: statusFilter }], }), - [debouncedSearch, pageIndex, pageSize, statusFilter] + [debouncedSearch, pageIndex, pageSize, orderBy, order, statusFilter] ); const query = useProformasListQuery({ criteria }); @@ -96,7 +153,7 @@ export const useListProformasController = () => { // Reset page to 1 when search changes setSearchParams((prev) => { const params = new URLSearchParams(prev); - params.set("page", "1"); + params.set("page", String(INITIAL_PAGE_INDEX + 1)); // Convert to 1-based for URL return params; }); return nextValue; @@ -124,7 +181,7 @@ export const useListProformasController = () => { // Reset page to 1 and update pageSize when it changes setSearchParams((prev) => { const params = new URLSearchParams(prev); - params.set("page", "1"); + params.set("page", String(INITIAL_PAGE_INDEX + 1)); // Convert to 1-based for URL params.set("pageSize", String(value)); return params; }); @@ -133,6 +190,31 @@ export const useListProformasController = () => { [pageSize, setSearchParams, setPageSizePreference] ); + const setSortValue = useCallback( + (nextSort: DataTableSort) => { + if (!isProformaListSortField(nextSort.field)) { + return; + } + + if (sort.field === nextSort.field && sort.direction === nextSort.direction) { + return; + } + + setSearchParams((prev) => { + const params = new URLSearchParams(prev); + + params.set("sortField", nextSort.field); + params.set("sortDirection", nextSort.direction); + params.set("page", String(INITIAL_PAGE_INDEX + 1)); + + return params; + }); + + setSortPreference(nextSort); + }, + [setSearchParams, sort.field, sort.direction, setSortPreference] + ); + return { data: query.data ?? EMPTY_PROFORMAS_LIST, isLoading: query.isLoading, @@ -153,6 +235,9 @@ export const useListProformasController = () => { search, setSearchValue, + sort, + setSort: setSortValue, + statusFilter, setStatusFilter: setStatusFilterValue, }; diff --git a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/proformas-grid.tsx b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/proformas-grid.tsx index be8203d4..d130eadb 100644 --- a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/proformas-grid.tsx +++ b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/proformas-grid.tsx @@ -1,4 +1,4 @@ -import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components"; +import { DataTable, type DataTableSort, SkeletonDataTable } from "@repo/rdx-ui/components"; import type { ColumnDef, OnChangeFn, VisibilityState } from "@tanstack/react-table"; import { useTranslation } from "../../../../../i18n"; @@ -16,6 +16,9 @@ interface ProformasGridProps { onPageChange: (pageIndex: number) => void; onPageSizeChange: (size: number) => void; + sort: DataTableSort; + onSortChange?: (nextSort: DataTableSort) => void; + columnVisibility?: VisibilityState; onColumnVisibilityChange?: OnChangeFn; @@ -24,15 +27,22 @@ interface ProformasGridProps { export const ProformasGrid = ({ data, + columns, + loading, fetching, - columns, + pageIndex, pageSize, onPageChange, onPageSizeChange, + + sort, + onSortChange, + columnVisibility, onColumnVisibilityChange, + onRowClick, }: ProformasGridProps) => { const { t } = useTranslation(); @@ -57,12 +67,15 @@ export const ProformasGrid = ({ enablePagination enableRowSelection manualPagination + manualSorting onColumnVisibilityChange={onColumnVisibilityChange} onPageChange={onPageChange} onPageSizeChange={onPageSizeChange} + onSortChange={onSortChange} //onRowClick={(row) => onRowClick?.(row.id)} pageIndex={pageIndex} pageSize={pageSize} + sort={sort} totalItems={totalItems} /> ); diff --git a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx index 9047952b..8d599e24 100644 --- a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx +++ b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx @@ -1,3 +1,4 @@ +import { DataTableColumnHeader } from "@repo/rdx-ui/components"; import { DateHelper } from "@repo/rdx-utils"; import { Button, @@ -9,7 +10,6 @@ import { } from "@repo/shadcn-ui/components"; import type { ColumnDef } from "@tanstack/react-table"; import { - ArrowUpDownIcon, ExternalLinkIcon, FileTextIcon, PencilIcon, @@ -46,6 +46,7 @@ export function useProformasGridColumns( ): ColumnDef[] { const { t } = useTranslation(); + // Todas las columnas deben tener un id return React.useMemo[]>( () => [ { @@ -70,37 +71,52 @@ export function useProformasGridColumns( onCheckedChange={(value) => row.toggleSelected(!!value)} /> ), - enableSorting: false, enableHiding: false, + enableSorting: false, }, { + id: "invoiceDate", + accessorKey: "invoiceDate", + enableHiding: false, + enableSorting: true, + header: ({ column }) => ( + + ), + cell: ({ row }) => DateHelper.format(row.original.invoiceDate), + }, + { + id: "series", accessorKey: "series", header: "Serie", enableHiding: true, + enableSorting: false, }, { + id: "invoiceNumber", accessorKey: "invoiceNumber", - header: ({ column }) => { - return ( - - ); - }, enableHiding: false, + enableSorting: true, + header: ({ column }) => ( + + ), meta: { cellClassName: "font-medium", }, }, { + id: "status", accessorKey: "status", - header: "Estado", enableHiding: true, + enableSorting: false, + header: ({ column }) => ( + + ), cell: ({ row }) => { const proforma = row.original; @@ -139,19 +155,16 @@ export function useProformasGridColumns( // Cliente { + id: "recipientName", accessorKey: "recipientName", - header: ({ column }) => { - return ( - - ); - }, + enableHiding: false, + enableSorting: true, + header: ({ column }) => ( + + ), cell: ({ row }) => { const proforma = row.original; return ( @@ -165,26 +178,18 @@ export function useProformasGridColumns( cellClassName: "max-w-128", }, }, + { - accessorKey: "invoiceDate", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => DateHelper.format(row.original.invoiceDate), - }, - { + id: "reference", accessorKey: "reference", - header: "Referencia", enableHiding: true, + enableSorting: false, + header: ({ column }) => ( + + ), cell: ({ row }) =>
{row.original.reference}
, meta: { cellClassName: "hidden lg:table-cell max-w-16", @@ -193,40 +198,56 @@ export function useProformasGridColumns( }, { + id: "subtotalAmountFmt", accessorKey: "subtotalAmountFmt", - header: () => "Subtotal", + enableHiding: true, + enableSorting: false, + header: ({ column }) => ( + + ), meta: { cellClassName: "hidden 2xl:table-cell text-right tabular-nums font-medium", headerClassName: "hidden 2xl:table-cell text-right", }, }, { + id: "totalDiscountAmountFmt", accessorKey: "totalDiscountAmountFmt", header: "Dtos", + enableSorting: false, meta: { cellClassName: "hidden 2xl:table-cell text-right tabular-nums font-medium", headerClassName: "hidden 2xl:table-cell text-right", }, }, { + id: "taxableAmountFmt", accessorKey: "taxableAmountFmt", header: "Base Imp.", + enableSorting: false, meta: { cellClassName: "hidden 2xl:table-cell text-right tabular-nums font-medium", headerClassName: "hidden 2xl:table-cell text-right", }, }, { + id: "taxesAmountFmt", accessorKey: "taxesAmountFmt", header: "Impuestos", + enableSorting: false, meta: { cellClassName: "hidden 2xl:table-cell text-right tabular-nums font-medium", headerClassName: "hidden 2xl:table-cell text-right", }, }, { + id: "totalAmountFmt", accessorKey: "totalAmountFmt", header: "Total", + enableSorting: false, meta: { cellClassName: "hidden xl:table-cell text-right tabular-nums font-semibold", headerClassName: "hidden xl:table-cell text-right", @@ -257,7 +278,12 @@ export function useProformasGridColumns( render={