This commit is contained in:
David Arranz 2026-04-12 21:12:10 +02:00
parent 71b9e857e7
commit 8a8282ddc3
48 changed files with 828 additions and 653 deletions

View File

@ -3,7 +3,7 @@ import { lazy } from "react";
import { Outlet, type RouteObject } from "react-router-dom"; import { Outlet, type RouteObject } from "react-router-dom";
const ProformaLayout = lazy(() => const ProformaLayout = lazy(() =>
import("./proformas/shared").then((m) => ({ default: m.ProformaLayout })) import("./proformas/shared/ui").then((m) => ({ default: m.ProformaLayout }))
); );
const ProformasListPage = lazy(() => const ProformasListPage = lazy(() =>
@ -19,7 +19,7 @@ const ProformaUpdatePage = lazy(() =>
); );
const IssuedInvoicesLayout = lazy(() => const IssuedInvoicesLayout = lazy(() =>
import("./issued-invoices/shared").then((m) => ({ default: m.IssuedInvoicesLayout })) import("./issued-invoices/shared/ui").then((m) => ({ default: m.IssuedInvoicesLayout }))
); );
const IssuedInvoiceListPage = lazy(() => const IssuedInvoiceListPage = lazy(() =>

View File

@ -1,7 +1,7 @@
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers"; import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
import * as React from "react"; import * as React from "react";
import { useDownloadInvoicePDFQuery } from "../hooks"; import { useDownloadInvoicePDFQuery } from "../../shared";
interface PendingDownload { interface PendingDownload {
id: string; id: string;

View File

@ -1 +0,0 @@
export * from "./issued-invoice-summary-dto.adapter";

View File

@ -1,71 +0,0 @@
import { MoneyDTOHelper, formatCurrency } from "@erp/core";
import type {
IssuedInvoiceSummaryData,
IssuedInvoiceSummaryPage,
IssuedInvoiceSummaryPageData,
} from "../../types";
/**
* Convierte el DTO completo de API a datos numéricos para el formulario.
*/
export const IssuedInvoiceSummaryDtoAdapter = {
fromDto(pageDto: IssuedInvoiceSummaryPage, context?: unknown): IssuedInvoiceSummaryPageData {
return {
...pageDto,
items: pageDto.items.map(
(summaryDto) =>
({
...summaryDto,
subtotal_amount: MoneyDTOHelper.toNumber(summaryDto.subtotal_amount),
subtotal_amount_fmt: formatCurrency(
MoneyDTOHelper.toNumber(summaryDto.subtotal_amount),
Number(summaryDto.total_amount.scale || 2),
summaryDto.currency_code,
summaryDto.language_code
),
/*discount_percentage: PercentageDTOHelper.toNumber(summaryDto.discount_percentage),
discount_percentage_fmt: PercentageDTOHelper.toNumericString(
summaryDto.discount_percentage
),*/
discount_amount: MoneyDTOHelper.toNumber(summaryDto.total_discount_amount),
discount_amount_fmt: formatCurrency(
MoneyDTOHelper.toNumber(summaryDto.total_discount_amount),
Number(summaryDto.total_amount.scale || 2),
summaryDto.currency_code,
summaryDto.language_code
),
taxable_amount: MoneyDTOHelper.toNumber(summaryDto.taxable_amount),
taxable_amount_fmt: formatCurrency(
MoneyDTOHelper.toNumber(summaryDto.taxable_amount),
Number(summaryDto.total_amount.scale || 2),
summaryDto.currency_code,
summaryDto.language_code
),
taxes_amount: MoneyDTOHelper.toNumber(summaryDto.taxes_amount),
taxes_amount_fmt: formatCurrency(
MoneyDTOHelper.toNumber(summaryDto.taxes_amount),
Number(summaryDto.total_amount.scale || 2),
summaryDto.currency_code,
summaryDto.language_code
),
total_amount: MoneyDTOHelper.toNumber(summaryDto.total_amount),
total_amount_fmt: formatCurrency(
MoneyDTOHelper.toNumber(summaryDto.total_amount),
Number(summaryDto.total_amount.scale || 2),
summaryDto.currency_code,
summaryDto.language_code
),
//taxes: dto.taxes,
}) as unknown as IssuedInvoiceSummaryData
),
};
},
};

View File

@ -1,18 +0,0 @@
import type { CriteriaDTO } from "@erp/core";
import type { IDataSource } from "@erp/core/client";
import type { IssuedInvoiceSummaryPage } from "../../types";
export async function getIssuedInvoiceListApi(
dataSource: IDataSource,
signal: AbortSignal,
criteria: CriteriaDTO
) {
const response = dataSource.getList<IssuedInvoiceSummaryPage>("issued-invoices", {
signal,
...criteria,
});
//return mapIssuedInvoiceList(raw);
return response;
}

View File

@ -1 +0,0 @@
export * from "./get-issued-invoice-list.api";

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { useDownloadInvoicePDFController } from "../../download-pdf/controller"; import { useDownloadInvoicePDFController } from "../../download-pdf/controller";
import type { IssuedInvoiceSummaryData } from "../../types"; import type { IssuedInvoiceListRow } from "../../shared";
import { useIssuedInvoiceListController } from "./use-issued-invoice-list.controller"; import { useIssuedInvoiceListController } from "./use-issued-invoice-list.controller";
@ -10,8 +10,8 @@ export function useIssuedInvoiceListPageController() {
const downloadPDFCtrl = useDownloadInvoicePDFController(); const downloadPDFCtrl = useDownloadInvoicePDFController();
const handleDownloadPDF = React.useCallback( const handleDownloadPDF = React.useCallback(
(issuedInvoice: IssuedInvoiceSummaryData) => { (issuedInvoice: IssuedInvoiceListRow) => {
downloadPDFCtrl.download(issuedInvoice.id, issuedInvoice.invoice_number); downloadPDFCtrl.download(issuedInvoice.id, issuedInvoice.invoiceNumber);
}, },
[downloadPDFCtrl] [downloadPDFCtrl]
); );

View File

@ -1,51 +1,101 @@
import type { CriteriaDTO } from "@erp/core";
import { useDebounce } from "@repo/rdx-ui/components"; import { useDebounce } from "@repo/rdx-ui/components";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { IssuedInvoiceSummaryDtoAdapter } from "../adapters"; import type {
import { useIssuedInvoiceListQuery } from "../hooks"; IssuedInvoiceList,
IssuedInvoiceStatus,
ListIssuedInvoicesByCriteriaParams,
} from "../../shared";
import { useIssuedInvoiceListQuery } from "../../shared/";
type IssuedInvoiceListStatusFilter = "all" | IssuedInvoiceStatus;
const EMPTY_ISSUED_INVOICES_LIST: IssuedInvoiceList = {
items: [],
page: 0,
perPage: 5,
totalPages: 0,
totalItems: 0,
};
export const useIssuedInvoiceListController = () => { export const useIssuedInvoiceListController = () => {
const [pageIndex, setPageIndex] = useState(0); const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(5);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [status, setStatus] = useState("all"); const [statusFilter, setStatusFilter] = useState<IssuedInvoiceListStatusFilter>("all");
const debouncedQ = useDebounce(search, 300); const debouncedSearch = useDebounce(search, 300);
const criteria = useMemo<CriteriaDTO>(() => { const criteria = useMemo<NonNullable<ListIssuedInvoicesByCriteriaParams["criteria"]>>(
const baseFilters = () => ({
status !== "all" ? [{ field: "status", operator: "CONTAINS", value: status }] : []; q: debouncedSearch || "",
return {
q: debouncedQ || "",
pageSize,
pageNumber: pageIndex, pageNumber: pageIndex,
pageSize,
order: "desc", order: "desc",
orderBy: "invoice_date", orderBy: "invoice_date",
filters: baseFilters, filters: status === "all" ? [] : [{ field: "status", operator: "eq", value: status }],
}; }),
}, [pageSize, pageIndex, debouncedQ, status]); [debouncedSearch, pageIndex, pageSize, status]
const query = useIssuedInvoiceListQuery({ criteria });
const data = useMemo(
() => (query.data ? IssuedInvoiceSummaryDtoAdapter.fromDto(query.data) : undefined),
[query.data]
); );
const setSearchValue = (value: string) => setSearch(value.trim().replace(/\s+/g, " ")); const query = useIssuedInvoiceListQuery({ criteria });
const setStatusFilter = (newStatus: string) => setStatus(newStatus); const setStatusFilterValue = (value: string) => {
const nextValue = (value || "all") as IssuedInvoiceListStatusFilter;
setStatusFilter((prev) => {
if (prev === nextValue) return prev;
// Sólo si la búsqueda realmente cambia,
// reseteamos la página a 0 para evitar inconsistencias
setPageIndex(0);
return nextValue;
});
};
const setSearchValue = (value: string) => {
const nextValue = value.trim().replace(/\s+/g, " ");
setSearch((prev) => {
if (prev === nextValue) return prev;
// Sólo si la búsqueda realmente cambia,
// reseteamos la página a 0 para evitar inconsistencias
setPageIndex(0);
return nextValue;
});
};
const setPageSizeValue = (value: number) => {
setPageSize((prev) => {
if (prev === value) return prev;
// Sólo si el tamaño de página realmente cambia,
// reseteamos la página a 0 para evitar inconsistencias
setPageIndex(0);
return value;
});
};
return { return {
...query, data: query.data ?? EMPTY_ISSUED_INVOICES_LIST,
data, isLoading: query.isLoading,
isFetching: query.isFetching,
isError: query.isError,
error: query.error,
refetch: query.refetch,
pageIndex, pageIndex,
pageSize, pageSize,
search,
setPageIndex, setPageIndex,
setPageSize, setPageSize: setPageSizeValue,
search,
setSearchValue, setSearchValue,
setStatusFilter,
statusFilter,
setStatusFilter: setStatusFilterValue,
}; };
}; };

View File

@ -1,42 +1,37 @@
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components"; import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
import type { ColumnDef } from "@tanstack/react-table"; import type { ColumnDef } from "@tanstack/react-table";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "../../../../../i18n"; import { useTranslation } from "../../../../../i18n";
import type { IssuedInvoiceSummaryData, IssuedInvoiceSummaryPageData } from "../../../../types"; import type { IssuedInvoiceList, IssuedInvoiceListRow } from "../../../../shared";
export type InvoiceUpdateCompProps = { interface IssuedInvoicesGridProps {
data: IssuedInvoiceSummaryPageData; data?: IssuedInvoiceList;
loading?: boolean; loading: boolean;
fetching?: boolean;
columns: ColumnDef<IssuedInvoiceSummaryData, unknown>[]; columns: ColumnDef<IssuedInvoiceListRow, unknown>[];
pageIndex: number; pageIndex: number;
pageSize: number; pageSize: number;
onPageChange?: (pageNumber: number) => void; onPageChange: (pageIndex: number) => void;
onPageSizeChange?: (pageSize: number) => void; onPageSizeChange: (size: number) => void;
onRowClick?: ( onRowClick?: (proformaId: string) => void;
row: IssuedInvoiceSummaryPageData, }
index: number,
event: React.MouseEvent<HTMLTableRowElement>
) => void;
};
// Create new GridExample component
export const IssuedInvoicesGrid = ({ export const IssuedInvoicesGrid = ({
data, data,
loading, loading,
fetching,
columns, columns,
pageIndex, pageIndex,
pageSize, pageSize,
onPageChange, onPageChange,
onPageSizeChange, onPageSizeChange,
onRowClick, onRowClick,
}: InvoiceUpdateCompProps) => { }: IssuedInvoicesGridProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const { items, totalItems } = data || { items: [], totalItems: 0 };
const { items, total_items } = data;
// Navegación accesible (click o teclado) // Navegación accesible (click o teclado)
/* const goToRow = useCallback( /* const goToRow = useCallback(
@ -90,36 +85,29 @@ export const IssuedInvoicesGrid = ({
if (loading) { if (loading) {
return ( return (
<div className="flex flex-col gap-4">
<SkeletonDataTable <SkeletonDataTable
columns={columns.length} columns={columns.length}
footerProps={{ pageIndex, pageSize, totalItems: total_items ?? 0 }} footerProps={{ pageIndex, pageSize, totalItems: totalItems ?? 0 }}
rows={Math.max(6, pageSize)} rows={Math.max(6, pageSize)}
showFooter showFooter
/> />
</div>
); );
} }
// Render principal // Render principal
return ( return (
<>
{/*<div className={preview.isPinned ? "flex-1 mr-[500px]" : "flex-1"}>*/}
<DataTable <DataTable
columns={columns} columns={columns}
data={items} data={items}
enablePagination enablePagination={false}
enableRowSelection enableRowSelection
manualPagination manualPagination
onPageChange={onPageChange} onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange} onPageSizeChange={onPageSizeChange}
//onRowClick={handleRowClick} //onRowClick={(row) => onRowClick?.(row.id)}
pageIndex={pageIndex} pageIndex={pageIndex}
pageSize={pageSize} pageSize={pageSize}
readOnly totalItems={totalItems}
totalItems={total_items}
/> />
{/*</div>*/}
</>
); );
}; };

View File

@ -5,6 +5,7 @@ import {
ButtonGroup, ButtonGroup,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
Spinner, Spinner,
@ -18,21 +19,21 @@ import * as React from "react";
import QrCode from "react-qr-code"; import QrCode from "react-qr-code";
import { useTranslation } from "../../../../../i18n"; import { useTranslation } from "../../../../../i18n";
import type { IssuedInvoiceSummaryData } from "../../../../types"; import type { IssuedInvoiceListRow } from "../../../../shared";
import { VerifactuStatusBadge } from "../../components"; import { VerifactuStatusBadge } from "../../components";
type GridActionHandlers = { type GridActionHandlers = {
onDownloadPdf?: (issuedInvoice: IssuedInvoiceSummaryData) => void; onDownloadPdf?: (issuedInvoice: IssuedInvoiceListRow) => void;
pdfDownloadingId?: string | null; pdfDownloadingId?: string | null;
isPdfDownloading?: boolean; isPdfDownloading?: boolean;
}; };
export function useIssuedInvoicesGridColumns( export function useIssuedInvoicesGridColumns(
actionHandlers: GridActionHandlers = {} actionHandlers: GridActionHandlers = {}
): ColumnDef<IssuedInvoiceSummaryData, unknown>[] { ): ColumnDef<IssuedInvoiceListRow, unknown>[] {
const { t } = useTranslation(); const { t } = useTranslation();
return React.useMemo<ColumnDef<IssuedInvoiceSummaryData>[]>( return React.useMemo<ColumnDef<IssuedInvoiceListRow, unknown>[]>(
() => [ () => [
// Nº // Nº
{ {
@ -47,7 +48,7 @@ export function useIssuedInvoicesGridColumns(
cell: ({ row }) => ( cell: ({ row }) => (
<div className="text-right tabular-nums"> <div className="text-right tabular-nums">
{row.original.series} {row.original.series}
{row.original.invoice_number} {row.original.invoiceNumber}
</div> </div>
), ),
enableHiding: false, enableHiding: false,
@ -106,11 +107,13 @@ export function useIssuedInvoicesGridColumns(
<QrCodeIcon className="size-8 text-muted-foreground" /> <QrCodeIcon className="size-8 text-muted-foreground" />
) : ( ) : (
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger
render={
<a href={verifactu.url} rel="noopener" target="_blank"> <a href={verifactu.url} rel="noopener" target="_blank">
<QrCodeIcon className="size-8" /> <QrCodeIcon className="size-8" />
</a> </a>
</TooltipTrigger> }
/>
<TooltipContent className="m-0 p-3"> <TooltipContent className="m-0 p-3">
<QrCode className="bg-white p-8" value={verifactu.url} /> <QrCode className="bg-white p-8" value={verifactu.url} />
</TooltipContent> </TooltipContent>
@ -192,7 +195,7 @@ export function useIssuedInvoicesGridColumns(
), ),
cell: ({ row }) => ( cell: ({ row }) => (
<div className="font-medium text-left tabular-nums"> <div className="font-medium text-left tabular-nums">
{formatDate(row.original.invoice_date)} {formatDate(row.original.invoiceDate)}
</div> </div>
), ),
enableSorting: false, enableSorting: false,
@ -214,7 +217,7 @@ export function useIssuedInvoicesGridColumns(
), ),
cell: ({ row }) => ( cell: ({ row }) => (
<div className="font-medium text-left tabular-nums"> <div className="font-medium text-left tabular-nums">
{formatDate(row.original.operation_date)} {formatDate(row.original.operationDate)}
</div> </div>
), ),
enableSorting: false, enableSorting: false,
@ -237,7 +240,7 @@ export function useIssuedInvoicesGridColumns(
), ),
cell: ({ row }) => ( cell: ({ row }) => (
<div className="font-medium text-right tabular-nums"> <div className="font-medium text-right tabular-nums">
{row.original.subtotal_amount_fmt} {row.original.subtotalAmountFmt}
</div> </div>
), ),
enableSorting: false, enableSorting: false,
@ -260,7 +263,7 @@ export function useIssuedInvoicesGridColumns(
), ),
cell: ({ row }) => ( cell: ({ row }) => (
<div className="font-medium text-right tabular-nums"> <div className="font-medium text-right tabular-nums">
{row.original.discount_amount_fmt} {row.original.totalDiscountAmountFmt}
</div> </div>
), ),
enableSorting: false, enableSorting: false,
@ -282,7 +285,7 @@ export function useIssuedInvoicesGridColumns(
/> />
), ),
cell: ({ row }) => ( cell: ({ row }) => (
<div className="font-medium text-right tabular-nums">{row.original.taxes_amount_fmt}</div> <div className="font-medium text-right tabular-nums">{row.original.taxesAmountFmt}</div>
), ),
enableSorting: false, enableSorting: false,
size: 120, size: 120,
@ -303,9 +306,7 @@ export function useIssuedInvoicesGridColumns(
/> />
), ),
cell: ({ row }) => ( cell: ({ row }) => (
<div className="font-semibold text-right tabular-nums"> <div className="font-semibold text-right tabular-nums">{row.original.totalAmountFmt}</div>
{row.original.total_amount_fmt}
</div>
), ),
enableSorting: false, enableSorting: false,
size: 140, size: 140,
@ -343,7 +344,8 @@ export function useIssuedInvoicesGridColumns(
{/* Descargar en PDF */} {/* Descargar en PDF */}
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger
render={
<Button <Button
className={"size-8"} className={"size-8"}
disabled={isPDFLoading || !isCompleted} disabled={isPDFLoading || !isCompleted}
@ -361,7 +363,8 @@ export function useIssuedInvoicesGridColumns(
)} )}
<span className="sr-only">Descargar PDF</span> <span className="sr-only">Descargar PDF</span>
</Button> </Button>
</TooltipTrigger> }
/>
<TooltipContent>Descargar PDF</TooltipContent> <TooltipContent>Descargar PDF</TooltipContent>
</Tooltip> </Tooltip>
@ -369,7 +372,8 @@ export function useIssuedInvoicesGridColumns(
{/** biome-ignore lint/suspicious/noSelfCompare: <Desactivado por ahora> */} {/** biome-ignore lint/suspicious/noSelfCompare: <Desactivado por ahora> */}
{false !== false && ( {false !== false && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger
render={
<Button <Button
aria-label={t("common.more_actions")} aria-label={t("common.more_actions")}
className="cursor-pointer text-muted-foreground hover:text-primary" className="cursor-pointer text-muted-foreground hover:text-primary"
@ -381,8 +385,10 @@ export function useIssuedInvoicesGridColumns(
<MoreVerticalIcon aria-hidden="true" className="size-4" /> <MoreVerticalIcon aria-hidden="true" className="size-4" />
<span className="sr-only">{t("common.more_actions")}</span> <span className="sr-only">{t("common.more_actions")}</span>
</Button> </Button>
</DropdownMenuTrigger> }
/>
<DropdownMenuContent align="end" className="w-48"> <DropdownMenuContent align="end" className="w-48">
<DropdownMenuGroup>
<DropdownMenuItem <DropdownMenuItem
className="cursor-pointer" className="cursor-pointer"
onClick={() => actionHandlers.onDownloadPdf?.(issuedInvoice)} onClick={() => actionHandlers.onDownloadPdf?.(issuedInvoice)}
@ -394,6 +400,7 @@ export function useIssuedInvoicesGridColumns(
<MailIcon className="mr-2 size-4" /> <MailIcon className="mr-2 size-4" />
{t("common.send_email")} {t("common.send_email")}
</DropdownMenuItem>{" "} </DropdownMenuItem>{" "}
</DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}

View File

@ -7,7 +7,7 @@ import {
getVerifactuRecordStatusButtonVariant, getVerifactuRecordStatusButtonVariant,
getVerifactuRecordStatusColor, getVerifactuRecordStatusColor,
getVerifactuRecordStatusIcon, getVerifactuRecordStatusIcon,
} from "../../../types"; } from "../../../shared";
export type VerifactuStatusBadgeProps = { export type VerifactuStatusBadgeProps = {
status: string | VerifactuRecordStatus; // permitir cualquier valor status: string | VerifactuRecordStatus; // permitir cualquier valor
@ -21,7 +21,8 @@ export const VerifactuStatusBadge = ({ status, className }: VerifactuStatusBadge
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger
render={
<Badge <Badge
className={cn( className={cn(
getVerifactuRecordStatusColor(normalizedStatus), getVerifactuRecordStatusColor(normalizedStatus),
@ -35,7 +36,8 @@ export const VerifactuStatusBadge = ({ status, className }: VerifactuStatusBadge
defaultValue: status, defaultValue: status,
})} })}
</Badge> </Badge>
</TooltipTrigger> }
/>
<TooltipContent> <TooltipContent>
<p>{t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.description`)}</p> <p>{t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.description`)}</p>
</TooltipContent> </TooltipContent>

View File

@ -1 +1 @@
export * from "./issued-invoice-list-page"; export * from "./list-issued-invoices-page";

View File

@ -18,10 +18,10 @@ import { useTranslation } from "../../../../i18n";
import { useIssuedInvoiceListPageController } from "../../controllers"; import { useIssuedInvoiceListPageController } from "../../controllers";
import { IssuedInvoicesGrid, useIssuedInvoicesGridColumns } from "../blocks"; import { IssuedInvoicesGrid, useIssuedInvoicesGridColumns } from "../blocks";
export const IssuedInvoiceListPage = () => { export const ListIssuedInvoicesPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { const {
listCtrl, listCtrl,
handleDownloadPDF, handleDownloadPDF,
@ -43,7 +43,7 @@ export const IssuedInvoiceListPage = () => {
widthClass: "w-[500px]", widthClass: "w-[500px]",
});*/ });*/
if (listCtrl.isError || !listCtrl.data) { if (listCtrl.isError) {
return ( return (
<AppContent> <AppContent>
<ErrorAlert <ErrorAlert
@ -56,7 +56,7 @@ export const IssuedInvoiceListPage = () => {
} }
return ( return (
<> <section>
<AppHeader> <AppHeader>
<PageHeader <PageHeader
description={t("pages.issued_invoices.list.description")} description={t("pages.issued_invoices.list.description")}
@ -73,7 +73,8 @@ export const IssuedInvoiceListPage = () => {
title={t("pages.issued_invoices.list.title")} title={t("pages.issued_invoices.list.title")}
/> />
</AppHeader> </AppHeader>
<AppContent>
<AppContent className="space-y-6">
<Alert className="bg-green-50 text-green-800 flex items-center justify-between gap-6"> <Alert className="bg-green-50 text-green-800 flex items-center justify-between gap-6">
<div> <div>
<AlertTitle className="font-semibold text-green-800">¡Atención!</AlertTitle> <AlertTitle className="font-semibold text-green-800">¡Atención!</AlertTitle>
@ -87,13 +88,17 @@ export const IssuedInvoiceListPage = () => {
</Alert> </Alert>
{/* Search and filters */} {/* Search and filters */}
<div className="flex items-center justify-between gap-16"> <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<SimpleSearchInput <SimpleSearchInput
loading={listCtrl.isLoading} loading={listCtrl.isLoading}
onSearchChange={listCtrl.setSearchValue} onSearchChange={listCtrl.setSearchValue}
value={listCtrl.search}
/> />
<Select defaultValue="all" onValueChange={listCtrl.setStatusFilter}> <Select
onValueChange={(value) => listCtrl.setStatusFilter(value ?? "all")}
value={listCtrl.statusFilter}
>
<SelectTrigger className="w-full sm:w-48"> <SelectTrigger className="w-full sm:w-48">
<FilterIcon aria-hidden className="mr-2 size-4" /> <FilterIcon aria-hidden className="mr-2 size-4" />
<SelectValue placeholder={t("filters.status")} /> <SelectValue placeholder={t("filters.status")} />
@ -127,7 +132,7 @@ export const IssuedInvoiceListPage = () => {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="min-h-0 flex-1 overflow-auto">
<IssuedInvoicesGrid <IssuedInvoicesGrid
columns={columns} columns={columns}
data={listCtrl.data} data={listCtrl.data}
@ -139,7 +144,8 @@ export const IssuedInvoiceListPage = () => {
pageIndex={listCtrl.pageIndex} pageIndex={listCtrl.pageIndex}
pageSize={listCtrl.pageSize} pageSize={listCtrl.pageSize}
/> />
</div>
</AppContent> </AppContent>
</> </section>
); );
}; };

View File

@ -0,0 +1 @@
export * from "./list-issued-invoice.adapter";

View File

@ -0,0 +1,101 @@
import { MoneyDTOHelper, formatCurrency } from "@erp/core";
import type { ListIssuedInvoicesResponseDTO } from "../../../../common";
import type { IssuedInvoiceList, IssuedInvoiceListRow, IssuedInvoiceStatus } from "../entities";
import type { VerifactuRecordStatus } from "../entities/verifactu-record-status.entity";
export const ListIssuedInvoicesAdapter = {
fromDto(dto: ListIssuedInvoicesResponseDTO): IssuedInvoiceList {
return {
items: dto.items.map(IssuedInvoiceListRowAdapter.fromDto),
page: dto.page,
perPage: dto.per_page,
totalPages: dto.total_pages,
totalItems: dto.total_items,
};
},
};
type ListIssuedInvoicesItemDTO = ListIssuedInvoicesResponseDTO["items"][number];
const IssuedInvoiceListRowAdapter = {
fromDto(dto: ListIssuedInvoicesItemDTO): IssuedInvoiceListRow {
return {
id: dto.id,
companyId: dto.company_id,
invoiceNumber: dto.invoice_number,
status: dto.status as IssuedInvoiceStatus,
series: dto.series,
invoiceDate: dto.invoice_date,
operationDate: dto.operation_date,
languageCode: dto.language_code,
currencyCode: dto.currency_code,
reference: dto.reference,
description: dto.description,
recipient: {
id: dto.customer_id,
tin: dto.recipient.tin,
name: dto.recipient.name,
street: dto.recipient.street,
street2: dto.recipient.street2,
city: dto.recipient.city,
province: dto.recipient.province,
postalCode: dto.recipient.postal_code,
country: dto.recipient.country,
},
subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
subtotalAmountFmt: formatCurrency(
MoneyDTOHelper.toNumber(dto.subtotal_amount),
Number(dto.total_amount.scale || 2),
dto.currency_code,
dto.language_code
),
totalDiscountAmount: MoneyDTOHelper.toNumber(dto.total_discount_amount),
totalDiscountAmountFmt: formatCurrency(
MoneyDTOHelper.toNumber(dto.total_discount_amount),
Number(dto.total_amount.scale || 2),
dto.currency_code,
dto.language_code
),
taxableAmount: MoneyDTOHelper.toNumber(dto.taxable_amount),
taxableAmountFmt: formatCurrency(
MoneyDTOHelper.toNumber(dto.taxable_amount),
Number(dto.total_amount.scale || 2),
dto.currency_code,
dto.language_code
),
taxesAmount: MoneyDTOHelper.toNumber(dto.taxes_amount),
taxesAmountFmt: formatCurrency(
MoneyDTOHelper.toNumber(dto.taxes_amount),
Number(dto.total_amount.scale || 2),
dto.currency_code,
dto.language_code
),
totalAmount: MoneyDTOHelper.toNumber(dto.total_amount),
totalAmountFmt: formatCurrency(
MoneyDTOHelper.toNumber(dto.total_amount),
Number(dto.total_amount.scale || 2),
dto.currency_code,
dto.language_code
),
//linkedProformaId: dto.,
verifactu: {
status: dto.verifactu.status as unknown as VerifactuRecordStatus,
url: dto.verifactu.url,
qr_code: dto.verifactu.qr_code,
},
};
},
};

View File

@ -0,0 +1 @@
export * from "./list-issued-invoices-by-criteria.api";

View File

@ -0,0 +1,32 @@
import type { CriteriaDTO } from "@erp/core";
import type { IDataSource } from "@erp/core/client";
import type { ListIssuedInvoicesResponseDTO } from "../../../../common";
/**
* Recupera una lista de facturas del sistema utilizando la
* fuente de datos proporcionada y los criterios de búsqueda especificados.
*
* @param dataSource - La fuente de datos para interactuar con la API.
* @param params - Los parámetros necesarios para listar las facturas, incluyendo los criterios de búsqueda.
* @returns Una promesa que resuelve con una lista de facturas que cumplen con los criterios especificados.
* @throws Error si la recuperación de la lista de facturas falla.
*/
export type ListIssuedInvoicesByCriteriaParams = {
criteria?: CriteriaDTO;
signal?: AbortSignal;
};
export type ListIssuedInvoicesResult = ListIssuedInvoicesResponseDTO;
export function getListIssuedInvoicesByCriteria(
dataSource: IDataSource,
params: ListIssuedInvoicesByCriteriaParams
): Promise<ListIssuedInvoicesResult> {
const { criteria, signal } = params || { criteria: undefined, signal: undefined };
return dataSource.getList<ListIssuedInvoicesResponseDTO>("issued-invoices", {
signal,
...criteria,
});
}

View File

@ -0,0 +1,4 @@
export * from "./issued-invoice-list.entity";
export * from "./issued-invoice-list-row.entity";
export * from "./issued-invoice-status.entity";
export * from "./verifactu-record-status.entity";

View File

@ -0,0 +1,47 @@
import type { IssuedInvoiceRecipient } from "./issued-invoice-recipient.entity";
import type { IssuedInvoiceStatus } from "./issued-invoice-status.entity";
import type { VerifactuRecord } from "./verifactu-record.entity";
/**
* Interface que representa una fila de la lista de
* facturas en el sistema, adaptada desde la respuesta de la API.
* Contiene los campos justos para mostrar
* la información básica de cada factura en la lista.
*/
export interface IssuedInvoiceListRow {
id: string;
companyId: string;
invoiceNumber: string;
status: IssuedInvoiceStatus;
series: string;
invoiceDate: string;
operationDate: string;
languageCode: string;
currencyCode: string;
reference: string;
description: string;
recipient: IssuedInvoiceRecipient;
subtotalAmount: number;
subtotalAmountFmt: string;
totalDiscountAmount: number;
totalDiscountAmountFmt: string;
taxableAmount: number;
taxableAmountFmt: string;
taxesAmount: number;
taxesAmountFmt: string;
totalAmount: number;
totalAmountFmt: string;
verifactu: VerifactuRecord;
}

View File

@ -0,0 +1,14 @@
import type { IssuedInvoiceListRow } from "./issued-invoice-list-row.entity";
/**
* Interface que representa la respuesta paginada de una lista de proformas,
* adaptada desde la respuesta de la API.
*/
export interface IssuedInvoiceList {
items: IssuedInvoiceListRow[];
totalPages: number;
totalItems: number;
page: number;
perPage: number;
}

View File

@ -0,0 +1,18 @@
/**
* Interface que representa el destinatario de una factura en el sistema,
* adaptada desde la respuesta de la API.
*/
export interface IssuedInvoiceRecipient {
id: string;
name: string;
tin: string;
street: string;
street2: string;
city: string;
province: string;
postalCode: string;
country: string;
}

View File

@ -0,0 +1,26 @@
/**
* Enumeración que representa
* los posibles estados de una proforma en el sistema.
*/
export enum ISSUED_INVOICE_STATUS {
DRAFT = "draft",
SENT = "sent",
APPROVED = "approved",
REJECTED = "rejected",
ISSUED = "issued",
}
// Transiciones válidas según reglas del dominio
export const ISSUED_INVOICE_STATUS_TRANSITIONS: Record<
ISSUED_INVOICE_STATUS,
ISSUED_INVOICE_STATUS[]
> = {
[ISSUED_INVOICE_STATUS.DRAFT]: [ISSUED_INVOICE_STATUS.SENT],
[ISSUED_INVOICE_STATUS.SENT]: [ISSUED_INVOICE_STATUS.APPROVED, ISSUED_INVOICE_STATUS.REJECTED],
[ISSUED_INVOICE_STATUS.APPROVED]: [ISSUED_INVOICE_STATUS.ISSUED, ISSUED_INVOICE_STATUS.DRAFT],
[ISSUED_INVOICE_STATUS.REJECTED]: [ISSUED_INVOICE_STATUS.DRAFT],
[ISSUED_INVOICE_STATUS.ISSUED]: [],
};
export type IssuedInvoiceStatus = `${ISSUED_INVOICE_STATUS}`;

View File

@ -0,0 +1,7 @@
import type { VerifactuRecordStatus } from "./verifactu-record-status.entity";
export interface VerifactuRecord {
status: VerifactuRecordStatus;
url: string;
qr_code: string;
}

View File

@ -0,0 +1,24 @@
import type { QueryKey } from "@tanstack/react-query";
import type { ListIssuedInvoicesRequestDTO } from "../../../../common";
/**
* Prefijo base para listados
*/
export const LIST_ISSUED_INVOICES_QUERY_KEY_PREFIX = ["issued_invoices"] as const;
/**
* Query key para listado de facturas
*/
export const LIST_ISSUED_INVOICES_QUERY_KEY = (criteria?: ListIssuedInvoicesRequestDTO): QueryKey =>
[
...LIST_ISSUED_INVOICES_QUERY_KEY_PREFIX,
{
pageNumber: criteria?.pageNumber ?? 1,
pageSize: criteria?.pageSize ?? 5,
q: criteria?.q ?? "",
filters: criteria?.filters ?? [],
orderBy: criteria?.orderBy ?? "",
order: criteria?.order ?? "",
},
] as const;

View File

@ -1,39 +1,31 @@
import type { CriteriaDTO } from "@erp/core"; import type { CriteriaDTO } from "@erp/core";
import { useDataSource } from "@erp/core/hooks"; import { useDataSource } from "@erp/core/hooks";
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria"; import { type DefaultError, useQuery } from "@tanstack/react-query";
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
import { getIssuedInvoiceListApi } from "../../list/api"; import { ListIssuedInvoicesAdapter } from "../adapters";
import type { IssuedInvoiceSummaryPage } from "../../types"; import { getListIssuedInvoicesByCriteria } from "../api";
import type { IssuedInvoiceList } from "../entities";
export const ISSUED_INVOICES_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => [ import { LIST_ISSUED_INVOICES_QUERY_KEY } from "./keys";
"issued_invoices",
{
pageNumber: criteria?.pageNumber ?? INITIAL_PAGE_INDEX,
pageSize: criteria?.pageSize ?? INITIAL_PAGE_SIZE,
q: criteria?.q ?? "",
filters: criteria?.filters ?? [],
orderBy: criteria?.orderBy ?? "",
order: criteria?.order ?? "",
},
];
type IssuedInvoicesQueryOptions = { export interface IssuedInvoicesListQueryOptions {
enabled?: boolean; enabled?: boolean;
criteria?: CriteriaDTO; criteria?: Partial<CriteriaDTO>;
}; }
// Obtener todas las facturas // Obtener todas las facturas
export const useIssuedInvoiceListQuery = (options?: IssuedInvoicesQueryOptions) => { export const useIssuedInvoiceListQuery = (options?: IssuedInvoicesListQueryOptions) => {
const dataSource = useDataSource(); const dataSource = useDataSource();
const enabled = options?.enabled ?? true; const enabled = options?.enabled ?? true;
const criteria = options?.criteria ?? {}; const criteria = options?.criteria ?? {};
return useQuery<IssuedInvoiceSummaryPage, DefaultError>({ return useQuery<IssuedInvoiceList, DefaultError>({
queryKey: ISSUED_INVOICES_QUERY_KEY(criteria), queryKey: LIST_ISSUED_INVOICES_QUERY_KEY(criteria),
queryFn: async ({ signal }) => getIssuedInvoiceListApi(dataSource, signal, criteria), queryFn: async ({ signal }) => {
const dto = await getListIssuedInvoicesByCriteria(dataSource, { signal, criteria });
return ListIssuedInvoicesAdapter.fromDto(dto);
},
enabled, enabled,
staleTime: 5000, placeholderData: (previousData) => previousData, // Mantiene la página anterior durante refetch por cambio de criteria
placeholderData: (previousData, _previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
}); });
}; };

View File

@ -1 +1,5 @@
export * from "./adapters";
export * from "./api";
export * from "./entities";
export * from "./hooks"; export * from "./hooks";
export * from "./ui";

View File

@ -1,5 +1,5 @@
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
export const IssuedInvoicesLayout = ({ children }: PropsWithChildren) => { export const IssuedInvoicesLayout = ({ children }: PropsWithChildren) => {
return <div>{children}</div>; return <div className="space-y-4">{children}</div>;
}; };

View File

@ -1,3 +0,0 @@
export * from "./issued-invoice.api.schema";
export * from "./issued-invoice-summary.web.schema";
export * from "./verifactu-record-status";

View File

@ -1,25 +0,0 @@
import type { IssuedInvoiceSummary, IssuedInvoiceSummaryPage } from "./issued-invoice.api.schema";
export type IssuedInvoiceSummaryData = IssuedInvoiceSummary & {
subtotal_amount_fmt: string;
subtotal_amount: number;
discount_percentage_fmt: string;
discount_percentage: number;
discount_amount_fmt: string;
discount_amount: number;
taxable_amount_fmt: string;
taxable_amount: number;
taxes_amount_fmt: string;
taxes_amount: number;
total_amount_fmt: string;
total_amount: number;
};
export type IssuedInvoiceSummaryPageData = IssuedInvoiceSummaryPage & {
items: IssuedInvoiceSummaryData[];
};

View File

@ -1,22 +0,0 @@
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"
>;

View File

@ -101,7 +101,8 @@ export function useProformasGridColumns(
{isIssued && ( {isIssued && (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger
render={
<Button <Button
className="size-6 text-foreground hover:text-primary" className="size-6 text-foreground hover:text-primary"
size="icon" size="icon"
@ -112,7 +113,8 @@ export function useProformasGridColumns(
<span className="sr-only">Ver factura {invoiceId}</span> <span className="sr-only">Ver factura {invoiceId}</span>
</a> </a>
</Button> </Button>
</TooltipTrigger> }
/>
<TooltipContent>Ver factura {invoiceId}</TooltipContent> <TooltipContent>Ver factura {invoiceId}</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@ -235,7 +237,8 @@ export function useProformasGridColumns(
{!isIssued && actionHandlers.onEditClick && ( {!isIssued && actionHandlers.onEditClick && (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger
render={
<Button <Button
className="size-8 cursor-pointer" className="size-8 cursor-pointer"
onClick={() => actionHandlers.onEditClick?.(proforma)} onClick={() => actionHandlers.onEditClick?.(proforma)}
@ -245,7 +248,8 @@ export function useProformasGridColumns(
<PencilIcon className="size-4" /> <PencilIcon className="size-4" />
<span className="sr-only">Editar</span> <span className="sr-only">Editar</span>
</Button> </Button>
</TooltipTrigger> }
/>
<TooltipContent>Editar</TooltipContent> <TooltipContent>Editar</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@ -255,7 +259,8 @@ export function useProformasGridColumns(
{!isIssued && availableTransitions.length && actionHandlers.onChangeStatusClick && ( {!isIssued && availableTransitions.length && actionHandlers.onChangeStatusClick && (
<TooltipProvider key={availableTransitions[0]}> <TooltipProvider key={availableTransitions[0]}>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger
render={
<Button <Button
className="size-8 cursor-pointer" className="size-8 cursor-pointer"
onClick={() => onClick={() =>
@ -267,7 +272,8 @@ export function useProformasGridColumns(
<RefreshCwIcon className="size-4" /> <RefreshCwIcon className="size-4" />
<span className="sr-only">Cambiar estado</span> <span className="sr-only">Cambiar estado</span>
</Button> </Button>
</TooltipTrigger> }
/>
<TooltipContent> <TooltipContent>
Cambiar a {t(`catalog.proformas.status.${availableTransitions[0]}.label`)} Cambiar a {t(`catalog.proformas.status.${availableTransitions[0]}.label`)}
</TooltipContent> </TooltipContent>
@ -279,7 +285,8 @@ export function useProformasGridColumns(
{!isIssued && isApproved && actionHandlers.onIssueClick && ( {!isIssued && isApproved && actionHandlers.onIssueClick && (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger
render={
<Button <Button
className="size-8 cursor-pointer" className="size-8 cursor-pointer"
onClick={() => actionHandlers.onIssueClick?.(proforma)} onClick={() => actionHandlers.onIssueClick?.(proforma)}
@ -288,7 +295,8 @@ export function useProformasGridColumns(
> >
<FileTextIcon className="size-4" /> <FileTextIcon className="size-4" />
</Button> </Button>
</TooltipTrigger> }
/>
<TooltipContent>Emitir a factura</TooltipContent> <TooltipContent>Emitir a factura</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@ -298,7 +306,8 @@ export function useProformasGridColumns(
{!isIssued && actionHandlers.onDeleteClick && ( {!isIssued && actionHandlers.onDeleteClick && (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger
render={
<Button <Button
className="size-8 text-destructive hover:text-destructive cursor-pointer" className="size-8 text-destructive hover:text-destructive cursor-pointer"
onClick={(e) => { onClick={(e) => {
@ -310,7 +319,8 @@ export function useProformasGridColumns(
> >
<Trash2Icon className="size-4" /> <Trash2Icon className="size-4" />
</Button> </Button>
</TooltipTrigger> }
/>
<TooltipContent>Eliminar</TooltipContent> <TooltipContent>Eliminar</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>

View File

@ -1,2 +1 @@
export * from "./initials";
export * from "./proforma-status-badge"; export * from "./proforma-status-badge";

View File

@ -21,7 +21,8 @@ export const ProformaStatusBadge = ({ status, className }: ProformaStatusBadgePr
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger
render={
<Badge <Badge
className={cn(getProformaStatusColor(normalizedStatus), "font-semibold", className)} className={cn(getProformaStatusColor(normalizedStatus), "font-semibold", className)}
variant={getProformaStatusButtonVariant(normalizedStatus)} variant={getProformaStatusButtonVariant(normalizedStatus)}
@ -29,7 +30,8 @@ export const ProformaStatusBadge = ({ status, className }: ProformaStatusBadgePr
<Icon /> <Icon />
{t(`catalog.proformas.status.${normalizedStatus}.label`, { defaultValue: status })} {t(`catalog.proformas.status.${normalizedStatus}.label`, { defaultValue: status })}
</Badge> </Badge>
</TooltipTrigger> }
/>
<TooltipContent> <TooltipContent>
<p>{t(`catalog.proformas.status.${normalizedStatus}.description`)}</p> <p>{t(`catalog.proformas.status.${normalizedStatus}.description`)}</p>
</TooltipContent> </TooltipContent>

View File

@ -15,7 +15,7 @@ export const LIST_PROFORMAS_QUERY_KEY = (criteria?: ProformasListRequestDTO): Qu
...LIST_PROFORMAS_QUERY_KEY_PREFIX, ...LIST_PROFORMAS_QUERY_KEY_PREFIX,
{ {
pageNumber: criteria?.pageNumber ?? 1, pageNumber: criteria?.pageNumber ?? 1,
pageSize: criteria?.pageSize ?? 10, pageSize: criteria?.pageSize ?? 5,
q: criteria?.q ?? "", q: criteria?.q ?? "",
filters: criteria?.filters ?? [], filters: criteria?.filters ?? [],
orderBy: criteria?.orderBy ?? "", orderBy: criteria?.orderBy ?? "",

View File

@ -5,6 +5,7 @@ import {
Button, Button,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
@ -152,13 +153,16 @@ export function useCustomersGridColumns(
</Button> </Button>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger
render={
<Button aria-label={t("pages.list.actions.more")} size="icon" variant="ghost"> <Button aria-label={t("pages.list.actions.more")} size="icon" variant="ghost">
<MoreHorizontalIcon className="size-4" /> <MoreHorizontalIcon className="size-4" />
</Button> </Button>
</DropdownMenuTrigger> }
/>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuGroup>
<DropdownMenuLabel>{t("pages.list.grid_columns.actions")}</DropdownMenuLabel> <DropdownMenuLabel>{t("pages.list.grid_columns.actions")}</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
@ -197,6 +201,7 @@ export function useCustomersGridColumns(
> >
{t("pages.list.actions.delete")} {t("pages.list.actions.delete")}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>

View File

@ -30,11 +30,13 @@ export const CustomerStatusBadge = ({ status }: { status: string }) => {
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger
render={
<div className={cn("flex-none rounded-full p-1", statusClass)}> <div className={cn("flex-none rounded-full p-1", statusClass)}>
<div className="size-2 rounded-full bg-current" /> <div className="size-2 rounded-full bg-current" />
</div> </div>
</TooltipTrigger> }
/>
<TooltipContent>{contentTxt}</TooltipContent> <TooltipContent>{contentTxt}</TooltipContent>
</Tooltip> </Tooltip>
); );

View File

@ -2,6 +2,7 @@ import {
Button, Button,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
@ -31,7 +32,8 @@ export function DataTableColumnHeader<TData, TValue>({
return ( return (
<div className={cn("flex items-center gap-2 ", className)}> <div className={cn("flex items-center gap-2 ", className)}>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger
render={
<Button <Button
//className="data-[state=open]:bg-accent -ml-4 h-8 text-xs text-muted-foreground font-semibold text-nowrap cursor-pointer" //className="data-[state=open]:bg-accent -ml-4 h-8 text-xs text-muted-foreground font-semibold text-nowrap cursor-pointer"
className="-ml-3 h-8 data-[state=open]:bg-accent cursor-pointer text-foreground" className="-ml-3 h-8 data-[state=open]:bg-accent cursor-pointer text-foreground"
@ -48,8 +50,10 @@ export function DataTableColumnHeader<TData, TValue>({
<ChevronsUpDownIcon /> <ChevronsUpDownIcon />
)} )}
</Button> </Button>
</DropdownMenuTrigger> }
/>
<DropdownMenuContent align="start"> <DropdownMenuContent align="start">
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => column.toggleSorting(false)}> <DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUpIcon /> <ArrowUpIcon />
{t("components.datatable.asc")} {t("components.datatable.asc")}
@ -63,6 +67,7 @@ export function DataTableColumnHeader<TData, TValue>({
<EyeOffIcon /> <EyeOffIcon />
{t("components.datatable.hide")} {t("components.datatable.hide")}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>

View File

@ -47,7 +47,8 @@ export function DataTablePagination<TData>({ table, className }: DataTablePagina
table.setPageIndex(nextIndex); table.setPageIndex(nextIndex);
}; };
const handlePageSizeChange = (size: string) => { const handlePageSizeChange = (size: string | null) => {
if (!size) return;
table.setPageSize(Number(size)); table.setPageSize(Number(size));
}; };

View File

@ -113,7 +113,8 @@ export function DataTableToolbar<TData>({
{!readOnly && meta?.bulkOps?.moveSelectedUp && ( {!readOnly && meta?.bulkOps?.moveSelectedUp && (
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger
render={
<Button <Button
aria-label={t("components.datatable.actions.move_up")} aria-label={t("components.datatable.actions.move_up")}
onClick={handleMoveSelectedUp} onClick={handleMoveSelectedUp}
@ -123,14 +124,16 @@ export function DataTableToolbar<TData>({
> >
<ArrowUpIcon aria-hidden="true" className="size-4" /> <ArrowUpIcon aria-hidden="true" className="size-4" />
</Button> </Button>
</TooltipTrigger> }
/>
<TooltipContent>{t("components.datatable.actions.move_up")}</TooltipContent> <TooltipContent>{t("components.datatable.actions.move_up")}</TooltipContent>
</Tooltip> </Tooltip>
)} )}
{!readOnly && meta?.bulkOps?.moveSelectedDown && ( {!readOnly && meta?.bulkOps?.moveSelectedDown && (
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger
render={
<Button <Button
aria-label={t("components.datatable.actions.move_down")} aria-label={t("components.datatable.actions.move_down")}
onClick={handleMoveSelectedDown} onClick={handleMoveSelectedDown}
@ -140,7 +143,8 @@ export function DataTableToolbar<TData>({
> >
<ArrowDownIcon aria-hidden="true" className="size-4" /> <ArrowDownIcon aria-hidden="true" className="size-4" />
</Button> </Button>
</TooltipTrigger> }
/>
<TooltipContent>{t("components.datatable.actions.move_down")}</TooltipContent> <TooltipContent>{t("components.datatable.actions.move_down")}</TooltipContent>
</Tooltip> </Tooltip>
)} )}
@ -164,12 +168,14 @@ export function DataTableToolbar<TData>({
<Separator className="h-6 mx-1 bg-muted/50" orientation="vertical" /> <Separator className="h-6 mx-1 bg-muted/50" orientation="vertical" />
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger
render={
<Button onClick={handleClearSelection} size="sm" type="button" variant="outline"> <Button onClick={handleClearSelection} size="sm" type="button" variant="outline">
<ScanIcon aria-hidden="true" className="size-4 mr-1" /> <ScanIcon aria-hidden="true" className="size-4 mr-1" />
<span>{t("components.datatable.actions.clear_selection")}</span> <span>{t("components.datatable.actions.clear_selection")}</span>
</Button> </Button>
</TooltipTrigger> }
/>
<TooltipContent>{t("components.datatable.actions.clear_selection")}</TooltipContent> <TooltipContent>{t("components.datatable.actions.clear_selection")}</TooltipContent>
</Tooltip> </Tooltip>
</> </>

View File

@ -5,6 +5,7 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
@ -18,7 +19,8 @@ export function DataTableViewOptions<TData>({ table }: { table: Table<TData> })
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger
render={
<Button <Button
className="ml-auto hidden h-8 lg:flex gap-2 items" className="ml-auto hidden h-8 lg:flex gap-2 items"
size="sm" size="sm"
@ -28,8 +30,11 @@ export function DataTableViewOptions<TData>({ table }: { table: Table<TData> })
<Settings2Icon /> <Settings2Icon />
{t("components.datatable_view_options.columns_button")} {t("components.datatable_view_options.columns_button")}
</Button> </Button>
</DropdownMenuTrigger> }
/>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuGroup>
<DropdownMenuLabel> <DropdownMenuLabel>
{t("components.datatable_view_options.toggle_columns")} {t("components.datatable_view_options.toggle_columns")}
</DropdownMenuLabel> </DropdownMenuLabel>
@ -48,6 +53,7 @@ export function DataTableViewOptions<TData>({ table }: { table: Table<TData> })
</DropdownMenuCheckboxItem> </DropdownMenuCheckboxItem>
); );
})} })}
</DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); );

View File

@ -1,13 +1,6 @@
"use client"; "use client";
import { import {
Button,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
TableBody, TableBody,
TableCell, TableCell,
Table as TableComp, Table as TableComp,
@ -80,7 +73,6 @@ export interface DataTableProps<TData, TValue> {
enablePagination?: boolean; enablePagination?: boolean;
pageSize?: number; pageSize?: number;
enableRowSelection?: boolean; enableRowSelection?: boolean;
EditorComponent?: React.ComponentType<{ row: TData; index: number; onClose: () => void }>;
getRowId?: (originalRow: TData, index: number, parent?: Row<TData>) => string; getRowId?: (originalRow: TData, index: number, parent?: Row<TData>) => string;
@ -105,7 +97,6 @@ export function DataTable<TData, TValue>({
enablePagination = true, enablePagination = true,
pageSize = 10, pageSize = 10,
enableRowSelection = false, enableRowSelection = false,
EditorComponent,
getRowId, getRowId,
@ -125,7 +116,6 @@ export function DataTable<TData, TValue>({
React.useState<VisibilityState>(inititalcolumnVisibility); React.useState<VisibilityState>(inititalcolumnVisibility);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]); const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [colSizes, setColSizes] = React.useState<ColumnSizingState>({}); const [colSizes, setColSizes] = React.useState<ColumnSizingState>({});
const [editIndex, setEditIndex] = React.useState<number | null>(null);
// Configuración TanStack // Configuración TanStack
const table = useReactTable({ const table = useReactTable({
@ -139,7 +129,7 @@ export function DataTable<TData, TValue>({
getRowId ?? getRowId ??
((originalRow: TData, i: number) => { ((originalRow: TData, i: number) => {
const row = originalRow as { id?: string | number }; const row = originalRow as { id?: string | number };
return row.id !== undefined ? String(row.id) : String(i); return row.id === undefined ? String(i) : String(row.id);
}), }),
state: { state: {
@ -183,8 +173,6 @@ export function DataTable<TData, TValue>({
getFacetedUniqueValues: getFacetedUniqueValues(), getFacetedUniqueValues: getFacetedUniqueValues(),
}); });
const handleCloseEditor = React.useCallback(() => setEditIndex(null), []);
// Render principal // Render principal
return ( return (
<div className="transition-[max-height] duration-300 ease-in-out"> <div className="transition-[max-height] duration-300 ease-in-out">
@ -192,9 +180,9 @@ export function DataTable<TData, TValue>({
<DataTableToolbar showViewOptions={!readOnly} table={table} /> <DataTableToolbar showViewOptions={!readOnly} table={table} />
<div className="overflow-hidden rounded-md border"> <div className="overflow-hidden rounded-md border">
<TableComp className="w-full text-sm"> <TableComp>
{/* CABECERA */} {/* CABECERA */}
<TableHeader className="sticky top-0 z-10"> <TableHeader>
{table.getHeaderGroups().map((hg) => ( {table.getHeaderGroups().map((hg) => (
<TableRow key={hg.id}> <TableRow key={hg.id}>
{hg.headers.map((h) => { {hg.headers.map((h) => {
@ -230,9 +218,6 @@ export function DataTable<TData, TValue>({
data-state={row.getIsSelected() && "selected"} data-state={row.getIsSelected() && "selected"}
key={row.id} key={row.id}
onClick={(e) => onRowClick?.(row.original, rowIndex, e)} onClick={(e) => onRowClick?.(row.original, rowIndex, e)}
onDoubleClick={
readOnly || onRowClick ? undefined : () => setEditIndex(rowIndex)
}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") if (e.key === "Enter" || e.key === " ")
onRowClick?.(row.original, rowIndex, e as any); onRowClick?.(row.original, rowIndex, e as any);
@ -284,32 +269,6 @@ export function DataTable<TData, TValue>({
)} )}
</TableComp> </TableComp>
</div> </div>
{/* Editor modal */}
{EditorComponent && editIndex !== null && (
<Dialog onOpenChange={handleCloseEditor} open>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>{t("components.datatable.editor.title")}</DialogTitle>
<DialogDescription>{t("components.datatable.editor.subtitle")}</DialogDescription>
</DialogHeader>
<div className="mt-4">
<EditorComponent
index={editIndex}
onClose={handleCloseEditor}
row={data[editIndex]!}
/>
</div>
<DialogFooter>
<Button onClick={handleCloseEditor} type="button" variant="secondary">
{t("common.close")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</div> </div>
</div> </div>
); );

View File

@ -8,7 +8,8 @@ import {
} from "@repo/shadcn-ui/components"; } from "@repo/shadcn-ui/components";
// SkeletonTable.tsx // SkeletonTable.tsx
import * as React from "react"; import * as React from "react";
import { SkeletonDataTableFooter } from './skeleton-data-table-footer.tsx';
import { SkeletonDataTableFooter } from "./skeleton-data-table-footer.tsx";
export type SkeletonTableProps = { export type SkeletonTableProps = {
columns?: number; columns?: number;
@ -16,7 +17,7 @@ export type SkeletonTableProps = {
stickyHeader?: boolean; stickyHeader?: boolean;
showFooter?: boolean; showFooter?: boolean;
footerProps?: { pageIndex?: number; pageSize?: number; totalItems?: number }; footerProps?: { pageIndex?: number; pageSize?: number; totalItems?: number };
} };
// Componente Skeleton de tabla genérico // Componente Skeleton de tabla genérico
export const SkeletonDataTable = ({ export const SkeletonDataTable = ({
@ -31,7 +32,7 @@ export const SkeletonDataTable = ({
const rws = React.useMemo(() => Array.from({ length: rows }), [rows]); const rws = React.useMemo(() => Array.from({ length: rows }), [rows]);
return ( return (
<div className="overflow-hidden rounded-md border bg-background" role="status" aria-busy="true"> <div aria-busy="true" className="overflow-hidden rounded-md border bg-background" role="status">
<TableComp className="w-full text-sm"> <TableComp className="w-full text-sm">
<TableHeader className={stickyHeader ? "sticky top-0 z-10 bg-muted" : ""}> <TableHeader className={stickyHeader ? "sticky top-0 z-10 bg-muted" : ""}>
<TableRow> <TableRow>
@ -47,7 +48,7 @@ export const SkeletonDataTable = ({
{rws.map((_, r) => ( {rws.map((_, r) => (
<TableRow key={`sk-tr-${r}`}> <TableRow key={`sk-tr-${r}`}>
{cols.map((_, c) => ( {cols.map((_, c) => (
<TableCell key={`sk-td-${r}-${c}`} className="align-top"> <TableCell className="align-top" key={`sk-td-${r}-${c}`}>
<div <div
className={[ className={[
"h-4 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none", "h-4 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none",
@ -71,4 +72,4 @@ export const SkeletonDataTable = ({
<span className="sr-only">Loading table</span> <span className="sr-only">Loading table</span>
</div> </div>
); );
} };

View File

@ -2,7 +2,6 @@ import {
Avatar, Avatar,
AvatarFallback, AvatarFallback,
AvatarImage, AvatarImage,
Button,
Separator, Separator,
SidebarTrigger, SidebarTrigger,
} from "@repo/shadcn-ui/components"; } from "@repo/shadcn-ui/components";
@ -22,12 +21,10 @@ export function AppTopbar() {
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<ProfileDropdown <ProfileDropdown
trigger={ trigger={
<Button className="size-9.5" size="icon" variant="ghost">
<Avatar className="size-9.5 rounded-md"> <Avatar className="size-9.5 rounded-md">
<AvatarImage src="https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png" /> <AvatarImage src="https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png" />
<AvatarFallback>JD</AvatarFallback> <AvatarFallback>JD</AvatarFallback>
</Avatar> </Avatar>
</Button>
} }
/> />
</div> </div>

View File

@ -3,7 +3,6 @@ export const LogoVerifactu = ({ color = "black", width = "100%", ...props }) =>
<div {...props}> <div {...props}>
<svg <svg
{...props} {...props}
height="auto"
preserveAspectRatio="xMidYMid meet" preserveAspectRatio="xMidYMid meet"
style={{ style={{
color, color,