Proformas: Ordenación y guardar preferencias
This commit is contained in:
parent
0aabff0d18
commit
00627096ed
@ -220,11 +220,19 @@ export class IssuedInvoiceRepository
|
|||||||
const query = criteriaConverter.convert(criteria, {
|
const query = criteriaConverter.convert(criteria, {
|
||||||
searchableFields: ["invoice_number", "reference", "description"],
|
searchableFields: ["invoice_number", "reference", "description"],
|
||||||
mappings: {
|
mappings: {
|
||||||
invoice_number: "CustomerInvoiceModel.invoice_number",
|
invoice_date: "invoice_date",
|
||||||
reference: "CustomerInvoiceModel.reference",
|
invoice_number: "invoice_number",
|
||||||
description: "CustomerInvoiceModel.description",
|
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,
|
enableFullText: true,
|
||||||
database: this.database,
|
database: this.database,
|
||||||
strictMode: true, // fuerza error si ORDER BY no permitido
|
strictMode: true, // fuerza error si ORDER BY no permitido
|
||||||
|
|||||||
@ -350,11 +350,19 @@ export class ProformaRepository
|
|||||||
const query = criteriaConverter.convert(criteria, {
|
const query = criteriaConverter.convert(criteria, {
|
||||||
searchableFields: ["invoice_number", "reference", "description"],
|
searchableFields: ["invoice_number", "reference", "description"],
|
||||||
mappings: {
|
mappings: {
|
||||||
invoice_number: "CustomerInvoiceModel.invoice_number",
|
invoice_date: "invoice_date",
|
||||||
reference: "CustomerInvoiceModel.reference",
|
invoice_number: "invoice_number",
|
||||||
description: "CustomerInvoiceModel.description",
|
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,
|
enableFullText: true,
|
||||||
database: this.database,
|
database: this.database,
|
||||||
strictMode: true, // fuerza error si ORDER BY no permitido
|
strictMode: true, // fuerza error si ORDER BY no permitido
|
||||||
|
|||||||
@ -150,12 +150,12 @@
|
|||||||
"list": {
|
"list": {
|
||||||
"title": "Customer proformas",
|
"title": "Customer proformas",
|
||||||
"description": "List all customer proformas",
|
"description": "List all customer proformas",
|
||||||
"grid_columns": {
|
"columns": {
|
||||||
"invoice_number": "#",
|
"invoice_number": "Num.",
|
||||||
"series": "Serie",
|
"series": "Serie",
|
||||||
"reference": "Reference",
|
"reference": "Reference",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"invoice_date": "Proforma date",
|
"invoice_date": "Date",
|
||||||
"operation_date": "Operation date",
|
"operation_date": "Operation date",
|
||||||
"recipient": "Customer",
|
"recipient": "Customer",
|
||||||
"recipient_tin": "TIN",
|
"recipient_tin": "TIN",
|
||||||
|
|||||||
@ -151,12 +151,12 @@
|
|||||||
"list": {
|
"list": {
|
||||||
"title": "Proformas",
|
"title": "Proformas",
|
||||||
"description": "Lista todas las proformas",
|
"description": "Lista todas las proformas",
|
||||||
"grid_columns": {
|
"columns": {
|
||||||
"invoice_number": "#",
|
"invoice_number": "Num.",
|
||||||
"series": "Serie",
|
"series": "Serie",
|
||||||
"reference": "Reference",
|
"reference": "Reference",
|
||||||
"status": "Estado",
|
"status": "Estado",
|
||||||
"invoice_date": "Fecha de proforma",
|
"invoice_date": "Fecha",
|
||||||
"operation_date": "Fecha de operación",
|
"operation_date": "Fecha de operación",
|
||||||
"recipient": "Cliente",
|
"recipient": "Cliente",
|
||||||
"recipient_tin": "NIF/CIF",
|
"recipient_tin": "NIF/CIF",
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { useDebounce } from "@erp/core/hooks";
|
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 { NumberHelper } from "@repo/rdx-utils";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
@ -13,14 +18,41 @@ import {
|
|||||||
|
|
||||||
type ProformaListStatusFilter = "all" | ProformaStatus;
|
type ProformaListStatusFilter = "all" | ProformaStatus;
|
||||||
|
|
||||||
|
// Datos por defecto mientras se carga la consulta o en caso de error.
|
||||||
const EMPTY_PROFORMAS_LIST: ProformaList = {
|
const EMPTY_PROFORMAS_LIST: ProformaList = {
|
||||||
items: [],
|
items: [],
|
||||||
page: 0,
|
page: INITIAL_PAGE_INDEX,
|
||||||
perPage: 5,
|
perPage: INITIAL_PAGE_SIZE,
|
||||||
totalPages: 0,
|
totalPages: 0,
|
||||||
totalItems: 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<ProformaListSortField, ProformaListApiSortField> = {
|
||||||
|
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 = () => {
|
export const useListProformasController = () => {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [statusFilter, setStatusFilter] = useState<ProformaListStatusFilter>("all");
|
const [statusFilter, setStatusFilter] = useState<ProformaListStatusFilter>("all");
|
||||||
@ -28,7 +60,7 @@ export const useListProformasController = () => {
|
|||||||
|
|
||||||
const tablePreferences = useDataTablePreferences({
|
const tablePreferences = useDataTablePreferences({
|
||||||
storageKey: "proformas:list:grid",
|
storageKey: "proformas:list:grid",
|
||||||
defaultPageSize: 5,
|
defaultPageSize: EMPTY_PROFORMAS_LIST.perPage,
|
||||||
defaultColumnVisibility: {
|
defaultColumnVisibility: {
|
||||||
reference: true,
|
reference: true,
|
||||||
recipientName: true,
|
recipientName: true,
|
||||||
@ -36,12 +68,17 @@ export const useListProformasController = () => {
|
|||||||
totalAmountFmt: true,
|
totalAmountFmt: true,
|
||||||
invoiceDate: true,
|
invoiceDate: true,
|
||||||
},
|
},
|
||||||
|
defaultSort: DEFAULT_SORT,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { pageSize: preferencesPageSize, setPageSize: setPageSizePreference } = tablePreferences;
|
const { pageSize: preferencesPageSize, setPageSize: setPageSizePreference } = tablePreferences;
|
||||||
|
const { sort: preferencesSorting, setSort: setSortPreference } = tablePreferences;
|
||||||
|
|
||||||
// Parse page from URL (1-based) or default to 1
|
// 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
|
const pageIndex = urlPage - 1; // Convert to 0-based for internal use
|
||||||
|
|
||||||
// Parse pageSize from URL or use preferences
|
// Parse pageSize from URL or use preferences
|
||||||
@ -52,17 +89,37 @@ export const useListProformasController = () => {
|
|||||||
|
|
||||||
const debouncedSearch = useDebounce(search, 300);
|
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<NonNullable<ListProformasByCriteriaParams["criteria"]>>(
|
const criteria = useMemo<NonNullable<ListProformasByCriteriaParams["criteria"]>>(
|
||||||
() => ({
|
() => ({
|
||||||
q: debouncedSearch || "",
|
q: debouncedSearch || "",
|
||||||
pageNumber: pageIndex,
|
pageNumber: pageIndex,
|
||||||
pageSize,
|
pageSize,
|
||||||
order: "desc",
|
orderBy,
|
||||||
orderBy: "invoice_date",
|
order,
|
||||||
filters:
|
filters:
|
||||||
statusFilter === "all" ? [] : [{ field: "status", operator: "eq", value: statusFilter }],
|
statusFilter === "all" ? [] : [{ field: "status", operator: "eq", value: statusFilter }],
|
||||||
}),
|
}),
|
||||||
[debouncedSearch, pageIndex, pageSize, statusFilter]
|
[debouncedSearch, pageIndex, pageSize, orderBy, order, statusFilter]
|
||||||
);
|
);
|
||||||
|
|
||||||
const query = useProformasListQuery({ criteria });
|
const query = useProformasListQuery({ criteria });
|
||||||
@ -96,7 +153,7 @@ export const useListProformasController = () => {
|
|||||||
// Reset page to 1 when search changes
|
// Reset page to 1 when search changes
|
||||||
setSearchParams((prev) => {
|
setSearchParams((prev) => {
|
||||||
const params = new URLSearchParams(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 params;
|
||||||
});
|
});
|
||||||
return nextValue;
|
return nextValue;
|
||||||
@ -124,7 +181,7 @@ export const useListProformasController = () => {
|
|||||||
// Reset page to 1 and update pageSize when it changes
|
// Reset page to 1 and update pageSize when it changes
|
||||||
setSearchParams((prev) => {
|
setSearchParams((prev) => {
|
||||||
const params = new URLSearchParams(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));
|
params.set("pageSize", String(value));
|
||||||
return params;
|
return params;
|
||||||
});
|
});
|
||||||
@ -133,6 +190,31 @@ export const useListProformasController = () => {
|
|||||||
[pageSize, setSearchParams, setPageSizePreference]
|
[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 {
|
return {
|
||||||
data: query.data ?? EMPTY_PROFORMAS_LIST,
|
data: query.data ?? EMPTY_PROFORMAS_LIST,
|
||||||
isLoading: query.isLoading,
|
isLoading: query.isLoading,
|
||||||
@ -153,6 +235,9 @@ export const useListProformasController = () => {
|
|||||||
search,
|
search,
|
||||||
setSearchValue,
|
setSearchValue,
|
||||||
|
|
||||||
|
sort,
|
||||||
|
setSort: setSortValue,
|
||||||
|
|
||||||
statusFilter,
|
statusFilter,
|
||||||
setStatusFilter: setStatusFilterValue,
|
setStatusFilter: setStatusFilterValue,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 type { ColumnDef, OnChangeFn, VisibilityState } from "@tanstack/react-table";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../../i18n";
|
import { useTranslation } from "../../../../../i18n";
|
||||||
@ -16,6 +16,9 @@ interface ProformasGridProps {
|
|||||||
onPageChange: (pageIndex: number) => void;
|
onPageChange: (pageIndex: number) => void;
|
||||||
onPageSizeChange: (size: number) => void;
|
onPageSizeChange: (size: number) => void;
|
||||||
|
|
||||||
|
sort: DataTableSort;
|
||||||
|
onSortChange?: (nextSort: DataTableSort) => void;
|
||||||
|
|
||||||
columnVisibility?: VisibilityState;
|
columnVisibility?: VisibilityState;
|
||||||
onColumnVisibilityChange?: OnChangeFn<VisibilityState>;
|
onColumnVisibilityChange?: OnChangeFn<VisibilityState>;
|
||||||
|
|
||||||
@ -24,15 +27,22 @@ interface ProformasGridProps {
|
|||||||
|
|
||||||
export const ProformasGrid = ({
|
export const ProformasGrid = ({
|
||||||
data,
|
data,
|
||||||
|
columns,
|
||||||
|
|
||||||
loading,
|
loading,
|
||||||
fetching,
|
fetching,
|
||||||
columns,
|
|
||||||
pageIndex,
|
pageIndex,
|
||||||
pageSize,
|
pageSize,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
onPageSizeChange,
|
onPageSizeChange,
|
||||||
|
|
||||||
|
sort,
|
||||||
|
onSortChange,
|
||||||
|
|
||||||
columnVisibility,
|
columnVisibility,
|
||||||
onColumnVisibilityChange,
|
onColumnVisibilityChange,
|
||||||
|
|
||||||
onRowClick,
|
onRowClick,
|
||||||
}: ProformasGridProps) => {
|
}: ProformasGridProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -57,12 +67,15 @@ export const ProformasGrid = ({
|
|||||||
enablePagination
|
enablePagination
|
||||||
enableRowSelection
|
enableRowSelection
|
||||||
manualPagination
|
manualPagination
|
||||||
|
manualSorting
|
||||||
onColumnVisibilityChange={onColumnVisibilityChange}
|
onColumnVisibilityChange={onColumnVisibilityChange}
|
||||||
onPageChange={onPageChange}
|
onPageChange={onPageChange}
|
||||||
onPageSizeChange={onPageSizeChange}
|
onPageSizeChange={onPageSizeChange}
|
||||||
|
onSortChange={onSortChange}
|
||||||
//onRowClick={(row) => onRowClick?.(row.id)}
|
//onRowClick={(row) => onRowClick?.(row.id)}
|
||||||
pageIndex={pageIndex}
|
pageIndex={pageIndex}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
|
sort={sort}
|
||||||
totalItems={totalItems}
|
totalItems={totalItems}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { DataTableColumnHeader } from "@repo/rdx-ui/components";
|
||||||
import { DateHelper } from "@repo/rdx-utils";
|
import { DateHelper } from "@repo/rdx-utils";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -9,7 +10,6 @@ import {
|
|||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
ArrowUpDownIcon,
|
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
FileTextIcon,
|
FileTextIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
@ -46,6 +46,7 @@ export function useProformasGridColumns(
|
|||||||
): ColumnDef<ProformaListRow, unknown>[] {
|
): ColumnDef<ProformaListRow, unknown>[] {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// Todas las columnas deben tener un id
|
||||||
return React.useMemo<ColumnDef<ProformaListRow, unknown>[]>(
|
return React.useMemo<ColumnDef<ProformaListRow, unknown>[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@ -70,37 +71,52 @@ export function useProformasGridColumns(
|
|||||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
enableSorting: false,
|
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "invoiceDate",
|
||||||
|
accessorKey: "invoiceDate",
|
||||||
|
enableHiding: false,
|
||||||
|
enableSorting: true,
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader
|
||||||
|
column={column}
|
||||||
|
title={t("pages.proformas.list.columns.invoice_date")}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => DateHelper.format(row.original.invoiceDate),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "series",
|
||||||
accessorKey: "series",
|
accessorKey: "series",
|
||||||
header: "Serie",
|
header: "Serie",
|
||||||
enableHiding: true,
|
enableHiding: true,
|
||||||
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "invoiceNumber",
|
||||||
accessorKey: "invoiceNumber",
|
accessorKey: "invoiceNumber",
|
||||||
header: ({ column }) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className="px-0"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
variant="ghost"
|
|
||||||
>
|
|
||||||
Num.
|
|
||||||
<ArrowUpDownIcon className="ml-2 size-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
|
enableSorting: true,
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader
|
||||||
|
column={column}
|
||||||
|
title={t("pages.proformas.list.columns.invoice_number")}
|
||||||
|
/>
|
||||||
|
),
|
||||||
meta: {
|
meta: {
|
||||||
cellClassName: "font-medium",
|
cellClassName: "font-medium",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "status",
|
||||||
accessorKey: "status",
|
accessorKey: "status",
|
||||||
header: "Estado",
|
|
||||||
enableHiding: true,
|
enableHiding: true,
|
||||||
|
enableSorting: false,
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title={t("pages.proformas.list.columns.status")} />
|
||||||
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const proforma = row.original;
|
const proforma = row.original;
|
||||||
|
|
||||||
@ -139,19 +155,16 @@ export function useProformasGridColumns(
|
|||||||
|
|
||||||
// Cliente
|
// Cliente
|
||||||
{
|
{
|
||||||
|
id: "recipientName",
|
||||||
accessorKey: "recipientName",
|
accessorKey: "recipientName",
|
||||||
header: ({ column }) => {
|
enableHiding: false,
|
||||||
return (
|
enableSorting: true,
|
||||||
<Button
|
header: ({ column }) => (
|
||||||
className="px-0"
|
<DataTableColumnHeader
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
column={column}
|
||||||
variant="ghost"
|
title={t("pages.proformas.list.columns.recipient")}
|
||||||
>
|
/>
|
||||||
Cliente
|
),
|
||||||
<ArrowUpDownIcon className="ml-2 size-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const proforma = row.original;
|
const proforma = row.original;
|
||||||
return (
|
return (
|
||||||
@ -165,26 +178,18 @@ export function useProformasGridColumns(
|
|||||||
cellClassName: "max-w-128",
|
cellClassName: "max-w-128",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
accessorKey: "invoiceDate",
|
id: "reference",
|
||||||
header: ({ column }) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className="px-0"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
variant="ghost"
|
|
||||||
>
|
|
||||||
Fecha prof.
|
|
||||||
<ArrowUpDownIcon className="ml-2 size-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
cell: ({ row }) => DateHelper.format(row.original.invoiceDate),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "reference",
|
accessorKey: "reference",
|
||||||
header: "Referencia",
|
|
||||||
enableHiding: true,
|
enableHiding: true,
|
||||||
|
enableSorting: false,
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader
|
||||||
|
column={column}
|
||||||
|
title={t("pages.proformas.list.columns.reference")}
|
||||||
|
/>
|
||||||
|
),
|
||||||
cell: ({ row }) => <div className="truncate">{row.original.reference}</div>,
|
cell: ({ row }) => <div className="truncate">{row.original.reference}</div>,
|
||||||
meta: {
|
meta: {
|
||||||
cellClassName: "hidden lg:table-cell max-w-16",
|
cellClassName: "hidden lg:table-cell max-w-16",
|
||||||
@ -193,40 +198,56 @@ export function useProformasGridColumns(
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
id: "subtotalAmountFmt",
|
||||||
accessorKey: "subtotalAmountFmt",
|
accessorKey: "subtotalAmountFmt",
|
||||||
header: () => "Subtotal",
|
enableHiding: true,
|
||||||
|
enableSorting: false,
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader
|
||||||
|
column={column}
|
||||||
|
title={t("pages.proformas.list.columns.subtotal_amount")}
|
||||||
|
/>
|
||||||
|
),
|
||||||
meta: {
|
meta: {
|
||||||
cellClassName: "hidden 2xl:table-cell text-right tabular-nums font-medium",
|
cellClassName: "hidden 2xl:table-cell text-right tabular-nums font-medium",
|
||||||
headerClassName: "hidden 2xl:table-cell text-right",
|
headerClassName: "hidden 2xl:table-cell text-right",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "totalDiscountAmountFmt",
|
||||||
accessorKey: "totalDiscountAmountFmt",
|
accessorKey: "totalDiscountAmountFmt",
|
||||||
header: "Dtos",
|
header: "Dtos",
|
||||||
|
enableSorting: false,
|
||||||
meta: {
|
meta: {
|
||||||
cellClassName: "hidden 2xl:table-cell text-right tabular-nums font-medium",
|
cellClassName: "hidden 2xl:table-cell text-right tabular-nums font-medium",
|
||||||
headerClassName: "hidden 2xl:table-cell text-right",
|
headerClassName: "hidden 2xl:table-cell text-right",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "taxableAmountFmt",
|
||||||
accessorKey: "taxableAmountFmt",
|
accessorKey: "taxableAmountFmt",
|
||||||
header: "Base Imp.",
|
header: "Base Imp.",
|
||||||
|
enableSorting: false,
|
||||||
meta: {
|
meta: {
|
||||||
cellClassName: "hidden 2xl:table-cell text-right tabular-nums font-medium",
|
cellClassName: "hidden 2xl:table-cell text-right tabular-nums font-medium",
|
||||||
headerClassName: "hidden 2xl:table-cell text-right",
|
headerClassName: "hidden 2xl:table-cell text-right",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "taxesAmountFmt",
|
||||||
accessorKey: "taxesAmountFmt",
|
accessorKey: "taxesAmountFmt",
|
||||||
header: "Impuestos",
|
header: "Impuestos",
|
||||||
|
enableSorting: false,
|
||||||
meta: {
|
meta: {
|
||||||
cellClassName: "hidden 2xl:table-cell text-right tabular-nums font-medium",
|
cellClassName: "hidden 2xl:table-cell text-right tabular-nums font-medium",
|
||||||
headerClassName: "hidden 2xl:table-cell text-right",
|
headerClassName: "hidden 2xl:table-cell text-right",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "totalAmountFmt",
|
||||||
accessorKey: "totalAmountFmt",
|
accessorKey: "totalAmountFmt",
|
||||||
header: "Total",
|
header: "Total",
|
||||||
|
enableSorting: false,
|
||||||
meta: {
|
meta: {
|
||||||
cellClassName: "hidden xl:table-cell text-right tabular-nums font-semibold",
|
cellClassName: "hidden xl:table-cell text-right tabular-nums font-semibold",
|
||||||
headerClassName: "hidden xl:table-cell text-right",
|
headerClassName: "hidden xl:table-cell text-right",
|
||||||
@ -257,7 +278,12 @@ export function useProformasGridColumns(
|
|||||||
render={
|
render={
|
||||||
<Button
|
<Button
|
||||||
className="size-7 cursor-pointer text-muted-foreground hover:text-primary"
|
className="size-7 cursor-pointer text-muted-foreground hover:text-primary"
|
||||||
onClick={() => actionHandlers.onEditClick?.(proforma)}
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
actionHandlers.onEditClick?.(proforma);
|
||||||
|
}}
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
@ -279,9 +305,12 @@ export function useProformasGridColumns(
|
|||||||
render={
|
render={
|
||||||
<Button
|
<Button
|
||||||
className="size-7 cursor-pointer text-muted-foreground hover:text-primary"
|
className="size-7 cursor-pointer text-muted-foreground hover:text-primary"
|
||||||
onClick={() =>
|
onClick={(event) => {
|
||||||
actionHandlers.onChangeStatusClick?.(proforma, availableTransitions[0])
|
event.preventDefault();
|
||||||
}
|
event.stopPropagation();
|
||||||
|
|
||||||
|
actionHandlers.onChangeStatusClick?.(proforma, availableTransitions[0]);
|
||||||
|
}}
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
@ -305,7 +334,12 @@ export function useProformasGridColumns(
|
|||||||
render={
|
render={
|
||||||
<Button
|
<Button
|
||||||
className="size-7 cursor-pointer text-muted-foreground hover:text-primary"
|
className="size-7 cursor-pointer text-muted-foreground hover:text-primary"
|
||||||
onClick={() => actionHandlers.onIssueClick?.(proforma)}
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
actionHandlers.onIssueClick?.(proforma);
|
||||||
|
}}
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
@ -326,8 +360,10 @@ export function useProformasGridColumns(
|
|||||||
render={
|
render={
|
||||||
<Button
|
<Button
|
||||||
className="size-8 text-destructive/75 hover:text-destructive cursor-pointer"
|
className="size-8 text-destructive/75 hover:text-destructive cursor-pointer"
|
||||||
onClick={(e) => {
|
onClick={(event) => {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
actionHandlers.onDeleteClick?.(proforma);
|
actionHandlers.onDeleteClick?.(proforma);
|
||||||
}}
|
}}
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|||||||
@ -98,8 +98,10 @@ export const ListProformasPage = () => {
|
|||||||
onPageChange={listCtrl.setPageIndex}
|
onPageChange={listCtrl.setPageIndex}
|
||||||
onPageSizeChange={listCtrl.setPageSize}
|
onPageSizeChange={listCtrl.setPageSize}
|
||||||
onRowClick={(proformaId) => panelCtrl.openProformaPanel(proformaId, "view")}
|
onRowClick={(proformaId) => panelCtrl.openProformaPanel(proformaId, "view")}
|
||||||
|
onSortChange={listCtrl.setSort}
|
||||||
pageIndex={listCtrl.pageIndex}
|
pageIndex={listCtrl.pageIndex}
|
||||||
pageSize={listCtrl.pageSize}
|
pageSize={listCtrl.pageSize}
|
||||||
|
sort={listCtrl.sort}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Explicación técnica */}
|
{/* Explicación técnica */}
|
||||||
|
|||||||
@ -61,11 +61,11 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
onKeyDown={preventEnterKeySubmitForm}
|
onKeyDown={preventEnterKeySubmitForm}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-12">
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-12">
|
||||||
<ProformaUpdateHeaderEditor className="md:col-span-8" disabled={isSubmitting} />
|
<ProformaUpdateHeaderEditor className="xl:col-span-9" disabled={isSubmitting} />
|
||||||
|
|
||||||
<ProformaUpdateRecipientEditor
|
<ProformaUpdateRecipientEditor
|
||||||
className="md:col-span-4"
|
className="xl:col-span-3"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onChangeCustomerClick={onChangeCustomerClick}
|
onChangeCustomerClick={onChangeCustomerClick}
|
||||||
onCreateCustomerClick={onCreateCustomerClick}
|
onCreateCustomerClick={onCreateCustomerClick}
|
||||||
@ -76,9 +76,9 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
<ProformaUpdateItemsEditor disabled={isSubmitting} itemsCtrl={itemsCtrl} taxCtrl={taxCtrl} />
|
<ProformaUpdateItemsEditor disabled={isSubmitting} itemsCtrl={itemsCtrl} taxCtrl={taxCtrl} />
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-12">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-12">
|
||||||
<ProformaUpdateTaxEditor className="md:col-span-6" taxCtrl={taxCtrl} />
|
<ProformaUpdateTaxEditor className="md:col-span-4" taxCtrl={taxCtrl} />
|
||||||
<ProformaTotalsSummary
|
<ProformaTotalsSummary
|
||||||
className="md:col-span-6"
|
className="md:col-span-8"
|
||||||
currency={currencyCode}
|
currency={currencyCode}
|
||||||
globalDiscountField={
|
globalDiscountField={
|
||||||
<PercentageField
|
<PercentageField
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
TextAreaField,
|
TextAreaField,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
|
import { Badge } from "@repo/shadcn-ui/components";
|
||||||
import { FileTextIcon } from "lucide-react";
|
import { FileTextIcon } from "lucide-react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
@ -34,7 +35,7 @@ export const ProformaUpdateHeaderEditor = ({
|
|||||||
>
|
>
|
||||||
<FormSectionGrid>
|
<FormSectionGrid>
|
||||||
<SelectField
|
<SelectField
|
||||||
className="md:col-span-2"
|
className="md:col-span-3"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("form_fields.proformas.series.label")}
|
label={t("form_fields.proformas.series.label")}
|
||||||
name="series"
|
name="series"
|
||||||
@ -42,9 +43,33 @@ export const ProformaUpdateHeaderEditor = ({
|
|||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DatePickerField
|
<TextField
|
||||||
|
className="md:col-span-6"
|
||||||
|
disabled={disabled}
|
||||||
|
label={t("form_fields.proformas.invoice_number.label")}
|
||||||
|
maxLength={16}
|
||||||
|
name="reference"
|
||||||
|
placeholder={t("form_fields.proformas.invoice_number.placeholder")}
|
||||||
|
readOnly={readOnly}
|
||||||
|
rightIcon={
|
||||||
|
<Badge className="text-sm" variant="secondary">
|
||||||
|
Autom.
|
||||||
|
</Badge>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectField
|
||||||
className="md:col-span-3"
|
className="md:col-span-3"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
label={t("form_fields.proformas.currency_code.label")}
|
||||||
|
name="currencyCode"
|
||||||
|
placeholder={t("form_fields.proformas.currency_code.placeholder")}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DatePickerField
|
||||||
|
className="md:col-span-3 md:col-start-1"
|
||||||
|
disabled={disabled}
|
||||||
label={t("form_fields.proformas.invoice_date.label")}
|
label={t("form_fields.proformas.invoice_date.label")}
|
||||||
name="invoiceDate"
|
name="invoiceDate"
|
||||||
placeholder={t("form_fields.proformas.invoice_date.placeholder")}
|
placeholder={t("form_fields.proformas.invoice_date.placeholder")}
|
||||||
@ -62,7 +87,7 @@ export const ProformaUpdateHeaderEditor = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="md:col-span-4"
|
className="md:col-span-6"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("form_fields.proformas.reference.label")}
|
label={t("form_fields.proformas.reference.label")}
|
||||||
maxLength={256}
|
maxLength={256}
|
||||||
|
|||||||
@ -64,7 +64,7 @@ export const ProformaUpdatePage = () => {
|
|||||||
return (
|
return (
|
||||||
<FormProvider {...updateCtrl.form}>
|
<FormProvider {...updateCtrl.form}>
|
||||||
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
|
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
|
||||||
<AppHeader className="mx-auto max-w-5xl space-y-4">
|
<AppHeader className="mx-auto max-w-7xl space-y-4">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
description={t("pages.proformas.update.description")}
|
description={t("pages.proformas.update.description")}
|
||||||
onBackClick={() => navigateBack()}
|
onBackClick={() => navigateBack()}
|
||||||
@ -87,7 +87,7 @@ export const ProformaUpdatePage = () => {
|
|||||||
/>
|
/>
|
||||||
</AppHeader>
|
</AppHeader>
|
||||||
|
|
||||||
<AppContent className="mx-auto max-w-5xl space-y-4">
|
<AppContent className="mx-auto max-w-7xl space-y-4">
|
||||||
{updateCtrl.isUpdateError && (
|
{updateCtrl.isUpdateError && (
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
message={
|
message={
|
||||||
|
|||||||
@ -242,7 +242,7 @@ export class CustomerRepository
|
|||||||
"email_primary",
|
"email_primary",
|
||||||
"mobile_primary",
|
"mobile_primary",
|
||||||
],
|
],
|
||||||
allowedFields: [
|
sortableFields: [
|
||||||
"name",
|
"name",
|
||||||
"trade_name",
|
"trade_name",
|
||||||
"reference",
|
"reference",
|
||||||
|
|||||||
@ -124,17 +124,18 @@ export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
|
|||||||
const field = mappings[criteria.order.orderBy.value] || criteria.order.orderBy.value;
|
const field = mappings[criteria.order.orderBy.value] || criteria.order.orderBy.value;
|
||||||
const direction = criteria.order.orderType.value.toUpperCase();
|
const direction = criteria.order.orderType.value.toUpperCase();
|
||||||
|
|
||||||
const allowedFields = params.allowedFields ?? ["id", "created_at"];
|
const sortableFields = params.sortableFields ?? ["id", "created_at"];
|
||||||
const strict = params.strictMode ?? false;
|
const strict = params.strictMode ?? false;
|
||||||
|
|
||||||
if (!allowedFields.includes(field)) {
|
if (!sortableFields.includes(field)) {
|
||||||
const msg = `[CriteriaToSequelizeConverter] Ignored ORDER BY '${field}' (not in allowedFields).`;
|
const msg = `[CriteriaToSequelizeConverter] Ignored ORDER BY '${field}' (not in sortableFields).`;
|
||||||
if (strict) throw new Error(msg);
|
if (strict) throw new Error(msg);
|
||||||
console.warn(msg);
|
console.warn(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
appendOrder(options, [[field, direction]] as OrderItem[]);
|
const orderItem = this.buildOrderItem(field, direction);
|
||||||
|
appendOrder(options, orderItem as OrderItem[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Paginación estándar */
|
/** Paginación estándar */
|
||||||
@ -182,4 +183,18 @@ export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
|
|||||||
|
|
||||||
return `\`${tableAlias}\`.\`${field}\``;
|
return `\`${tableAlias}\`.\`${field}\``;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildOrderItem(field: string, direction: string): OrderItem {
|
||||||
|
if (field.includes(".")) {
|
||||||
|
const [associationAlias, column] = field.split(".");
|
||||||
|
|
||||||
|
if (!associationAlias || !column) {
|
||||||
|
throw new Error(`[CriteriaToSequelizeConverter] Invalid nested ORDER BY field '${field}'.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{ as: associationAlias }, column, direction] as OrderItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [field, direction];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export type CriteriaMappings = Record<string, string>;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Parámetros de conversión → FindOptions (Sequelize).
|
* Parámetros de conversión → FindOptions (Sequelize).
|
||||||
* - allowedFields: lista blanca de campos ordenables (deben estar indexados).
|
* - sortableFields: lista blanca de campos ordenables (deben estar indexados).
|
||||||
* - searchableFields: columnas FULLTEXT (deben tener índice FT en BD).
|
* - searchableFields: columnas FULLTEXT (deben tener índice FT en BD).
|
||||||
* - enableFullText: activa MATCH ... AGAINST si true.
|
* - enableFullText: activa MATCH ... AGAINST si true.
|
||||||
* - mappings: mapeo Criteria→SQL.
|
* - mappings: mapeo Criteria→SQL.
|
||||||
@ -20,7 +20,7 @@ export type CriteriaMappings = Record<string, string>;
|
|||||||
* - strictMode?: true = lanza error si orden no permitido
|
* - strictMode?: true = lanza error si orden no permitido
|
||||||
*/
|
*/
|
||||||
export interface ConvertParams {
|
export interface ConvertParams {
|
||||||
allowedFields?: string[]; // p.ej. ['invoice_date','id','created_at']
|
sortableFields?: string[]; // p.ej. ['invoice_date','id','created_at']
|
||||||
searchableFields?: string[]; // p.ej. ['reference','description','notes']
|
searchableFields?: string[]; // p.ej. ['reference','description','notes']
|
||||||
enableFullText?: boolean; // default: false
|
enableFullText?: boolean; // default: false
|
||||||
mappings?: CriteriaMappings; // default: {}
|
mappings?: CriteriaMappings; // default: {}
|
||||||
|
|||||||
@ -41,7 +41,7 @@ export function DataTableColumnHeader<TData, TValue>({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
<span>{title}</span>
|
{title}
|
||||||
{column.getIsSorted() === "desc" ? (
|
{column.getIsSorted() === "desc" ? (
|
||||||
<ArrowDownIcon />
|
<ArrowDownIcon />
|
||||||
) : column.getIsSorted() === "asc" ? (
|
) : column.getIsSorted() === "asc" ? (
|
||||||
|
|||||||
@ -65,6 +65,13 @@ export type DataTableMeta<TData> = TableMeta<TData> & {
|
|||||||
bulkOps?: DataTableBulkRowOps<TData>;
|
bulkOps?: DataTableBulkRowOps<TData>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DataTableSortDirection = "asc" | "desc";
|
||||||
|
|
||||||
|
export interface DataTableSort {
|
||||||
|
field: string;
|
||||||
|
direction: DataTableSortDirection;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DataTableProps<TData, TValue> {
|
export interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
data: TData[];
|
data: TData[];
|
||||||
@ -81,6 +88,11 @@ export interface DataTableProps<TData, TValue> {
|
|||||||
|
|
||||||
getRowId?: (originalRow: TData, index: number, parent?: Row<TData>) => string;
|
getRowId?: (originalRow: TData, index: number, parent?: Row<TData>) => string;
|
||||||
|
|
||||||
|
// Soporte para ordenamiento server-side
|
||||||
|
sort?: DataTableSort;
|
||||||
|
onSortChange?: (sort: DataTableSort) => void;
|
||||||
|
manualSorting?: boolean;
|
||||||
|
|
||||||
// Soporte para paginación server-side
|
// Soporte para paginación server-side
|
||||||
manualPagination?: boolean;
|
manualPagination?: boolean;
|
||||||
pageIndex?: number; // 0-based
|
pageIndex?: number; // 0-based
|
||||||
@ -111,6 +123,10 @@ export function DataTable<TData, TValue>({
|
|||||||
|
|
||||||
getRowId,
|
getRowId,
|
||||||
|
|
||||||
|
sort: sorting,
|
||||||
|
onSortChange: onSortingChange,
|
||||||
|
manualSorting,
|
||||||
|
|
||||||
manualPagination,
|
manualPagination,
|
||||||
pageIndex = 0,
|
pageIndex = 0,
|
||||||
totalItems,
|
totalItems,
|
||||||
@ -122,7 +138,7 @@ export function DataTable<TData, TValue>({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = React.useState({});
|
const [rowSelection, setRowSelection] = React.useState({});
|
||||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
const [internalSorting, setInternalSorting] = React.useState<DataTableSort | undefined>();
|
||||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
||||||
const [colSizes, setColSizes] = React.useState<ColumnSizingState>({});
|
const [colSizes, setColSizes] = React.useState<ColumnSizingState>({});
|
||||||
|
|
||||||
@ -130,10 +146,57 @@ export function DataTable<TData, TValue>({
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Visibilidad
|
||||||
const resolvedColumnVisibility = columnVisibility ?? internalColumnVisibility;
|
const resolvedColumnVisibility = columnVisibility ?? internalColumnVisibility;
|
||||||
|
|
||||||
const handleColumnVisibilityChange = onColumnVisibilityChange ?? setInternalColumnVisibility;
|
const handleColumnVisibilityChange = onColumnVisibilityChange ?? setInternalColumnVisibility;
|
||||||
|
|
||||||
|
// Ordenación
|
||||||
|
const resolvedSorting = sorting ?? internalSorting;
|
||||||
|
const resolvedSortingState = React.useMemo<SortingState>(
|
||||||
|
() =>
|
||||||
|
resolvedSorting
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: resolvedSorting.field,
|
||||||
|
desc: resolvedSorting.direction === "desc",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
[resolvedSorting]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSortingChange: OnChangeFn<SortingState> = (updater) => {
|
||||||
|
const nextSorting = typeof updater === "function" ? updater(resolvedSortingState) : updater;
|
||||||
|
|
||||||
|
const next = nextSorting[0];
|
||||||
|
|
||||||
|
if (!next?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSort: DataTableSort = {
|
||||||
|
field: next.id,
|
||||||
|
direction: next.desc ? "desc" : "asc",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
resolvedSorting?.field === nextSort.field &&
|
||||||
|
resolvedSorting.direction === nextSort.direction
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onSortingChange) {
|
||||||
|
onSortingChange(nextSort);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInternalSorting(nextSort);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isManualSorting = manualSorting ?? false;
|
||||||
|
const isManualPagination = manualPagination ?? false;
|
||||||
|
|
||||||
// Configuración TanStack
|
// Configuración TanStack
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
@ -151,14 +214,15 @@ export function DataTable<TData, TValue>({
|
|||||||
|
|
||||||
state: {
|
state: {
|
||||||
columnSizing: colSizes,
|
columnSizing: colSizes,
|
||||||
sorting,
|
sorting: resolvedSortingState,
|
||||||
columnVisibility: resolvedColumnVisibility,
|
columnVisibility: resolvedColumnVisibility,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
columnFilters,
|
columnFilters,
|
||||||
pagination: { pageIndex, pageSize },
|
pagination: { pageIndex, pageSize },
|
||||||
},
|
},
|
||||||
|
|
||||||
manualPagination,
|
manualSorting: isManualSorting,
|
||||||
|
manualPagination: isManualPagination,
|
||||||
autoResetPageIndex: false,
|
autoResetPageIndex: false,
|
||||||
|
|
||||||
pageCount: manualPagination ? Math.max(1, Math.ceil((totalItems ?? 0) / pageSize)) : undefined,
|
pageCount: manualPagination ? Math.max(1, Math.ceil((totalItems ?? 0) / pageSize)) : undefined,
|
||||||
@ -176,16 +240,17 @@ export function DataTable<TData, TValue>({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onSortingChange: handleSortingChange,
|
||||||
|
|
||||||
enableRowSelection,
|
enableRowSelection,
|
||||||
onRowSelectionChange: setRowSelection,
|
onRowSelectionChange: setRowSelection,
|
||||||
onSortingChange: setSorting,
|
|
||||||
onColumnFiltersChange: setColumnFilters,
|
onColumnFiltersChange: setColumnFilters,
|
||||||
onColumnVisibilityChange: handleColumnVisibilityChange,
|
onColumnVisibilityChange: handleColumnVisibilityChange,
|
||||||
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
getPaginationRowModel: manualPagination ? undefined : getPaginationRowModel(),
|
getPaginationRowModel: isManualPagination ? undefined : getPaginationRowModel(),
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: isManualSorting ? undefined : getSortedRowModel(),
|
||||||
getFacetedRowModel: getFacetedRowModel(),
|
getFacetedRowModel: getFacetedRowModel(),
|
||||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,21 +1,28 @@
|
|||||||
import type { OnChangeFn, VisibilityState } from "@tanstack/react-table";
|
import type { OnChangeFn, VisibilityState } from "@tanstack/react-table";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
import type { DataTableSort } from "../data-table.tsx";
|
||||||
|
|
||||||
|
// Es preferible utilizar tipos de TanStack/Table
|
||||||
|
// para guardar las preferencias de la tabla.
|
||||||
export interface DataTablePreferences {
|
export interface DataTablePreferences {
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
columnVisibility: VisibilityState;
|
columnVisibility: VisibilityState;
|
||||||
|
sort?: DataTableSort;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseDataTablePreferencesParams {
|
interface UseDataTablePreferencesParams {
|
||||||
storageKey: string;
|
storageKey: string;
|
||||||
defaultPageSize: number;
|
defaultPageSize: number;
|
||||||
defaultColumnVisibility?: VisibilityState;
|
defaultColumnVisibility?: VisibilityState;
|
||||||
|
defaultSort?: DataTableSort;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDataTablePreferences = ({
|
export const useDataTablePreferences = ({
|
||||||
storageKey,
|
storageKey,
|
||||||
defaultPageSize,
|
defaultPageSize,
|
||||||
defaultColumnVisibility = {},
|
defaultColumnVisibility = {},
|
||||||
|
defaultSort = { field: "", direction: "asc" },
|
||||||
}: UseDataTablePreferencesParams) => {
|
}: UseDataTablePreferencesParams) => {
|
||||||
const [preferences, setPreferences] = React.useState<DataTablePreferences>(() => {
|
const [preferences, setPreferences] = React.useState<DataTablePreferences>(() => {
|
||||||
try {
|
try {
|
||||||
@ -25,6 +32,7 @@ export const useDataTablePreferences = ({
|
|||||||
return {
|
return {
|
||||||
pageSize: defaultPageSize,
|
pageSize: defaultPageSize,
|
||||||
columnVisibility: defaultColumnVisibility,
|
columnVisibility: defaultColumnVisibility,
|
||||||
|
sort: defaultSort,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,11 +44,13 @@ export const useDataTablePreferences = ({
|
|||||||
...defaultColumnVisibility,
|
...defaultColumnVisibility,
|
||||||
...parsed.columnVisibility,
|
...parsed.columnVisibility,
|
||||||
},
|
},
|
||||||
|
sort: parsed.sort ?? defaultSort,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return {
|
||||||
pageSize: defaultPageSize,
|
pageSize: defaultPageSize,
|
||||||
columnVisibility: defaultColumnVisibility,
|
columnVisibility: defaultColumnVisibility,
|
||||||
|
sort: defaultSort,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -82,9 +92,28 @@ export const useDataTablePreferences = ({
|
|||||||
[persist]
|
[persist]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setSort = React.useCallback(
|
||||||
|
(sort: DataTableSort) => {
|
||||||
|
setPreferences((previous) => {
|
||||||
|
const next: DataTablePreferences = {
|
||||||
|
...previous,
|
||||||
|
sort,
|
||||||
|
};
|
||||||
|
|
||||||
|
persist(next);
|
||||||
|
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[persist]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageSize: preferences.pageSize,
|
pageSize: preferences.pageSize,
|
||||||
columnVisibility: preferences.columnVisibility,
|
columnVisibility: preferences.columnVisibility,
|
||||||
|
sort: preferences.sort,
|
||||||
|
|
||||||
|
setSort,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
setColumnVisibility,
|
setColumnVisibility,
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user