Guardar preferencias y returnTo en grid de listados
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
c98bc6cafc
commit
6e53e5bd58
@ -1,6 +1,7 @@
|
|||||||
export * from "./use-datasource";
|
export * from "./use-datasource";
|
||||||
export * from "./use-debounce";
|
export * from "./use-debounce";
|
||||||
export * from "./use-hook-form";
|
export * from "./use-hook-form";
|
||||||
|
export * from "./use-return-to-navigate";
|
||||||
export * from "./use-rhf-error-focus";
|
export * from "./use-rhf-error-focus";
|
||||||
export * from "./use-unsaved-changes-notifier";
|
export * from "./use-unsaved-changes-notifier";
|
||||||
export * from "./use-url-param-id";
|
export * from "./use-url-param-id";
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./use-return-to-navigation";
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,5 +1,8 @@
|
|||||||
import { useDebounce } from "@erp/core/hooks";
|
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 {
|
import {
|
||||||
type ListProformasByCriteriaParams,
|
type ListProformasByCriteriaParams,
|
||||||
@ -19,10 +22,33 @@ const EMPTY_PROFORMAS_LIST: ProformaList = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useListProformasController = () => {
|
export const useListProformasController = () => {
|
||||||
const [pageIndex, setPageIndex] = useState(0);
|
|
||||||
const [pageSize, setPageSize] = useState(5);
|
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [statusFilter, setStatusFilter] = useState<ProformaListStatusFilter>("all");
|
const [statusFilter, setStatusFilter] = useState<ProformaListStatusFilter>("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);
|
const debouncedSearch = useDebounce(search, 300);
|
||||||
|
|
||||||
@ -41,42 +67,71 @@ export const useListProformasController = () => {
|
|||||||
|
|
||||||
const query = useProformasListQuery({ criteria });
|
const query = useProformasListQuery({ criteria });
|
||||||
|
|
||||||
const setStatusFilterValue = (value: string) => {
|
const setStatusFilterValue = useCallback(
|
||||||
const nextValue = (value || "all") as ProformaListStatusFilter;
|
(value: string) => {
|
||||||
|
const nextValue = (value || "all") as ProformaListStatusFilter;
|
||||||
|
|
||||||
setStatusFilter((prev) => {
|
setStatusFilter((prev) => {
|
||||||
if (prev === nextValue) return prev;
|
if (prev === nextValue) return prev;
|
||||||
|
|
||||||
// Sólo si la búsqueda realmente cambia,
|
// Reset page to 1 when status filter changes
|
||||||
// reseteamos la página a 0 para evitar inconsistencias
|
setSearchParams((prev) => {
|
||||||
setPageIndex(0);
|
const params = new URLSearchParams(prev);
|
||||||
return nextValue;
|
params.set("page", "1");
|
||||||
});
|
return params;
|
||||||
};
|
});
|
||||||
|
return nextValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setSearchParams]
|
||||||
|
);
|
||||||
|
|
||||||
const setSearchValue = (value: string) => {
|
const setSearchValue = useCallback(
|
||||||
const nextValue = value.trim().replace(/\s+/g, " ");
|
(value: string) => {
|
||||||
|
const nextValue = value.trim().replace(/\s+/g, " ");
|
||||||
|
|
||||||
setSearch((prev) => {
|
setSearch((prev) => {
|
||||||
if (prev === nextValue) return prev;
|
if (prev === nextValue) return prev;
|
||||||
|
|
||||||
// Sólo si la búsqueda realmente cambia,
|
// Reset page to 1 when search changes
|
||||||
// reseteamos la página a 0 para evitar inconsistencias
|
setSearchParams((prev) => {
|
||||||
setPageIndex(0);
|
const params = new URLSearchParams(prev);
|
||||||
return nextValue;
|
params.set("page", "1");
|
||||||
});
|
return params;
|
||||||
};
|
});
|
||||||
|
return nextValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setSearchParams]
|
||||||
|
);
|
||||||
|
|
||||||
const setPageSizeValue = (value: number) => {
|
const setPageIndexValue = useCallback(
|
||||||
setPageSize((prev) => {
|
(newPageIndex: number) => {
|
||||||
if (prev === value) return prev;
|
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,
|
const setPageSizeValue = useCallback(
|
||||||
// reseteamos la página a 0 para evitar inconsistencias
|
(value: number) => {
|
||||||
setPageIndex(0);
|
if (pageSize === value) return;
|
||||||
return value;
|
|
||||||
});
|
// 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 {
|
return {
|
||||||
data: query.data ?? EMPTY_PROFORMAS_LIST,
|
data: query.data ?? EMPTY_PROFORMAS_LIST,
|
||||||
@ -88,9 +143,11 @@ export const useListProformasController = () => {
|
|||||||
|
|
||||||
refetch: query.refetch,
|
refetch: query.refetch,
|
||||||
|
|
||||||
|
tablePreferences,
|
||||||
|
|
||||||
pageIndex,
|
pageIndex,
|
||||||
pageSize,
|
pageSize,
|
||||||
setPageIndex,
|
setPageIndex: setPageIndexValue,
|
||||||
setPageSize: setPageSizeValue,
|
setPageSize: setPageSizeValue,
|
||||||
|
|
||||||
search,
|
search,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
|
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 { useTranslation } from "../../../../../i18n";
|
||||||
import type { ProformaList, ProformaListRow } from "../../../../shared";
|
import type { ProformaList, ProformaListRow } from "../../../../shared";
|
||||||
@ -16,6 +16,9 @@ interface ProformasGridProps {
|
|||||||
onPageChange: (pageIndex: number) => void;
|
onPageChange: (pageIndex: number) => void;
|
||||||
onPageSizeChange: (size: number) => void;
|
onPageSizeChange: (size: number) => void;
|
||||||
|
|
||||||
|
columnVisibility?: VisibilityState;
|
||||||
|
onColumnVisibilityChange?: OnChangeFn<VisibilityState>;
|
||||||
|
|
||||||
onRowClick?: (proformaId: string) => void;
|
onRowClick?: (proformaId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,6 +31,8 @@ export const ProformasGrid = ({
|
|||||||
pageSize,
|
pageSize,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
onPageSizeChange,
|
onPageSizeChange,
|
||||||
|
columnVisibility,
|
||||||
|
onColumnVisibilityChange,
|
||||||
onRowClick,
|
onRowClick,
|
||||||
}: ProformasGridProps) => {
|
}: ProformasGridProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -47,10 +52,12 @@ export const ProformasGrid = ({
|
|||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
columnVisibility={columnVisibility}
|
||||||
data={items}
|
data={items}
|
||||||
enablePagination
|
enablePagination
|
||||||
enableRowSelection
|
enableRowSelection
|
||||||
manualPagination
|
manualPagination
|
||||||
|
onColumnVisibilityChange={onColumnVisibilityChange}
|
||||||
onPageChange={onPageChange}
|
onPageChange={onPageChange}
|
||||||
onPageSizeChange={onPageSizeChange}
|
onPageSizeChange={onPageSizeChange}
|
||||||
//onRowClick={(row) => onRowClick?.(row.id)}
|
//onRowClick={(row) => onRowClick?.(row.id)}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { ErrorAlert, PageHeader, SimpleSearchInput } from "@erp/core/components";
|
import { ErrorAlert, PageHeader, SimpleSearchInput } from "@erp/core/components";
|
||||||
|
import { useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -12,7 +13,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { FilterIcon, PlusIcon } from "lucide-react";
|
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 { useTranslation } from "../../../../i18n";
|
||||||
import { ChangeProformaStatusDialog } from "../../../change-status";
|
import { ChangeProformaStatusDialog } from "../../../change-status";
|
||||||
@ -29,12 +30,26 @@ import { ProformaStatusBadge } from "../components";
|
|||||||
export const ListProformasPage = () => {
|
export const ListProformasPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const { currentReturnTo } = useReturnToNavigation({
|
||||||
|
fallbackPath: "/proformas",
|
||||||
|
});
|
||||||
|
|
||||||
const { listCtrl, panelCtrl, deleteDialogCtrl, issueDialogCtrl, changeStatusDialogCtrl } =
|
const { listCtrl, panelCtrl, deleteDialogCtrl, issueDialogCtrl, changeStatusDialogCtrl } =
|
||||||
useListProformasPageController();
|
useListProformasPageController();
|
||||||
|
|
||||||
|
const handleEditClick = (proformaId: string) => {
|
||||||
|
navigate({
|
||||||
|
pathname: `/proformas/${proformaId}/edit`,
|
||||||
|
search: createSearchParams({
|
||||||
|
returnTo: currentReturnTo,
|
||||||
|
}).toString(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const columns = useProformasGridColumns({
|
const columns = useProformasGridColumns({
|
||||||
onEditClick: (proforma) => navigate(`/proformas/${proforma.id}/edit`),
|
onEditClick: (proforma) => handleEditClick(proforma.id),
|
||||||
onIssueClick: (proformaRow) =>
|
onIssueClick: (proformaRow) =>
|
||||||
issueDialogCtrl.openDialog(prepareIssueProformaTarget(proformaRow)),
|
issueDialogCtrl.openDialog(prepareIssueProformaTarget(proformaRow)),
|
||||||
onDeleteClick: (proformaRow: ProformaListRow) =>
|
onDeleteClick: (proformaRow: ProformaListRow) =>
|
||||||
@ -75,9 +90,11 @@ export const ListProformasPage = () => {
|
|||||||
<div>
|
<div>
|
||||||
<ProformasGrid
|
<ProformasGrid
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
columnVisibility={listCtrl.tablePreferences.columnVisibility}
|
||||||
data={listCtrl.data}
|
data={listCtrl.data}
|
||||||
fetching={listCtrl.isFetching}
|
fetching={listCtrl.isFetching}
|
||||||
loading={listCtrl.isLoading}
|
loading={listCtrl.isLoading}
|
||||||
|
onColumnVisibilityChange={listCtrl.tablePreferences.setColumnVisibility}
|
||||||
onPageChange={listCtrl.setPageIndex}
|
onPageChange={listCtrl.setPageIndex}
|
||||||
onPageSizeChange={listCtrl.setPageSize}
|
onPageSizeChange={listCtrl.setPageSize}
|
||||||
onRowClick={(proformaId) => panelCtrl.openProformaPanel(proformaId, "view")}
|
onRowClick={(proformaId) => panelCtrl.openProformaPanel(proformaId, "view")}
|
||||||
@ -86,26 +103,33 @@ export const ListProformasPage = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Explicación técnica */}
|
{/* Explicación técnica */}
|
||||||
<div className="mt-8 rounded border border-border bg-card p-3 sm:p-4 text-xs sm:text-sm text-muted-foreground space-y-2">
|
<div className="mt-8 rounded border border-border bg-card p-3 sm:p-4 text-xs sm:text-sm text-muted-foreground space-y-4">
|
||||||
<p className="font-semibold text-foreground">Estado de proforma</p>
|
<p className="font-semibold text-foreground">Estado de proforma</p>
|
||||||
<ul className="list-disc list-inside space-y-1">
|
<ul className="list-disc list-inside space-y-4">
|
||||||
<li>
|
<li>
|
||||||
<ProformaStatusBadge status="draft" />
|
<ProformaStatusBadge status="draft" />
|
||||||
<strong className="text-foreground">Borrador:</strong> Un{" "}
|
<strong className="text-foreground">Borrador:</strong> Una proforma sin terminar y
|
||||||
<code className="rounded bg-muted px-1 text-xs">div</code> absoluto con{" "}
|
pendiente de modificaciones.
|
||||||
<code className="rounded bg-muted px-1 text-xs">pointer-events: none</code> y{" "}
|
|
||||||
<code className="rounded bg-muted px-1 text-xs">linear-gradient</code> se superpone
|
|
||||||
encima del scroll.
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong className="text-foreground">Columna sticky:</strong> La celda de acciones usa{" "}
|
<ProformaStatusBadge status="sent" />
|
||||||
<code className="rounded bg-muted px-1 text-xs">sticky right-0 z-20</code>.
|
<strong className="text-foreground">Enviada:</strong> Se ha enviado la proforma al
|
||||||
|
cliente para su aprobación.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong className="text-foreground">Responsive:</strong> Columnas ocultas en móviles (
|
<ProformaStatusBadge status="approved" />
|
||||||
<code className="rounded bg-muted px-1 text-xs">hidden sm:</code>,{" "}
|
<strong className="text-foreground">Aprobada:</strong> La proforma ha sido aprobada
|
||||||
<code className="rounded bg-muted px-1 text-xs">hidden md:</code>,{" "}
|
por el cliente y puede pasar a factura.
|
||||||
<code className="rounded bg-muted px-1 text-xs">hidden lg:</code>).
|
</li>
|
||||||
|
<li>
|
||||||
|
<ProformaStatusBadge status="rejected" />
|
||||||
|
<strong className="text-foreground">Rechazada:</strong> La proforma ha sido rechazada
|
||||||
|
por el cliente. Puede quedarse así o volver a "borrador".
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ProformaStatusBadge status="issued" />
|
||||||
|
<strong className="text-foreground">Facturada:</strong> La proforma ya ha sido pasada
|
||||||
|
a factura.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -162,7 +186,7 @@ export const ListProformasPage = () => {
|
|||||||
<ProformaSummaryPanel
|
<ProformaSummaryPanel
|
||||||
className="border bg-background"
|
className="border bg-background"
|
||||||
mode={panelCtrl.panelState.mode}
|
mode={panelCtrl.panelState.mode}
|
||||||
onEdit={(proforma) => navigate(`/proformas/${proforma.id}/edit`)}
|
onEdit={(proforma) => handleEditClick(proforma.id)}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
panelCtrl.closePanel();
|
panelCtrl.closePanel();
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
import { useUrlParamId } from "@erp/core/hooks";
|
import { useUrlParamId } from "@erp/core/hooks";
|
||||||
import { useCustomerSelectionFlow } from "@erp/customers/common";
|
import { useCustomerSelectionFlow } from "@erp/customers/common";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
import { useUpdateProformaController } from "./use-update-proforma-controller";
|
import { useUpdateProformaController } from "./use-update-proforma-controller";
|
||||||
|
|
||||||
export const useUpdateProformaPageController = () => {
|
export const useUpdateProformaPageController = () => {
|
||||||
const proformaId = useUrlParamId();
|
const proformaId = useUrlParamId();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const updateCtrl = useUpdateProformaController(proformaId);
|
const updateCtrl = useUpdateProformaController(proformaId);
|
||||||
|
|
||||||
|
const returnTo = searchParams.get("returnTo") ?? "/proformas";
|
||||||
|
|
||||||
const selectCustomerCtrl = useCustomerSelectionFlow({
|
const selectCustomerCtrl = useCustomerSelectionFlow({
|
||||||
defaultLanguageCode: updateCtrl.form.watch("languageCode"),
|
defaultLanguageCode: updateCtrl.form.watch("languageCode"),
|
||||||
defaultCurrencyCode: updateCtrl.form.watch("currencyCode"),
|
defaultCurrencyCode: updateCtrl.form.watch("currencyCode"),
|
||||||
@ -19,5 +23,6 @@ export const useUpdateProformaPageController = () => {
|
|||||||
return {
|
return {
|
||||||
updateCtrl,
|
updateCtrl,
|
||||||
selectCustomerCtrl,
|
selectCustomerCtrl,
|
||||||
|
returnTo,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
import { SpainTaxCatalogProvider } from "@erp/core";
|
import { SpainTaxCatalogProvider } from "@erp/core";
|
||||||
import { ErrorAlert, NotFoundCard, PageHeader } from "@erp/core/components";
|
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 { SelectCustomerDialog } from "@erp/customers";
|
||||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { FormProvider } from "react-hook-form";
|
import { FormProvider } from "react-hook-form";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import { useUpdateProformaPageController } from "../../controllers/use-update-proforma-page-controller";
|
import { useUpdateProformaPageController } from "../../controllers/use-update-proforma-page-controller";
|
||||||
@ -14,10 +17,13 @@ import { ProformaUpdateEditorForm } from "../editors";
|
|||||||
|
|
||||||
export const ProformaUpdatePage = () => {
|
export const ProformaUpdatePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
|
||||||
const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []);
|
const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []);
|
||||||
|
|
||||||
const { updateCtrl, selectCustomerCtrl } = useUpdateProformaPageController();
|
const { updateCtrl, selectCustomerCtrl, returnTo } = useUpdateProformaPageController();
|
||||||
|
|
||||||
|
const { navigateBack } = useReturnToNavigation({
|
||||||
|
fallbackPath: returnTo,
|
||||||
|
});
|
||||||
|
|
||||||
if (updateCtrl.isLoading) {
|
if (updateCtrl.isLoading) {
|
||||||
return <ProformaUpdateSkeleton />;
|
return <ProformaUpdateSkeleton />;
|
||||||
@ -36,7 +42,7 @@ export const ProformaUpdatePage = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<BackHistoryButton />
|
<BackHistoryButton onClick={() => navigateBack()} />
|
||||||
</div>
|
</div>
|
||||||
</AppContent>
|
</AppContent>
|
||||||
);
|
);
|
||||||
@ -61,14 +67,15 @@ export const ProformaUpdatePage = () => {
|
|||||||
<AppHeader className="mx-auto max-w-5xl space-y-4">
|
<AppHeader className="mx-auto max-w-5xl space-y-4">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
description={t("pages.proformas.update.description")}
|
description={t("pages.proformas.update.description")}
|
||||||
onBackClick={() => navigate("/proformas/list")}
|
onBackClick={() => navigateBack()}
|
||||||
rightSlot={
|
rightSlot={
|
||||||
<FormCommitButtonGroup
|
<FormCommitButtonGroup
|
||||||
cancel={{
|
cancel={{
|
||||||
to: "/proformas/list",
|
onCancel: () => navigateBack(),
|
||||||
}}
|
}}
|
||||||
disabled={updateCtrl.isUpdating}
|
disabled={updateCtrl.isUpdating}
|
||||||
isLoading={updateCtrl.isUpdating}
|
isLoading={updateCtrl.isUpdating}
|
||||||
|
onBack={() => navigateBack()}
|
||||||
onReset={updateCtrl.form.formState.isDirty ? updateCtrl.resetForm : undefined}
|
onReset={updateCtrl.form.formState.isDirty ? updateCtrl.resetForm : undefined}
|
||||||
submit={{
|
submit={{
|
||||||
formId: updateCtrl.formId,
|
formId: updateCtrl.formId,
|
||||||
|
|||||||
@ -4,12 +4,17 @@ import { useNavigate } from "react-router-dom";
|
|||||||
|
|
||||||
import { useTranslation } from "../../locales/i18n.ts";
|
import { useTranslation } from "../../locales/i18n.ts";
|
||||||
|
|
||||||
export const BackHistoryButton = () => {
|
export const BackHistoryButton = ({ onClick }: { onClick?: () => void }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button className="h-7 w-7" onClick={() => navigate(-1)} size="icon" variant="outline">
|
<Button
|
||||||
|
className="h-7 w-7"
|
||||||
|
onClick={() => navigate(-1) || onClick}
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
<ChevronLeftIcon className="w-4 h-4" />
|
<ChevronLeftIcon className="w-4 h-4" />
|
||||||
<span className="sr-only">{t("common.back")}</span>
|
<span className="sr-only">{t("common.back")}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
type ColumnDef,
|
type ColumnDef,
|
||||||
type ColumnFiltersState,
|
type ColumnFiltersState,
|
||||||
type ColumnSizingState,
|
type ColumnSizingState,
|
||||||
|
type OnChangeFn,
|
||||||
type Row,
|
type Row,
|
||||||
type SortingState,
|
type SortingState,
|
||||||
type Table,
|
type Table,
|
||||||
@ -71,6 +72,8 @@ export interface DataTableProps<TData, TValue> {
|
|||||||
|
|
||||||
// Configuración
|
// Configuración
|
||||||
columnVisibility?: VisibilityState;
|
columnVisibility?: VisibilityState;
|
||||||
|
onColumnVisibilityChange?: OnChangeFn<VisibilityState>;
|
||||||
|
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
enablePagination?: boolean;
|
enablePagination?: boolean;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
@ -86,7 +89,11 @@ export interface DataTableProps<TData, TValue> {
|
|||||||
onPageSizeChange?: (pageSize: number) => void;
|
onPageSizeChange?: (pageSize: number) => void;
|
||||||
|
|
||||||
// Acción al hacer click en una fila
|
// Acción al hacer click en una fila
|
||||||
onRowClick?: (row: TData, index: number, event: React.MouseEvent<HTMLTableRowElement>) => void;
|
onRowClick?: (
|
||||||
|
row: TData,
|
||||||
|
index: number,
|
||||||
|
event: React.MouseEvent<HTMLTableRowElement> | React.KeyboardEvent<HTMLTableRowElement>
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTable<TData, TValue>({
|
export function DataTable<TData, TValue>({
|
||||||
@ -94,7 +101,9 @@ export function DataTable<TData, TValue>({
|
|||||||
data,
|
data,
|
||||||
meta,
|
meta,
|
||||||
|
|
||||||
columnVisibility: inititalcolumnVisibility = {},
|
columnVisibility,
|
||||||
|
onColumnVisibilityChange,
|
||||||
|
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
enablePagination = true,
|
enablePagination = true,
|
||||||
pageSize = 10,
|
pageSize = 10,
|
||||||
@ -114,11 +123,17 @@ export function DataTable<TData, TValue>({
|
|||||||
|
|
||||||
const [rowSelection, setRowSelection] = React.useState({});
|
const [rowSelection, setRowSelection] = React.useState({});
|
||||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||||
const [columnVisibility, setColumnVisibility] =
|
|
||||||
React.useState<VisibilityState>(inititalcolumnVisibility);
|
|
||||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
||||||
const [colSizes, setColSizes] = React.useState<ColumnSizingState>({});
|
const [colSizes, setColSizes] = React.useState<ColumnSizingState>({});
|
||||||
|
|
||||||
|
const [internalColumnVisibility, setInternalColumnVisibility] = React.useState<VisibilityState>(
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const resolvedColumnVisibility = columnVisibility ?? internalColumnVisibility;
|
||||||
|
|
||||||
|
const handleColumnVisibilityChange = onColumnVisibilityChange ?? setInternalColumnVisibility;
|
||||||
|
|
||||||
// Configuración TanStack
|
// Configuración TanStack
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
@ -137,7 +152,7 @@ export function DataTable<TData, TValue>({
|
|||||||
state: {
|
state: {
|
||||||
columnSizing: colSizes,
|
columnSizing: colSizes,
|
||||||
sorting,
|
sorting,
|
||||||
columnVisibility,
|
columnVisibility: resolvedColumnVisibility,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
columnFilters,
|
columnFilters,
|
||||||
pagination: { pageIndex, pageSize },
|
pagination: { pageIndex, pageSize },
|
||||||
@ -165,7 +180,7 @@ export function DataTable<TData, TValue>({
|
|||||||
onRowSelectionChange: setRowSelection,
|
onRowSelectionChange: setRowSelection,
|
||||||
onSortingChange: setSorting,
|
onSortingChange: setSorting,
|
||||||
onColumnFiltersChange: setColumnFilters,
|
onColumnFiltersChange: setColumnFilters,
|
||||||
onColumnVisibilityChange: setColumnVisibility,
|
onColumnVisibilityChange: handleColumnVisibilityChange,
|
||||||
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
@ -229,10 +244,16 @@ export function DataTable<TData, TValue>({
|
|||||||
data-state={row.getIsSelected() && "selected"}
|
data-state={row.getIsSelected() && "selected"}
|
||||||
key={row.id}
|
key={row.id}
|
||||||
onClick={(e) => onRowClick?.(row.original, rowIndex, e)}
|
onClick={(e) => onRowClick?.(row.original, rowIndex, e)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(event) => {
|
||||||
if (e.key === "Enter" || e.key === " ")
|
if (event.key !== "Enter" && event.key !== " ") {
|
||||||
onRowClick?.(row.original, rowIndex, e as any);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
onRowClick?.(row.original, rowIndex, event);
|
||||||
}}
|
}}
|
||||||
|
role={onRowClick ? "button" : undefined}
|
||||||
|
tabIndex={onRowClick ? 0 : undefined}
|
||||||
>
|
>
|
||||||
{row.getVisibleCells().map((cell) => {
|
{row.getVisibleCells().map((cell) => {
|
||||||
/*
|
/*
|
||||||
|
|||||||
1
packages/rdx-ui/src/components/datatable/hooks/index.ts
Normal file
1
packages/rdx-ui/src/components/datatable/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./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<DataTablePreferences>(() => {
|
||||||
|
try {
|
||||||
|
const rawValue = window.localStorage.getItem(storageKey);
|
||||||
|
|
||||||
|
if (!rawValue) {
|
||||||
|
return {
|
||||||
|
pageSize: defaultPageSize,
|
||||||
|
columnVisibility: defaultColumnVisibility,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = JSON.parse(rawValue) as Partial<DataTablePreferences>;
|
||||||
|
|
||||||
|
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<OnChangeFn<VisibilityState>>(
|
||||||
|
(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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,5 +1,4 @@
|
|||||||
export * from "./data-table-column-header.tsx";
|
|
||||||
export * from "./data-table.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 "./skeleton-data-table.tsx";
|
||||||
export * from "./with-row-selection.tsx";
|
|
||||||
|
|||||||
@ -51,7 +51,18 @@ const roundToScale = (n: number, scale = 2) => {
|
|||||||
// Stepping teclado con redondeo a escala
|
// Stepping teclado con redondeo a escala
|
||||||
const stepNumber = (base: number, step = 0.01, scale = 2) => roundToScale(base + step, scale);
|
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 = {
|
export const NumberHelper = {
|
||||||
|
parsePositiveInteger,
|
||||||
toSafeNumber,
|
toSafeNumber,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
parseLocaleNumber,
|
parseLocaleNumber,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user