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,
pageSize,
order: "desc",
orderBy: "invoice_date",
filters: status === "all" ? [] : [{ field: "status", operator: "eq", value: status }],
}),
[debouncedSearch, pageIndex, pageSize, status]
);
const query = useIssuedInvoiceListQuery({ criteria });
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 {
data: query.data ?? EMPTY_ISSUED_INVOICES_LIST, q: debouncedQ || "",
isLoading: query.isLoading, pageSize,
isFetching: query.isFetching, pageNumber: pageIndex,
order: "desc",
orderBy: "invoice_date",
filters: baseFilters,
};
}, [pageSize, pageIndex, debouncedQ, status]);
isError: query.isError, const query = useIssuedInvoiceListQuery({ criteria });
error: query.error, const data = useMemo(
() => (query.data ? IssuedInvoiceSummaryDtoAdapter.fromDto(query.data) : undefined),
[query.data]
);
refetch: query.refetch, const setSearchValue = (value: string) => setSearch(value.trim().replace(/\s+/g, " "));
const setStatusFilter = (newStatus: string) => setStatus(newStatus);
return {
...query,
data,
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,17 +90,21 @@ 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: totalItems ?? 0 }} footerProps={{ pageIndex, pageSize, totalItems: total_items ?? 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}
@ -104,10 +113,13 @@ export const IssuedInvoicesGrid = ({
manualPagination manualPagination
onPageChange={onPageChange} onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange} onPageSizeChange={onPageSizeChange}
//onRowClick={(row) => onRowClick?.(row.id)} //onRowClick={handleRowClick}
pageIndex={pageIndex} pageIndex={pageIndex}
pageSize={pageSize} pageSize={pageSize}
totalItems={totalItems} 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,8 +343,7 @@ 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}
@ -363,8 +361,7 @@ 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>
@ -372,8 +369,7 @@ 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"
@ -385,10 +381,8 @@ 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)}
@ -400,7 +394,6 @@ 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

@ -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,8 +21,7 @@ export const VerifactuStatusBadge = ({ status, className }: VerifactuStatusBadge
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger>
render={
<Badge <Badge
className={cn( className={cn(
getVerifactuRecordStatusColor(normalizedStatus), getVerifactuRecordStatusColor(normalizedStatus),
@ -36,8 +35,7 @@ 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 "./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,8 +101,7 @@ 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"
@ -113,8 +112,7 @@ 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>
@ -237,8 +235,7 @@ 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)}
@ -248,8 +245,7 @@ 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>
@ -259,8 +255,7 @@ 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={() =>
@ -272,8 +267,7 @@ 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>
@ -285,8 +279,7 @@ 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)}
@ -295,8 +288,7 @@ 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>
@ -306,8 +298,7 @@ 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) => {
@ -319,8 +310,7 @@ 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 +1,2 @@
export * from "./initials";
export * from "./proforma-status-badge"; export * from "./proforma-status-badge";

View File

@ -21,8 +21,7 @@ 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)}
@ -30,8 +29,7 @@ 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 ?? 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,16 +152,13 @@ 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 />
@ -201,7 +197,6 @@ 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,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,8 +31,7 @@ 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"
@ -50,10 +48,8 @@ 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")}
@ -67,7 +63,6 @@ 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,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,8 +113,7 @@ 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}
@ -124,16 +123,14 @@ 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}
@ -143,8 +140,7 @@ 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>
)} )}
@ -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,8 +18,7 @@ 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"
@ -30,11 +28,8 @@ 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>
@ -53,7 +48,6 @@ export function DataTableViewOptions<TData>({ table }: { table: Table<TData> })
</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

@ -8,8 +8,7 @@ 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;
@ -17,7 +16,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 = ({
@ -32,7 +31,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 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>
@ -48,7 +47,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 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",
@ -72,4 +71,4 @@ export const SkeletonDataTable = ({
<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={
<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,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,