Facturas de cliente
This commit is contained in:
parent
f19ab6022b
commit
99f8b5fb8e
@ -45,7 +45,7 @@
|
||||
"title": "Customer proformas",
|
||||
"description": "List all customer proformas",
|
||||
"grid_columns": {
|
||||
"invoice_number": "Inv. number",
|
||||
"invoice_number": "#",
|
||||
"series": "Serie",
|
||||
"reference": "Reference",
|
||||
"status": "Status",
|
||||
@ -82,6 +82,33 @@
|
||||
"title": "View customer proforma",
|
||||
"description": "View the details of the selected customer proforma"
|
||||
}
|
||||
},
|
||||
"issued_invoices": {
|
||||
"title": "Customer Invoices",
|
||||
"description": "Manage your customer invoices",
|
||||
"list": {
|
||||
"title": "Customer invoices",
|
||||
"description": "List all customer invoices",
|
||||
"grid_columns": {
|
||||
"invoice_number": "Inv. number",
|
||||
"series": "Serie",
|
||||
"reference": "Reference",
|
||||
"invoice_date": "Invoice date",
|
||||
"operation_date": "Operation date",
|
||||
"recipient": "Customer",
|
||||
"recipient_tin": "TIN",
|
||||
"recipient_name": "Customer name",
|
||||
"recipient_street": "Street",
|
||||
"recipient_city": "City",
|
||||
"recipient_province": "Province",
|
||||
"recipient_postal_code": "Postal code",
|
||||
"recipient_country": "Country",
|
||||
"subtotal_amount": "Subtotal",
|
||||
"discount_amount": "Discount",
|
||||
"taxes_amount": "Taxes",
|
||||
"total_amount": "Total"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"form_groups": {
|
||||
|
||||
@ -44,7 +44,7 @@
|
||||
"title": "Proformas",
|
||||
"description": "Lista todas las proformas",
|
||||
"grid_columns": {
|
||||
"invoice_number": "Nº proforma",
|
||||
"invoice_number": "#",
|
||||
"series": "Serie",
|
||||
"reference": "Reference",
|
||||
"status": "Estado",
|
||||
@ -81,6 +81,33 @@
|
||||
"title": "Ver proforma",
|
||||
"description": "Ver los detalles de la proforma seleccionada"
|
||||
}
|
||||
},
|
||||
"issued_invoices": {
|
||||
"title": "Facturas de cliente",
|
||||
"description": "Gestiona tus facturas de cliente",
|
||||
"list": {
|
||||
"title": "Facturas de cliente",
|
||||
"description": "Lista todas las facturas de cliente",
|
||||
"grid_columns": {
|
||||
"invoice_number": "Nº factura",
|
||||
"series": "Serie",
|
||||
"reference": "Reference",
|
||||
"invoice_date": "Fecha de proforma",
|
||||
"operation_date": "Fecha de operación",
|
||||
"recipient": "Cliente",
|
||||
"recipient_tin": "NIF/CIF",
|
||||
"recipient_name": "Cliente",
|
||||
"recipient_street": "Dirección",
|
||||
"recipient_city": "Ciudad",
|
||||
"recipient_province": "Provincia",
|
||||
"recipient_postal_code": "Código postal",
|
||||
"recipient_country": "País",
|
||||
"subtotal_amount": "Subtotal",
|
||||
"discount_amount": "Descuentos",
|
||||
"taxes_amount": "Impuestos",
|
||||
"total_amount": "Importe total"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -2,6 +2,8 @@ import type { ModuleClientParams } from "@erp/core/client";
|
||||
import { lazy } from "react";
|
||||
import { Outlet, type RouteObject } from "react-router-dom";
|
||||
|
||||
import { IssuedInvoiceListPage } from "./issued-invoices/pages/list/issued-invoice-list-page";
|
||||
|
||||
// Lazy load components
|
||||
const InvoicesLayout = lazy(() =>
|
||||
import("./shared/ui").then((m) => ({ default: m.CustomerInvoicesLayout }))
|
||||
@ -34,7 +36,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
||||
{ path: ":id/edit", element: <InvoiceUpdatePage /> },
|
||||
],
|
||||
},
|
||||
/*{
|
||||
{
|
||||
path: "customer-invoices",
|
||||
element: (
|
||||
<InvoicesLayout>
|
||||
@ -42,9 +44,9 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
||||
</InvoicesLayout>
|
||||
),
|
||||
children: [
|
||||
//{ path: "", index: true, element: <InvoiceListPage /> }, // index
|
||||
//{ path: "list", element: <InvoiceListPage /> },
|
||||
//
|
||||
{ path: "", index: true, element: <IssuedInvoiceListPage /> }, // index
|
||||
{ path: "list", element: <IssuedInvoiceListPage /> },
|
||||
/*
|
||||
{ path: "create", element: <CustomerInvoicesList /> },
|
||||
{ path: ":id", element: <CustomerInvoicesList /> },
|
||||
{ path: ":id/edit", element: <CustomerInvoicesList /> },
|
||||
@ -55,7 +57,8 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
||||
{ path: ":id/download", element: <CustomerInvoicesList /> },
|
||||
{ path: ":id/duplicate", element: <CustomerInvoicesList /> },
|
||||
{ path: ":id/preview", element: <CustomerInvoicesList /> },
|
||||
*/
|
||||
],
|
||||
},*/
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./issued-invoice-summary-dto.adapter";
|
||||
@ -1,16 +1,16 @@
|
||||
import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core";
|
||||
|
||||
import type { IssuedInvoicesummaryPage } from "./issued-invoice.api.schema";
|
||||
import type {
|
||||
IssuedInvoicesummaryData,
|
||||
IssuedInvoicesummaryPageData,
|
||||
} from "./issued-invoice-resume.form.schema";
|
||||
IssuedInvoiceSummaryData,
|
||||
IssuedInvoiceSummaryPage,
|
||||
IssuedInvoiceSummaryPageData,
|
||||
} from "../schema";
|
||||
|
||||
/**
|
||||
* Convierte el DTO completo de API a datos numéricos para el formulario.
|
||||
*/
|
||||
export const IssueInvoiceResumeDtoAdapter = {
|
||||
fromDto(pageDto: IssuedInvoicesummaryPage, context?: unknown): IssuedInvoicesummaryPageData {
|
||||
export const IssuedInvoiceSummaryDtoAdapter = {
|
||||
fromDto(pageDto: IssuedInvoiceSummaryPage, context?: unknown): IssuedInvoiceSummaryPageData {
|
||||
return {
|
||||
...pageDto,
|
||||
items: pageDto.items.map(
|
||||
@ -64,7 +64,7 @@ export const IssueInvoiceResumeDtoAdapter = {
|
||||
),
|
||||
|
||||
//taxes: dto.taxes,
|
||||
}) as unknown as IssuedInvoicesummaryData
|
||||
}) as unknown as IssuedInvoiceSummaryData
|
||||
),
|
||||
};
|
||||
},
|
||||
@ -1 +1 @@
|
||||
export * from "./hooks";
|
||||
export * from "./pages";
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
import {
|
||||
GetIssuedInvoiceByIdResponseSchema,
|
||||
ListIssuedInvoicesResponseSchema,
|
||||
} from "@erp/customer-invoices/common";
|
||||
import type { ArrayElement } from "@repo/rdx-utils";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
// IssuedInvoices
|
||||
export const IssuedInvoiceschema = GetIssuedInvoiceByIdResponseSchema.omit({
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
export type IssueInvoice = z.infer<typeof IssuedInvoiceschema>;
|
||||
export type IssueInvoiceRecipient = IssueInvoice["recipient"];
|
||||
export type IssueInvoiceItem = ArrayElement<IssueInvoice["items"]>;
|
||||
|
||||
// Resultado de consulta con criteria (paginado, etc.)
|
||||
export const IssuedInvoicesummaryPageSchema = ListIssuedInvoicesResponseSchema.omit({
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
export type IssuedInvoicesummaryPage = z.infer<typeof IssuedInvoicesummaryPageSchema>;
|
||||
export type IssuedInvoicesummary = Omit<
|
||||
ArrayElement<IssuedInvoicesummaryPage["items"]>,
|
||||
"metadata"
|
||||
>;
|
||||
@ -0,0 +1 @@
|
||||
export * from "./list";
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./use-issued-invoices-grid-columns";
|
||||
export * from "./use-issued-invoices-list";
|
||||
@ -0,0 +1,348 @@
|
||||
import { formatDate } from "@erp/core/client";
|
||||
import { DataTableColumnHeader } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { DownloadIcon, MailIcon, MoreVerticalIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { IssuedInvoiceSummaryData } from "../../../schema";
|
||||
|
||||
type GridActionHandlers = {
|
||||
onDownloadPdf?: (proforma: IssuedInvoiceSummaryData) => void;
|
||||
onSendEmail?: (proforma: IssuedInvoiceSummaryData) => void;
|
||||
};
|
||||
|
||||
export function useIssuedInvoicesGridColumns(
|
||||
actionHandlers: GridActionHandlers = {}
|
||||
): ColumnDef<IssuedInvoiceSummaryData, unknown>[] {
|
||||
const { t } = useTranslation();
|
||||
const { onDownloadPdf, onSendEmail } = actionHandlers;
|
||||
|
||||
return React.useMemo<ColumnDef<IssuedInvoiceSummaryData>[]>(
|
||||
() => [
|
||||
// Nº
|
||||
{
|
||||
accessorKey: "invoice_number",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-right tabular-nums justify-end"
|
||||
column={column}
|
||||
title={t("pages.issued_invoices.list.grid_columns.invoice_number")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="text-right tabular-nums">{row.original.invoice_number}</div>
|
||||
),
|
||||
enableHiding: false,
|
||||
enableSorting: false,
|
||||
maxSize: 48,
|
||||
size: 48,
|
||||
minSize: 48,
|
||||
meta: {
|
||||
title: t("pages.issued_invoices.list.grid_columns.invoice_number"),
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: "recipient",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.issued_invoices.list.grid_columns.recipient")}
|
||||
/>
|
||||
),
|
||||
accessorFn: (row) => row.recipient.name, // para ordenar/buscar por nombre
|
||||
enableHiding: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
cell: ({ row }) => {
|
||||
const c = row.original.recipient;
|
||||
return (
|
||||
<div className="flex items-start gap-1">
|
||||
<div className="min-w-0 grid gap-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-semibold truncate text-primary">{c.name}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{c.tin && <span className="font-base truncate">{c.tin}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
title: t("pages.issued_invoices.list.grid_columns.recipient"),
|
||||
},
|
||||
},
|
||||
// Serie
|
||||
{
|
||||
accessorKey: "series",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.issued_invoices.list.grid_columns.series")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => <div className="font-normal text-left">{row.original.series}</div>,
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
meta: {
|
||||
title: t("pages.issued_invoices.list.grid_columns.series"),
|
||||
},
|
||||
},
|
||||
// Referencia
|
||||
{
|
||||
accessorKey: "reference",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.issued_invoices.list.grid_columns.reference")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => <div className="font-medium text-left">{row.original.reference}</div>,
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
meta: {
|
||||
title: t("pages.issued_invoices.list.grid_columns.reference"),
|
||||
},
|
||||
},
|
||||
|
||||
// Fecha factura
|
||||
{
|
||||
accessorKey: "invoice_date",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left tabular-nums"
|
||||
column={column}
|
||||
title={t("pages.issued_invoices.list.grid_columns.invoice_date")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-left tabular-nums">
|
||||
{formatDate(row.original.invoice_date)}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
meta: {
|
||||
title: t("pages.issued_invoices.list.grid_columns.invoice_date"),
|
||||
},
|
||||
},
|
||||
// Fecha operación
|
||||
{
|
||||
accessorKey: "operation_date",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left tabular-nums"
|
||||
column={column}
|
||||
title={t("pages.issued_invoices.list.grid_columns.operation_date")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-left tabular-nums">
|
||||
{formatDate(row.original.operation_date)}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
meta: {
|
||||
title: t("pages.issued_invoices.list.grid_columns.operation_date"),
|
||||
},
|
||||
},
|
||||
|
||||
// Subtotal amount
|
||||
{
|
||||
accessorKey: "subtotal_amount_fmt",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-right tabular-nums"
|
||||
column={column}
|
||||
title={t("pages.issued_invoices.list.grid_columns.subtotal_amount")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-right tabular-nums">
|
||||
{row.original.subtotal_amount_fmt}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
meta: {
|
||||
title: t("pages.issued_invoices.list.grid_columns.subtotal_amount"),
|
||||
},
|
||||
},
|
||||
|
||||
// Discount amount
|
||||
{
|
||||
accessorKey: "discount_amount_fmt",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-right tabular-nums"
|
||||
column={column}
|
||||
title={t("pages.issued_invoices.list.grid_columns.discount_amount")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-right tabular-nums">
|
||||
{row.original.discount_amount_fmt}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
meta: {
|
||||
title: t("pages.issued_invoices.list.grid_columns.discount_amount"),
|
||||
},
|
||||
},
|
||||
|
||||
// Taxes amount
|
||||
{
|
||||
accessorKey: "taxes_amount_fmt",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-right tabular-nums"
|
||||
column={column}
|
||||
title={t("pages.issued_invoices.list.grid_columns.taxes_amount")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-right tabular-nums">{row.original.taxes_amount_fmt}</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
meta: {
|
||||
title: t("pages.issued_invoices.list.grid_columns.taxes_amount"),
|
||||
},
|
||||
},
|
||||
|
||||
// Total amount
|
||||
{
|
||||
accessorKey: "total_amount_fmt",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-right tabular-nums"
|
||||
column={column}
|
||||
title={t("pages.issued_invoices.list.grid_columns.total_amount")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-semibold text-right tabular-nums">
|
||||
{row.original.total_amount_fmt}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
meta: {
|
||||
title: t("pages.issued_invoices.list.grid_columns.total_amount"),
|
||||
},
|
||||
},
|
||||
|
||||
// ─────────────────────────────
|
||||
// Acciones
|
||||
// ─────────────────────────────
|
||||
{
|
||||
id: "actions",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("common.actions")}
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
size: 64,
|
||||
minSize: 64,
|
||||
cell: ({ row }) => {
|
||||
const proforma = row.original;
|
||||
const stop = (e: React.MouseEvent | React.KeyboardEvent) => e.stopPropagation();
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
{/* Descargar en PDF */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
aria-label={t("common.download_pdf")}
|
||||
className="cursor-pointer text-muted-foreground hover:text-primary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDownloadPdf?.(proforma);
|
||||
}}
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("common.download_pdf")}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Menú demás acciones */}
|
||||
{/** biome-ignore lint/suspicious/noSelfCompare: <Desactivado por ahora> */}
|
||||
{false !== false && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-label={t("common.more_actions")}
|
||||
className="cursor-pointer text-muted-foreground hover:text-primary"
|
||||
onClick={stop}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" className="size-4" />
|
||||
<span className="sr-only">{t("common.more_actions")}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => onDownloadPdf?.(proforma)}
|
||||
>
|
||||
<DownloadIcon className="mr-2 size-4" />
|
||||
{t("common.download_pdf")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => onSendEmail?.(proforma)}
|
||||
>
|
||||
<MailIcon className="mr-2 size-4" />
|
||||
{t("common.send_email")}
|
||||
</DropdownMenuItem>{" "}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
title: t("common.actions"),
|
||||
},
|
||||
},
|
||||
],
|
||||
[t, onDownloadPdf, onSendEmail]
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
// src/modules/issued-invoices/hooks/use-proformas-list.ts
|
||||
|
||||
import type { CriteriaDTO } from "@erp/core";
|
||||
import { useDebounce } from "@repo/rdx-ui/components";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { IssuedInvoiceSummaryDtoAdapter } from "../../../adapters/issued-invoice-summary-dto.adapter";
|
||||
import { useIssuedInvoicesQuery } from "../../../hooks";
|
||||
|
||||
export const useIssuedInvoicesList = () => {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [search, setSearch] = useState("");
|
||||
const [status, setStatus] = useState("all");
|
||||
|
||||
const debouncedQ = useDebounce(search, 300);
|
||||
|
||||
const criteria = useMemo<CriteriaDTO>(() => {
|
||||
const baseFilters =
|
||||
status !== "all" ? [{ field: "status", operator: "CONTAINS", value: status }] : [];
|
||||
|
||||
return {
|
||||
q: debouncedQ || "",
|
||||
pageSize,
|
||||
pageNumber: pageIndex,
|
||||
order: "desc",
|
||||
orderBy: "invoice_date",
|
||||
filters: baseFilters,
|
||||
};
|
||||
}, [pageSize, pageIndex, debouncedQ, status]);
|
||||
|
||||
const query = useIssuedInvoicesQuery({ criteria });
|
||||
const data = useMemo(
|
||||
() => (query.data ? IssuedInvoiceSummaryDtoAdapter.fromDto(query.data) : undefined),
|
||||
[query.data]
|
||||
);
|
||||
|
||||
const setSearchValue = (value: string) => setSearch(value.trim().replace(/\s+/g, " "));
|
||||
|
||||
const setStatusFilter = (newStatus: string) => setStatus(newStatus);
|
||||
|
||||
return {
|
||||
...query,
|
||||
data,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
search,
|
||||
setPageIndex,
|
||||
setPageSize,
|
||||
setSearchValue,
|
||||
setStatusFilter,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from "./issued-invoice-list-page";
|
||||
@ -0,0 +1,62 @@
|
||||
import { PageHeader } from "@erp/core/components";
|
||||
import { ErrorAlert } from "@erp/customers/components";
|
||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useTranslation } from "../../../i18n";
|
||||
|
||||
import { useIssuedInvoicesList } from "./hooks";
|
||||
import { IssuedInvoicesGrid } from "./ui/proformas-grid";
|
||||
|
||||
export const IssuedInvoiceListPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const list = useIssuedInvoicesList();
|
||||
|
||||
if (list.isError || !list.data) {
|
||||
return (
|
||||
<AppContent>
|
||||
<ErrorAlert
|
||||
message={(list.error as Error)?.message || "Error al cargar el listado"}
|
||||
title={t("pages.issued_invoices.list.loadErrorTitle")}
|
||||
/>
|
||||
<BackHistoryButton />
|
||||
</AppContent>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppHeader>
|
||||
<PageHeader
|
||||
description={t("pages.issued_invoices.list.description")}
|
||||
rightSlot={
|
||||
<Button
|
||||
aria-label={t("pages.issued_invoices.create.title")}
|
||||
onClick={() => navigate("/issued-invoices/create")}
|
||||
>
|
||||
<PlusIcon aria-hidden className="mr-2 size-4" />
|
||||
{t("pages.issued_invoices.create.title")}
|
||||
</Button>
|
||||
}
|
||||
title={t("pages.issued_invoices.list.title")}
|
||||
/>
|
||||
</AppHeader>
|
||||
<AppContent>
|
||||
<IssuedInvoicesGrid
|
||||
data={list.data}
|
||||
loading={list.isLoading}
|
||||
onPageChange={list.setPageIndex}
|
||||
onPageSizeChange={list.setPageSize}
|
||||
onSearchChange={list.setSearchValue}
|
||||
onStatusFilterChange={list.setStatusFilter}
|
||||
pageIndex={list.pageIndex}
|
||||
pageSize={list.pageSize}
|
||||
searchValue={list.search}
|
||||
/>
|
||||
</AppContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./issued-invoice-status-badge";
|
||||
export * from "./issued-invoices-grid";
|
||||
@ -0,0 +1,68 @@
|
||||
import { Badge } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
|
||||
export type IssuedInvoiceStatus = "draft" | "sent" | "approved" | "rejected" | "issued";
|
||||
|
||||
export type IssuedInvoiceStatusBadgeProps = {
|
||||
status: string | IssuedInvoiceStatus; // permitir cualquier valor
|
||||
dotVisible?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const statusColorConfig: Record<IssuedInvoiceStatus, { badge: string; dot: string }> = {
|
||||
draft: {
|
||||
badge:
|
||||
"bg-gray-500/10 dark:bg-gray-500/20 hover:bg-gray-500/10 text-gray-600 border-gray-400/60",
|
||||
dot: "bg-gray-500",
|
||||
},
|
||||
sent: {
|
||||
badge:
|
||||
"bg-amber-500/10 dark:bg-amber-500/20 hover:bg-amber-500/10 text-amber-500 border-amber-600/60",
|
||||
dot: "bg-amber-500",
|
||||
},
|
||||
approved: {
|
||||
badge:
|
||||
"bg-emerald-500/10 dark:bg-emerald-500/20 hover:bg-emerald-500/10 text-emerald-500 border-emerald-600/60",
|
||||
dot: "bg-emerald-500",
|
||||
},
|
||||
rejected: {
|
||||
badge: "bg-red-500/10 dark:bg-red-500/20 hover:bg-red-500/10 text-red-500 border-red-600/60",
|
||||
dot: "bg-red-500",
|
||||
},
|
||||
issued: {
|
||||
badge:
|
||||
"bg-blue-600/10 dark:bg-blue-600/20 hover:bg-blue-600/10 text-blue-500 border-blue-600/60",
|
||||
dot: "bg-blue-500",
|
||||
},
|
||||
};
|
||||
|
||||
export const IssuedInvoiceStatusBadge = forwardRef<HTMLDivElement, IssuedInvoiceStatusBadgeProps>(
|
||||
({ status, dotVisible, className, ...props }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const normalizedStatus = status.toLowerCase() as IssuedInvoiceStatus;
|
||||
const config = statusColorConfig[normalizedStatus];
|
||||
const commonClassName =
|
||||
"transition-colors duration-200 cursor-pointer shadow-none rounded-full";
|
||||
|
||||
if (!config) {
|
||||
return (
|
||||
<Badge className={cn(commonClassName, className)} ref={ref} {...props}>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge className={cn(commonClassName, config.badge, className)} {...props}>
|
||||
{dotVisible && <div className={cn("h-1.5 w-1.5 rounded-full mr-2", config.dot)} />}
|
||||
{t(`catalog.proformas.status.${normalizedStatus}`, { defaultValue: status })}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
IssuedInvoiceStatusBadge.displayName = "IssuedInvoiceStatusBadge";
|
||||
@ -0,0 +1,105 @@
|
||||
import { SimpleSearchInput } from "@erp/core/components";
|
||||
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { FilterIcon } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { IssuedInvoiceSummaryPageData } from "../../../schema/issued-invoice-summary.web.schema";
|
||||
import { useIssuedInvoicesGridColumns } from "../hooks";
|
||||
|
||||
interface IssuedInvoicesGridProps {
|
||||
data: IssuedInvoiceSummaryPageData;
|
||||
loading?: boolean;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
searchValue: string;
|
||||
onSearchChange: (v: string) => void;
|
||||
onPageChange: (p: number) => void;
|
||||
onPageSizeChange: (s: number) => void;
|
||||
onRowClick?: (id: string) => void;
|
||||
onExportClick?: () => void;
|
||||
onStatusFilterChange?: (newStatus: string) => void;
|
||||
}
|
||||
|
||||
export const IssuedInvoicesGrid = ({
|
||||
data,
|
||||
loading,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
onRowClick,
|
||||
onExportClick,
|
||||
onStatusFilterChange,
|
||||
}: IssuedInvoicesGridProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const { items, total_items } = data;
|
||||
|
||||
const columns = useIssuedInvoicesGridColumns({
|
||||
onEdit: (proforma) => navigate(`/issued-invoices/${proforma.id}/edit`),
|
||||
onDuplicate: (proforma) => null, //duplicateInvoice(inv.id),
|
||||
onDownloadPdf: (proforma) => null, //downloadInvoicePdf(inv.id),
|
||||
onSendEmail: (proforma) => null, //sendInvoiceEmail(inv.id),
|
||||
onDelete: (proforma) => null, //confirmDelete(inv.id),
|
||||
});
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
<SkeletonDataTable
|
||||
columns={columns.length}
|
||||
footerProps={{ pageIndex, pageSize, totalItems: total_items ?? 0 }}
|
||||
rows={Math.max(6, pageSize)}
|
||||
showFooter
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<SimpleSearchInput loading={loading} onSearchChange={onSearchChange} />
|
||||
<Select defaultValue="all" onValueChange={onStatusFilterChange}>
|
||||
<SelectTrigger className="w-full sm:w-48">
|
||||
<FilterIcon aria-hidden className="mr-2 size-4" />
|
||||
<SelectValue placeholder={t("filters.status")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t("catalog.proformas.status.all")}</SelectItem>
|
||||
<SelectItem value="draft">{t("catalog.proformas.status.draft")}</SelectItem>
|
||||
<SelectItem value="sent">{t("catalog.proformas.status.sent")}</SelectItem>
|
||||
<SelectItem value="approved">{t("catalog.proformas.status.approved")}</SelectItem>
|
||||
<SelectItem value="rejected">{t("catalog.proformas.status.rejected")}</SelectItem>
|
||||
<SelectItem value="issued">{t("catalog.proformas.status.issued")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
columnVisibility={{
|
||||
subtotal_amount_fmt: false,
|
||||
discount_amount_fmt: false,
|
||||
taxes_amount_fmt: false,
|
||||
}}
|
||||
data={items}
|
||||
enablePagination
|
||||
manualPagination
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
onRowClick={(row, _index) => onRowClick?.(row.id)}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
totalItems={total_items}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./issued-invoice.api.schema";
|
||||
export * from "./issued-invoice-summary.web.schema";
|
||||
@ -1,6 +1,6 @@
|
||||
import type { IssuedInvoicesummary, IssuedInvoicesummaryPage } from "./issued-invoice.api.schema";
|
||||
import type { IssuedInvoiceSummary, IssuedInvoiceSummaryPage } from "./issued-invoice.api.schema";
|
||||
|
||||
export type IssuedInvoicesummaryData = IssuedInvoicesummary & {
|
||||
export type IssuedInvoiceSummaryData = IssuedInvoiceSummary & {
|
||||
subtotal_amount_fmt: string;
|
||||
subtotal_amount: number;
|
||||
|
||||
@ -13,13 +13,13 @@ export type IssuedInvoicesummaryData = IssuedInvoicesummary & {
|
||||
taxable_amount_fmt: string;
|
||||
taxable_amount: number;
|
||||
|
||||
taxes_amoun_fmt: string;
|
||||
taxes_amount_fmt: string;
|
||||
taxes_amount: number;
|
||||
|
||||
total_amount_fmt: string;
|
||||
total_amount: number;
|
||||
};
|
||||
|
||||
export type IssuedInvoicesummaryPageData = IssuedInvoicesummaryPage & {
|
||||
items: IssuedInvoicesummary[];
|
||||
export type IssuedInvoiceSummaryPageData = IssuedInvoiceSummaryPage & {
|
||||
items: IssuedInvoiceSummaryData[];
|
||||
};
|
||||
@ -0,0 +1,22 @@
|
||||
import {
|
||||
GetIssuedInvoiceByIdResponseSchema,
|
||||
type ListIssuedInvoicesResponseDTO,
|
||||
} from "@erp/customer-invoices/common";
|
||||
import type { ArrayElement } from "@repo/rdx-utils";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
// IssuedInvoices
|
||||
export const IssuedInvoiceSchema = GetIssuedInvoiceByIdResponseSchema.omit({
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
export type IssuedInvoice = z.infer<typeof IssuedInvoiceSchema>;
|
||||
export type IssuedInvoiceRecipient = IssuedInvoice["recipient"];
|
||||
export type IssuedInvoiceItem = ArrayElement<IssuedInvoice["items"]>;
|
||||
|
||||
// Resultado de consulta con criteria (paginado, etc.)
|
||||
export type IssuedInvoiceSummaryPage = Omit<ListIssuedInvoicesResponseDTO, "metadata">;
|
||||
export type IssuedInvoiceSummary = Omit<
|
||||
ArrayElement<IssuedInvoiceSummaryPage["items"]>,
|
||||
"metadata"
|
||||
>;
|
||||
@ -48,18 +48,18 @@ export function useProformasGridColumns(
|
||||
accessorKey: "invoice_number",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
className="text-right tabular-nums justify-end"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.invoice_number")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-semibold text-left text-primary">{row.original.invoice_number}</div>
|
||||
<div className="text-right tabular-nums">{row.original.invoice_number}</div>
|
||||
),
|
||||
enableHiding: false,
|
||||
enableSorting: false,
|
||||
size: 160,
|
||||
minSize: 120,
|
||||
maxSize: 48,
|
||||
size: 48,
|
||||
minSize: 48,
|
||||
meta: {
|
||||
title: t("pages.proformas.list.grid_columns.invoice_number"),
|
||||
},
|
||||
@ -74,10 +74,10 @@ export function useProformasGridColumns(
|
||||
title={t("pages.proformas.list.grid_columns.status")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => <ProformaStatusBadge status={row.original.status} />,
|
||||
cell: ({ row }) => <ProformaStatusBadge className="my-0.5" status={row.original.status} />,
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
size: 64,
|
||||
minSize: 64,
|
||||
meta: {
|
||||
title: t("pages.proformas.list.grid_columns.status"),
|
||||
},
|
||||
@ -93,15 +93,14 @@ export function useProformasGridColumns(
|
||||
),
|
||||
accessorFn: (row) => row.recipient.name, // para ordenar/buscar por nombre
|
||||
enableHiding: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
cell: ({ row }) => {
|
||||
const c = row.original.recipient;
|
||||
return (
|
||||
<div className="flex items-start gap-1 my-1.5">
|
||||
<div className="flex items-start gap-1">
|
||||
<div className="min-w-0 grid gap-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium truncate text-primary">{c.name}</span>
|
||||
<span className="font-semibold truncate text-primary">{c.name}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{c.tin && <span className="font-base truncate">{c.tin}</span>}
|
||||
@ -126,8 +125,8 @@ export function useProformasGridColumns(
|
||||
),
|
||||
cell: ({ row }) => <div className="font-normal text-left">{row.original.series}</div>,
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
size: 64,
|
||||
minSize: 64,
|
||||
meta: {
|
||||
title: t("pages.proformas.list.grid_columns.series"),
|
||||
},
|
||||
@ -166,9 +165,8 @@ export function useProformasGridColumns(
|
||||
{formatDate(row.original.invoice_date)}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
size: 96,
|
||||
minSize: 96,
|
||||
meta: {
|
||||
title: t("pages.proformas.list.grid_columns.invoice_date"),
|
||||
},
|
||||
@ -188,9 +186,8 @@ export function useProformasGridColumns(
|
||||
{formatDate(row.original.operation_date)}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
size: 96,
|
||||
minSize: 96,
|
||||
meta: {
|
||||
title: t("pages.proformas.list.grid_columns.operation_date"),
|
||||
},
|
||||
@ -291,7 +288,13 @@ export function useProformasGridColumns(
|
||||
// ─────────────────────────────
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <span className="sr-only">{t("common.actions")}</span>,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("common.actions")}
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
size: 110,
|
||||
@ -381,7 +384,7 @@ export function useProformasGridColumns(
|
||||
|
||||
{/* Menú demás acciones */}
|
||||
{/** biome-ignore lint/suspicious/noSelfCompare: <Desactivado por ahora> */}
|
||||
{false === false && (
|
||||
{false !== false && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
|
||||
@ -1,21 +1,19 @@
|
||||
import { Column } from "@tanstack/react-table";
|
||||
import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from "lucide-react";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import type { Column } from "@tanstack/react-table";
|
||||
import { ArrowDown, ArrowUp, ChevronsUpDown } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../locales/i18n.ts";
|
||||
|
||||
import {
|
||||
Button, DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@repo/shadcn-ui/components';
|
||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||
|
||||
interface DataTableColumnHeaderProps<TData, TValue>
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
column: Column<TData, TValue>
|
||||
title: string
|
||||
interface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
|
||||
column: Column<TData, TValue>;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function DataTableColumnHeader<TData, TValue>({
|
||||
@ -26,7 +24,16 @@ export function DataTableColumnHeader<TData, TValue>({
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!column.getCanSort()) {
|
||||
return <div className={cn("text-xs text-muted-foreground text-nowrap cursor-default", className)}>{title}</div>
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"text-xs text-muted-foreground text-nowrap cursor-default font-semibold",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -34,10 +41,10 @@ export function DataTableColumnHeader<TData, TValue>({
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="data-[state=open]:bg-accent -ml-3 h-8 text-xs text-muted-foreground font-semibold text-nowrap cursor-pointer"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="data-[state=open]:bg-accent -ml-3 h-8 text-xs text-muted-foreground text-nowrap cursor-pointer"
|
||||
>
|
||||
<span>{title}</span>
|
||||
{column.getIsSorted() === "desc" ? (
|
||||
@ -52,19 +59,14 @@ export function DataTableColumnHeader<TData, TValue>({
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
|
||||
<ArrowUp />
|
||||
{t("components.datatabla.asc")}
|
||||
{t("components.datatable.asc")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
|
||||
<ArrowDown />
|
||||
{t("components.datatabla.desc")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
||||
<EyeOff />
|
||||
{t("components.datatabla.hide")}
|
||||
{t("components.datatable.desc")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,23 +1,26 @@
|
||||
import { Table } from "@tanstack/react-table";
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronsLeftIcon,
|
||||
ChevronsRightIcon
|
||||
} from "lucide-react";
|
||||
|
||||
import {
|
||||
Pagination, PaginationContent,
|
||||
PaginationItem, PaginationLink,
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@repo/shadcn-ui/components';
|
||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||
import { useTranslation } from '../../locales/i18n.ts';
|
||||
import { DataTableMeta } from './data-table.tsx';
|
||||
SelectValue,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import type { Table } from "@tanstack/react-table";
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronsLeftIcon,
|
||||
ChevronsRightIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../locales/i18n.ts";
|
||||
|
||||
import type { DataTableMeta } from "./data-table.tsx";
|
||||
|
||||
interface DataTablePaginationProps<TData> {
|
||||
table: Table<TData>;
|
||||
@ -28,7 +31,11 @@ interface DataTablePaginationProps<TData> {
|
||||
}
|
||||
|
||||
export function DataTablePagination<TData>({
|
||||
table, onPageChange, onPageSizeChange, className }: DataTablePaginationProps<TData>) {
|
||||
table,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
className,
|
||||
}: DataTablePaginationProps<TData>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { pageIndex: rawIndex, pageSize: rawSize } = table.getState().pagination;
|
||||
@ -50,10 +57,10 @@ export function DataTablePagination<TData>({
|
||||
const notify = (next: Partial<{ pageIndex: number; pageSize: number }>) =>
|
||||
table.options.onPaginationChange?.({ pageIndex, pageSize, ...next });
|
||||
|
||||
const gotoPage = (index: number) => onPageChange ? onPageChange(index) : notify({ pageIndex: index });
|
||||
const handlePageSizeChange = (size: string) => onPageSizeChange ?
|
||||
onPageSizeChange(Number(size)) :
|
||||
notify({ pageSize: Number(size) });
|
||||
const gotoPage = (index: number) =>
|
||||
onPageChange ? onPageChange(index) : notify({ pageIndex: index });
|
||||
const handlePageSizeChange = (size: string) =>
|
||||
onPageSizeChange ? onPageSizeChange(Number(size)) : notify({ pageSize: Number(size) });
|
||||
|
||||
const gotoPreviousPage = () => gotoPage(pageIndex - 1);
|
||||
const gotoNextPage = () => gotoPage(pageIndex + 1);
|
||||
@ -79,7 +86,7 @@ export function DataTablePagination<TData>({
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{t("components.datatable.pagination.rows_per_page")}</span>
|
||||
<Select value={String(pageSize)} onValueChange={handlePageSizeChange}>
|
||||
<Select onValueChange={handlePageSizeChange} value={String(pageSize)}>
|
||||
<SelectTrigger className="w-20 h-8 bg-white border-gray-200">
|
||||
<SelectValue placeholder={String(pageSize)} />
|
||||
</SelectTrigger>
|
||||
@ -91,7 +98,6 @@ export function DataTablePagination<TData>({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{pageIndex + 1} / {pageCount}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -102,12 +108,12 @@ export function DataTablePagination<TData>({
|
||||
<PaginationItem>
|
||||
<PaginationLink
|
||||
aria-label={t("components.datatable.pagination.goto_first_page")}
|
||||
className="px-2.5 cursor-pointer"
|
||||
isActive={pageIndex > 0}
|
||||
onClick={() => {
|
||||
if (pageIndex > 0) gotoFirstPage();
|
||||
}}
|
||||
isActive={pageIndex > 0}
|
||||
size="sm"
|
||||
className="px-2.5 cursor-pointer"
|
||||
>
|
||||
<ChevronsLeftIcon className="size-4" />
|
||||
</PaginationLink>
|
||||
@ -116,30 +122,30 @@ export function DataTablePagination<TData>({
|
||||
<PaginationItem>
|
||||
<PaginationLink
|
||||
aria-label={t("components.datatable.pagination.goto_previous_page")}
|
||||
className="px-2.5 cursor-pointer"
|
||||
isActive={pageIndex > 0}
|
||||
onClick={() => {
|
||||
if (pageIndex > 0) gotoPreviousPage();
|
||||
}}
|
||||
isActive={pageIndex > 0}
|
||||
size="sm"
|
||||
className="px-2.5 cursor-pointer"
|
||||
>
|
||||
<ChevronLeftIcon className="size-4" />
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
|
||||
<span className="text-sm text-muted-foreground px-2" aria-live="polite">
|
||||
<span aria-live="polite" className="text-sm text-muted-foreground px-2">
|
||||
{t("components.datatable.pagination.page_of", { page: pageIndex + 1, of: pageCount })}
|
||||
</span>
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationLink
|
||||
aria-label={t("components.datatable.pagination.goto_next_page")}
|
||||
className="px-2.5 cursor-pointer"
|
||||
isActive={pageIndex < pageCount - 1}
|
||||
onClick={() => {
|
||||
if (pageIndex < pageCount - 1) gotoNextPage();
|
||||
}}
|
||||
isActive={pageIndex < pageCount - 1}
|
||||
size="sm"
|
||||
className="px-2.5 cursor-pointer"
|
||||
>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</PaginationLink>
|
||||
@ -148,12 +154,12 @@ export function DataTablePagination<TData>({
|
||||
<PaginationItem>
|
||||
<PaginationLink
|
||||
aria-label={t("components.datatable.pagination.goto_last_page")}
|
||||
className="px-2.5 cursor-pointer"
|
||||
isActive={pageIndex < pageCount - 1}
|
||||
onClick={() => {
|
||||
if (pageIndex < pageCount - 1) gotoLastPage();
|
||||
}}
|
||||
isActive={pageIndex < pageCount - 1}
|
||||
size="sm"
|
||||
className="px-2.5 cursor-pointer"
|
||||
>
|
||||
<ChevronsRightIcon className="size-4" />
|
||||
</PaginationLink>
|
||||
@ -163,4 +169,4 @@ export function DataTablePagination<TData>({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ export function DataTableViewOptions<TData>({ table }: { table: Table<TData> })
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="ml-auto hidden h-8 lg:flex" size="sm" type="button" variant="outline">
|
||||
<Settings2 />
|
||||
{t("components.datatable_view_options.view_button")}
|
||||
{t("components.datatable_view_options.columns_button")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
}
|
||||
},
|
||||
"datatable_view_options": {
|
||||
"view_button": "View",
|
||||
"columns_button": "Columns",
|
||||
"toggle_columns": "Toggle columns"
|
||||
},
|
||||
"loading_indicator": {
|
||||
|
||||
@ -11,8 +11,8 @@
|
||||
},
|
||||
"components": {
|
||||
"datatable": {
|
||||
"asc": "Asc",
|
||||
"desc": "Desc",
|
||||
"asc": "Ascendente",
|
||||
"desc": "Descendente",
|
||||
"hide": "Ocultar",
|
||||
"empty": "No hay resultados",
|
||||
"selection_summary": "{{count}} filas seleccionadas de {{total}}",
|
||||
@ -39,7 +39,7 @@
|
||||
}
|
||||
},
|
||||
"datatable_view_options": {
|
||||
"view_button": "Ver",
|
||||
"columns_button": "Columnas",
|
||||
"toggle_columns": "Alternar columnas"
|
||||
},
|
||||
"loading_indicator": {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user