Compare commits

..

No commits in common. "430ae8e5eabf737613e3fcd3113437a9c88bf012" and "71b9e857e72fbf552276e6817629b868a9780c2f" have entirely different histories.

52 changed files with 786 additions and 988 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/ui").then((m) => ({ default: m.ProformaLayout })) import("./proformas/shared").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/ui").then((m) => ({ default: m.IssuedInvoicesLayout })) import("./issued-invoices/shared").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 "../../shared"; import { useDownloadInvoicePDFQuery } from "../hooks";
interface PendingDownload { interface PendingDownload {
id: string; id: string;

View File

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

View File

@ -0,0 +1,71 @@
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

@ -0,0 +1,18 @@
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

@ -0,0 +1 @@
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 { IssuedInvoiceListRow } from "../../shared"; import type { IssuedInvoiceSummaryData } from "../../types";
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: IssuedInvoiceListRow) => { (issuedInvoice: IssuedInvoiceSummaryData) => {
downloadPDFCtrl.download(issuedInvoice.id, issuedInvoice.invoiceNumber); downloadPDFCtrl.download(issuedInvoice.id, issuedInvoice.invoice_number);
}, },
[downloadPDFCtrl] [downloadPDFCtrl]
); );

View File

@ -1,101 +1,51 @@
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 type { import { IssuedInvoiceSummaryDtoAdapter } from "../adapters";
IssuedInvoiceList, import { useIssuedInvoiceListQuery } from "../hooks";
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(5); const [pageSize, setPageSize] = useState(10);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState<IssuedInvoiceListStatusFilter>("all"); const [status, setStatus] = useState("all");
const debouncedSearch = useDebounce(search, 300); const debouncedQ = useDebounce(search, 300);
const criteria = useMemo<NonNullable<ListIssuedInvoicesByCriteriaParams["criteria"]>>( const criteria = useMemo<CriteriaDTO>(() => {
() => ({ const baseFilters =
q: debouncedSearch || "", status !== "all" ? [{ field: "status", operator: "CONTAINS", value: status }] : [];
pageNumber: pageIndex,
return {
q: debouncedQ || "",
pageSize, pageSize,
pageNumber: pageIndex,
order: "desc", order: "desc",
orderBy: "invoice_date", orderBy: "invoice_date",
filters: status === "all" ? [] : [{ field: "status", operator: "eq", value: status }], filters: baseFilters,
}), };
[debouncedSearch, pageIndex, pageSize, status] }, [pageSize, pageIndex, debouncedQ, status]);
);
const query = useIssuedInvoiceListQuery({ criteria }); const query = useIssuedInvoiceListQuery({ criteria });
const data = useMemo(
() => (query.data ? IssuedInvoiceSummaryDtoAdapter.fromDto(query.data) : undefined),
[query.data]
);
const setStatusFilterValue = (value: string) => { const setSearchValue = (value: string) => setSearch(value.trim().replace(/\s+/g, " "));
const nextValue = (value || "all") as IssuedInvoiceListStatusFilter;
setStatusFilter((prev) => { const setStatusFilter = (newStatus: string) => setStatus(newStatus);
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 {
data: query.data ?? EMPTY_ISSUED_INVOICES_LIST, ...query,
isLoading: query.isLoading, data,
isFetching: query.isFetching,
isError: query.isError,
error: query.error,
refetch: query.refetch,
pageIndex, pageIndex,
pageSize, pageSize,
setPageIndex,
setPageSize: setPageSizeValue,
search, search,
setPageIndex,
setPageSize,
setSearchValue, setSearchValue,
setStatusFilter,
statusFilter,
setStatusFilter: setStatusFilterValue,
}; };
}; };

View File

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

View File

@ -5,7 +5,6 @@ import {
ButtonGroup, ButtonGroup,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
Spinner, Spinner,
@ -19,21 +18,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 { IssuedInvoiceListRow } from "../../../../shared"; import type { IssuedInvoiceSummaryData } from "../../../../types";
import { VerifactuStatusBadge } from "../../components"; import { VerifactuStatusBadge } from "../../components";
type GridActionHandlers = { type GridActionHandlers = {
onDownloadPdf?: (issuedInvoice: IssuedInvoiceListRow) => void; onDownloadPdf?: (issuedInvoice: IssuedInvoiceSummaryData) => void;
pdfDownloadingId?: string | null; pdfDownloadingId?: string | null;
isPdfDownloading?: boolean; isPdfDownloading?: boolean;
}; };
export function useIssuedInvoicesGridColumns( export function useIssuedInvoicesGridColumns(
actionHandlers: GridActionHandlers = {} actionHandlers: GridActionHandlers = {}
): ColumnDef<IssuedInvoiceListRow, unknown>[] { ): ColumnDef<IssuedInvoiceSummaryData, unknown>[] {
const { t } = useTranslation(); const { t } = useTranslation();
return React.useMemo<ColumnDef<IssuedInvoiceListRow, unknown>[]>( return React.useMemo<ColumnDef<IssuedInvoiceSummaryData>[]>(
() => [ () => [
// Nº // Nº
{ {
@ -48,7 +47,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.invoiceNumber} {row.original.invoice_number}
</div> </div>
), ),
enableHiding: false, enableHiding: false,
@ -107,13 +106,11 @@ 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>
@ -195,7 +192,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.invoiceDate)} {formatDate(row.original.invoice_date)}
</div> </div>
), ),
enableSorting: false, enableSorting: false,
@ -217,7 +214,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.operationDate)} {formatDate(row.original.operation_date)}
</div> </div>
), ),
enableSorting: false, enableSorting: false,
@ -240,7 +237,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.subtotalAmountFmt} {row.original.subtotal_amount_fmt}
</div> </div>
), ),
enableSorting: false, enableSorting: false,
@ -263,7 +260,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.totalDiscountAmountFmt} {row.original.discount_amount_fmt}
</div> </div>
), ),
enableSorting: false, enableSorting: false,
@ -285,7 +282,7 @@ export function useIssuedInvoicesGridColumns(
/> />
), ),
cell: ({ row }) => ( cell: ({ row }) => (
<div className="font-medium text-right tabular-nums">{row.original.taxesAmountFmt}</div> <div className="font-medium text-right tabular-nums">{row.original.taxes_amount_fmt}</div>
), ),
enableSorting: false, enableSorting: false,
size: 120, size: 120,
@ -306,7 +303,9 @@ export function useIssuedInvoicesGridColumns(
/> />
), ),
cell: ({ row }) => ( cell: ({ row }) => (
<div className="font-semibold text-right tabular-nums">{row.original.totalAmountFmt}</div> <div className="font-semibold text-right tabular-nums">
{row.original.total_amount_fmt}
</div>
), ),
enableSorting: false, enableSorting: false,
size: 140, size: 140,
@ -344,27 +343,25 @@ 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} onClick={(e) => {
onClick={(e) => { stop(e);
stop(e); isCompleted ? actionHandlers.onDownloadPdf?.(issuedInvoice) : null;
isCompleted ? actionHandlers.onDownloadPdf?.(issuedInvoice) : null; }}
}} size="icon"
size="icon" variant="ghost"
variant="ghost" >
> {isPDFLoading ? (
{isPDFLoading ? ( <Spinner className="size-4 cursor-progress" />
<Spinner className="size-4 cursor-progress" /> ) : (
) : ( <FileDownIcon className="size-4 cursor-pointer" />
<FileDownIcon className="size-4 cursor-pointer" /> )}
)} <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>
@ -372,35 +369,31 @@ 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" onClick={stop}
onClick={stop} size="sm"
size="sm" type="button"
type="button" variant="ghost"
variant="ghost" >
> <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)} >
> <DownloadIcon className="mr-2 size-4" />
<DownloadIcon className="mr-2 size-4" /> {t("common.download_pdf")}
{t("common.download_pdf")} </DropdownMenuItem>
</DropdownMenuItem> <DropdownMenuItem className="cursor-pointer" onClick={stop}>
<DropdownMenuItem className="cursor-pointer" onClick={stop}> <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

@ -1,2 +1 @@
export * from "./issued-invoices-grid-empty";
export * from "./verifactu-status-badge"; export * from "./verifactu-status-badge";

View File

@ -1,17 +0,0 @@
import { Card, CardContent } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { ChartNoAxesColumnIncreasingIcon } from "lucide-react";
export const IssuedInvoicesEmptyGrid = ({ className }: React.ComponentProps<"div">) => {
return (
<Card className={cn("w-full max-w-lg", className)}>
<CardContent>
<div className="rounded-md border border-dashed p-6 text-center">
<ChartNoAxesColumnIncreasingIcon className="text-muted-foreground mx-auto size-12" />
<p className="mt-2 text-sm font-medium">No data to show</p>
<p className="text-muted-foreground mt-1 text-sm">May take 15 minutes for data to load</p>
</div>
</CardContent>
</Card>
);
};

View File

@ -7,7 +7,7 @@ import {
getVerifactuRecordStatusButtonVariant, getVerifactuRecordStatusButtonVariant,
getVerifactuRecordStatusColor, getVerifactuRecordStatusColor,
getVerifactuRecordStatusIcon, getVerifactuRecordStatusIcon,
} from "../../../shared"; } from "../../../types";
export type VerifactuStatusBadgeProps = { export type VerifactuStatusBadgeProps = {
status: string | VerifactuRecordStatus; // permitir cualquier valor status: string | VerifactuRecordStatus; // permitir cualquier valor
@ -21,23 +21,21 @@ export const VerifactuStatusBadge = ({ status, className }: VerifactuStatusBadge
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger>
render={ <Badge
<Badge className={cn(
className={cn( getVerifactuRecordStatusColor(normalizedStatus),
getVerifactuRecordStatusColor(normalizedStatus), "font-semibold",
"font-semibold", className
className )}
)} variant={getVerifactuRecordStatusButtonVariant(normalizedStatus)}
variant={getVerifactuRecordStatusButtonVariant(normalizedStatus)} >
> <Icon />
<Icon /> {t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.label`, {
{t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.label`, { 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 "./list-issued-invoices-page"; export * from "./issued-invoice-list-page";

View File

@ -0,0 +1,145 @@
import { ErrorAlert, PageHeader, SimpleSearchInput } from "@erp/core/components";
import { AppContent, AppHeader, BackHistoryButton, LogoVerifactu } from "@repo/rdx-ui/components";
import {
Alert,
AlertDescription,
AlertTitle,
Button,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@repo/shadcn-ui/components";
import { FilterIcon, PlusIcon } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "../../../../i18n";
import { useIssuedInvoiceListPageController } from "../../controllers";
import { IssuedInvoicesGrid, useIssuedInvoicesGridColumns } from "../blocks";
export const IssuedInvoiceListPage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const {
listCtrl,
handleDownloadPDF,
isPDFDownloading: isPdfDownloading,
pdfDownloadingId,
} = useIssuedInvoiceListPageController();
const columns = useIssuedInvoicesGridColumns({
onDownloadPdf: handleDownloadPDF,
pdfDownloadingId,
isPdfDownloading,
//onSendEmail: () => null,
});
// Hook con Sheet de shadcn
/*const preview = usePinnedPreviewSheet<IssuedInvoiceSummaryPageData>({
persistKey: "invoice-preview-pin",
widthClass: "w-[500px]",
});*/
if (listCtrl.isError || !listCtrl.data) {
return (
<AppContent>
<ErrorAlert
message={(listCtrl.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")}
className="hidden"
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>
<Alert className="bg-green-50 text-green-800 flex items-center justify-between gap-6">
<div>
<AlertTitle className="font-semibold text-green-800">¡Atención!</AlertTitle>
<AlertDescription className="text-green-800">
Las facturas de esta pantalla son de solo lectura. Se generan automáticamente al
emitir una proforma aprobada y no se pueden modificar ni eliminar. Solo puedes
descargar el PDF de cada factura.
</AlertDescription>
</div>
<LogoVerifactu className="text-green-800 w-50" color="" />
</Alert>
{/* Search and filters */}
<div className="flex items-center justify-between gap-16">
<SimpleSearchInput
loading={listCtrl.isLoading}
onSearchChange={listCtrl.setSearchValue}
/>
<Select defaultValue="all" onValueChange={listCtrl.setStatusFilter}>
<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.issued_invoices.status.all.label")}</SelectItem>
<SelectItem value="Correcto">
{t("catalog.issued_invoices.status.correcto.label")}
</SelectItem>
<SelectItem value="Pendiente">
{t("catalog.issued_invoices.status.pendiente.label")}
</SelectItem>
<SelectItem value="aceptado_con_error">
{t("catalog.issued_invoices.status.aceptado_con_error.label")}
</SelectItem>
<SelectItem value="incorrecto">
{t("catalog.issued_invoices.status.incorrecto.label")}
</SelectItem>
<SelectItem value="duplicado">
{t("catalog.issued_invoices.status.duplicado.label")}
</SelectItem>
<SelectItem value="anulado">
{t("catalog.issued_invoices.status.anulado.label")}
</SelectItem>
<SelectItem value="factura_inexistente">
{t("catalog.issued_invoices.status.duplicado.label")}
</SelectItem>
<SelectItem value="rechazado">
{t("catalog.issued_invoices.status.rechazado.label")}
</SelectItem>
</SelectContent>
</Select>
</div>
<IssuedInvoicesGrid
columns={columns}
data={listCtrl.data}
loading={listCtrl.isLoading}
onPageChange={listCtrl.setPageIndex}
onPageSizeChange={listCtrl.setPageSize}
// acciones rápidas del grid → page controller
//onRowClick={(id) => navigate(`/issuedInvoices/${id}`)}
pageIndex={listCtrl.pageIndex}
pageSize={listCtrl.pageSize}
/>
</AppContent>
</>
);
};

View File

@ -1,160 +0,0 @@
import { ErrorAlert, PageHeader, SimpleSearchInput } from "@erp/core/components";
import { AppContent, AppHeader, BackHistoryButton, LogoVerifactu } from "@repo/rdx-ui/components";
import {
Alert,
AlertDescription,
AlertTitle,
Button,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@repo/shadcn-ui/components";
import { FilterIcon, PlusIcon } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "../../../../i18n";
import { useIssuedInvoiceListPageController } from "../../controllers";
import { IssuedInvoicesGrid, useIssuedInvoicesGridColumns } from "../blocks";
import { IssuedInvoicesEmptyGrid } from "../components";
export const ListIssuedInvoicesPage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const {
listCtrl,
handleDownloadPDF,
isPDFDownloading: isPdfDownloading,
pdfDownloadingId,
} = useIssuedInvoiceListPageController();
const columns = useIssuedInvoicesGridColumns({
onDownloadPdf: handleDownloadPDF,
pdfDownloadingId,
isPdfDownloading,
//onSendEmail: () => null,
});
// Hook con Sheet de shadcn
/*const preview = usePinnedPreviewSheet<IssuedInvoiceSummaryPageData>({
persistKey: "invoice-preview-pin",
widthClass: "w-[500px]",
});*/
if (listCtrl.isError) {
return (
<AppContent>
<ErrorAlert
message={(listCtrl.error as Error)?.message || "Error al cargar el listado"}
title={t("pages.issued_invoices.list.loadErrorTitle")}
/>
<BackHistoryButton />
</AppContent>
);
}
return (
<section>
<AppHeader>
<PageHeader
description={t("pages.issued_invoices.list.description")}
rightSlot={
<Button
aria-label={t("pages.issued_invoices.create.title")}
className="hidden"
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 className="space-y-6">
<Alert className="bg-green-50 text-green-800 flex items-center justify-between gap-6">
<div>
<AlertTitle className="font-semibold text-green-800">¡Atención!</AlertTitle>
<AlertDescription className="text-green-800">
Las facturas de esta pantalla son de solo lectura. Se generan automáticamente al
emitir una proforma aprobada y no se pueden modificar ni eliminar. Solo puedes
descargar el PDF de cada factura.
</AlertDescription>
</div>
<LogoVerifactu className="text-green-800 w-50" color="" />
</Alert>
{/* Search and filters */}
{listCtrl.data.totalItems === 0 && <IssuedInvoicesEmptyGrid className="mx-auto" />}
{listCtrl.data.totalItems !== 0 && (
<>
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<SimpleSearchInput
loading={listCtrl.isLoading}
onSearchChange={listCtrl.setSearchValue}
value={listCtrl.search}
/>
<Select
onValueChange={(value) => listCtrl.setStatusFilter(value ?? "all")}
value={listCtrl.statusFilter}
>
<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.issued_invoices.status.all.label")}
</SelectItem>
<SelectItem value="Correcto">
{t("catalog.issued_invoices.status.correcto.label")}
</SelectItem>
<SelectItem value="Pendiente">
{t("catalog.issued_invoices.status.pendiente.label")}
</SelectItem>
<SelectItem value="aceptado_con_error">
{t("catalog.issued_invoices.status.aceptado_con_error.label")}
</SelectItem>
<SelectItem value="incorrecto">
{t("catalog.issued_invoices.status.incorrecto.label")}
</SelectItem>
<SelectItem value="duplicado">
{t("catalog.issued_invoices.status.duplicado.label")}
</SelectItem>
<SelectItem value="anulado">
{t("catalog.issued_invoices.status.anulado.label")}
</SelectItem>
<SelectItem value="factura_inexistente">
{t("catalog.issued_invoices.status.duplicado.label")}
</SelectItem>
<SelectItem value="rechazado">
{t("catalog.issued_invoices.status.rechazado.label")}
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="min-h-0 flex-1 overflow-auto">
<IssuedInvoicesGrid
columns={columns}
data={listCtrl.data}
loading={listCtrl.isLoading}
onPageChange={listCtrl.setPageIndex}
onPageSizeChange={listCtrl.setPageSize}
// acciones rápidas del grid → page controller
//onRowClick={(id) => navigate(`/issuedInvoices/${id}`)}
pageIndex={listCtrl.pageIndex}
pageSize={listCtrl.pageSize}
/>
</div>
</>
)}
</AppContent>
</section>
);
};

View File

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

View File

@ -1,101 +0,0 @@
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

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

View File

@ -1,32 +0,0 @@
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

@ -1,4 +0,0 @@
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

@ -1,47 +0,0 @@
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

@ -1,14 +0,0 @@
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

@ -1,18 +0,0 @@
/**
* 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

@ -1,26 +0,0 @@
/**
* 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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,25 @@
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

@ -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"
>;

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 className="space-y-4">{children}</div>; return <div>{children}</div>;
}; };

View File

@ -101,20 +101,18 @@ 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" variant="ghost"
variant="ghost" >
> <a href={`/facturas/${invoiceId}`}>
<a href={`/facturas/${invoiceId}`}> <ExternalLinkIcon />
<ExternalLinkIcon /> <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>
@ -237,19 +235,17 @@ 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)} size="icon"
size="icon" variant="ghost"
variant="ghost" >
> <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>
@ -259,21 +255,19 @@ 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={() => actionHandlers.onChangeStatusClick?.(proforma, availableTransitions[0])
actionHandlers.onChangeStatusClick?.(proforma, availableTransitions[0]) }
} size="icon"
size="icon" variant="ghost"
variant="ghost" >
> <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>
@ -285,18 +279,16 @@ 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)} size="icon"
size="icon" variant="ghost"
variant="ghost" >
> <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>
@ -306,21 +298,19 @@ 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) => { e.preventDefault();
e.preventDefault(); actionHandlers.onDeleteClick?.(proforma);
actionHandlers.onDeleteClick?.(proforma); }}
}} size="icon"
size="icon" variant="ghost"
variant="ghost" >
> <Trash2Icon className="size-4" />
<Trash2Icon className="size-4" /> </Button>
</Button> </TooltipTrigger>
}
/>
<TooltipContent>Eliminar</TooltipContent> <TooltipContent>Eliminar</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>

View File

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

View File

@ -21,17 +21,15 @@ 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)} >
> <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 ?? 5, pageSize: criteria?.pageSize ?? 10,
q: criteria?.q ?? "", q: criteria?.q ?? "",
filters: criteria?.filters ?? [], filters: criteria?.filters ?? [],
orderBy: criteria?.orderBy ?? "", orderBy: criteria?.orderBy ?? "",

View File

@ -3,21 +3,21 @@ import { UserSearchIcon } from "lucide-react";
export const CustomerEmptyCard = (props: React.ComponentProps<"button">) => { export const CustomerEmptyCard = (props: React.ComponentProps<"button">) => {
return ( return (
<button <button
aria-label="Seleccionar cliente"
className="group w-full cursor-pointer rounded-lg border border-border bg-card p-4 transition hover:bg-accent/50 hover:border-primary"
tabIndex={0} tabIndex={0}
type="button" type='button'
className='group w-full cursor-pointer rounded-lg border border-border bg-card p-4 transition hover:bg-accent/50 hover:border-primary'
aria-label='Seleccionar cliente'
{...props} {...props}
> >
<div className="flex items-center gap-4 group-hover:text-primary"> <div className='flex items-center gap-4 group-hover:text-primary'>
<div className="flex size-12 items-center justify-center rounded-full bg-muted group-hover:bg-primary/15"> <div className='flex size-12 items-center justify-center rounded-full bg-muted group-hover:bg-primary/15'>
<UserSearchIcon className="size-6 text-muted-foreground group-hover:text-primary" /> <UserSearchIcon className='size-6 text-muted-foreground group-hover:text-primary' />
</div> </div>
<div className="flex-1 text-left"> <div className='flex-1 text-left'>
<h3 className="font-medium text-muted-foreground mb-1 group-hover:text-primary"> <h3 className='font-medium text-muted-foreground mb-1 group-hover:text-primary'>
Seleccionar Cliente Seleccionar Cliente
</h3> </h3>
<p className="text-sm text-muted-foreground/70 group-hover:text-primary"> <p className='text-sm text-muted-foreground/70 group-hover:text-primary'>
Haz clic para buscar un cliente existente o crear uno nuevo Haz clic para buscar un cliente existente o crear uno nuevo
</p> </p>
</div> </div>

View File

@ -5,7 +5,6 @@ import {
Button, Button,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
@ -153,55 +152,51 @@ 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 />
<DropdownMenuItem onClick={() => onViewClick?.(customer)}> <DropdownMenuItem onClick={() => onViewClick?.(customer)}>
{t("pages.list.actions.view")} {t("pages.list.actions.view")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => onEditClick?.(customer)}> <DropdownMenuItem onClick={() => onEditClick?.(customer)}>
{t("pages.list.actions.edit")} {t("pages.list.actions.edit")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
disabled={!website} disabled={!website}
onClick={() => onClick={() =>
window.open(safeHTTPUrl(website), "_blank", "noopener,noreferrer") window.open(safeHTTPUrl(website), "_blank", "noopener,noreferrer")
} }
> >
{t("pages.list.actions.visit_website")} {t("pages.list.actions.visit_website")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
disabled={!email_primary} disabled={!email_primary}
onClick={() => navigator.clipboard.writeText(email_primary)} onClick={() => navigator.clipboard.writeText(email_primary)}
> >
{t("pages.list.actions.copy_email")} {t("pages.list.actions.copy_email")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
className="text-destructive" className="text-destructive"
onClick={() => onDeleteClick?.(customer)} onClick={() => onDeleteClick?.(customer)}
> >
{t("pages.list.actions.delete")} {t("pages.list.actions.delete")}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>

View File

@ -30,13 +30,11 @@ 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,7 +2,6 @@ import {
Button, Button,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
@ -32,42 +31,38 @@ 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" size="sm"
size="sm" type="button"
type="button" variant="ghost"
variant="ghost" >
> <span>{title}</span>
<span>{title}</span> {column.getIsSorted() === "desc" ? (
{column.getIsSorted() === "desc" ? (
<ArrowDownIcon />
) : column.getIsSorted() === "asc" ? (
<ArrowUpIcon />
) : (
<ChevronsUpDownIcon />
)}
</Button>
}
/>
<DropdownMenuContent align="start">
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUpIcon />
{t("components.datatable.asc")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<ArrowDownIcon /> <ArrowDownIcon />
{t("components.datatable.desc")} ) : column.getIsSorted() === "asc" ? (
</DropdownMenuItem> <ArrowUpIcon />
<DropdownMenuSeparator /> ) : (
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}> <ChevronsUpDownIcon />
<EyeOffIcon /> )}
{t("components.datatable.hide")} </Button>
</DropdownMenuItem> </DropdownMenuTrigger>
</DropdownMenuGroup> <DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUpIcon />
{t("components.datatable.asc")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<ArrowDownIcon />
{t("components.datatable.desc")}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeOffIcon />
{t("components.datatable.hide")}
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>

View File

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

View File

@ -113,38 +113,34 @@ 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} size="sm"
size="sm" type="button"
type="button" variant="outline"
variant="outline" >
> <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} size="sm"
size="sm" type="button"
type="button" variant="outline"
variant="outline" >
> <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>
)} )}
@ -168,14 +164,12 @@ 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,7 +5,6 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
@ -19,41 +18,36 @@ 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" type="button"
type="button" variant="outline"
variant="outline" >
> <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> <DropdownMenuSeparator />
<DropdownMenuSeparator /> {table
{table .getAllColumns()
.getAllColumns() .filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide())
.filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide()) .map((column) => {
.map((column) => { return (
return ( <DropdownMenuCheckboxItem
<DropdownMenuCheckboxItem checked={column.getIsVisible()}
checked={column.getIsVisible()} key={column.id}
key={column.id} onCheckedChange={(value) => column.toggleVisibility(!!value)}
onCheckedChange={(value) => column.toggleVisibility(!!value)} >
> {getColumnLabel(column)}
{getColumnLabel(column)} </DropdownMenuCheckboxItem>
</DropdownMenuCheckboxItem> );
); })}
})}
</DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); );

View File

@ -1,6 +1,13 @@
"use client"; "use client";
import { import {
Button,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
TableBody, TableBody,
TableCell, TableCell,
Table as TableComp, Table as TableComp,
@ -73,6 +80,7 @@ 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;
@ -97,6 +105,7 @@ export function DataTable<TData, TValue>({
enablePagination = true, enablePagination = true,
pageSize = 10, pageSize = 10,
enableRowSelection = false, enableRowSelection = false,
EditorComponent,
getRowId, getRowId,
@ -116,6 +125,7 @@ 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({
@ -129,7 +139,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(i) : String(row.id); return row.id !== undefined ? String(row.id) : String(i);
}), }),
state: { state: {
@ -173,6 +183,8 @@ 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">
@ -180,9 +192,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> <TableComp className="w-full text-sm">
{/* CABECERA */} {/* CABECERA */}
<TableHeader> <TableHeader className="sticky top-0 z-10">
{table.getHeaderGroups().map((hg) => ( {table.getHeaderGroups().map((hg) => (
<TableRow key={hg.id}> <TableRow key={hg.id}>
{hg.headers.map((h) => { {hg.headers.map((h) => {
@ -218,6 +230,9 @@ 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);
@ -269,6 +284,32 @@ 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

@ -1,75 +1,74 @@
import { import {
TableBody, TableBody,
TableCell, TableCell,
Table as TableComp, Table as TableComp,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
} 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;
rows?: number; rows?: number;
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 = ({
columns = 6, columns = 6,
rows = 10, rows = 10,
stickyHeader = true, stickyHeader = true,
showFooter = true, showFooter = true,
footerProps, footerProps,
}: SkeletonTableProps) => { }: SkeletonTableProps) => {
// Genera arrays para mapear // Genera arrays para mapear
const cols = React.useMemo(() => Array.from({ length: columns }), [columns]); const cols = React.useMemo(() => Array.from({ length: columns }), [columns]);
const rws = React.useMemo(() => Array.from({ length: rows }), [rows]); const rws = React.useMemo(() => Array.from({ length: rows }), [rows]);
return ( return (
<div aria-busy="true" className="overflow-hidden rounded-md border bg-background" role="status"> <div className="overflow-hidden rounded-md border bg-background" role="status" aria-busy="true">
<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>
{cols.map((_, i) => ( {cols.map((_, i) => (
<TableHead key={`sk-th-${i}`}> <TableHead key={`sk-th-${i}`}>
<div className="h-4 w-24 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none" /> <div className="h-4 w-24 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none" />
</TableHead> </TableHead>
))} ))}
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{rws.map((_, r) => ( {rws.map((_, r) => (
<TableRow key={`sk-tr-${r}`}> <TableRow key={`sk-tr-${r}`}>
{cols.map((_, c) => ( {cols.map((_, c) => (
<TableCell className="align-top" key={`sk-td-${r}-${c}`}> <TableCell key={`sk-td-${r}-${c}`} className="align-top">
<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",
c === 0 ? "w-36" : "w-full", c === 0 ? "w-36" : "w-full",
"my-2", "my-2",
].join(" ")} ].join(" ")}
style={{ maxWidth: c % 3 === 0 ? "14rem" : c % 3 === 1 ? "10rem" : "100%" }} style={{ maxWidth: c % 3 === 0 ? "14rem" : c % 3 === 1 ? "10rem" : "100%" }}
/> />
{c > 0 && ( {c > 0 && (
<div className="mt-2 h-3 w-1/2 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none" /> <div className="mt-2 h-3 w-1/2 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none" />
)} )}
</TableCell> </TableCell>
))} ))}
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
</TableComp> </TableComp>
{showFooter && <SkeletonDataTableFooter {...footerProps} />} {showFooter && <SkeletonDataTableFooter {...footerProps} />}
<span className="sr-only">Loading table…</span> <span className="sr-only">Loading table…</span>
</div> </div>
); );
}; }

View File

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

View File

@ -3,6 +3,7 @@ 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,