From 6e53e5bd58314b9df933d7064a3a64822253c5bc Mon Sep 17 00:00:00 2001 From: david Date: Thu, 14 May 2026 19:36:25 +0200 Subject: [PATCH] Guardar preferencias y returnTo en grid de listados Co-authored-by: Copilot --- modules/core/src/web/hooks/index.ts | 1 + .../use-return-to-navigate/build-return-to.ts | 0 .../web/hooks/use-return-to-navigate/index.ts | 1 + .../use-return-to-navigation.ts | 28 ++++ .../use-list-proformas.controller.ts | 123 +++++++++++++----- .../blocks/proformas-grid/proformas-grid.tsx | 9 +- .../list/ui/pages/list-proformas-page.tsx | 56 +++++--- .../use-update-proforma-page-controller.ts | 5 + .../update/ui/pages/proforma-update-page.tsx | 21 ++- .../buttons/back-history-button.tsx | 9 +- .../src/components/datatable/data-table.tsx | 39 ++++-- .../src/components/datatable/hooks/index.ts | 1 + .../hooks/use-data-table-preferences.ts | 91 +++++++++++++ .../rdx-ui/src/components/datatable/index.ts | 5 +- .../rdx-utils/src/helpers/number-helper.ts | 11 ++ 15 files changed, 329 insertions(+), 71 deletions(-) create mode 100644 modules/core/src/web/hooks/use-return-to-navigate/build-return-to.ts create mode 100644 modules/core/src/web/hooks/use-return-to-navigate/index.ts create mode 100644 modules/core/src/web/hooks/use-return-to-navigate/use-return-to-navigation.ts create mode 100644 packages/rdx-ui/src/components/datatable/hooks/index.ts create mode 100644 packages/rdx-ui/src/components/datatable/hooks/use-data-table-preferences.ts diff --git a/modules/core/src/web/hooks/index.ts b/modules/core/src/web/hooks/index.ts index 167ce6ac..be3af3f6 100644 --- a/modules/core/src/web/hooks/index.ts +++ b/modules/core/src/web/hooks/index.ts @@ -1,6 +1,7 @@ export * from "./use-datasource"; export * from "./use-debounce"; export * from "./use-hook-form"; +export * from "./use-return-to-navigate"; export * from "./use-rhf-error-focus"; export * from "./use-unsaved-changes-notifier"; export * from "./use-url-param-id"; diff --git a/modules/core/src/web/hooks/use-return-to-navigate/build-return-to.ts b/modules/core/src/web/hooks/use-return-to-navigate/build-return-to.ts new file mode 100644 index 00000000..e69de29b diff --git a/modules/core/src/web/hooks/use-return-to-navigate/index.ts b/modules/core/src/web/hooks/use-return-to-navigate/index.ts new file mode 100644 index 00000000..0c031ff8 --- /dev/null +++ b/modules/core/src/web/hooks/use-return-to-navigate/index.ts @@ -0,0 +1 @@ +export * from "./use-return-to-navigation"; diff --git a/modules/core/src/web/hooks/use-return-to-navigate/use-return-to-navigation.ts b/modules/core/src/web/hooks/use-return-to-navigate/use-return-to-navigation.ts new file mode 100644 index 00000000..8c9a6666 --- /dev/null +++ b/modules/core/src/web/hooks/use-return-to-navigate/use-return-to-navigation.ts @@ -0,0 +1,28 @@ +import * as React from "react"; +import { useLocation, useNavigate, useSearchParams } from "react-router-dom"; + +interface UseReturnToNavigationParams { + fallbackPath: string; +} + +export const useReturnToNavigation = ({ fallbackPath }: UseReturnToNavigationParams) => { + const navigate = useNavigate(); + const location = useLocation(); + const [searchParams] = useSearchParams(); + + const currentReturnTo = React.useMemo(() => { + return `${location.pathname}${location.search}`; + }, [location.pathname, location.search]); + + const returnTo = searchParams.get("returnTo") || fallbackPath; + + const navigateBack = React.useCallback(() => { + navigate(returnTo); + }, [navigate, returnTo]); + + return { + currentReturnTo, + returnTo, + navigateBack, + }; +}; 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 38b05bd4..955f9f32 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,8 @@ import { useDebounce } from "@erp/core/hooks"; -import { useMemo, useState } from "react"; +import { useDataTablePreferences } from "@repo/rdx-ui/components"; +import { NumberHelper } from "@repo/rdx-utils"; +import { useCallback, useMemo, useState } from "react"; +import { useSearchParams } from "react-router-dom"; import { type ListProformasByCriteriaParams, @@ -19,10 +22,33 @@ const EMPTY_PROFORMAS_LIST: ProformaList = { }; export const useListProformasController = () => { - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(5); const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); + const [searchParams, setSearchParams] = useSearchParams(); + + const tablePreferences = useDataTablePreferences({ + storageKey: "proformas:list:grid", + defaultPageSize: 5, + defaultColumnVisibility: { + reference: true, + recipientName: true, + status: true, + totalAmountFmt: true, + invoiceDate: true, + }, + }); + + const { pageSize: preferencesPageSize, setPageSize: setPageSizePreference } = tablePreferences; + + // Parse page from URL (1-based) or default to 1 + const urlPage = NumberHelper.parsePositiveInteger(searchParams.get("page"), 1); + const pageIndex = urlPage - 1; // Convert to 0-based for internal use + + // Parse pageSize from URL or use preferences + const urlPageSize = searchParams.get("pageSize"); + const pageSize = urlPageSize + ? NumberHelper.parsePositiveInteger(urlPageSize, preferencesPageSize) + : preferencesPageSize; const debouncedSearch = useDebounce(search, 300); @@ -41,42 +67,71 @@ export const useListProformasController = () => { const query = useProformasListQuery({ criteria }); - const setStatusFilterValue = (value: string) => { - const nextValue = (value || "all") as ProformaListStatusFilter; + const setStatusFilterValue = useCallback( + (value: string) => { + const nextValue = (value || "all") as ProformaListStatusFilter; - setStatusFilter((prev) => { - if (prev === nextValue) return prev; + setStatusFilter((prev) => { + if (prev === nextValue) return prev; - // Sólo si la búsqueda realmente cambia, - // reseteamos la página a 0 para evitar inconsistencias - setPageIndex(0); - return nextValue; - }); - }; + // Reset page to 1 when status filter changes + setSearchParams((prev) => { + const params = new URLSearchParams(prev); + params.set("page", "1"); + return params; + }); + return nextValue; + }); + }, + [setSearchParams] + ); - const setSearchValue = (value: string) => { - const nextValue = value.trim().replace(/\s+/g, " "); + const setSearchValue = useCallback( + (value: string) => { + const nextValue = value.trim().replace(/\s+/g, " "); - setSearch((prev) => { - if (prev === nextValue) return prev; + setSearch((prev) => { + if (prev === nextValue) return prev; - // Sólo si la búsqueda realmente cambia, - // reseteamos la página a 0 para evitar inconsistencias - setPageIndex(0); - return nextValue; - }); - }; + // Reset page to 1 when search changes + setSearchParams((prev) => { + const params = new URLSearchParams(prev); + params.set("page", "1"); + return params; + }); + return nextValue; + }); + }, + [setSearchParams] + ); - const setPageSizeValue = (value: number) => { - setPageSize((prev) => { - if (prev === value) return prev; + const setPageIndexValue = useCallback( + (newPageIndex: number) => { + const newPage = newPageIndex + 1; // Convert to 1-based for URL + setSearchParams((prev) => { + const params = new URLSearchParams(prev); + params.set("page", String(newPage)); + return params; + }); + }, + [setSearchParams] + ); - // Sólo si el tamaño de página realmente cambia, - // reseteamos la página a 0 para evitar inconsistencias - setPageIndex(0); - return value; - }); - }; + const setPageSizeValue = useCallback( + (value: number) => { + if (pageSize === value) return; + + // Reset page to 1 and update pageSize when it changes + setSearchParams((prev) => { + const params = new URLSearchParams(prev); + params.set("page", "1"); + params.set("pageSize", String(value)); + return params; + }); + setPageSizePreference(value); + }, + [pageSize, setSearchParams, setPageSizePreference] + ); return { data: query.data ?? EMPTY_PROFORMAS_LIST, @@ -88,9 +143,11 @@ export const useListProformasController = () => { refetch: query.refetch, + tablePreferences, + pageIndex, pageSize, - setPageIndex, + setPageIndex: setPageIndexValue, setPageSize: setPageSizeValue, search, 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 8108430d..be8203d4 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,5 +1,5 @@ import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components"; -import type { ColumnDef } from "@tanstack/react-table"; +import type { ColumnDef, OnChangeFn, VisibilityState } from "@tanstack/react-table"; import { useTranslation } from "../../../../../i18n"; import type { ProformaList, ProformaListRow } from "../../../../shared"; @@ -16,6 +16,9 @@ interface ProformasGridProps { onPageChange: (pageIndex: number) => void; onPageSizeChange: (size: number) => void; + columnVisibility?: VisibilityState; + onColumnVisibilityChange?: OnChangeFn; + onRowClick?: (proformaId: string) => void; } @@ -28,6 +31,8 @@ export const ProformasGrid = ({ pageSize, onPageChange, onPageSizeChange, + columnVisibility, + onColumnVisibilityChange, onRowClick, }: ProformasGridProps) => { const { t } = useTranslation(); @@ -47,10 +52,12 @@ export const ProformasGrid = ({ return ( onRowClick?.(row.id)} diff --git a/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx b/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx index caa78309..9e5e420b 100644 --- a/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx +++ b/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx @@ -1,4 +1,5 @@ import { ErrorAlert, PageHeader, SimpleSearchInput } from "@erp/core/components"; +import { useReturnToNavigation } from "@erp/core/hooks"; import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components"; import { Button, @@ -12,7 +13,7 @@ import { SelectValue, } from "@repo/shadcn-ui/components"; import { FilterIcon, PlusIcon } from "lucide-react"; -import { useNavigate } from "react-router-dom"; +import { createSearchParams, useLocation, useNavigate } from "react-router-dom"; import { useTranslation } from "../../../../i18n"; import { ChangeProformaStatusDialog } from "../../../change-status"; @@ -29,12 +30,26 @@ import { ProformaStatusBadge } from "../components"; export const ListProformasPage = () => { const { t } = useTranslation(); const navigate = useNavigate(); + const location = useLocation(); + + const { currentReturnTo } = useReturnToNavigation({ + fallbackPath: "/proformas", + }); const { listCtrl, panelCtrl, deleteDialogCtrl, issueDialogCtrl, changeStatusDialogCtrl } = useListProformasPageController(); + const handleEditClick = (proformaId: string) => { + navigate({ + pathname: `/proformas/${proformaId}/edit`, + search: createSearchParams({ + returnTo: currentReturnTo, + }).toString(), + }); + }; + const columns = useProformasGridColumns({ - onEditClick: (proforma) => navigate(`/proformas/${proforma.id}/edit`), + onEditClick: (proforma) => handleEditClick(proforma.id), onIssueClick: (proformaRow) => issueDialogCtrl.openDialog(prepareIssueProformaTarget(proformaRow)), onDeleteClick: (proformaRow: ProformaListRow) => @@ -75,9 +90,11 @@ export const ListProformasPage = () => {
panelCtrl.openProformaPanel(proformaId, "view")} @@ -86,26 +103,33 @@ export const ListProformasPage = () => { /> {/* Explicación técnica */} -
+

Estado de proforma

-
    +
    • - Borrador: Un{" "} - div absoluto con{" "} - pointer-events: none y{" "} - linear-gradient se superpone - encima del scroll. + Borrador: Una proforma sin terminar y + pendiente de modificaciones.
    • - Columna sticky: La celda de acciones usa{" "} - sticky right-0 z-20. + + Enviada: Se ha enviado la proforma al + cliente para su aprobación.
    • - Responsive: Columnas ocultas en móviles ( - hidden sm:,{" "} - hidden md:,{" "} - hidden lg:). + + Aprobada: La proforma ha sido aprobada + por el cliente y puede pasar a factura. +
    • +
    • + + Rechazada: La proforma ha sido rechazada + por el cliente. Puede quedarse así o volver a "borrador". +
    • +
    • + + Facturada: La proforma ya ha sido pasada + a factura.
@@ -162,7 +186,7 @@ export const ListProformasPage = () => { navigate(`/proformas/${proforma.id}/edit`)} + onEdit={(proforma) => handleEditClick(proforma.id)} onOpenChange={(open) => { if (!open) { panelCtrl.closePanel(); diff --git a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-page-controller.ts b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-page-controller.ts index ccabe2d3..2543653a 100644 --- a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-page-controller.ts +++ b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-page-controller.ts @@ -1,13 +1,17 @@ import { useUrlParamId } from "@erp/core/hooks"; import { useCustomerSelectionFlow } from "@erp/customers/common"; +import { useSearchParams } from "react-router-dom"; import { useUpdateProformaController } from "./use-update-proforma-controller"; export const useUpdateProformaPageController = () => { const proformaId = useUrlParamId(); + const [searchParams] = useSearchParams(); const updateCtrl = useUpdateProformaController(proformaId); + const returnTo = searchParams.get("returnTo") ?? "/proformas"; + const selectCustomerCtrl = useCustomerSelectionFlow({ defaultLanguageCode: updateCtrl.form.watch("languageCode"), defaultCurrencyCode: updateCtrl.form.watch("currencyCode"), @@ -19,5 +23,6 @@ export const useUpdateProformaPageController = () => { return { updateCtrl, selectCustomerCtrl, + returnTo, }; }; diff --git a/modules/customer-invoices/src/web/proformas/update/ui/pages/proforma-update-page.tsx b/modules/customer-invoices/src/web/proformas/update/ui/pages/proforma-update-page.tsx index 4647def3..d5b2f6c9 100644 --- a/modules/customer-invoices/src/web/proformas/update/ui/pages/proforma-update-page.tsx +++ b/modules/customer-invoices/src/web/proformas/update/ui/pages/proforma-update-page.tsx @@ -1,11 +1,14 @@ import { SpainTaxCatalogProvider } from "@erp/core"; import { ErrorAlert, NotFoundCard, PageHeader } from "@erp/core/components"; -import { FormCommitButtonGroup, UnsavedChangesProvider } from "@erp/core/hooks"; +import { + FormCommitButtonGroup, + UnsavedChangesProvider, + useReturnToNavigation, +} from "@erp/core/hooks"; import { SelectCustomerDialog } from "@erp/customers"; import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components"; import { useMemo } from "react"; import { FormProvider } from "react-hook-form"; -import { useNavigate } from "react-router-dom"; import { useTranslation } from "../../../../i18n"; import { useUpdateProformaPageController } from "../../controllers/use-update-proforma-page-controller"; @@ -14,10 +17,13 @@ import { ProformaUpdateEditorForm } from "../editors"; export const ProformaUpdatePage = () => { const { t } = useTranslation(); - const navigate = useNavigate(); const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []); - const { updateCtrl, selectCustomerCtrl } = useUpdateProformaPageController(); + const { updateCtrl, selectCustomerCtrl, returnTo } = useUpdateProformaPageController(); + + const { navigateBack } = useReturnToNavigation({ + fallbackPath: returnTo, + }); if (updateCtrl.isLoading) { return ; @@ -36,7 +42,7 @@ export const ProformaUpdatePage = () => { />
- + navigateBack()} />
); @@ -61,14 +67,15 @@ export const ProformaUpdatePage = () => { navigate("/proformas/list")} + onBackClick={() => navigateBack()} rightSlot={ navigateBack(), }} disabled={updateCtrl.isUpdating} isLoading={updateCtrl.isUpdating} + onBack={() => navigateBack()} onReset={updateCtrl.form.formState.isDirty ? updateCtrl.resetForm : undefined} submit={{ formId: updateCtrl.formId, diff --git a/packages/rdx-ui/src/components/buttons/back-history-button.tsx b/packages/rdx-ui/src/components/buttons/back-history-button.tsx index d7d13a2b..a983c2dc 100644 --- a/packages/rdx-ui/src/components/buttons/back-history-button.tsx +++ b/packages/rdx-ui/src/components/buttons/back-history-button.tsx @@ -4,12 +4,17 @@ import { useNavigate } from "react-router-dom"; import { useTranslation } from "../../locales/i18n.ts"; -export const BackHistoryButton = () => { +export const BackHistoryButton = ({ onClick }: { onClick?: () => void }) => { const { t } = useTranslation(); const navigate = useNavigate(); return ( - diff --git a/packages/rdx-ui/src/components/datatable/data-table.tsx b/packages/rdx-ui/src/components/datatable/data-table.tsx index c92640c0..bb33c7c7 100644 --- a/packages/rdx-ui/src/components/datatable/data-table.tsx +++ b/packages/rdx-ui/src/components/datatable/data-table.tsx @@ -11,6 +11,7 @@ import { type ColumnDef, type ColumnFiltersState, type ColumnSizingState, + type OnChangeFn, type Row, type SortingState, type Table, @@ -71,6 +72,8 @@ export interface DataTableProps { // Configuración columnVisibility?: VisibilityState; + onColumnVisibilityChange?: OnChangeFn; + readOnly?: boolean; enablePagination?: boolean; pageSize?: number; @@ -86,7 +89,11 @@ export interface DataTableProps { onPageSizeChange?: (pageSize: number) => void; // Acción al hacer click en una fila - onRowClick?: (row: TData, index: number, event: React.MouseEvent) => void; + onRowClick?: ( + row: TData, + index: number, + event: React.MouseEvent | React.KeyboardEvent + ) => void; } export function DataTable({ @@ -94,7 +101,9 @@ export function DataTable({ data, meta, - columnVisibility: inititalcolumnVisibility = {}, + columnVisibility, + onColumnVisibilityChange, + readOnly = false, enablePagination = true, pageSize = 10, @@ -114,11 +123,17 @@ export function DataTable({ const [rowSelection, setRowSelection] = React.useState({}); const [sorting, setSorting] = React.useState([]); - const [columnVisibility, setColumnVisibility] = - React.useState(inititalcolumnVisibility); const [columnFilters, setColumnFilters] = React.useState([]); const [colSizes, setColSizes] = React.useState({}); + const [internalColumnVisibility, setInternalColumnVisibility] = React.useState( + {} + ); + + const resolvedColumnVisibility = columnVisibility ?? internalColumnVisibility; + + const handleColumnVisibilityChange = onColumnVisibilityChange ?? setInternalColumnVisibility; + // Configuración TanStack const table = useReactTable({ data, @@ -137,7 +152,7 @@ export function DataTable({ state: { columnSizing: colSizes, sorting, - columnVisibility, + columnVisibility: resolvedColumnVisibility, rowSelection, columnFilters, pagination: { pageIndex, pageSize }, @@ -165,7 +180,7 @@ export function DataTable({ onRowSelectionChange: setRowSelection, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, + onColumnVisibilityChange: handleColumnVisibilityChange, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), @@ -229,10 +244,16 @@ export function DataTable({ data-state={row.getIsSelected() && "selected"} key={row.id} onClick={(e) => onRowClick?.(row.original, rowIndex, e)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") - onRowClick?.(row.original, rowIndex, e as any); + onKeyDown={(event) => { + if (event.key !== "Enter" && event.key !== " ") { + return; + } + + event.preventDefault(); + onRowClick?.(row.original, rowIndex, event); }} + role={onRowClick ? "button" : undefined} + tabIndex={onRowClick ? 0 : undefined} > {row.getVisibleCells().map((cell) => { /* diff --git a/packages/rdx-ui/src/components/datatable/hooks/index.ts b/packages/rdx-ui/src/components/datatable/hooks/index.ts new file mode 100644 index 00000000..3db886d1 --- /dev/null +++ b/packages/rdx-ui/src/components/datatable/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-data-table-preferences.ts"; diff --git a/packages/rdx-ui/src/components/datatable/hooks/use-data-table-preferences.ts b/packages/rdx-ui/src/components/datatable/hooks/use-data-table-preferences.ts new file mode 100644 index 00000000..daa15a24 --- /dev/null +++ b/packages/rdx-ui/src/components/datatable/hooks/use-data-table-preferences.ts @@ -0,0 +1,91 @@ +import type { OnChangeFn, VisibilityState } from "@tanstack/react-table"; +import * as React from "react"; + +export interface DataTablePreferences { + pageSize: number; + columnVisibility: VisibilityState; +} + +interface UseDataTablePreferencesParams { + storageKey: string; + defaultPageSize: number; + defaultColumnVisibility?: VisibilityState; +} + +export const useDataTablePreferences = ({ + storageKey, + defaultPageSize, + defaultColumnVisibility = {}, +}: UseDataTablePreferencesParams) => { + const [preferences, setPreferences] = React.useState(() => { + try { + const rawValue = window.localStorage.getItem(storageKey); + + if (!rawValue) { + return { + pageSize: defaultPageSize, + columnVisibility: defaultColumnVisibility, + }; + } + + const parsed = JSON.parse(rawValue) as Partial; + + return { + pageSize: parsed.pageSize ?? defaultPageSize, + columnVisibility: { + ...defaultColumnVisibility, + ...parsed.columnVisibility, + }, + }; + } catch { + return { + pageSize: defaultPageSize, + columnVisibility: defaultColumnVisibility, + }; + } + }); + + const persist = React.useCallback( + (next: DataTablePreferences) => { + window.localStorage.setItem(storageKey, JSON.stringify(next)); + }, + [storageKey] + ); + + const setPageSize = React.useCallback( + (pageSize: number) => { + setPreferences((previous) => { + const next = { ...previous, pageSize }; + persist(next); + return next; + }); + }, + [persist] + ); + + const setColumnVisibility = React.useCallback>( + (updater) => { + setPreferences((previous) => { + const nextColumnVisibility = + typeof updater === "function" ? updater(previous.columnVisibility) : updater; + + const next = { + ...previous, + columnVisibility: nextColumnVisibility, + }; + + persist(next); + + return next; + }); + }, + [persist] + ); + + return { + pageSize: preferences.pageSize, + columnVisibility: preferences.columnVisibility, + setPageSize, + setColumnVisibility, + }; +}; diff --git a/packages/rdx-ui/src/components/datatable/index.ts b/packages/rdx-ui/src/components/datatable/index.ts index 918d2ae7..7a4aa64f 100644 --- a/packages/rdx-ui/src/components/datatable/index.ts +++ b/packages/rdx-ui/src/components/datatable/index.ts @@ -1,5 +1,4 @@ -export * from "./data-table-column-header.tsx"; export * from "./data-table.tsx"; - +export * from "./data-table-column-header.tsx"; +export * from "./hooks/index.ts"; export * from "./skeleton-data-table.tsx"; -export * from "./with-row-selection.tsx"; diff --git a/packages/rdx-utils/src/helpers/number-helper.ts b/packages/rdx-utils/src/helpers/number-helper.ts index 5b8274b7..d3b286e0 100644 --- a/packages/rdx-utils/src/helpers/number-helper.ts +++ b/packages/rdx-utils/src/helpers/number-helper.ts @@ -51,7 +51,18 @@ const roundToScale = (n: number, scale = 2) => { // Stepping teclado con redondeo a escala const stepNumber = (base: number, step = 0.01, scale = 2) => roundToScale(base + step, scale); +const parsePositiveInteger = (value: string | null, fallback: number): number => { + const parsed = Number(value); + + if (!Number.isInteger(parsed) || parsed < 1) { + return fallback; + } + + return parsed; +}; + export const NumberHelper = { + parsePositiveInteger, toSafeNumber, formatNumber, parseLocaleNumber,