diff --git a/biome.json b/biome.json index c659f6a8..3520a236 100644 --- a/biome.json +++ b/biome.json @@ -31,7 +31,8 @@ }, "suspicious": { "noImplicitAnyLet": "info", - "noExplicitAny": "info" + "noExplicitAny": "info", + "noArrayIndexKey": "info" }, "style": { "useImportType": "off", diff --git a/modules/customer-invoices/src/common/locales/en.json b/modules/customer-invoices/src/common/locales/en.json index a4af1d77..cd1e4a81 100644 --- a/modules/customer-invoices/src/common/locales/en.json +++ b/modules/customer-invoices/src/common/locales/en.json @@ -14,7 +14,11 @@ "actions": "Actions", "rows_selected": "{{count}} fila(s) seleccionadas.", - "rows_selected_of_total": "{{count}} de {{total}} fila(s) seleccionadas." + "rows_selected_of_total": "{{count}} de {{total}} fila(s) seleccionadas.", + + "search_placeholder": "Type for search...", + "search": "Search", + "clear": "Clear" }, "catalog": { diff --git a/modules/customer-invoices/src/common/locales/es.json b/modules/customer-invoices/src/common/locales/es.json index 4c4b5fd6..6d63d19e 100644 --- a/modules/customer-invoices/src/common/locales/es.json +++ b/modules/customer-invoices/src/common/locales/es.json @@ -14,7 +14,11 @@ "actions": "Acciones", "rows_selected": "{{count}} fila(s) seleccionadas.", - "rows_selected_of_total": "{{count}} de {{total}} fila(s) seleccionadas." + "rows_selected_of_total": "{{count}} de {{total}} fila(s) seleccionadas.", + + "search_placeholder": "Escribe aquí para buscar...", + "search": "Buscar", + "clear": "Limpiar" }, "catalog": { "status": { 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 8f9360cd..e2024e32 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 @@ -3,8 +3,17 @@ 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; +export const CUSTOMER_INVOICES_QUERY_KEY = (criteria: CriteriaDTO): QueryKey => [ + "customer_invoices", { + pageNumber: criteria.pageNumber ?? 0, + pageSize: criteria.pageSize ?? 10, + q: criteria.q ?? "", + filters: criteria.filters ?? [], + orderBy: criteria.orderBy ?? "", + order: criteria.order ?? "", + }, +]; + type InvoicesQueryOptions = { enabled?: boolean; @@ -21,8 +30,8 @@ export const useInvoicesQuery = (options?: InvoicesQueryOptions) => { queryKey: CUSTOMER_INVOICES_QUERY_KEY(criteria), queryFn: async ({ signal }) => { return await dataSource.getList("customer-invoices", { - ...criteria, signal, + ...criteria, }); }, enabled, 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 08410c4f..74349b23 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 @@ -2,9 +2,9 @@ import type { CellKeyDownEvent, RowClickedEvent } from "ag-grid-community"; import { useCallback, useState } from "react"; -import { DataTable, LoadingOverlay } 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 { DataTable, SkeletonDataTable } from '@repo/rdx-ui/components'; +import { Button, InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Spinner } from '@repo/shadcn-ui/components'; +import { FileDownIcon, FilterIcon, SearchIcon, XIcon } from 'lucide-react'; import { useNavigate } from "react-router-dom"; import { useTranslation } from "../../i18n"; import { InvoiceSummaryFormData, InvoicesPageFormData } from '../../schemas'; @@ -14,8 +14,13 @@ export type InvoiceUpdateCompProps = { invoicesPage: InvoicesPageFormData; loading?: boolean; - onPageChange?: (pageIndex: number) => void; + pageIndex: number; + pageSize: number; + onPageChange?: (pageNumber: number) => void; onPageSizeChange?: (pageSize: number) => void; + + searchValue: string; + onSearchChange: (value: string) => void; } @@ -23,8 +28,11 @@ export type InvoiceUpdateCompProps = { export const InvoicesListGrid = ({ invoicesPage, loading, + pageIndex, + pageSize, onPageChange, onPageSizeChange, + searchValue, onSearchChange, }: InvoiceUpdateCompProps) => { const { t } = useTranslation(); const navigate = useNavigate(); @@ -33,8 +41,7 @@ export const InvoicesListGrid = ({ const [statusFilter, setStatusFilter] = useState("todas"); const columns = useInvoicesListColumns(); - const { items, page, per_page, total_pages, total_items } = invoicesPage; - + const { items, total_items } = invoicesPage; // Navegación accesible (click o teclado) const goToRow = useCallback( @@ -77,6 +84,15 @@ export const InvoicesListGrid = ({ [goToRow] ); + // Handlers de búsqueda + const handleInputChange = (e: React.ChangeEvent) => onSearchChange(e.target.value); + const handleClear = () => onSearchChange(""); + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + // Envío inmediato: forzar “salto” del debounce + onSearchChange((e.target as HTMLInputElement).value); + } + }; const handleRowClick = useCallback( (invoice: InvoiceSummaryFormData, _index: number, e: React.MouseEvent) => { @@ -87,20 +103,53 @@ export const InvoicesListGrid = ({ [navigate] ); + if (loading) { + return ( +
+ +
+ ); + } // Render principal return (
{/* Barra de filtros */}
-
- - setSearchTerm(e.target.value)} - className="pl-10 " - /> +
+ + + + + + + + + {loading && } + {!searchValue && !loading && + {t("common.search")} + } + {searchValue && !loading && + + {t("common.search")} + } + + +
table.setPageSize(Number(value))} - > + {t("components.datatable.pagination.rows_per_page")} + + {pageIndex + 1} / {pageCount}
@@ -93,63 +99,61 @@ export function DataTablePagination({ table, className }: DataTablePagina
- {/* Primera página */} table.setPageIndex(0)} - isActive={!table.getCanPreviousPage()} + onClick={() => { + if (pageIndex > 0) gotoFirstPage(); + }} + isActive={pageIndex > 0} size="sm" - className="px-2.5" + className="px-2.5 cursor-pointer" > - {/* Anterior */} table.previousPage()} - isActive={!table.getCanPreviousPage()} + onClick={() => { + if (pageIndex > 0) gotoPreviousPage(); + }} + isActive={pageIndex > 0} size="sm" - className="px-2.5" + className="px-2.5 cursor-pointer" > - - {t("components.datatable.pagination.page_of", { - page: pageIndex + 1, - of: pageCount || 1, - })} + + {t("components.datatable.pagination.page_of", { page: pageIndex + 1, of: pageCount })} - {/* Siguiente */} table.nextPage()} - isActive={!table.getCanNextPage()} + onClick={() => { + if (pageIndex < pageCount - 1) gotoNextPage(); + }} + isActive={pageIndex < pageCount - 1} size="sm" - className="px-2.5" + className="px-2.5 cursor-pointer" > - {/* Última página */} table.setPageIndex(pageCount - 1)} - isActive={!table.getCanNextPage()} + onClick={() => { + if (pageIndex < pageCount - 1) gotoLastPage(); + }} + isActive={pageIndex < pageCount - 1} size="sm" - className="px-2.5" + className="px-2.5 cursor-pointer" > diff --git a/packages/rdx-ui/src/components/datatable/data-table.tsx b/packages/rdx-ui/src/components/datatable/data-table.tsx index 181850b0..db963392 100644 --- a/packages/rdx-ui/src/components/datatable/data-table.tsx +++ b/packages/rdx-ui/src/components/datatable/data-table.tsx @@ -101,7 +101,7 @@ export function DataTable({ readOnly = false, enablePagination = true, - pageSize = 25, + pageSize = 10, enableRowSelection = false, EditorComponent, @@ -145,10 +145,7 @@ export function DataTable({ columnVisibility, rowSelection, columnFilters, - pagination: { - pageIndex, - pageSize, - }, + pagination: { pageIndex, pageSize }, }, manualPagination, @@ -156,13 +153,14 @@ export function DataTable({ ? Math.ceil((totalItems ?? data.length) / (pageSize ?? 25)) : undefined, + // Propagar cambios al padre 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); + const next = typeof updater === "function" + ? updater({ pageIndex, pageSize }) + : updater; + + if (typeof next.pageIndex === "number") onPageChange?.(next.pageIndex); + if (typeof next.pageSize === "number") onPageSizeChange?.(next.pageSize); }, enableRowSelection, @@ -183,126 +181,129 @@ export function DataTable({ // Render principal return ( -
- +
+
+ -
- - {/* CABECERA */} - - {table.getHeaderGroups().map((hg) => ( - - {hg.headers.map((h) => { - const w = h.getSize(); - const minW = h.column.columnDef.minSize; - const maxW = h.column.columnDef.maxSize; - return ( - -
- {h.isPlaceholder - ? null - : flexRender(h.column.columnDef.header, h.getContext())} -
-
- ); - })} -
- ))} -
- - {/* CUERPO */} - - {table.getRowModel().rows.length ? ( - 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; - const maxW = cell.column.columnDef.maxSize; +
+ + {/* CABECERA */} + + {table.getHeaderGroups().map((hg) => ( + + {hg.headers.map((h) => { + const w = h.getSize(); + const minW = h.column.columnDef.minSize; + const maxW = h.column.columnDef.maxSize; return ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - +
+ {h.isPlaceholder + ? null + : flexRender(h.column.columnDef.header, h.getContext())} +
+ ); })}
- )) - ) : ( - - - {t("components.datatable.empty")} - - - )} - - {/* Paginación */} - {enablePagination && ( - - - - - - - ) - } + ))} +
-
+ {/* CUERPO */} + + {table.getRowModel().rows.length ? ( + 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; + const maxW = cell.column.columnDef.maxSize; + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ); + })} + + )) + ) : ( + + + {t("components.datatable.empty")} + + + )} + + {/* Paginación */} + {enablePagination && ( + + + + + + + ) + } + + +
+ + {/* Editor modal */} + {EditorComponent && editIndex !== null && ( + + + + {t("components.datatable.editor.title")} + {t("components.datatable.editor.subtitle")} + + +
+ +
+ + + + +
+
+ )}
- - - - {/* Editor modal */} - {EditorComponent && editIndex !== null && ( - - - - {t("components.datatable.editor.title")} - {t("components.datatable.editor.subtitle")} - - -
- -
- - - - -
-
- )}
) } \ No newline at end of file diff --git a/packages/rdx-ui/src/components/datatable/index.tsx b/packages/rdx-ui/src/components/datatable/index.tsx index df73ca10..918d2ae7 100644 --- a/packages/rdx-ui/src/components/datatable/index.tsx +++ b/packages/rdx-ui/src/components/datatable/index.tsx @@ -1,4 +1,5 @@ export * from "./data-table-column-header.tsx"; export * from "./data-table.tsx"; +export * from "./skeleton-data-table.tsx"; export * from "./with-row-selection.tsx"; diff --git a/packages/rdx-ui/src/components/datatable/skeleton-data-table-footer.tsx b/packages/rdx-ui/src/components/datatable/skeleton-data-table-footer.tsx new file mode 100644 index 00000000..cfedaa6a --- /dev/null +++ b/packages/rdx-ui/src/components/datatable/skeleton-data-table-footer.tsx @@ -0,0 +1,69 @@ +// SkeletonTableFooter.tsx + +/** Skeleton del footer de paginación, imita DataTablePagination */ +export function SkeletonDataTableFooter({ + pageIndex = 0, + pageSize = 10, + totalItems = 0, +}: { + pageIndex?: number; // 0-based + pageSize?: number; + totalItems?: number; +}) { + // Cálculo de rango (1-based visual) + const start = totalItems > 0 ? pageIndex * pageSize + 1 : 0; + const end = totalItems > 0 ? Math.min(start + pageSize - 1, totalItems) : 0; + + return ( +
+ {/* Izquierda: rango visible */} +
+ + {/* Texto real + shimmer leve para coherencia visual */} + {`Mostrando ${start}–${end} de ${totalItems} registros`} + + + {/* 'Filas por página' + trigger del select simulado */} +
+ Filas por página +
+
+
+ + {/* Derecha: controles de paginación simulados */} +
+ {/* Primera */} +
+ {/* Anterior */} +
+ {/* Indicador de página */} +
+ {/* Siguiente */} +
+ {/* Última */} +
+
+ + Loading pagination… +
+ ); +} diff --git a/packages/rdx-ui/src/components/datatable/skeleton-data-table.tsx b/packages/rdx-ui/src/components/datatable/skeleton-data-table.tsx new file mode 100644 index 00000000..9636c06e --- /dev/null +++ b/packages/rdx-ui/src/components/datatable/skeleton-data-table.tsx @@ -0,0 +1,74 @@ +import { + TableBody, + TableCell, + Table as TableComp, + TableHead, + TableHeader, + TableRow, +} from "@repo/shadcn-ui/components"; +// SkeletonTable.tsx +import * as React from "react"; +import { SkeletonDataTableFooter } from './skeleton-data-table-footer.tsx'; + +export type SkeletonTableProps = { + columns?: number; + rows?: number; + stickyHeader?: boolean; + showFooter?: boolean; + footerProps?: { pageIndex?: number; pageSize?: number; totalItems?: number }; +} + +// Componente Skeleton de tabla genérico +export const SkeletonDataTable = ({ + columns = 6, + rows = 10, + stickyHeader = true, + showFooter = true, + footerProps, +}: SkeletonTableProps) => { + // Genera arrays para mapear + const cols = React.useMemo(() => Array.from({ length: columns }), [columns]); + const rws = React.useMemo(() => Array.from({ length: rows }), [rows]); + + return ( +
+ + + + {cols.map((_, i) => ( + +
+ + ))} + + + + + {rws.map((_, r) => ( + + {cols.map((_, c) => ( + +
+ {c > 0 && ( +
+ )} + + ))} + + ))} + + + + {showFooter && } + + Loading table… +
+ ); +} \ No newline at end of file