.
This commit is contained in:
parent
71b9e857e7
commit
8a8282ddc3
@ -3,7 +3,7 @@ import { lazy } from "react";
|
||||
import { Outlet, type RouteObject } from "react-router-dom";
|
||||
|
||||
const ProformaLayout = lazy(() =>
|
||||
import("./proformas/shared").then((m) => ({ default: m.ProformaLayout }))
|
||||
import("./proformas/shared/ui").then((m) => ({ default: m.ProformaLayout }))
|
||||
);
|
||||
|
||||
const ProformasListPage = lazy(() =>
|
||||
@ -19,7 +19,7 @@ const ProformaUpdatePage = lazy(() =>
|
||||
);
|
||||
|
||||
const IssuedInvoicesLayout = lazy(() =>
|
||||
import("./issued-invoices/shared").then((m) => ({ default: m.IssuedInvoicesLayout }))
|
||||
import("./issued-invoices/shared/ui").then((m) => ({ default: m.IssuedInvoicesLayout }))
|
||||
);
|
||||
|
||||
const IssuedInvoiceListPage = lazy(() =>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
|
||||
import * as React from "react";
|
||||
|
||||
import { useDownloadInvoicePDFQuery } from "../hooks";
|
||||
import { useDownloadInvoicePDFQuery } from "../../shared";
|
||||
|
||||
interface PendingDownload {
|
||||
id: string;
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./issued-invoice-summary-dto.adapter";
|
||||
@ -1,71 +0,0 @@
|
||||
import { MoneyDTOHelper, formatCurrency } from "@erp/core";
|
||||
|
||||
import type {
|
||||
IssuedInvoiceSummaryData,
|
||||
IssuedInvoiceSummaryPage,
|
||||
IssuedInvoiceSummaryPageData,
|
||||
} from "../../types";
|
||||
|
||||
/**
|
||||
* Convierte el DTO completo de API a datos numéricos para el formulario.
|
||||
*/
|
||||
export const IssuedInvoiceSummaryDtoAdapter = {
|
||||
fromDto(pageDto: IssuedInvoiceSummaryPage, context?: unknown): IssuedInvoiceSummaryPageData {
|
||||
return {
|
||||
...pageDto,
|
||||
items: pageDto.items.map(
|
||||
(summaryDto) =>
|
||||
({
|
||||
...summaryDto,
|
||||
|
||||
subtotal_amount: MoneyDTOHelper.toNumber(summaryDto.subtotal_amount),
|
||||
subtotal_amount_fmt: formatCurrency(
|
||||
MoneyDTOHelper.toNumber(summaryDto.subtotal_amount),
|
||||
Number(summaryDto.total_amount.scale || 2),
|
||||
summaryDto.currency_code,
|
||||
summaryDto.language_code
|
||||
),
|
||||
|
||||
/*discount_percentage: PercentageDTOHelper.toNumber(summaryDto.discount_percentage),
|
||||
discount_percentage_fmt: PercentageDTOHelper.toNumericString(
|
||||
summaryDto.discount_percentage
|
||||
),*/
|
||||
|
||||
discount_amount: MoneyDTOHelper.toNumber(summaryDto.total_discount_amount),
|
||||
discount_amount_fmt: formatCurrency(
|
||||
MoneyDTOHelper.toNumber(summaryDto.total_discount_amount),
|
||||
Number(summaryDto.total_amount.scale || 2),
|
||||
summaryDto.currency_code,
|
||||
summaryDto.language_code
|
||||
),
|
||||
|
||||
taxable_amount: MoneyDTOHelper.toNumber(summaryDto.taxable_amount),
|
||||
taxable_amount_fmt: formatCurrency(
|
||||
MoneyDTOHelper.toNumber(summaryDto.taxable_amount),
|
||||
Number(summaryDto.total_amount.scale || 2),
|
||||
summaryDto.currency_code,
|
||||
summaryDto.language_code
|
||||
),
|
||||
|
||||
taxes_amount: MoneyDTOHelper.toNumber(summaryDto.taxes_amount),
|
||||
taxes_amount_fmt: formatCurrency(
|
||||
MoneyDTOHelper.toNumber(summaryDto.taxes_amount),
|
||||
Number(summaryDto.total_amount.scale || 2),
|
||||
summaryDto.currency_code,
|
||||
summaryDto.language_code
|
||||
),
|
||||
|
||||
total_amount: MoneyDTOHelper.toNumber(summaryDto.total_amount),
|
||||
total_amount_fmt: formatCurrency(
|
||||
MoneyDTOHelper.toNumber(summaryDto.total_amount),
|
||||
Number(summaryDto.total_amount.scale || 2),
|
||||
summaryDto.currency_code,
|
||||
summaryDto.language_code
|
||||
),
|
||||
|
||||
//taxes: dto.taxes,
|
||||
}) as unknown as IssuedInvoiceSummaryData
|
||||
),
|
||||
};
|
||||
},
|
||||
};
|
||||
@ -1,18 +0,0 @@
|
||||
import type { CriteriaDTO } from "@erp/core";
|
||||
import type { IDataSource } from "@erp/core/client";
|
||||
|
||||
import type { IssuedInvoiceSummaryPage } from "../../types";
|
||||
|
||||
export async function getIssuedInvoiceListApi(
|
||||
dataSource: IDataSource,
|
||||
signal: AbortSignal,
|
||||
criteria: CriteriaDTO
|
||||
) {
|
||||
const response = dataSource.getList<IssuedInvoiceSummaryPage>("issued-invoices", {
|
||||
signal,
|
||||
...criteria,
|
||||
});
|
||||
|
||||
//return mapIssuedInvoiceList(raw);
|
||||
return response;
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export * from "./get-issued-invoice-list.api";
|
||||
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { useDownloadInvoicePDFController } from "../../download-pdf/controller";
|
||||
import type { IssuedInvoiceSummaryData } from "../../types";
|
||||
import type { IssuedInvoiceListRow } from "../../shared";
|
||||
|
||||
import { useIssuedInvoiceListController } from "./use-issued-invoice-list.controller";
|
||||
|
||||
@ -10,8 +10,8 @@ export function useIssuedInvoiceListPageController() {
|
||||
const downloadPDFCtrl = useDownloadInvoicePDFController();
|
||||
|
||||
const handleDownloadPDF = React.useCallback(
|
||||
(issuedInvoice: IssuedInvoiceSummaryData) => {
|
||||
downloadPDFCtrl.download(issuedInvoice.id, issuedInvoice.invoice_number);
|
||||
(issuedInvoice: IssuedInvoiceListRow) => {
|
||||
downloadPDFCtrl.download(issuedInvoice.id, issuedInvoice.invoiceNumber);
|
||||
},
|
||||
[downloadPDFCtrl]
|
||||
);
|
||||
|
||||
@ -1,51 +1,101 @@
|
||||
import type { CriteriaDTO } from "@erp/core";
|
||||
import { useDebounce } from "@repo/rdx-ui/components";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { IssuedInvoiceSummaryDtoAdapter } from "../adapters";
|
||||
import { useIssuedInvoiceListQuery } from "../hooks";
|
||||
import type {
|
||||
IssuedInvoiceList,
|
||||
IssuedInvoiceStatus,
|
||||
ListIssuedInvoicesByCriteriaParams,
|
||||
} from "../../shared";
|
||||
import { useIssuedInvoiceListQuery } from "../../shared/";
|
||||
|
||||
type IssuedInvoiceListStatusFilter = "all" | IssuedInvoiceStatus;
|
||||
|
||||
const EMPTY_ISSUED_INVOICES_LIST: IssuedInvoiceList = {
|
||||
items: [],
|
||||
page: 0,
|
||||
perPage: 5,
|
||||
totalPages: 0,
|
||||
totalItems: 0,
|
||||
};
|
||||
|
||||
export const useIssuedInvoiceListController = () => {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
const [search, setSearch] = useState("");
|
||||
const [status, setStatus] = useState("all");
|
||||
const [statusFilter, setStatusFilter] = useState<IssuedInvoiceListStatusFilter>("all");
|
||||
|
||||
const debouncedQ = useDebounce(search, 300);
|
||||
const debouncedSearch = useDebounce(search, 300);
|
||||
|
||||
const criteria = useMemo<CriteriaDTO>(() => {
|
||||
const baseFilters =
|
||||
status !== "all" ? [{ field: "status", operator: "CONTAINS", value: status }] : [];
|
||||
|
||||
return {
|
||||
q: debouncedQ || "",
|
||||
pageSize,
|
||||
const criteria = useMemo<NonNullable<ListIssuedInvoicesByCriteriaParams["criteria"]>>(
|
||||
() => ({
|
||||
q: debouncedSearch || "",
|
||||
pageNumber: pageIndex,
|
||||
pageSize,
|
||||
order: "desc",
|
||||
orderBy: "invoice_date",
|
||||
filters: baseFilters,
|
||||
};
|
||||
}, [pageSize, pageIndex, debouncedQ, status]);
|
||||
|
||||
const query = useIssuedInvoiceListQuery({ criteria });
|
||||
const data = useMemo(
|
||||
() => (query.data ? IssuedInvoiceSummaryDtoAdapter.fromDto(query.data) : undefined),
|
||||
[query.data]
|
||||
filters: status === "all" ? [] : [{ field: "status", operator: "eq", value: status }],
|
||||
}),
|
||||
[debouncedSearch, pageIndex, pageSize, status]
|
||||
);
|
||||
|
||||
const setSearchValue = (value: string) => setSearch(value.trim().replace(/\s+/g, " "));
|
||||
const query = useIssuedInvoiceListQuery({ criteria });
|
||||
|
||||
const setStatusFilter = (newStatus: string) => setStatus(newStatus);
|
||||
const setStatusFilterValue = (value: string) => {
|
||||
const nextValue = (value || "all") as IssuedInvoiceListStatusFilter;
|
||||
|
||||
setStatusFilter((prev) => {
|
||||
if (prev === nextValue) return prev;
|
||||
|
||||
// Sólo si la búsqueda realmente cambia,
|
||||
// reseteamos la página a 0 para evitar inconsistencias
|
||||
setPageIndex(0);
|
||||
return nextValue;
|
||||
});
|
||||
};
|
||||
|
||||
const setSearchValue = (value: string) => {
|
||||
const nextValue = value.trim().replace(/\s+/g, " ");
|
||||
|
||||
setSearch((prev) => {
|
||||
if (prev === nextValue) return prev;
|
||||
|
||||
// Sólo si la búsqueda realmente cambia,
|
||||
// reseteamos la página a 0 para evitar inconsistencias
|
||||
setPageIndex(0);
|
||||
return nextValue;
|
||||
});
|
||||
};
|
||||
|
||||
const setPageSizeValue = (value: number) => {
|
||||
setPageSize((prev) => {
|
||||
if (prev === value) return prev;
|
||||
|
||||
// Sólo si el tamaño de página realmente cambia,
|
||||
// reseteamos la página a 0 para evitar inconsistencias
|
||||
setPageIndex(0);
|
||||
return value;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
...query,
|
||||
data,
|
||||
data: query.data ?? EMPTY_ISSUED_INVOICES_LIST,
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
|
||||
isError: query.isError,
|
||||
error: query.error,
|
||||
|
||||
refetch: query.refetch,
|
||||
|
||||
pageIndex,
|
||||
pageSize,
|
||||
search,
|
||||
setPageIndex,
|
||||
setPageSize,
|
||||
setPageSize: setPageSizeValue,
|
||||
|
||||
search,
|
||||
setSearchValue,
|
||||
setStatusFilter,
|
||||
|
||||
statusFilter,
|
||||
setStatusFilter: setStatusFilterValue,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,42 +1,37 @@
|
||||
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useTranslation } from "../../../../../i18n";
|
||||
import type { IssuedInvoiceSummaryData, IssuedInvoiceSummaryPageData } from "../../../../types";
|
||||
import type { IssuedInvoiceList, IssuedInvoiceListRow } from "../../../../shared";
|
||||
|
||||
export type InvoiceUpdateCompProps = {
|
||||
data: IssuedInvoiceSummaryPageData;
|
||||
loading?: boolean;
|
||||
interface IssuedInvoicesGridProps {
|
||||
data?: IssuedInvoiceList;
|
||||
loading: boolean;
|
||||
fetching?: boolean;
|
||||
|
||||
columns: ColumnDef<IssuedInvoiceSummaryData, unknown>[];
|
||||
columns: ColumnDef<IssuedInvoiceListRow, unknown>[];
|
||||
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
onPageChange?: (pageNumber: number) => void;
|
||||
onPageSizeChange?: (pageSize: number) => void;
|
||||
onPageChange: (pageIndex: number) => void;
|
||||
onPageSizeChange: (size: number) => void;
|
||||
|
||||
onRowClick?: (
|
||||
row: IssuedInvoiceSummaryPageData,
|
||||
index: number,
|
||||
event: React.MouseEvent<HTMLTableRowElement>
|
||||
) => void;
|
||||
};
|
||||
onRowClick?: (proformaId: string) => void;
|
||||
}
|
||||
|
||||
// Create new GridExample component
|
||||
export const IssuedInvoicesGrid = ({
|
||||
data,
|
||||
loading,
|
||||
fetching,
|
||||
columns,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
onRowClick,
|
||||
}: InvoiceUpdateCompProps) => {
|
||||
}: IssuedInvoicesGridProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { items, total_items } = data;
|
||||
const { items, totalItems } = data || { items: [], totalItems: 0 };
|
||||
|
||||
// Navegación accesible (click o teclado)
|
||||
/* const goToRow = useCallback(
|
||||
@ -90,36 +85,29 @@ export const IssuedInvoicesGrid = ({
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<SkeletonDataTable
|
||||
columns={columns.length}
|
||||
footerProps={{ pageIndex, pageSize, totalItems: total_items ?? 0 }}
|
||||
rows={Math.max(6, pageSize)}
|
||||
showFooter
|
||||
/>
|
||||
</div>
|
||||
<SkeletonDataTable
|
||||
columns={columns.length}
|
||||
footerProps={{ pageIndex, pageSize, totalItems: totalItems ?? 0 }}
|
||||
rows={Math.max(6, pageSize)}
|
||||
showFooter
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Render principal
|
||||
return (
|
||||
<>
|
||||
{/*<div className={preview.isPinned ? "flex-1 mr-[500px]" : "flex-1"}>*/}
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={items}
|
||||
enablePagination
|
||||
enableRowSelection
|
||||
manualPagination
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
//onRowClick={handleRowClick}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
readOnly
|
||||
totalItems={total_items}
|
||||
/>
|
||||
{/*</div>*/}
|
||||
</>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={items}
|
||||
enablePagination={false}
|
||||
enableRowSelection
|
||||
manualPagination
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
//onRowClick={(row) => onRowClick?.(row.id)}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
totalItems={totalItems}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
ButtonGroup,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Spinner,
|
||||
@ -18,21 +19,21 @@ import * as React from "react";
|
||||
import QrCode from "react-qr-code";
|
||||
|
||||
import { useTranslation } from "../../../../../i18n";
|
||||
import type { IssuedInvoiceSummaryData } from "../../../../types";
|
||||
import type { IssuedInvoiceListRow } from "../../../../shared";
|
||||
import { VerifactuStatusBadge } from "../../components";
|
||||
|
||||
type GridActionHandlers = {
|
||||
onDownloadPdf?: (issuedInvoice: IssuedInvoiceSummaryData) => void;
|
||||
onDownloadPdf?: (issuedInvoice: IssuedInvoiceListRow) => void;
|
||||
pdfDownloadingId?: string | null;
|
||||
isPdfDownloading?: boolean;
|
||||
};
|
||||
|
||||
export function useIssuedInvoicesGridColumns(
|
||||
actionHandlers: GridActionHandlers = {}
|
||||
): ColumnDef<IssuedInvoiceSummaryData, unknown>[] {
|
||||
): ColumnDef<IssuedInvoiceListRow, unknown>[] {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return React.useMemo<ColumnDef<IssuedInvoiceSummaryData>[]>(
|
||||
return React.useMemo<ColumnDef<IssuedInvoiceListRow, unknown>[]>(
|
||||
() => [
|
||||
// Nº
|
||||
{
|
||||
@ -47,7 +48,7 @@ export function useIssuedInvoicesGridColumns(
|
||||
cell: ({ row }) => (
|
||||
<div className="text-right tabular-nums">
|
||||
{row.original.series}
|
||||
{row.original.invoice_number}
|
||||
{row.original.invoiceNumber}
|
||||
</div>
|
||||
),
|
||||
enableHiding: false,
|
||||
@ -106,11 +107,13 @@ export function useIssuedInvoicesGridColumns(
|
||||
<QrCodeIcon className="size-8 text-muted-foreground" />
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<a href={verifactu.url} rel="noopener" target="_blank">
|
||||
<QrCodeIcon className="size-8" />
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<a href={verifactu.url} rel="noopener" target="_blank">
|
||||
<QrCodeIcon className="size-8" />
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
<TooltipContent className="m-0 p-3">
|
||||
<QrCode className="bg-white p-8" value={verifactu.url} />
|
||||
</TooltipContent>
|
||||
@ -192,7 +195,7 @@ export function useIssuedInvoicesGridColumns(
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-left tabular-nums">
|
||||
{formatDate(row.original.invoice_date)}
|
||||
{formatDate(row.original.invoiceDate)}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
@ -214,7 +217,7 @@ export function useIssuedInvoicesGridColumns(
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-left tabular-nums">
|
||||
{formatDate(row.original.operation_date)}
|
||||
{formatDate(row.original.operationDate)}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
@ -237,7 +240,7 @@ export function useIssuedInvoicesGridColumns(
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-right tabular-nums">
|
||||
{row.original.subtotal_amount_fmt}
|
||||
{row.original.subtotalAmountFmt}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
@ -260,7 +263,7 @@ export function useIssuedInvoicesGridColumns(
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-right tabular-nums">
|
||||
{row.original.discount_amount_fmt}
|
||||
{row.original.totalDiscountAmountFmt}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
@ -282,7 +285,7 @@ export function useIssuedInvoicesGridColumns(
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-right tabular-nums">{row.original.taxes_amount_fmt}</div>
|
||||
<div className="font-medium text-right tabular-nums">{row.original.taxesAmountFmt}</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
@ -303,9 +306,7 @@ export function useIssuedInvoicesGridColumns(
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-semibold text-right tabular-nums">
|
||||
{row.original.total_amount_fmt}
|
||||
</div>
|
||||
<div className="font-semibold text-right tabular-nums">{row.original.totalAmountFmt}</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
@ -343,25 +344,27 @@ export function useIssuedInvoicesGridColumns(
|
||||
{/* Descargar en PDF */}
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
className={"size-8"}
|
||||
disabled={isPDFLoading || !isCompleted}
|
||||
onClick={(e) => {
|
||||
stop(e);
|
||||
isCompleted ? actionHandlers.onDownloadPdf?.(issuedInvoice) : null;
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
{isPDFLoading ? (
|
||||
<Spinner className="size-4 cursor-progress" />
|
||||
) : (
|
||||
<FileDownIcon className="size-4 cursor-pointer" />
|
||||
)}
|
||||
<span className="sr-only">Descargar PDF</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
className={"size-8"}
|
||||
disabled={isPDFLoading || !isCompleted}
|
||||
onClick={(e) => {
|
||||
stop(e);
|
||||
isCompleted ? actionHandlers.onDownloadPdf?.(issuedInvoice) : null;
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
{isPDFLoading ? (
|
||||
<Spinner className="size-4 cursor-progress" />
|
||||
) : (
|
||||
<FileDownIcon className="size-4 cursor-pointer" />
|
||||
)}
|
||||
<span className="sr-only">Descargar PDF</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>Descargar PDF</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@ -369,31 +372,35 @@ export function useIssuedInvoicesGridColumns(
|
||||
{/** biome-ignore lint/suspicious/noSelfCompare: <Desactivado por ahora> */}
|
||||
{false !== false && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
aria-label={t("common.more_actions")}
|
||||
className="cursor-pointer text-muted-foreground hover:text-primary"
|
||||
onClick={stop}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" className="size-4" />
|
||||
<span className="sr-only">{t("common.more_actions")}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
aria-label={t("common.more_actions")}
|
||||
className="cursor-pointer text-muted-foreground hover:text-primary"
|
||||
onClick={stop}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" className="size-4" />
|
||||
<span className="sr-only">{t("common.more_actions")}</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => actionHandlers.onDownloadPdf?.(issuedInvoice)}
|
||||
>
|
||||
<DownloadIcon className="mr-2 size-4" />
|
||||
{t("common.download_pdf")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer" onClick={stop}>
|
||||
<MailIcon className="mr-2 size-4" />
|
||||
{t("common.send_email")}
|
||||
</DropdownMenuItem>{" "}
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => actionHandlers.onDownloadPdf?.(issuedInvoice)}
|
||||
>
|
||||
<DownloadIcon className="mr-2 size-4" />
|
||||
{t("common.download_pdf")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer" onClick={stop}>
|
||||
<MailIcon className="mr-2 size-4" />
|
||||
{t("common.send_email")}
|
||||
</DropdownMenuItem>{" "}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
getVerifactuRecordStatusButtonVariant,
|
||||
getVerifactuRecordStatusColor,
|
||||
getVerifactuRecordStatusIcon,
|
||||
} from "../../../types";
|
||||
} from "../../../shared";
|
||||
|
||||
export type VerifactuStatusBadgeProps = {
|
||||
status: string | VerifactuRecordStatus; // permitir cualquier valor
|
||||
@ -21,21 +21,23 @@ export const VerifactuStatusBadge = ({ status, className }: VerifactuStatusBadge
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Badge
|
||||
className={cn(
|
||||
getVerifactuRecordStatusColor(normalizedStatus),
|
||||
"font-semibold",
|
||||
className
|
||||
)}
|
||||
variant={getVerifactuRecordStatusButtonVariant(normalizedStatus)}
|
||||
>
|
||||
<Icon />
|
||||
{t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.label`, {
|
||||
defaultValue: status,
|
||||
})}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Badge
|
||||
className={cn(
|
||||
getVerifactuRecordStatusColor(normalizedStatus),
|
||||
"font-semibold",
|
||||
className
|
||||
)}
|
||||
variant={getVerifactuRecordStatusButtonVariant(normalizedStatus)}
|
||||
>
|
||||
<Icon />
|
||||
{t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.label`, {
|
||||
defaultValue: status,
|
||||
})}
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<p>{t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.description`)}</p>
|
||||
</TooltipContent>
|
||||
|
||||
@ -1 +1 @@
|
||||
export * from "./issued-invoice-list-page";
|
||||
export * from "./list-issued-invoices-page";
|
||||
|
||||
@ -18,10 +18,10 @@ import { useTranslation } from "../../../../i18n";
|
||||
import { useIssuedInvoiceListPageController } from "../../controllers";
|
||||
import { IssuedInvoicesGrid, useIssuedInvoicesGridColumns } from "../blocks";
|
||||
|
||||
export const IssuedInvoiceListPage = () => {
|
||||
export const ListIssuedInvoicesPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
listCtrl,
|
||||
handleDownloadPDF,
|
||||
@ -43,7 +43,7 @@ export const IssuedInvoiceListPage = () => {
|
||||
widthClass: "w-[500px]",
|
||||
});*/
|
||||
|
||||
if (listCtrl.isError || !listCtrl.data) {
|
||||
if (listCtrl.isError) {
|
||||
return (
|
||||
<AppContent>
|
||||
<ErrorAlert
|
||||
@ -56,7 +56,7 @@ export const IssuedInvoiceListPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section>
|
||||
<AppHeader>
|
||||
<PageHeader
|
||||
description={t("pages.issued_invoices.list.description")}
|
||||
@ -73,7 +73,8 @@ export const IssuedInvoiceListPage = () => {
|
||||
title={t("pages.issued_invoices.list.title")}
|
||||
/>
|
||||
</AppHeader>
|
||||
<AppContent>
|
||||
|
||||
<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>
|
||||
@ -87,13 +88,17 @@ export const IssuedInvoiceListPage = () => {
|
||||
</Alert>
|
||||
|
||||
{/* Search and filters */}
|
||||
<div className="flex items-center justify-between gap-16">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<SimpleSearchInput
|
||||
loading={listCtrl.isLoading}
|
||||
onSearchChange={listCtrl.setSearchValue}
|
||||
value={listCtrl.search}
|
||||
/>
|
||||
|
||||
<Select defaultValue="all" onValueChange={listCtrl.setStatusFilter}>
|
||||
<Select
|
||||
onValueChange={(value) => listCtrl.setStatusFilter(value ?? "all")}
|
||||
value={listCtrl.statusFilter}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-48">
|
||||
<FilterIcon aria-hidden className="mr-2 size-4" />
|
||||
<SelectValue placeholder={t("filters.status")} />
|
||||
@ -127,19 +132,20 @@ export const IssuedInvoiceListPage = () => {
|
||||
</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}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from "./list-issued-invoice.adapter";
|
||||
@ -0,0 +1,101 @@
|
||||
import { MoneyDTOHelper, formatCurrency } from "@erp/core";
|
||||
|
||||
import type { ListIssuedInvoicesResponseDTO } from "../../../../common";
|
||||
import type { IssuedInvoiceList, IssuedInvoiceListRow, IssuedInvoiceStatus } from "../entities";
|
||||
import type { VerifactuRecordStatus } from "../entities/verifactu-record-status.entity";
|
||||
|
||||
export const ListIssuedInvoicesAdapter = {
|
||||
fromDto(dto: ListIssuedInvoicesResponseDTO): IssuedInvoiceList {
|
||||
return {
|
||||
items: dto.items.map(IssuedInvoiceListRowAdapter.fromDto),
|
||||
page: dto.page,
|
||||
perPage: dto.per_page,
|
||||
totalPages: dto.total_pages,
|
||||
totalItems: dto.total_items,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
type ListIssuedInvoicesItemDTO = ListIssuedInvoicesResponseDTO["items"][number];
|
||||
|
||||
const IssuedInvoiceListRowAdapter = {
|
||||
fromDto(dto: ListIssuedInvoicesItemDTO): IssuedInvoiceListRow {
|
||||
return {
|
||||
id: dto.id,
|
||||
companyId: dto.company_id,
|
||||
|
||||
invoiceNumber: dto.invoice_number,
|
||||
status: dto.status as IssuedInvoiceStatus,
|
||||
series: dto.series,
|
||||
|
||||
invoiceDate: dto.invoice_date,
|
||||
operationDate: dto.operation_date,
|
||||
|
||||
languageCode: dto.language_code,
|
||||
currencyCode: dto.currency_code,
|
||||
|
||||
reference: dto.reference,
|
||||
description: dto.description,
|
||||
recipient: {
|
||||
id: dto.customer_id,
|
||||
tin: dto.recipient.tin,
|
||||
name: dto.recipient.name,
|
||||
|
||||
street: dto.recipient.street,
|
||||
street2: dto.recipient.street2,
|
||||
city: dto.recipient.city,
|
||||
province: dto.recipient.province,
|
||||
postalCode: dto.recipient.postal_code,
|
||||
country: dto.recipient.country,
|
||||
},
|
||||
|
||||
subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
|
||||
subtotalAmountFmt: formatCurrency(
|
||||
MoneyDTOHelper.toNumber(dto.subtotal_amount),
|
||||
Number(dto.total_amount.scale || 2),
|
||||
dto.currency_code,
|
||||
dto.language_code
|
||||
),
|
||||
|
||||
totalDiscountAmount: MoneyDTOHelper.toNumber(dto.total_discount_amount),
|
||||
totalDiscountAmountFmt: formatCurrency(
|
||||
MoneyDTOHelper.toNumber(dto.total_discount_amount),
|
||||
Number(dto.total_amount.scale || 2),
|
||||
dto.currency_code,
|
||||
dto.language_code
|
||||
),
|
||||
|
||||
taxableAmount: MoneyDTOHelper.toNumber(dto.taxable_amount),
|
||||
taxableAmountFmt: formatCurrency(
|
||||
MoneyDTOHelper.toNumber(dto.taxable_amount),
|
||||
Number(dto.total_amount.scale || 2),
|
||||
dto.currency_code,
|
||||
dto.language_code
|
||||
),
|
||||
|
||||
taxesAmount: MoneyDTOHelper.toNumber(dto.taxes_amount),
|
||||
taxesAmountFmt: formatCurrency(
|
||||
MoneyDTOHelper.toNumber(dto.taxes_amount),
|
||||
Number(dto.total_amount.scale || 2),
|
||||
dto.currency_code,
|
||||
dto.language_code
|
||||
),
|
||||
|
||||
totalAmount: MoneyDTOHelper.toNumber(dto.total_amount),
|
||||
totalAmountFmt: formatCurrency(
|
||||
MoneyDTOHelper.toNumber(dto.total_amount),
|
||||
Number(dto.total_amount.scale || 2),
|
||||
dto.currency_code,
|
||||
dto.language_code
|
||||
),
|
||||
|
||||
//linkedProformaId: dto.,
|
||||
|
||||
verifactu: {
|
||||
status: dto.verifactu.status as unknown as VerifactuRecordStatus,
|
||||
url: dto.verifactu.url,
|
||||
qr_code: dto.verifactu.qr_code,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from "./list-issued-invoices-by-criteria.api";
|
||||
@ -0,0 +1,32 @@
|
||||
import type { CriteriaDTO } from "@erp/core";
|
||||
import type { IDataSource } from "@erp/core/client";
|
||||
|
||||
import type { ListIssuedInvoicesResponseDTO } from "../../../../common";
|
||||
|
||||
/**
|
||||
* Recupera una lista de facturas del sistema utilizando la
|
||||
* fuente de datos proporcionada y los criterios de búsqueda especificados.
|
||||
*
|
||||
* @param dataSource - La fuente de datos para interactuar con la API.
|
||||
* @param params - Los parámetros necesarios para listar las facturas, incluyendo los criterios de búsqueda.
|
||||
* @returns Una promesa que resuelve con una lista de facturas que cumplen con los criterios especificados.
|
||||
* @throws Error si la recuperación de la lista de facturas falla.
|
||||
*/
|
||||
|
||||
export type ListIssuedInvoicesByCriteriaParams = {
|
||||
criteria?: CriteriaDTO;
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
export type ListIssuedInvoicesResult = ListIssuedInvoicesResponseDTO;
|
||||
|
||||
export function getListIssuedInvoicesByCriteria(
|
||||
dataSource: IDataSource,
|
||||
params: ListIssuedInvoicesByCriteriaParams
|
||||
): Promise<ListIssuedInvoicesResult> {
|
||||
const { criteria, signal } = params || { criteria: undefined, signal: undefined };
|
||||
return dataSource.getList<ListIssuedInvoicesResponseDTO>("issued-invoices", {
|
||||
signal,
|
||||
...criteria,
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export * from "./issued-invoice-list.entity";
|
||||
export * from "./issued-invoice-list-row.entity";
|
||||
export * from "./issued-invoice-status.entity";
|
||||
export * from "./verifactu-record-status.entity";
|
||||
@ -0,0 +1,47 @@
|
||||
import type { IssuedInvoiceRecipient } from "./issued-invoice-recipient.entity";
|
||||
import type { IssuedInvoiceStatus } from "./issued-invoice-status.entity";
|
||||
import type { VerifactuRecord } from "./verifactu-record.entity";
|
||||
|
||||
/**
|
||||
* Interface que representa una fila de la lista de
|
||||
* facturas en el sistema, adaptada desde la respuesta de la API.
|
||||
* Contiene los campos justos para mostrar
|
||||
* la información básica de cada factura en la lista.
|
||||
*/
|
||||
|
||||
export interface IssuedInvoiceListRow {
|
||||
id: string;
|
||||
companyId: string;
|
||||
|
||||
invoiceNumber: string;
|
||||
status: IssuedInvoiceStatus;
|
||||
series: string;
|
||||
|
||||
invoiceDate: string;
|
||||
operationDate: string;
|
||||
|
||||
languageCode: string;
|
||||
currencyCode: string;
|
||||
|
||||
reference: string;
|
||||
description: string;
|
||||
|
||||
recipient: IssuedInvoiceRecipient;
|
||||
|
||||
subtotalAmount: number;
|
||||
subtotalAmountFmt: string;
|
||||
|
||||
totalDiscountAmount: number;
|
||||
totalDiscountAmountFmt: string;
|
||||
|
||||
taxableAmount: number;
|
||||
taxableAmountFmt: string;
|
||||
|
||||
taxesAmount: number;
|
||||
taxesAmountFmt: string;
|
||||
|
||||
totalAmount: number;
|
||||
totalAmountFmt: string;
|
||||
|
||||
verifactu: VerifactuRecord;
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import type { IssuedInvoiceListRow } from "./issued-invoice-list-row.entity";
|
||||
|
||||
/**
|
||||
* Interface que representa la respuesta paginada de una lista de proformas,
|
||||
* adaptada desde la respuesta de la API.
|
||||
*/
|
||||
|
||||
export interface IssuedInvoiceList {
|
||||
items: IssuedInvoiceListRow[];
|
||||
totalPages: number;
|
||||
totalItems: number;
|
||||
page: number;
|
||||
perPage: number;
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Interface que representa el destinatario de una factura en el sistema,
|
||||
* adaptada desde la respuesta de la API.
|
||||
*/
|
||||
|
||||
export interface IssuedInvoiceRecipient {
|
||||
id: string;
|
||||
name: string;
|
||||
tin: string;
|
||||
|
||||
street: string;
|
||||
street2: string;
|
||||
|
||||
city: string;
|
||||
province: string;
|
||||
postalCode: string;
|
||||
country: string;
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Enumeración que representa
|
||||
* los posibles estados de una proforma en el sistema.
|
||||
*/
|
||||
|
||||
export enum ISSUED_INVOICE_STATUS {
|
||||
DRAFT = "draft",
|
||||
SENT = "sent",
|
||||
APPROVED = "approved",
|
||||
REJECTED = "rejected",
|
||||
ISSUED = "issued",
|
||||
}
|
||||
|
||||
// Transiciones válidas según reglas del dominio
|
||||
export const ISSUED_INVOICE_STATUS_TRANSITIONS: Record<
|
||||
ISSUED_INVOICE_STATUS,
|
||||
ISSUED_INVOICE_STATUS[]
|
||||
> = {
|
||||
[ISSUED_INVOICE_STATUS.DRAFT]: [ISSUED_INVOICE_STATUS.SENT],
|
||||
[ISSUED_INVOICE_STATUS.SENT]: [ISSUED_INVOICE_STATUS.APPROVED, ISSUED_INVOICE_STATUS.REJECTED],
|
||||
[ISSUED_INVOICE_STATUS.APPROVED]: [ISSUED_INVOICE_STATUS.ISSUED, ISSUED_INVOICE_STATUS.DRAFT],
|
||||
[ISSUED_INVOICE_STATUS.REJECTED]: [ISSUED_INVOICE_STATUS.DRAFT],
|
||||
[ISSUED_INVOICE_STATUS.ISSUED]: [],
|
||||
};
|
||||
|
||||
export type IssuedInvoiceStatus = `${ISSUED_INVOICE_STATUS}`;
|
||||
@ -0,0 +1,7 @@
|
||||
import type { VerifactuRecordStatus } from "./verifactu-record-status.entity";
|
||||
|
||||
export interface VerifactuRecord {
|
||||
status: VerifactuRecordStatus;
|
||||
url: string;
|
||||
qr_code: string;
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import type { QueryKey } from "@tanstack/react-query";
|
||||
|
||||
import type { ListIssuedInvoicesRequestDTO } from "../../../../common";
|
||||
|
||||
/**
|
||||
* Prefijo base para listados
|
||||
*/
|
||||
export const LIST_ISSUED_INVOICES_QUERY_KEY_PREFIX = ["issued_invoices"] as const;
|
||||
|
||||
/**
|
||||
* Query key para listado de facturas
|
||||
*/
|
||||
export const LIST_ISSUED_INVOICES_QUERY_KEY = (criteria?: ListIssuedInvoicesRequestDTO): QueryKey =>
|
||||
[
|
||||
...LIST_ISSUED_INVOICES_QUERY_KEY_PREFIX,
|
||||
{
|
||||
pageNumber: criteria?.pageNumber ?? 1,
|
||||
pageSize: criteria?.pageSize ?? 5,
|
||||
q: criteria?.q ?? "",
|
||||
filters: criteria?.filters ?? [],
|
||||
orderBy: criteria?.orderBy ?? "",
|
||||
order: criteria?.order ?? "",
|
||||
},
|
||||
] as const;
|
||||
@ -1,39 +1,31 @@
|
||||
import type { CriteriaDTO } from "@erp/core";
|
||||
import { useDataSource } from "@erp/core/hooks";
|
||||
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria";
|
||||
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
||||
import { type DefaultError, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { getIssuedInvoiceListApi } from "../../list/api";
|
||||
import type { IssuedInvoiceSummaryPage } from "../../types";
|
||||
import { ListIssuedInvoicesAdapter } from "../adapters";
|
||||
import { getListIssuedInvoicesByCriteria } from "../api";
|
||||
import type { IssuedInvoiceList } from "../entities";
|
||||
|
||||
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 ?? "",
|
||||
},
|
||||
];
|
||||
import { LIST_ISSUED_INVOICES_QUERY_KEY } from "./keys";
|
||||
|
||||
type IssuedInvoicesQueryOptions = {
|
||||
export interface IssuedInvoicesListQueryOptions {
|
||||
enabled?: boolean;
|
||||
criteria?: CriteriaDTO;
|
||||
};
|
||||
criteria?: Partial<CriteriaDTO>;
|
||||
}
|
||||
|
||||
// Obtener todas las facturas
|
||||
export const useIssuedInvoiceListQuery = (options?: IssuedInvoicesQueryOptions) => {
|
||||
export const useIssuedInvoiceListQuery = (options?: IssuedInvoicesListQueryOptions) => {
|
||||
const dataSource = useDataSource();
|
||||
const enabled = options?.enabled ?? true;
|
||||
const criteria = options?.criteria ?? {};
|
||||
|
||||
return useQuery<IssuedInvoiceSummaryPage, DefaultError>({
|
||||
queryKey: ISSUED_INVOICES_QUERY_KEY(criteria),
|
||||
queryFn: async ({ signal }) => getIssuedInvoiceListApi(dataSource, signal, criteria),
|
||||
return useQuery<IssuedInvoiceList, DefaultError>({
|
||||
queryKey: LIST_ISSUED_INVOICES_QUERY_KEY(criteria),
|
||||
queryFn: async ({ signal }) => {
|
||||
const dto = await getListIssuedInvoicesByCriteria(dataSource, { signal, criteria });
|
||||
return ListIssuedInvoicesAdapter.fromDto(dto);
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5000,
|
||||
placeholderData: (previousData, _previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
|
||||
placeholderData: (previousData) => previousData, // Mantiene la página anterior durante refetch por cambio de criteria
|
||||
});
|
||||
};
|
||||
|
||||
@ -1 +1,5 @@
|
||||
export * from "./adapters";
|
||||
export * from "./api";
|
||||
export * from "./entities";
|
||||
export * from "./hooks";
|
||||
export * from "./ui";
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
export const IssuedInvoicesLayout = ({ children }: PropsWithChildren) => {
|
||||
return <div>{children}</div>;
|
||||
return <div className="space-y-4">{children}</div>;
|
||||
};
|
||||
@ -1,3 +0,0 @@
|
||||
export * from "./issued-invoice.api.schema";
|
||||
export * from "./issued-invoice-summary.web.schema";
|
||||
export * from "./verifactu-record-status";
|
||||
@ -1,25 +0,0 @@
|
||||
import type { IssuedInvoiceSummary, IssuedInvoiceSummaryPage } from "./issued-invoice.api.schema";
|
||||
|
||||
export type IssuedInvoiceSummaryData = IssuedInvoiceSummary & {
|
||||
subtotal_amount_fmt: string;
|
||||
subtotal_amount: number;
|
||||
|
||||
discount_percentage_fmt: string;
|
||||
discount_percentage: number;
|
||||
|
||||
discount_amount_fmt: string;
|
||||
discount_amount: number;
|
||||
|
||||
taxable_amount_fmt: string;
|
||||
taxable_amount: number;
|
||||
|
||||
taxes_amount_fmt: string;
|
||||
taxes_amount: number;
|
||||
|
||||
total_amount_fmt: string;
|
||||
total_amount: number;
|
||||
};
|
||||
|
||||
export type IssuedInvoiceSummaryPageData = IssuedInvoiceSummaryPage & {
|
||||
items: IssuedInvoiceSummaryData[];
|
||||
};
|
||||
@ -1,22 +0,0 @@
|
||||
import {
|
||||
GetIssuedInvoiceByIdResponseSchema,
|
||||
type ListIssuedInvoicesResponseDTO,
|
||||
} from "@erp/customer-invoices/common";
|
||||
import type { ArrayElement } from "@repo/rdx-utils";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
// IssuedInvoices
|
||||
export const IssuedInvoiceSchema = GetIssuedInvoiceByIdResponseSchema.omit({
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
export type IssuedInvoice = z.infer<typeof IssuedInvoiceSchema>;
|
||||
export type IssuedInvoiceRecipient = IssuedInvoice["recipient"];
|
||||
export type IssuedInvoiceItem = ArrayElement<IssuedInvoice["items"]>;
|
||||
|
||||
// Resultado de consulta con criteria (paginado, etc.)
|
||||
export type IssuedInvoiceSummaryPage = Omit<ListIssuedInvoicesResponseDTO, "metadata">;
|
||||
export type IssuedInvoiceSummary = Omit<
|
||||
ArrayElement<IssuedInvoiceSummaryPage["items"]>,
|
||||
"metadata"
|
||||
>;
|
||||
@ -101,18 +101,20 @@ export function useProformasGridColumns(
|
||||
{isIssued && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
className="size-6 text-foreground hover:text-primary"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<a href={`/facturas/${invoiceId}`}>
|
||||
<ExternalLinkIcon />
|
||||
<span className="sr-only">Ver factura {invoiceId}</span>
|
||||
</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
className="size-6 text-foreground hover:text-primary"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<a href={`/facturas/${invoiceId}`}>
|
||||
<ExternalLinkIcon />
|
||||
<span className="sr-only">Ver factura {invoiceId}</span>
|
||||
</a>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>Ver factura {invoiceId}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@ -235,17 +237,19 @@ export function useProformasGridColumns(
|
||||
{!isIssued && actionHandlers.onEditClick && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
className="size-8 cursor-pointer"
|
||||
onClick={() => actionHandlers.onEditClick?.(proforma)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<PencilIcon className="size-4" />
|
||||
<span className="sr-only">Editar</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
className="size-8 cursor-pointer"
|
||||
onClick={() => actionHandlers.onEditClick?.(proforma)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<PencilIcon className="size-4" />
|
||||
<span className="sr-only">Editar</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>Editar</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@ -255,19 +259,21 @@ export function useProformasGridColumns(
|
||||
{!isIssued && availableTransitions.length && actionHandlers.onChangeStatusClick && (
|
||||
<TooltipProvider key={availableTransitions[0]}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
className="size-8 cursor-pointer"
|
||||
onClick={() =>
|
||||
actionHandlers.onChangeStatusClick?.(proforma, availableTransitions[0])
|
||||
}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<RefreshCwIcon className="size-4" />
|
||||
<span className="sr-only">Cambiar estado</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
className="size-8 cursor-pointer"
|
||||
onClick={() =>
|
||||
actionHandlers.onChangeStatusClick?.(proforma, availableTransitions[0])
|
||||
}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<RefreshCwIcon className="size-4" />
|
||||
<span className="sr-only">Cambiar estado</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
Cambiar a {t(`catalog.proformas.status.${availableTransitions[0]}.label`)}
|
||||
</TooltipContent>
|
||||
@ -279,16 +285,18 @@ export function useProformasGridColumns(
|
||||
{!isIssued && isApproved && actionHandlers.onIssueClick && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
className="size-8 cursor-pointer"
|
||||
onClick={() => actionHandlers.onIssueClick?.(proforma)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<FileTextIcon className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
className="size-8 cursor-pointer"
|
||||
onClick={() => actionHandlers.onIssueClick?.(proforma)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<FileTextIcon className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>Emitir a factura</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@ -298,19 +306,21 @@ export function useProformasGridColumns(
|
||||
{!isIssued && actionHandlers.onDeleteClick && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
className="size-8 text-destructive hover:text-destructive cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
actionHandlers.onDeleteClick?.(proforma);
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<Trash2Icon className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
className="size-8 text-destructive hover:text-destructive cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
actionHandlers.onDeleteClick?.(proforma);
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<Trash2Icon className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>Eliminar</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@ -1,2 +1 @@
|
||||
export * from "./initials";
|
||||
export * from "./proforma-status-badge";
|
||||
|
||||
@ -21,15 +21,17 @@ export const ProformaStatusBadge = ({ status, className }: ProformaStatusBadgePr
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Badge
|
||||
className={cn(getProformaStatusColor(normalizedStatus), "font-semibold", className)}
|
||||
variant={getProformaStatusButtonVariant(normalizedStatus)}
|
||||
>
|
||||
<Icon />
|
||||
{t(`catalog.proformas.status.${normalizedStatus}.label`, { defaultValue: status })}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Badge
|
||||
className={cn(getProformaStatusColor(normalizedStatus), "font-semibold", className)}
|
||||
variant={getProformaStatusButtonVariant(normalizedStatus)}
|
||||
>
|
||||
<Icon />
|
||||
{t(`catalog.proformas.status.${normalizedStatus}.label`, { defaultValue: status })}
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<p>{t(`catalog.proformas.status.${normalizedStatus}.description`)}</p>
|
||||
</TooltipContent>
|
||||
|
||||
@ -15,7 +15,7 @@ export const LIST_PROFORMAS_QUERY_KEY = (criteria?: ProformasListRequestDTO): Qu
|
||||
...LIST_PROFORMAS_QUERY_KEY_PREFIX,
|
||||
{
|
||||
pageNumber: criteria?.pageNumber ?? 1,
|
||||
pageSize: criteria?.pageSize ?? 10,
|
||||
pageSize: criteria?.pageSize ?? 5,
|
||||
q: criteria?.q ?? "",
|
||||
filters: criteria?.filters ?? [],
|
||||
orderBy: criteria?.orderBy ?? "",
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
@ -152,51 +153,55 @@ export function useCustomersGridColumns(
|
||||
</Button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button aria-label={t("pages.list.actions.more")} size="icon" variant="ghost">
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button aria-label={t("pages.list.actions.more")} size="icon" variant="ghost">
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>{t("pages.list.grid_columns.actions")}</DropdownMenuLabel>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>{t("pages.list.grid_columns.actions")}</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem onClick={() => onViewClick?.(customer)}>
|
||||
{t("pages.list.actions.view")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onViewClick?.(customer)}>
|
||||
{t("pages.list.actions.view")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={() => onEditClick?.(customer)}>
|
||||
{t("pages.list.actions.edit")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onEditClick?.(customer)}>
|
||||
{t("pages.list.actions.edit")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
disabled={!website}
|
||||
onClick={() =>
|
||||
window.open(safeHTTPUrl(website), "_blank", "noopener,noreferrer")
|
||||
}
|
||||
>
|
||||
{t("pages.list.actions.visit_website")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
disabled={!website}
|
||||
onClick={() =>
|
||||
window.open(safeHTTPUrl(website), "_blank", "noopener,noreferrer")
|
||||
}
|
||||
>
|
||||
{t("pages.list.actions.visit_website")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
disabled={!email_primary}
|
||||
onClick={() => navigator.clipboard.writeText(email_primary)}
|
||||
>
|
||||
{t("pages.list.actions.copy_email")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
disabled={!email_primary}
|
||||
onClick={() => navigator.clipboard.writeText(email_primary)}
|
||||
>
|
||||
{t("pages.list.actions.copy_email")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
onClick={() => onDeleteClick?.(customer)}
|
||||
>
|
||||
{t("pages.list.actions.delete")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
onClick={() => onDeleteClick?.(customer)}
|
||||
>
|
||||
{t("pages.list.actions.delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
@ -30,11 +30,13 @@ export const CustomerStatusBadge = ({ status }: { status: string }) => {
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className={cn("flex-none rounded-full p-1", statusClass)}>
|
||||
<div className="size-2 rounded-full bg-current" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<div className={cn("flex-none rounded-full p-1", statusClass)}>
|
||||
<div className="size-2 rounded-full bg-current" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>{contentTxt}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
@ -31,38 +32,42 @@ export function DataTableColumnHeader<TData, TValue>({
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2 ", className)}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
//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"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<span>{title}</span>
|
||||
{column.getIsSorted() === "desc" ? (
|
||||
<ArrowDownIcon />
|
||||
) : column.getIsSorted() === "asc" ? (
|
||||
<ArrowUpIcon />
|
||||
) : (
|
||||
<ChevronsUpDownIcon />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
//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"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<span>{title}</span>
|
||||
{column.getIsSorted() === "desc" ? (
|
||||
<ArrowDownIcon />
|
||||
) : column.getIsSorted() === "asc" ? (
|
||||
<ArrowUpIcon />
|
||||
) : (
|
||||
<ChevronsUpDownIcon />
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<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>
|
||||
<DropdownMenuGroup>
|
||||
<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>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
@ -47,7 +47,8 @@ export function DataTablePagination<TData>({ table, className }: DataTablePagina
|
||||
table.setPageIndex(nextIndex);
|
||||
};
|
||||
|
||||
const handlePageSizeChange = (size: string) => {
|
||||
const handlePageSizeChange = (size: string | null) => {
|
||||
if (!size) return;
|
||||
table.setPageSize(Number(size));
|
||||
};
|
||||
|
||||
|
||||
@ -113,34 +113,38 @@ export function DataTableToolbar<TData>({
|
||||
|
||||
{!readOnly && meta?.bulkOps?.moveSelectedUp && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
aria-label={t("components.datatable.actions.move_up")}
|
||||
onClick={handleMoveSelectedUp}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<ArrowUpIcon aria-hidden="true" className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
aria-label={t("components.datatable.actions.move_up")}
|
||||
onClick={handleMoveSelectedUp}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<ArrowUpIcon aria-hidden="true" className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>{t("components.datatable.actions.move_up")}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!readOnly && meta?.bulkOps?.moveSelectedDown && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
aria-label={t("components.datatable.actions.move_down")}
|
||||
onClick={handleMoveSelectedDown}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<ArrowDownIcon aria-hidden="true" className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
aria-label={t("components.datatable.actions.move_down")}
|
||||
onClick={handleMoveSelectedDown}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<ArrowDownIcon aria-hidden="true" className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>{t("components.datatable.actions.move_down")}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
@ -164,12 +168,14 @@ export function DataTableToolbar<TData>({
|
||||
<Separator className="h-6 mx-1 bg-muted/50" orientation="vertical" />
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button onClick={handleClearSelection} size="sm" type="button" variant="outline">
|
||||
<ScanIcon aria-hidden="true" className="size-4 mr-1" />
|
||||
<span>{t("components.datatable.actions.clear_selection")}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button onClick={handleClearSelection} size="sm" type="button" variant="outline">
|
||||
<ScanIcon aria-hidden="true" className="size-4 mr-1" />
|
||||
<span>{t("components.datatable.actions.clear_selection")}</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>{t("components.datatable.actions.clear_selection")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
@ -18,36 +19,41 @@ export function DataTableViewOptions<TData>({ table }: { table: Table<TData> })
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
className="ml-auto hidden h-8 lg:flex gap-2 items"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<Settings2Icon />
|
||||
{t("components.datatable_view_options.columns_button")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
className="ml-auto hidden h-8 lg:flex gap-2 items"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<Settings2Icon />
|
||||
{t("components.datatable_view_options.columns_button")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>
|
||||
{t("components.datatable_view_options.toggle_columns")}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide())
|
||||
.map((column) => {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={column.getIsVisible()}
|
||||
key={column.id}
|
||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||
>
|
||||
{getColumnLabel(column)}
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
})}
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>
|
||||
{t("components.datatable_view_options.toggle_columns")}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide())
|
||||
.map((column) => {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={column.getIsVisible()}
|
||||
key={column.id}
|
||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||
>
|
||||
{getColumnLabel(column)}
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
@ -1,13 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
TableBody,
|
||||
TableCell,
|
||||
Table as TableComp,
|
||||
@ -80,7 +73,6 @@ export interface DataTableProps<TData, TValue> {
|
||||
enablePagination?: boolean;
|
||||
pageSize?: number;
|
||||
enableRowSelection?: boolean;
|
||||
EditorComponent?: React.ComponentType<{ row: TData; index: number; onClose: () => void }>;
|
||||
|
||||
getRowId?: (originalRow: TData, index: number, parent?: Row<TData>) => string;
|
||||
|
||||
@ -105,7 +97,6 @@ export function DataTable<TData, TValue>({
|
||||
enablePagination = true,
|
||||
pageSize = 10,
|
||||
enableRowSelection = false,
|
||||
EditorComponent,
|
||||
|
||||
getRowId,
|
||||
|
||||
@ -125,7 +116,6 @@ export function DataTable<TData, TValue>({
|
||||
React.useState<VisibilityState>(inititalcolumnVisibility);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
||||
const [colSizes, setColSizes] = React.useState<ColumnSizingState>({});
|
||||
const [editIndex, setEditIndex] = React.useState<number | null>(null);
|
||||
|
||||
// Configuración TanStack
|
||||
const table = useReactTable({
|
||||
@ -139,7 +129,7 @@ export function DataTable<TData, TValue>({
|
||||
getRowId ??
|
||||
((originalRow: TData, i: number) => {
|
||||
const row = originalRow as { id?: string | number };
|
||||
return row.id !== undefined ? String(row.id) : String(i);
|
||||
return row.id === undefined ? String(i) : String(row.id);
|
||||
}),
|
||||
|
||||
state: {
|
||||
@ -183,8 +173,6 @@ export function DataTable<TData, TValue>({
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
});
|
||||
|
||||
const handleCloseEditor = React.useCallback(() => setEditIndex(null), []);
|
||||
|
||||
// Render principal
|
||||
return (
|
||||
<div className="transition-[max-height] duration-300 ease-in-out">
|
||||
@ -192,9 +180,9 @@ export function DataTable<TData, TValue>({
|
||||
<DataTableToolbar showViewOptions={!readOnly} table={table} />
|
||||
|
||||
<div className="overflow-hidden rounded-md border">
|
||||
<TableComp className="w-full text-sm">
|
||||
<TableComp>
|
||||
{/* CABECERA */}
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((hg) => (
|
||||
<TableRow key={hg.id}>
|
||||
{hg.headers.map((h) => {
|
||||
@ -230,9 +218,6 @@ export function DataTable<TData, TValue>({
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
key={row.id}
|
||||
onClick={(e) => onRowClick?.(row.original, rowIndex, e)}
|
||||
onDoubleClick={
|
||||
readOnly || onRowClick ? undefined : () => setEditIndex(rowIndex)
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ")
|
||||
onRowClick?.(row.original, rowIndex, e as any);
|
||||
@ -284,32 +269,6 @@ export function DataTable<TData, TValue>({
|
||||
)}
|
||||
</TableComp>
|
||||
</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>
|
||||
);
|
||||
|
||||
@ -1,74 +1,75 @@
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
Table as TableComp,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableBody,
|
||||
TableCell,
|
||||
Table as TableComp,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
// SkeletonTable.tsx
|
||||
import * as React from "react";
|
||||
import { SkeletonDataTableFooter } from './skeleton-data-table-footer.tsx';
|
||||
|
||||
import { SkeletonDataTableFooter } from "./skeleton-data-table-footer.tsx";
|
||||
|
||||
export type SkeletonTableProps = {
|
||||
columns?: number;
|
||||
rows?: number;
|
||||
stickyHeader?: boolean;
|
||||
showFooter?: boolean;
|
||||
footerProps?: { pageIndex?: number; pageSize?: number; totalItems?: number };
|
||||
}
|
||||
columns?: number;
|
||||
rows?: number;
|
||||
stickyHeader?: boolean;
|
||||
showFooter?: boolean;
|
||||
footerProps?: { pageIndex?: number; pageSize?: number; totalItems?: number };
|
||||
};
|
||||
|
||||
// Componente Skeleton de tabla genérico
|
||||
export const SkeletonDataTable = ({
|
||||
columns = 6,
|
||||
rows = 10,
|
||||
stickyHeader = true,
|
||||
showFooter = true,
|
||||
footerProps,
|
||||
columns = 6,
|
||||
rows = 10,
|
||||
stickyHeader = true,
|
||||
showFooter = true,
|
||||
footerProps,
|
||||
}: SkeletonTableProps) => {
|
||||
// Genera arrays para mapear
|
||||
const cols = React.useMemo(() => Array.from({ length: columns }), [columns]);
|
||||
const rws = React.useMemo(() => Array.from({ length: rows }), [rows]);
|
||||
// Genera arrays para mapear
|
||||
const cols = React.useMemo(() => Array.from({ length: columns }), [columns]);
|
||||
const rws = React.useMemo(() => Array.from({ length: rows }), [rows]);
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-md border bg-background" role="status" aria-busy="true">
|
||||
<TableComp className="w-full text-sm">
|
||||
<TableHeader className={stickyHeader ? "sticky top-0 z-10 bg-muted" : ""}>
|
||||
<TableRow>
|
||||
{cols.map((_, i) => (
|
||||
<TableHead key={`sk-th-${i}`}>
|
||||
<div className="h-4 w-24 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none" />
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
return (
|
||||
<div aria-busy="true" className="overflow-hidden rounded-md border bg-background" role="status">
|
||||
<TableComp className="w-full text-sm">
|
||||
<TableHeader className={stickyHeader ? "sticky top-0 z-10 bg-muted" : ""}>
|
||||
<TableRow>
|
||||
{cols.map((_, i) => (
|
||||
<TableHead key={`sk-th-${i}`}>
|
||||
<div className="h-4 w-24 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none" />
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{rws.map((_, r) => (
|
||||
<TableRow key={`sk-tr-${r}`}>
|
||||
{cols.map((_, c) => (
|
||||
<TableCell key={`sk-td-${r}-${c}`} className="align-top">
|
||||
<div
|
||||
className={[
|
||||
"h-4 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none",
|
||||
c === 0 ? "w-36" : "w-full",
|
||||
"my-2",
|
||||
].join(" ")}
|
||||
style={{ maxWidth: c % 3 === 0 ? "14rem" : c % 3 === 1 ? "10rem" : "100%" }}
|
||||
/>
|
||||
{c > 0 && (
|
||||
<div className="mt-2 h-3 w-1/2 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none" />
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</TableComp>
|
||||
<TableBody>
|
||||
{rws.map((_, r) => (
|
||||
<TableRow key={`sk-tr-${r}`}>
|
||||
{cols.map((_, c) => (
|
||||
<TableCell className="align-top" key={`sk-td-${r}-${c}`}>
|
||||
<div
|
||||
className={[
|
||||
"h-4 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none",
|
||||
c === 0 ? "w-36" : "w-full",
|
||||
"my-2",
|
||||
].join(" ")}
|
||||
style={{ maxWidth: c % 3 === 0 ? "14rem" : c % 3 === 1 ? "10rem" : "100%" }}
|
||||
/>
|
||||
{c > 0 && (
|
||||
<div className="mt-2 h-3 w-1/2 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none" />
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</TableComp>
|
||||
|
||||
{showFooter && <SkeletonDataTableFooter {...footerProps} />}
|
||||
{showFooter && <SkeletonDataTableFooter {...footerProps} />}
|
||||
|
||||
<span className="sr-only">Loading table…</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<span className="sr-only">Loading table…</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,7 +2,6 @@ import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
Button,
|
||||
Separator,
|
||||
SidebarTrigger,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
@ -22,12 +21,10 @@ export function AppTopbar() {
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ProfileDropdown
|
||||
trigger={
|
||||
<Button className="size-9.5" size="icon" variant="ghost">
|
||||
<Avatar className="size-9.5 rounded-md">
|
||||
<AvatarImage src="https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png" />
|
||||
<AvatarFallback>JD</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
<Avatar className="size-9.5 rounded-md">
|
||||
<AvatarImage src="https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png" />
|
||||
<AvatarFallback>JD</AvatarFallback>
|
||||
</Avatar>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -3,7 +3,6 @@ export const LogoVerifactu = ({ color = "black", width = "100%", ...props }) =>
|
||||
<div {...props}>
|
||||
<svg
|
||||
{...props}
|
||||
height="auto"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
style={{
|
||||
color,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user