Facturas de cliente

This commit is contained in:
David Arranz 2025-10-18 22:33:01 +02:00
parent 8b3008f6d8
commit af2997465e
5 changed files with 84 additions and 23 deletions

View File

@ -21,10 +21,11 @@ export const useInvoicesQuery = (options?: InvoicesQueryOptions) => {
queryKey: CUSTOMER_INVOICES_QUERY_KEY(criteria), queryKey: CUSTOMER_INVOICES_QUERY_KEY(criteria),
queryFn: async ({ signal }) => { queryFn: async ({ signal }) => {
return await dataSource.getList<CustomerInvoicesPage>("customer-invoices", { return await dataSource.getList<CustomerInvoicesPage>("customer-invoices", {
params: criteria, ...criteria,
signal, signal,
}); });
}, },
enabled enabled,
placeholderData: (previousData, previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
}); });
}; };

View File

@ -2,22 +2,30 @@ import type { CellKeyDownEvent, RowClickedEvent } from "ag-grid-community";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { DataTable } from '@repo/rdx-ui/components'; import { DataTable, LoadingOverlay } from '@repo/rdx-ui/components';
import { Button, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@repo/shadcn-ui/components'; import { Button, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@repo/shadcn-ui/components';
import { FileDownIcon, FilterIcon, SearchIcon } from 'lucide-react'; import { FileDownIcon, FilterIcon, SearchIcon } from 'lucide-react';
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useTranslation } from "../../i18n"; import { useTranslation } from "../../i18n";
import { CustomerInvoicesPage } from '../../schemas'; import { InvoiceSummaryFormData, InvoicesPageFormData } from '../../schemas';
import { useInvoicesListColumns } from './use-invoices-list-columns'; import { useInvoicesListColumns } from './use-invoices-list-columns';
export type InvoiceUpdateCompProps = { export type InvoiceUpdateCompProps = {
invoicesPage: CustomerInvoicesPage; invoicesPage: InvoicesPageFormData;
loading?: boolean; loading?: boolean;
onPageChange?: (pageIndex: number) => void;
onPageSizeChange?: (pageSize: number) => void;
} }
// Create new GridExample component // Create new GridExample component
export const InvoicesListGrid = ({ invoicesPage, loading }: InvoiceUpdateCompProps) => { export const InvoicesListGrid = ({
invoicesPage,
loading,
onPageChange,
onPageSizeChange,
}: InvoiceUpdateCompProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
@ -25,8 +33,7 @@ export const InvoicesListGrid = ({ invoicesPage, loading }: InvoiceUpdateCompPro
const [statusFilter, setStatusFilter] = useState("todas"); const [statusFilter, setStatusFilter] = useState("todas");
const columns = useInvoicesListColumns(); const columns = useInvoicesListColumns();
const { items, page, per_page, total_pages, total_items } = invoicesPage;
const { items, page, total_pages, total_items } = invoicesPage;
// Navegación accesible (click o teclado) // Navegación accesible (click o teclado)
@ -71,10 +78,20 @@ export const InvoicesListGrid = ({ invoicesPage, loading }: InvoiceUpdateCompPro
); );
const handleRowClick = useCallback(
(invoice: InvoiceSummaryFormData, _index: number, e: React.MouseEvent) => {
const url = `/customer-invoices/${invoice.id}/edit`;
if (e.metaKey || e.ctrlKey) window.open(url, "_blank", "noopener,noreferrer");
else navigate(url);
},
[navigate]
);
// Render principal // Render principal
return ( return (
<> <div className="flex flex-col gap-4">
{/* Filters and Actions */} {/* Barra de filtros */}
<div className="flex flex-col sm:flex-row gap-4 mb-6"> <div className="flex flex-col sm:flex-row gap-4 mb-6">
<div className="relative flex-1"> <div className="relative flex-1">
<SearchIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" /> <SearchIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
@ -103,8 +120,28 @@ export const InvoicesListGrid = ({ invoicesPage, loading }: InvoiceUpdateCompPro
</Button> </Button>
</div> </div>
<div className="overflow-hidden"> <div className="overflow-hidden">
<DataTable columns={columns} data={items} enablePagination={true} /> <DataTable
columns={columns}
data={items}
readOnly
enableRowSelection
enablePagination
manualPagination
pageIndex={page - 1}
pageSize={per_page}
totalItems={total_items}
onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange}
onRowClick={handleRowClick}
/>
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-background/50">
<LoadingOverlay />
</div>
)}
</div> </div>
</> </div>
); );
}; };

View File

@ -2,7 +2,7 @@ import { ErrorAlert } from '@erp/customers/components';
import { AppBreadcrumb, AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components"; import { AppBreadcrumb, AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { FilePenIcon, PlusIcon } from "lucide-react"; import { FilePenIcon, PlusIcon } from "lucide-react";
import { useMemo } from 'react'; import { useMemo, useState } from 'react';
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { InvoicesListGrid, PageHeader } from '../../components'; import { InvoicesListGrid, PageHeader } from '../../components';
import { useInvoicesQuery } from '../../hooks'; import { useInvoicesQuery } from '../../hooks';
@ -13,6 +13,9 @@ export const InvoiceListPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const [pageIndex, setOageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const { const {
data, data,
isLoading, isLoading,
@ -20,7 +23,8 @@ export const InvoiceListPage = () => {
error, error,
} = useInvoicesQuery({ } = useInvoicesQuery({
criteria: { criteria: {
pageSize: 999, pageSize,
pageNumber: pageIndex
} }
}); });
@ -32,6 +36,15 @@ export const InvoiceListPage = () => {
} }
}, [data]); }, [data]);
const handlePageChange = (newPageIndex: number) => {
// TanStack usa pageIndex 0-based → API usa 0-based también
setOageIndex(newPageIndex);
};
const handlePageSizeChange = (newSize: number) => {
setPageSize(newSize);
setOageIndex(0);
};
if (isError || !invoicesPageData) { if (isError || !invoicesPageData) {
return ( return (
@ -92,7 +105,12 @@ export const InvoiceListPage = () => {
</div> </div>
</div> </div>
<div className='flex flex-col w-full h-full py-3'> <div className='flex flex-col w-full h-full py-3'>
<InvoicesListGrid invoicesPage={invoicesPageData} loading={isLoading} /> <InvoicesListGrid
invoicesPage={invoicesPageData}
loading={isLoading}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
/>
</div> </div>
</AppContent> </AppContent>
</> </>

View File

@ -4,16 +4,16 @@ import type { ColumnDef } from "@tanstack/react-table";
import * as React from "react"; import * as React from "react";
import { CustomerInvoiceStatusBadge } from '../../components'; import { CustomerInvoiceStatusBadge } from '../../components';
import { useTranslation } from '../../i18n'; import { useTranslation } from '../../i18n';
import { InvoicesPageFormData } from '../../schemas/invoice-resume.form.schema'; import { InvoiceSummaryFormData } from '../../schemas/invoice-resume.form.schema';
export function useInvoicesListColumns(): ColumnDef<InvoicesPageFormData>[] { export function useInvoicesListColumns(): ColumnDef<InvoiceSummaryFormData>[] {
//const { t, readOnly, currency_code, language_code } = useInvoiceContext(); //const { t, readOnly, currency_code, language_code } = useInvoiceContext();
const { t } = useTranslation(); const { t } = useTranslation();
// Atención: Memoizar siempre para evitar reconstrucciones y resets de estado de tabla // Atención: Memoizar siempre para evitar reconstrucciones y resets de estado de tabla
return React.useMemo<ColumnDef<InvoicesPageFormData>[]>(() => [ return React.useMemo<ColumnDef<InvoiceSummaryFormData>[]>(() => [
{ {
accessorKey: "invoice_number", accessorKey: "invoice_number",
header: ({ column }) => ( header: ({ column }) => (

View File

@ -27,14 +27,19 @@ interface DataTablePaginationProps<TData> {
export function DataTablePagination<TData>({ table, className }: DataTablePaginationProps<TData>) { export function DataTablePagination<TData>({ table, className }: DataTablePaginationProps<TData>) {
const { t } = useTranslation(); const { t } = useTranslation();
const { pageIndex, pageSize } = table.getState().pagination; const { pageIndex: rawIndex, pageSize: rawSize } = table.getState().pagination;
const pageCount = table.getPageCount() || 1;
const totalRows = (table.options.meta as DataTableMeta<TData>)?.totalItems ?? table.getFilteredRowModel().rows.length; const totalRows = (table.options.meta as DataTableMeta<TData>)?.totalItems ?? table.getFilteredRowModel().rows.length;
// Normalización segura
const pageIndex = Number.isFinite(rawIndex) && rawIndex >= 0 ? rawIndex : 0;
const pageSize = Number.isFinite(rawSize) && rawSize > 0 ? rawSize : 10;
const pageCount = table.getPageCount() || Math.max(1, Math.ceil((totalRows || 0) / pageSize));
const hasSelected = table.getFilteredSelectedRowModel().rows.length > 0; const hasSelected = table.getFilteredSelectedRowModel().rows.length > 0;
// Calcula rango visible (inicio-fin) // Rango visible (1-based en UI)
const start = totalRows === 0 ? 0 : pageIndex * pageSize + 1; const start = totalRows > 0 ? pageIndex * pageSize + 1 : 0;
const end = Math.min((pageIndex + 1) * pageSize, totalRows); const end = totalRows > 0 ? Math.min(start + pageSize - 1, totalRows) : 0;
return ( return (
<div className={cn( <div className={cn(