.
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";
|
import { Outlet, type RouteObject } from "react-router-dom";
|
||||||
|
|
||||||
const ProformaLayout = lazy(() =>
|
const ProformaLayout = lazy(() =>
|
||||||
import("./proformas/shared").then((m) => ({ default: m.ProformaLayout }))
|
import("./proformas/shared/ui").then((m) => ({ default: m.ProformaLayout }))
|
||||||
);
|
);
|
||||||
|
|
||||||
const ProformasListPage = lazy(() =>
|
const ProformasListPage = lazy(() =>
|
||||||
@ -19,7 +19,7 @@ const ProformaUpdatePage = lazy(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const IssuedInvoicesLayout = lazy(() =>
|
const IssuedInvoicesLayout = lazy(() =>
|
||||||
import("./issued-invoices/shared").then((m) => ({ default: m.IssuedInvoicesLayout }))
|
import("./issued-invoices/shared/ui").then((m) => ({ default: m.IssuedInvoicesLayout }))
|
||||||
);
|
);
|
||||||
|
|
||||||
const IssuedInvoiceListPage = lazy(() =>
|
const IssuedInvoiceListPage = lazy(() =>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
|
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { useDownloadInvoicePDFQuery } from "../hooks";
|
import { useDownloadInvoicePDFQuery } from "../../shared";
|
||||||
|
|
||||||
interface PendingDownload {
|
interface PendingDownload {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@ -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 React from "react";
|
||||||
|
|
||||||
import { useDownloadInvoicePDFController } from "../../download-pdf/controller";
|
import { useDownloadInvoicePDFController } from "../../download-pdf/controller";
|
||||||
import type { IssuedInvoiceSummaryData } from "../../types";
|
import type { IssuedInvoiceListRow } from "../../shared";
|
||||||
|
|
||||||
import { useIssuedInvoiceListController } from "./use-issued-invoice-list.controller";
|
import { useIssuedInvoiceListController } from "./use-issued-invoice-list.controller";
|
||||||
|
|
||||||
@ -10,8 +10,8 @@ export function useIssuedInvoiceListPageController() {
|
|||||||
const downloadPDFCtrl = useDownloadInvoicePDFController();
|
const downloadPDFCtrl = useDownloadInvoicePDFController();
|
||||||
|
|
||||||
const handleDownloadPDF = React.useCallback(
|
const handleDownloadPDF = React.useCallback(
|
||||||
(issuedInvoice: IssuedInvoiceSummaryData) => {
|
(issuedInvoice: IssuedInvoiceListRow) => {
|
||||||
downloadPDFCtrl.download(issuedInvoice.id, issuedInvoice.invoice_number);
|
downloadPDFCtrl.download(issuedInvoice.id, issuedInvoice.invoiceNumber);
|
||||||
},
|
},
|
||||||
[downloadPDFCtrl]
|
[downloadPDFCtrl]
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,51 +1,101 @@
|
|||||||
import type { CriteriaDTO } from "@erp/core";
|
|
||||||
import { useDebounce } from "@repo/rdx-ui/components";
|
import { useDebounce } from "@repo/rdx-ui/components";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
import { IssuedInvoiceSummaryDtoAdapter } from "../adapters";
|
import type {
|
||||||
import { useIssuedInvoiceListQuery } from "../hooks";
|
IssuedInvoiceList,
|
||||||
|
IssuedInvoiceStatus,
|
||||||
|
ListIssuedInvoicesByCriteriaParams,
|
||||||
|
} from "../../shared";
|
||||||
|
import { useIssuedInvoiceListQuery } from "../../shared/";
|
||||||
|
|
||||||
|
type IssuedInvoiceListStatusFilter = "all" | IssuedInvoiceStatus;
|
||||||
|
|
||||||
|
const EMPTY_ISSUED_INVOICES_LIST: IssuedInvoiceList = {
|
||||||
|
items: [],
|
||||||
|
page: 0,
|
||||||
|
perPage: 5,
|
||||||
|
totalPages: 0,
|
||||||
|
totalItems: 0,
|
||||||
|
};
|
||||||
|
|
||||||
export const useIssuedInvoiceListController = () => {
|
export const useIssuedInvoiceListController = () => {
|
||||||
const [pageIndex, setPageIndex] = useState(0);
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(5);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [status, setStatus] = useState("all");
|
const [statusFilter, setStatusFilter] = useState<IssuedInvoiceListStatusFilter>("all");
|
||||||
|
|
||||||
const debouncedQ = useDebounce(search, 300);
|
const debouncedSearch = useDebounce(search, 300);
|
||||||
|
|
||||||
const criteria = useMemo<CriteriaDTO>(() => {
|
const criteria = useMemo<NonNullable<ListIssuedInvoicesByCriteriaParams["criteria"]>>(
|
||||||
const baseFilters =
|
() => ({
|
||||||
status !== "all" ? [{ field: "status", operator: "CONTAINS", value: status }] : [];
|
q: debouncedSearch || "",
|
||||||
|
|
||||||
return {
|
|
||||||
q: debouncedQ || "",
|
|
||||||
pageSize,
|
|
||||||
pageNumber: pageIndex,
|
pageNumber: pageIndex,
|
||||||
|
pageSize,
|
||||||
order: "desc",
|
order: "desc",
|
||||||
orderBy: "invoice_date",
|
orderBy: "invoice_date",
|
||||||
filters: baseFilters,
|
filters: status === "all" ? [] : [{ field: "status", operator: "eq", value: status }],
|
||||||
};
|
}),
|
||||||
}, [pageSize, pageIndex, debouncedQ, status]);
|
[debouncedSearch, pageIndex, pageSize, status]
|
||||||
|
|
||||||
const query = useIssuedInvoiceListQuery({ criteria });
|
|
||||||
const data = useMemo(
|
|
||||||
() => (query.data ? IssuedInvoiceSummaryDtoAdapter.fromDto(query.data) : undefined),
|
|
||||||
[query.data]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const setSearchValue = (value: string) => setSearch(value.trim().replace(/\s+/g, " "));
|
const query = useIssuedInvoiceListQuery({ criteria });
|
||||||
|
|
||||||
const setStatusFilter = (newStatus: string) => setStatus(newStatus);
|
const setStatusFilterValue = (value: string) => {
|
||||||
|
const nextValue = (value || "all") as IssuedInvoiceListStatusFilter;
|
||||||
|
|
||||||
|
setStatusFilter((prev) => {
|
||||||
|
if (prev === nextValue) return prev;
|
||||||
|
|
||||||
|
// Sólo si la búsqueda realmente cambia,
|
||||||
|
// reseteamos la página a 0 para evitar inconsistencias
|
||||||
|
setPageIndex(0);
|
||||||
|
return nextValue;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSearchValue = (value: string) => {
|
||||||
|
const nextValue = value.trim().replace(/\s+/g, " ");
|
||||||
|
|
||||||
|
setSearch((prev) => {
|
||||||
|
if (prev === nextValue) return prev;
|
||||||
|
|
||||||
|
// Sólo si la búsqueda realmente cambia,
|
||||||
|
// reseteamos la página a 0 para evitar inconsistencias
|
||||||
|
setPageIndex(0);
|
||||||
|
return nextValue;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPageSizeValue = (value: number) => {
|
||||||
|
setPageSize((prev) => {
|
||||||
|
if (prev === value) return prev;
|
||||||
|
|
||||||
|
// Sólo si el tamaño de página realmente cambia,
|
||||||
|
// reseteamos la página a 0 para evitar inconsistencias
|
||||||
|
setPageIndex(0);
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...query,
|
data: query.data ?? EMPTY_ISSUED_INVOICES_LIST,
|
||||||
data,
|
isLoading: query.isLoading,
|
||||||
|
isFetching: query.isFetching,
|
||||||
|
|
||||||
|
isError: query.isError,
|
||||||
|
error: query.error,
|
||||||
|
|
||||||
|
refetch: query.refetch,
|
||||||
|
|
||||||
pageIndex,
|
pageIndex,
|
||||||
pageSize,
|
pageSize,
|
||||||
search,
|
|
||||||
setPageIndex,
|
setPageIndex,
|
||||||
setPageSize,
|
setPageSize: setPageSizeValue,
|
||||||
|
|
||||||
|
search,
|
||||||
setSearchValue,
|
setSearchValue,
|
||||||
setStatusFilter,
|
|
||||||
|
statusFilter,
|
||||||
|
setStatusFilter: setStatusFilterValue,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,42 +1,37 @@
|
|||||||
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
|
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../../../i18n";
|
import { useTranslation } from "../../../../../i18n";
|
||||||
import type { IssuedInvoiceSummaryData, IssuedInvoiceSummaryPageData } from "../../../../types";
|
import type { IssuedInvoiceList, IssuedInvoiceListRow } from "../../../../shared";
|
||||||
|
|
||||||
export type InvoiceUpdateCompProps = {
|
interface IssuedInvoicesGridProps {
|
||||||
data: IssuedInvoiceSummaryPageData;
|
data?: IssuedInvoiceList;
|
||||||
loading?: boolean;
|
loading: boolean;
|
||||||
|
fetching?: boolean;
|
||||||
|
|
||||||
columns: ColumnDef<IssuedInvoiceSummaryData, unknown>[];
|
columns: ColumnDef<IssuedInvoiceListRow, unknown>[];
|
||||||
|
|
||||||
pageIndex: number;
|
pageIndex: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
onPageChange?: (pageNumber: number) => void;
|
onPageChange: (pageIndex: number) => void;
|
||||||
onPageSizeChange?: (pageSize: number) => void;
|
onPageSizeChange: (size: number) => void;
|
||||||
|
|
||||||
onRowClick?: (
|
onRowClick?: (proformaId: string) => void;
|
||||||
row: IssuedInvoiceSummaryPageData,
|
}
|
||||||
index: number,
|
|
||||||
event: React.MouseEvent<HTMLTableRowElement>
|
|
||||||
) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create new GridExample component
|
|
||||||
export const IssuedInvoicesGrid = ({
|
export const IssuedInvoicesGrid = ({
|
||||||
data,
|
data,
|
||||||
loading,
|
loading,
|
||||||
|
fetching,
|
||||||
columns,
|
columns,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
pageSize,
|
pageSize,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
onPageSizeChange,
|
onPageSizeChange,
|
||||||
onRowClick,
|
onRowClick,
|
||||||
}: InvoiceUpdateCompProps) => {
|
}: IssuedInvoicesGridProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const { items, totalItems } = data || { items: [], totalItems: 0 };
|
||||||
const { items, total_items } = data;
|
|
||||||
|
|
||||||
// Navegación accesible (click o teclado)
|
// Navegación accesible (click o teclado)
|
||||||
/* const goToRow = useCallback(
|
/* const goToRow = useCallback(
|
||||||
@ -90,36 +85,29 @@ export const IssuedInvoicesGrid = ({
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<SkeletonDataTable
|
||||||
<SkeletonDataTable
|
columns={columns.length}
|
||||||
columns={columns.length}
|
footerProps={{ pageIndex, pageSize, totalItems: 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 (
|
||||||
<>
|
<DataTable
|
||||||
{/*<div className={preview.isPinned ? "flex-1 mr-[500px]" : "flex-1"}>*/}
|
columns={columns}
|
||||||
<DataTable
|
data={items}
|
||||||
columns={columns}
|
enablePagination={false}
|
||||||
data={items}
|
enableRowSelection
|
||||||
enablePagination
|
manualPagination
|
||||||
enableRowSelection
|
onPageChange={onPageChange}
|
||||||
manualPagination
|
onPageSizeChange={onPageSizeChange}
|
||||||
onPageChange={onPageChange}
|
//onRowClick={(row) => onRowClick?.(row.id)}
|
||||||
onPageSizeChange={onPageSizeChange}
|
pageIndex={pageIndex}
|
||||||
//onRowClick={handleRowClick}
|
pageSize={pageSize}
|
||||||
pageIndex={pageIndex}
|
totalItems={totalItems}
|
||||||
pageSize={pageSize}
|
/>
|
||||||
readOnly
|
|
||||||
totalItems={total_items}
|
|
||||||
/>
|
|
||||||
{/*</div>*/}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
Spinner,
|
Spinner,
|
||||||
@ -18,21 +19,21 @@ import * as React from "react";
|
|||||||
import QrCode from "react-qr-code";
|
import QrCode from "react-qr-code";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../../i18n";
|
import { useTranslation } from "../../../../../i18n";
|
||||||
import type { IssuedInvoiceSummaryData } from "../../../../types";
|
import type { IssuedInvoiceListRow } from "../../../../shared";
|
||||||
import { VerifactuStatusBadge } from "../../components";
|
import { VerifactuStatusBadge } from "../../components";
|
||||||
|
|
||||||
type GridActionHandlers = {
|
type GridActionHandlers = {
|
||||||
onDownloadPdf?: (issuedInvoice: IssuedInvoiceSummaryData) => void;
|
onDownloadPdf?: (issuedInvoice: IssuedInvoiceListRow) => void;
|
||||||
pdfDownloadingId?: string | null;
|
pdfDownloadingId?: string | null;
|
||||||
isPdfDownloading?: boolean;
|
isPdfDownloading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useIssuedInvoicesGridColumns(
|
export function useIssuedInvoicesGridColumns(
|
||||||
actionHandlers: GridActionHandlers = {}
|
actionHandlers: GridActionHandlers = {}
|
||||||
): ColumnDef<IssuedInvoiceSummaryData, unknown>[] {
|
): ColumnDef<IssuedInvoiceListRow, unknown>[] {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return React.useMemo<ColumnDef<IssuedInvoiceSummaryData>[]>(
|
return React.useMemo<ColumnDef<IssuedInvoiceListRow, unknown>[]>(
|
||||||
() => [
|
() => [
|
||||||
// Nº
|
// Nº
|
||||||
{
|
{
|
||||||
@ -47,7 +48,7 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="text-right tabular-nums">
|
<div className="text-right tabular-nums">
|
||||||
{row.original.series}
|
{row.original.series}
|
||||||
{row.original.invoice_number}
|
{row.original.invoiceNumber}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
@ -106,11 +107,13 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
<QrCodeIcon className="size-8 text-muted-foreground" />
|
<QrCodeIcon className="size-8 text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger
|
||||||
<a href={verifactu.url} rel="noopener" target="_blank">
|
render={
|
||||||
<QrCodeIcon className="size-8" />
|
<a href={verifactu.url} rel="noopener" target="_blank">
|
||||||
</a>
|
<QrCodeIcon className="size-8" />
|
||||||
</TooltipTrigger>
|
</a>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TooltipContent className="m-0 p-3">
|
<TooltipContent className="m-0 p-3">
|
||||||
<QrCode className="bg-white p-8" value={verifactu.url} />
|
<QrCode className="bg-white p-8" value={verifactu.url} />
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
@ -192,7 +195,7 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="font-medium text-left tabular-nums">
|
<div className="font-medium text-left tabular-nums">
|
||||||
{formatDate(row.original.invoice_date)}
|
{formatDate(row.original.invoiceDate)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
@ -214,7 +217,7 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="font-medium text-left tabular-nums">
|
<div className="font-medium text-left tabular-nums">
|
||||||
{formatDate(row.original.operation_date)}
|
{formatDate(row.original.operationDate)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
@ -237,7 +240,7 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="font-medium text-right tabular-nums">
|
<div className="font-medium text-right tabular-nums">
|
||||||
{row.original.subtotal_amount_fmt}
|
{row.original.subtotalAmountFmt}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
@ -260,7 +263,7 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="font-medium text-right tabular-nums">
|
<div className="font-medium text-right tabular-nums">
|
||||||
{row.original.discount_amount_fmt}
|
{row.original.totalDiscountAmountFmt}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
@ -282,7 +285,7 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="font-medium text-right tabular-nums">{row.original.taxes_amount_fmt}</div>
|
<div className="font-medium text-right tabular-nums">{row.original.taxesAmountFmt}</div>
|
||||||
),
|
),
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
size: 120,
|
size: 120,
|
||||||
@ -303,9 +306,7 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="font-semibold text-right tabular-nums">
|
<div className="font-semibold text-right tabular-nums">{row.original.totalAmountFmt}</div>
|
||||||
{row.original.total_amount_fmt}
|
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
size: 140,
|
size: 140,
|
||||||
@ -343,25 +344,27 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
{/* Descargar en PDF */}
|
{/* Descargar en PDF */}
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger
|
||||||
<Button
|
render={
|
||||||
className={"size-8"}
|
<Button
|
||||||
disabled={isPDFLoading || !isCompleted}
|
className={"size-8"}
|
||||||
onClick={(e) => {
|
disabled={isPDFLoading || !isCompleted}
|
||||||
stop(e);
|
onClick={(e) => {
|
||||||
isCompleted ? actionHandlers.onDownloadPdf?.(issuedInvoice) : null;
|
stop(e);
|
||||||
}}
|
isCompleted ? actionHandlers.onDownloadPdf?.(issuedInvoice) : null;
|
||||||
size="icon"
|
}}
|
||||||
variant="ghost"
|
size="icon"
|
||||||
>
|
variant="ghost"
|
||||||
{isPDFLoading ? (
|
>
|
||||||
<Spinner className="size-4 cursor-progress" />
|
{isPDFLoading ? (
|
||||||
) : (
|
<Spinner className="size-4 cursor-progress" />
|
||||||
<FileDownIcon className="size-4 cursor-pointer" />
|
) : (
|
||||||
)}
|
<FileDownIcon className="size-4 cursor-pointer" />
|
||||||
<span className="sr-only">Descargar PDF</span>
|
)}
|
||||||
</Button>
|
<span className="sr-only">Descargar PDF</span>
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TooltipContent>Descargar PDF</TooltipContent>
|
<TooltipContent>Descargar PDF</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
@ -369,31 +372,35 @@ 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
|
||||||
<Button
|
render={
|
||||||
aria-label={t("common.more_actions")}
|
<Button
|
||||||
className="cursor-pointer text-muted-foreground hover:text-primary"
|
aria-label={t("common.more_actions")}
|
||||||
onClick={stop}
|
className="cursor-pointer text-muted-foreground hover:text-primary"
|
||||||
size="sm"
|
onClick={stop}
|
||||||
type="button"
|
size="sm"
|
||||||
variant="ghost"
|
type="button"
|
||||||
>
|
variant="ghost"
|
||||||
<MoreVerticalIcon aria-hidden="true" className="size-4" />
|
>
|
||||||
<span className="sr-only">{t("common.more_actions")}</span>
|
<MoreVerticalIcon aria-hidden="true" className="size-4" />
|
||||||
</Button>
|
<span className="sr-only">{t("common.more_actions")}</span>
|
||||||
</DropdownMenuTrigger>
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<DropdownMenuContent align="end" className="w-48">
|
<DropdownMenuContent align="end" className="w-48">
|
||||||
<DropdownMenuItem
|
<DropdownMenuGroup>
|
||||||
className="cursor-pointer"
|
<DropdownMenuItem
|
||||||
onClick={() => actionHandlers.onDownloadPdf?.(issuedInvoice)}
|
className="cursor-pointer"
|
||||||
>
|
onClick={() => actionHandlers.onDownloadPdf?.(issuedInvoice)}
|
||||||
<DownloadIcon className="mr-2 size-4" />
|
>
|
||||||
{t("common.download_pdf")}
|
<DownloadIcon className="mr-2 size-4" />
|
||||||
</DropdownMenuItem>
|
{t("common.download_pdf")}
|
||||||
<DropdownMenuItem className="cursor-pointer" onClick={stop}>
|
</DropdownMenuItem>
|
||||||
<MailIcon className="mr-2 size-4" />
|
<DropdownMenuItem className="cursor-pointer" onClick={stop}>
|
||||||
{t("common.send_email")}
|
<MailIcon className="mr-2 size-4" />
|
||||||
</DropdownMenuItem>{" "}
|
{t("common.send_email")}
|
||||||
|
</DropdownMenuItem>{" "}
|
||||||
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
getVerifactuRecordStatusButtonVariant,
|
getVerifactuRecordStatusButtonVariant,
|
||||||
getVerifactuRecordStatusColor,
|
getVerifactuRecordStatusColor,
|
||||||
getVerifactuRecordStatusIcon,
|
getVerifactuRecordStatusIcon,
|
||||||
} from "../../../types";
|
} from "../../../shared";
|
||||||
|
|
||||||
export type VerifactuStatusBadgeProps = {
|
export type VerifactuStatusBadgeProps = {
|
||||||
status: string | VerifactuRecordStatus; // permitir cualquier valor
|
status: string | VerifactuRecordStatus; // permitir cualquier valor
|
||||||
@ -21,21 +21,23 @@ export const VerifactuStatusBadge = ({ status, className }: VerifactuStatusBadge
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger
|
||||||
<Badge
|
render={
|
||||||
className={cn(
|
<Badge
|
||||||
getVerifactuRecordStatusColor(normalizedStatus),
|
className={cn(
|
||||||
"font-semibold",
|
getVerifactuRecordStatusColor(normalizedStatus),
|
||||||
className
|
"font-semibold",
|
||||||
)}
|
className
|
||||||
variant={getVerifactuRecordStatusButtonVariant(normalizedStatus)}
|
)}
|
||||||
>
|
variant={getVerifactuRecordStatusButtonVariant(normalizedStatus)}
|
||||||
<Icon />
|
>
|
||||||
{t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.label`, {
|
<Icon />
|
||||||
defaultValue: status,
|
{t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.label`, {
|
||||||
})}
|
defaultValue: status,
|
||||||
</Badge>
|
})}
|
||||||
</TooltipTrigger>
|
</Badge>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>{t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.description`)}</p>
|
<p>{t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.description`)}</p>
|
||||||
</TooltipContent>
|
</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 { useIssuedInvoiceListPageController } from "../../controllers";
|
||||||
import { IssuedInvoicesGrid, useIssuedInvoicesGridColumns } from "../blocks";
|
import { IssuedInvoicesGrid, useIssuedInvoicesGridColumns } from "../blocks";
|
||||||
|
|
||||||
export const IssuedInvoiceListPage = () => {
|
export const ListIssuedInvoicesPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
listCtrl,
|
listCtrl,
|
||||||
handleDownloadPDF,
|
handleDownloadPDF,
|
||||||
@ -43,7 +43,7 @@ export const IssuedInvoiceListPage = () => {
|
|||||||
widthClass: "w-[500px]",
|
widthClass: "w-[500px]",
|
||||||
});*/
|
});*/
|
||||||
|
|
||||||
if (listCtrl.isError || !listCtrl.data) {
|
if (listCtrl.isError) {
|
||||||
return (
|
return (
|
||||||
<AppContent>
|
<AppContent>
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
@ -56,7 +56,7 @@ export const IssuedInvoiceListPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<section>
|
||||||
<AppHeader>
|
<AppHeader>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
description={t("pages.issued_invoices.list.description")}
|
description={t("pages.issued_invoices.list.description")}
|
||||||
@ -73,7 +73,8 @@ export const IssuedInvoiceListPage = () => {
|
|||||||
title={t("pages.issued_invoices.list.title")}
|
title={t("pages.issued_invoices.list.title")}
|
||||||
/>
|
/>
|
||||||
</AppHeader>
|
</AppHeader>
|
||||||
<AppContent>
|
|
||||||
|
<AppContent className="space-y-6">
|
||||||
<Alert className="bg-green-50 text-green-800 flex items-center justify-between gap-6">
|
<Alert className="bg-green-50 text-green-800 flex items-center justify-between gap-6">
|
||||||
<div>
|
<div>
|
||||||
<AlertTitle className="font-semibold text-green-800">¡Atención!</AlertTitle>
|
<AlertTitle className="font-semibold text-green-800">¡Atención!</AlertTitle>
|
||||||
@ -87,13 +88,17 @@ export const IssuedInvoiceListPage = () => {
|
|||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
{/* Search and filters */}
|
{/* Search and filters */}
|
||||||
<div className="flex items-center justify-between gap-16">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<SimpleSearchInput
|
<SimpleSearchInput
|
||||||
loading={listCtrl.isLoading}
|
loading={listCtrl.isLoading}
|
||||||
onSearchChange={listCtrl.setSearchValue}
|
onSearchChange={listCtrl.setSearchValue}
|
||||||
|
value={listCtrl.search}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select defaultValue="all" onValueChange={listCtrl.setStatusFilter}>
|
<Select
|
||||||
|
onValueChange={(value) => listCtrl.setStatusFilter(value ?? "all")}
|
||||||
|
value={listCtrl.statusFilter}
|
||||||
|
>
|
||||||
<SelectTrigger className="w-full sm:w-48">
|
<SelectTrigger className="w-full sm:w-48">
|
||||||
<FilterIcon aria-hidden className="mr-2 size-4" />
|
<FilterIcon aria-hidden className="mr-2 size-4" />
|
||||||
<SelectValue placeholder={t("filters.status")} />
|
<SelectValue placeholder={t("filters.status")} />
|
||||||
@ -127,19 +132,20 @@ export const IssuedInvoiceListPage = () => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="min-h-0 flex-1 overflow-auto">
|
||||||
<IssuedInvoicesGrid
|
<IssuedInvoicesGrid
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={listCtrl.data}
|
data={listCtrl.data}
|
||||||
loading={listCtrl.isLoading}
|
loading={listCtrl.isLoading}
|
||||||
onPageChange={listCtrl.setPageIndex}
|
onPageChange={listCtrl.setPageIndex}
|
||||||
onPageSizeChange={listCtrl.setPageSize}
|
onPageSizeChange={listCtrl.setPageSize}
|
||||||
// acciones rápidas del grid → page controller
|
// acciones rápidas del grid → page controller
|
||||||
//onRowClick={(id) => navigate(`/issuedInvoices/${id}`)}
|
//onRowClick={(id) => navigate(`/issuedInvoices/${id}`)}
|
||||||
pageIndex={listCtrl.pageIndex}
|
pageIndex={listCtrl.pageIndex}
|
||||||
pageSize={listCtrl.pageSize}
|
pageSize={listCtrl.pageSize}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</AppContent>
|
</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 type { CriteriaDTO } from "@erp/core";
|
||||||
import { useDataSource } from "@erp/core/hooks";
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria";
|
import { type DefaultError, useQuery } from "@tanstack/react-query";
|
||||||
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import { getIssuedInvoiceListApi } from "../../list/api";
|
import { ListIssuedInvoicesAdapter } from "../adapters";
|
||||||
import type { IssuedInvoiceSummaryPage } from "../../types";
|
import { getListIssuedInvoicesByCriteria } from "../api";
|
||||||
|
import type { IssuedInvoiceList } from "../entities";
|
||||||
|
|
||||||
export const ISSUED_INVOICES_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => [
|
import { LIST_ISSUED_INVOICES_QUERY_KEY } from "./keys";
|
||||||
"issued_invoices",
|
|
||||||
{
|
|
||||||
pageNumber: criteria?.pageNumber ?? INITIAL_PAGE_INDEX,
|
|
||||||
pageSize: criteria?.pageSize ?? INITIAL_PAGE_SIZE,
|
|
||||||
q: criteria?.q ?? "",
|
|
||||||
filters: criteria?.filters ?? [],
|
|
||||||
orderBy: criteria?.orderBy ?? "",
|
|
||||||
order: criteria?.order ?? "",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
type IssuedInvoicesQueryOptions = {
|
export interface IssuedInvoicesListQueryOptions {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
criteria?: CriteriaDTO;
|
criteria?: Partial<CriteriaDTO>;
|
||||||
};
|
}
|
||||||
|
|
||||||
// Obtener todas las facturas
|
// Obtener todas las facturas
|
||||||
export const useIssuedInvoiceListQuery = (options?: IssuedInvoicesQueryOptions) => {
|
export const useIssuedInvoiceListQuery = (options?: IssuedInvoicesListQueryOptions) => {
|
||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const enabled = options?.enabled ?? true;
|
const enabled = options?.enabled ?? true;
|
||||||
const criteria = options?.criteria ?? {};
|
const criteria = options?.criteria ?? {};
|
||||||
|
|
||||||
return useQuery<IssuedInvoiceSummaryPage, DefaultError>({
|
return useQuery<IssuedInvoiceList, DefaultError>({
|
||||||
queryKey: ISSUED_INVOICES_QUERY_KEY(criteria),
|
queryKey: LIST_ISSUED_INVOICES_QUERY_KEY(criteria),
|
||||||
queryFn: async ({ signal }) => getIssuedInvoiceListApi(dataSource, signal, criteria),
|
queryFn: async ({ signal }) => {
|
||||||
|
const dto = await getListIssuedInvoicesByCriteria(dataSource, { signal, criteria });
|
||||||
|
return ListIssuedInvoicesAdapter.fromDto(dto);
|
||||||
|
},
|
||||||
enabled,
|
enabled,
|
||||||
staleTime: 5000,
|
placeholderData: (previousData) => previousData, // Mantiene la página anterior durante refetch por cambio de criteria
|
||||||
placeholderData: (previousData, _previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1 +1,5 @@
|
|||||||
|
export * from "./adapters";
|
||||||
|
export * from "./api";
|
||||||
|
export * from "./entities";
|
||||||
export * from "./hooks";
|
export * from "./hooks";
|
||||||
|
export * from "./ui";
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
|
|
||||||
export const IssuedInvoicesLayout = ({ children }: PropsWithChildren) => {
|
export const IssuedInvoicesLayout = ({ children }: PropsWithChildren) => {
|
||||||
return <div>{children}</div>;
|
return <div className="space-y-4">{children}</div>;
|
||||||
};
|
};
|
||||||
@ -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 && (
|
{isIssued && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger
|
||||||
<Button
|
render={
|
||||||
className="size-6 text-foreground hover:text-primary"
|
<Button
|
||||||
size="icon"
|
className="size-6 text-foreground hover:text-primary"
|
||||||
variant="ghost"
|
size="icon"
|
||||||
>
|
variant="ghost"
|
||||||
<a href={`/facturas/${invoiceId}`}>
|
>
|
||||||
<ExternalLinkIcon />
|
<a href={`/facturas/${invoiceId}`}>
|
||||||
<span className="sr-only">Ver factura {invoiceId}</span>
|
<ExternalLinkIcon />
|
||||||
</a>
|
<span className="sr-only">Ver factura {invoiceId}</span>
|
||||||
</Button>
|
</a>
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TooltipContent>Ver factura {invoiceId}</TooltipContent>
|
<TooltipContent>Ver factura {invoiceId}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@ -235,17 +237,19 @@ export function useProformasGridColumns(
|
|||||||
{!isIssued && actionHandlers.onEditClick && (
|
{!isIssued && actionHandlers.onEditClick && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger
|
||||||
<Button
|
render={
|
||||||
className="size-8 cursor-pointer"
|
<Button
|
||||||
onClick={() => actionHandlers.onEditClick?.(proforma)}
|
className="size-8 cursor-pointer"
|
||||||
size="icon"
|
onClick={() => actionHandlers.onEditClick?.(proforma)}
|
||||||
variant="ghost"
|
size="icon"
|
||||||
>
|
variant="ghost"
|
||||||
<PencilIcon className="size-4" />
|
>
|
||||||
<span className="sr-only">Editar</span>
|
<PencilIcon className="size-4" />
|
||||||
</Button>
|
<span className="sr-only">Editar</span>
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TooltipContent>Editar</TooltipContent>
|
<TooltipContent>Editar</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@ -255,19 +259,21 @@ 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
|
||||||
<Button
|
render={
|
||||||
className="size-8 cursor-pointer"
|
<Button
|
||||||
onClick={() =>
|
className="size-8 cursor-pointer"
|
||||||
actionHandlers.onChangeStatusClick?.(proforma, availableTransitions[0])
|
onClick={() =>
|
||||||
}
|
actionHandlers.onChangeStatusClick?.(proforma, availableTransitions[0])
|
||||||
size="icon"
|
}
|
||||||
variant="ghost"
|
size="icon"
|
||||||
>
|
variant="ghost"
|
||||||
<RefreshCwIcon className="size-4" />
|
>
|
||||||
<span className="sr-only">Cambiar estado</span>
|
<RefreshCwIcon className="size-4" />
|
||||||
</Button>
|
<span className="sr-only">Cambiar estado</span>
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
Cambiar a {t(`catalog.proformas.status.${availableTransitions[0]}.label`)}
|
Cambiar a {t(`catalog.proformas.status.${availableTransitions[0]}.label`)}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
@ -279,16 +285,18 @@ export function useProformasGridColumns(
|
|||||||
{!isIssued && isApproved && actionHandlers.onIssueClick && (
|
{!isIssued && isApproved && actionHandlers.onIssueClick && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger
|
||||||
<Button
|
render={
|
||||||
className="size-8 cursor-pointer"
|
<Button
|
||||||
onClick={() => actionHandlers.onIssueClick?.(proforma)}
|
className="size-8 cursor-pointer"
|
||||||
size="icon"
|
onClick={() => actionHandlers.onIssueClick?.(proforma)}
|
||||||
variant="ghost"
|
size="icon"
|
||||||
>
|
variant="ghost"
|
||||||
<FileTextIcon className="size-4" />
|
>
|
||||||
</Button>
|
<FileTextIcon className="size-4" />
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TooltipContent>Emitir a factura</TooltipContent>
|
<TooltipContent>Emitir a factura</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@ -298,19 +306,21 @@ export function useProformasGridColumns(
|
|||||||
{!isIssued && actionHandlers.onDeleteClick && (
|
{!isIssued && actionHandlers.onDeleteClick && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger
|
||||||
<Button
|
render={
|
||||||
className="size-8 text-destructive hover:text-destructive cursor-pointer"
|
<Button
|
||||||
onClick={(e) => {
|
className="size-8 text-destructive hover:text-destructive cursor-pointer"
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
actionHandlers.onDeleteClick?.(proforma);
|
e.preventDefault();
|
||||||
}}
|
actionHandlers.onDeleteClick?.(proforma);
|
||||||
size="icon"
|
}}
|
||||||
variant="ghost"
|
size="icon"
|
||||||
>
|
variant="ghost"
|
||||||
<Trash2Icon className="size-4" />
|
>
|
||||||
</Button>
|
<Trash2Icon className="size-4" />
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TooltipContent>Eliminar</TooltipContent>
|
<TooltipContent>Eliminar</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
export * from "./initials";
|
|
||||||
export * from "./proforma-status-badge";
|
export * from "./proforma-status-badge";
|
||||||
|
|||||||
@ -21,15 +21,17 @@ export const ProformaStatusBadge = ({ status, className }: ProformaStatusBadgePr
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger
|
||||||
<Badge
|
render={
|
||||||
className={cn(getProformaStatusColor(normalizedStatus), "font-semibold", className)}
|
<Badge
|
||||||
variant={getProformaStatusButtonVariant(normalizedStatus)}
|
className={cn(getProformaStatusColor(normalizedStatus), "font-semibold", className)}
|
||||||
>
|
variant={getProformaStatusButtonVariant(normalizedStatus)}
|
||||||
<Icon />
|
>
|
||||||
{t(`catalog.proformas.status.${normalizedStatus}.label`, { defaultValue: status })}
|
<Icon />
|
||||||
</Badge>
|
{t(`catalog.proformas.status.${normalizedStatus}.label`, { defaultValue: status })}
|
||||||
</TooltipTrigger>
|
</Badge>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>{t(`catalog.proformas.status.${normalizedStatus}.description`)}</p>
|
<p>{t(`catalog.proformas.status.${normalizedStatus}.description`)}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export const LIST_PROFORMAS_QUERY_KEY = (criteria?: ProformasListRequestDTO): Qu
|
|||||||
...LIST_PROFORMAS_QUERY_KEY_PREFIX,
|
...LIST_PROFORMAS_QUERY_KEY_PREFIX,
|
||||||
{
|
{
|
||||||
pageNumber: criteria?.pageNumber ?? 1,
|
pageNumber: criteria?.pageNumber ?? 1,
|
||||||
pageSize: criteria?.pageSize ?? 10,
|
pageSize: criteria?.pageSize ?? 5,
|
||||||
q: criteria?.q ?? "",
|
q: criteria?.q ?? "",
|
||||||
filters: criteria?.filters ?? [],
|
filters: criteria?.filters ?? [],
|
||||||
orderBy: criteria?.orderBy ?? "",
|
orderBy: criteria?.orderBy ?? "",
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
@ -152,51 +153,55 @@ export function useCustomersGridColumns(
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger
|
||||||
<Button aria-label={t("pages.list.actions.more")} size="icon" variant="ghost">
|
render={
|
||||||
<MoreHorizontalIcon className="size-4" />
|
<Button aria-label={t("pages.list.actions.more")} size="icon" variant="ghost">
|
||||||
</Button>
|
<MoreHorizontalIcon className="size-4" />
|
||||||
</DropdownMenuTrigger>
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<DropdownMenuContent align="end">
|
<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)}>
|
<DropdownMenuItem onClick={() => onViewClick?.(customer)}>
|
||||||
{t("pages.list.actions.view")}
|
{t("pages.list.actions.view")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem onClick={() => onEditClick?.(customer)}>
|
<DropdownMenuItem onClick={() => onEditClick?.(customer)}>
|
||||||
{t("pages.list.actions.edit")}
|
{t("pages.list.actions.edit")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
disabled={!website}
|
disabled={!website}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
window.open(safeHTTPUrl(website), "_blank", "noopener,noreferrer")
|
window.open(safeHTTPUrl(website), "_blank", "noopener,noreferrer")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t("pages.list.actions.visit_website")}
|
{t("pages.list.actions.visit_website")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
disabled={!email_primary}
|
disabled={!email_primary}
|
||||||
onClick={() => navigator.clipboard.writeText(email_primary)}
|
onClick={() => navigator.clipboard.writeText(email_primary)}
|
||||||
>
|
>
|
||||||
{t("pages.list.actions.copy_email")}
|
{t("pages.list.actions.copy_email")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-destructive"
|
className="text-destructive"
|
||||||
onClick={() => onDeleteClick?.(customer)}
|
onClick={() => onDeleteClick?.(customer)}
|
||||||
>
|
>
|
||||||
{t("pages.list.actions.delete")}
|
{t("pages.list.actions.delete")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -30,11 +30,13 @@ export const CustomerStatusBadge = ({ status }: { status: string }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger
|
||||||
<div className={cn("flex-none rounded-full p-1", statusClass)}>
|
render={
|
||||||
<div className="size-2 rounded-full bg-current" />
|
<div className={cn("flex-none rounded-full p-1", statusClass)}>
|
||||||
</div>
|
<div className="size-2 rounded-full bg-current" />
|
||||||
</TooltipTrigger>
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TooltipContent>{contentTxt}</TooltipContent>
|
<TooltipContent>{contentTxt}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
@ -31,38 +32,42 @@ 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
|
||||||
<Button
|
render={
|
||||||
//className="data-[state=open]:bg-accent -ml-4 h-8 text-xs text-muted-foreground font-semibold text-nowrap cursor-pointer"
|
<Button
|
||||||
className="-ml-3 h-8 data-[state=open]:bg-accent cursor-pointer text-foreground"
|
//className="data-[state=open]:bg-accent -ml-4 h-8 text-xs text-muted-foreground font-semibold text-nowrap cursor-pointer"
|
||||||
size="sm"
|
className="-ml-3 h-8 data-[state=open]:bg-accent cursor-pointer text-foreground"
|
||||||
type="button"
|
size="sm"
|
||||||
variant="ghost"
|
type="button"
|
||||||
>
|
variant="ghost"
|
||||||
<span>{title}</span>
|
>
|
||||||
{column.getIsSorted() === "desc" ? (
|
<span>{title}</span>
|
||||||
<ArrowDownIcon />
|
{column.getIsSorted() === "desc" ? (
|
||||||
) : column.getIsSorted() === "asc" ? (
|
<ArrowDownIcon />
|
||||||
<ArrowUpIcon />
|
) : column.getIsSorted() === "asc" ? (
|
||||||
) : (
|
<ArrowUpIcon />
|
||||||
<ChevronsUpDownIcon />
|
) : (
|
||||||
)}
|
<ChevronsUpDownIcon />
|
||||||
</Button>
|
)}
|
||||||
</DropdownMenuTrigger>
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
|
<DropdownMenuGroup>
|
||||||
<ArrowUpIcon />
|
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
|
||||||
{t("components.datatable.asc")}
|
<ArrowUpIcon />
|
||||||
</DropdownMenuItem>
|
{t("components.datatable.asc")}
|
||||||
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
|
</DropdownMenuItem>
|
||||||
<ArrowDownIcon />
|
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
|
||||||
{t("components.datatable.desc")}
|
<ArrowDownIcon />
|
||||||
</DropdownMenuItem>
|
{t("components.datatable.desc")}
|
||||||
<DropdownMenuSeparator />
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
<DropdownMenuSeparator />
|
||||||
<EyeOffIcon />
|
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
||||||
{t("components.datatable.hide")}
|
<EyeOffIcon />
|
||||||
</DropdownMenuItem>
|
{t("components.datatable.hide")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -47,7 +47,8 @@ export function DataTablePagination<TData>({ table, className }: DataTablePagina
|
|||||||
table.setPageIndex(nextIndex);
|
table.setPageIndex(nextIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePageSizeChange = (size: string) => {
|
const handlePageSizeChange = (size: string | null) => {
|
||||||
|
if (!size) return;
|
||||||
table.setPageSize(Number(size));
|
table.setPageSize(Number(size));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -113,34 +113,38 @@ export function DataTableToolbar<TData>({
|
|||||||
|
|
||||||
{!readOnly && meta?.bulkOps?.moveSelectedUp && (
|
{!readOnly && meta?.bulkOps?.moveSelectedUp && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger
|
||||||
<Button
|
render={
|
||||||
aria-label={t("components.datatable.actions.move_up")}
|
<Button
|
||||||
onClick={handleMoveSelectedUp}
|
aria-label={t("components.datatable.actions.move_up")}
|
||||||
size="sm"
|
onClick={handleMoveSelectedUp}
|
||||||
type="button"
|
size="sm"
|
||||||
variant="outline"
|
type="button"
|
||||||
>
|
variant="outline"
|
||||||
<ArrowUpIcon aria-hidden="true" className="size-4" />
|
>
|
||||||
</Button>
|
<ArrowUpIcon aria-hidden="true" className="size-4" />
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<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
|
||||||
<Button
|
render={
|
||||||
aria-label={t("components.datatable.actions.move_down")}
|
<Button
|
||||||
onClick={handleMoveSelectedDown}
|
aria-label={t("components.datatable.actions.move_down")}
|
||||||
size="sm"
|
onClick={handleMoveSelectedDown}
|
||||||
type="button"
|
size="sm"
|
||||||
variant="outline"
|
type="button"
|
||||||
>
|
variant="outline"
|
||||||
<ArrowDownIcon aria-hidden="true" className="size-4" />
|
>
|
||||||
</Button>
|
<ArrowDownIcon aria-hidden="true" className="size-4" />
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TooltipContent>{t("components.datatable.actions.move_down")}</TooltipContent>
|
<TooltipContent>{t("components.datatable.actions.move_down")}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@ -164,12 +168,14 @@ export function DataTableToolbar<TData>({
|
|||||||
<Separator className="h-6 mx-1 bg-muted/50" orientation="vertical" />
|
<Separator className="h-6 mx-1 bg-muted/50" orientation="vertical" />
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger
|
||||||
<Button onClick={handleClearSelection} size="sm" type="button" variant="outline">
|
render={
|
||||||
<ScanIcon aria-hidden="true" className="size-4 mr-1" />
|
<Button onClick={handleClearSelection} size="sm" type="button" variant="outline">
|
||||||
<span>{t("components.datatable.actions.clear_selection")}</span>
|
<ScanIcon aria-hidden="true" className="size-4 mr-1" />
|
||||||
</Button>
|
<span>{t("components.datatable.actions.clear_selection")}</span>
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TooltipContent>{t("components.datatable.actions.clear_selection")}</TooltipContent>
|
<TooltipContent>{t("components.datatable.actions.clear_selection")}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
@ -18,36 +19,41 @@ export function DataTableViewOptions<TData>({ table }: { table: Table<TData> })
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger
|
||||||
<Button
|
render={
|
||||||
className="ml-auto hidden h-8 lg:flex gap-2 items"
|
<Button
|
||||||
size="sm"
|
className="ml-auto hidden h-8 lg:flex gap-2 items"
|
||||||
type="button"
|
size="sm"
|
||||||
variant="outline"
|
type="button"
|
||||||
>
|
variant="outline"
|
||||||
<Settings2Icon />
|
>
|
||||||
{t("components.datatable_view_options.columns_button")}
|
<Settings2Icon />
|
||||||
</Button>
|
{t("components.datatable_view_options.columns_button")}
|
||||||
</DropdownMenuTrigger>
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuLabel>
|
<DropdownMenuGroup>
|
||||||
{t("components.datatable_view_options.toggle_columns")}
|
<DropdownMenuLabel>
|
||||||
</DropdownMenuLabel>
|
{t("components.datatable_view_options.toggle_columns")}
|
||||||
<DropdownMenuSeparator />
|
</DropdownMenuLabel>
|
||||||
{table
|
<DropdownMenuSeparator />
|
||||||
.getAllColumns()
|
{table
|
||||||
.filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide())
|
.getAllColumns()
|
||||||
.map((column) => {
|
.filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide())
|
||||||
return (
|
.map((column) => {
|
||||||
<DropdownMenuCheckboxItem
|
return (
|
||||||
checked={column.getIsVisible()}
|
<DropdownMenuCheckboxItem
|
||||||
key={column.id}
|
checked={column.getIsVisible()}
|
||||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
key={column.id}
|
||||||
>
|
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||||
{getColumnLabel(column)}
|
>
|
||||||
</DropdownMenuCheckboxItem>
|
{getColumnLabel(column)}
|
||||||
);
|
</DropdownMenuCheckboxItem>
|
||||||
})}
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,13 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
Table as TableComp,
|
Table as TableComp,
|
||||||
@ -80,7 +73,6 @@ export interface DataTableProps<TData, TValue> {
|
|||||||
enablePagination?: boolean;
|
enablePagination?: boolean;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
enableRowSelection?: boolean;
|
enableRowSelection?: boolean;
|
||||||
EditorComponent?: React.ComponentType<{ row: TData; index: number; onClose: () => void }>;
|
|
||||||
|
|
||||||
getRowId?: (originalRow: TData, index: number, parent?: Row<TData>) => string;
|
getRowId?: (originalRow: TData, index: number, parent?: Row<TData>) => string;
|
||||||
|
|
||||||
@ -105,7 +97,6 @@ export function DataTable<TData, TValue>({
|
|||||||
enablePagination = true,
|
enablePagination = true,
|
||||||
pageSize = 10,
|
pageSize = 10,
|
||||||
enableRowSelection = false,
|
enableRowSelection = false,
|
||||||
EditorComponent,
|
|
||||||
|
|
||||||
getRowId,
|
getRowId,
|
||||||
|
|
||||||
@ -125,7 +116,6 @@ export function DataTable<TData, TValue>({
|
|||||||
React.useState<VisibilityState>(inititalcolumnVisibility);
|
React.useState<VisibilityState>(inititalcolumnVisibility);
|
||||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
||||||
const [colSizes, setColSizes] = React.useState<ColumnSizingState>({});
|
const [colSizes, setColSizes] = React.useState<ColumnSizingState>({});
|
||||||
const [editIndex, setEditIndex] = React.useState<number | null>(null);
|
|
||||||
|
|
||||||
// Configuración TanStack
|
// Configuración TanStack
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
@ -139,7 +129,7 @@ export function DataTable<TData, TValue>({
|
|||||||
getRowId ??
|
getRowId ??
|
||||||
((originalRow: TData, i: number) => {
|
((originalRow: TData, i: number) => {
|
||||||
const row = originalRow as { id?: string | number };
|
const row = originalRow as { id?: string | number };
|
||||||
return row.id !== undefined ? String(row.id) : String(i);
|
return row.id === undefined ? String(i) : String(row.id);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
state: {
|
state: {
|
||||||
@ -183,8 +173,6 @@ export function DataTable<TData, TValue>({
|
|||||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCloseEditor = React.useCallback(() => setEditIndex(null), []);
|
|
||||||
|
|
||||||
// Render principal
|
// Render principal
|
||||||
return (
|
return (
|
||||||
<div className="transition-[max-height] duration-300 ease-in-out">
|
<div className="transition-[max-height] duration-300 ease-in-out">
|
||||||
@ -192,9 +180,9 @@ export function DataTable<TData, TValue>({
|
|||||||
<DataTableToolbar showViewOptions={!readOnly} table={table} />
|
<DataTableToolbar showViewOptions={!readOnly} table={table} />
|
||||||
|
|
||||||
<div className="overflow-hidden rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<TableComp className="w-full text-sm">
|
<TableComp>
|
||||||
{/* CABECERA */}
|
{/* CABECERA */}
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((hg) => (
|
{table.getHeaderGroups().map((hg) => (
|
||||||
<TableRow key={hg.id}>
|
<TableRow key={hg.id}>
|
||||||
{hg.headers.map((h) => {
|
{hg.headers.map((h) => {
|
||||||
@ -230,9 +218,6 @@ export function DataTable<TData, TValue>({
|
|||||||
data-state={row.getIsSelected() && "selected"}
|
data-state={row.getIsSelected() && "selected"}
|
||||||
key={row.id}
|
key={row.id}
|
||||||
onClick={(e) => onRowClick?.(row.original, rowIndex, e)}
|
onClick={(e) => onRowClick?.(row.original, rowIndex, e)}
|
||||||
onDoubleClick={
|
|
||||||
readOnly || onRowClick ? undefined : () => setEditIndex(rowIndex)
|
|
||||||
}
|
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" || e.key === " ")
|
if (e.key === "Enter" || e.key === " ")
|
||||||
onRowClick?.(row.original, rowIndex, e as any);
|
onRowClick?.(row.original, rowIndex, e as any);
|
||||||
@ -284,32 +269,6 @@ export function DataTable<TData, TValue>({
|
|||||||
)}
|
)}
|
||||||
</TableComp>
|
</TableComp>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Editor modal */}
|
|
||||||
{EditorComponent && editIndex !== null && (
|
|
||||||
<Dialog onOpenChange={handleCloseEditor} open>
|
|
||||||
<DialogContent className="max-w-3xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{t("components.datatable.editor.title")}</DialogTitle>
|
|
||||||
<DialogDescription>{t("components.datatable.editor.subtitle")}</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<EditorComponent
|
|
||||||
index={editIndex}
|
|
||||||
onClose={handleCloseEditor}
|
|
||||||
row={data[editIndex]!}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button onClick={handleCloseEditor} type="button" variant="secondary">
|
|
||||||
{t("common.close")}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,74 +1,75 @@
|
|||||||
import {
|
import {
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
Table as TableComp,
|
Table as TableComp,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
// SkeletonTable.tsx
|
// SkeletonTable.tsx
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { SkeletonDataTableFooter } from './skeleton-data-table-footer.tsx';
|
|
||||||
|
import { SkeletonDataTableFooter } from "./skeleton-data-table-footer.tsx";
|
||||||
|
|
||||||
export type SkeletonTableProps = {
|
export type SkeletonTableProps = {
|
||||||
columns?: number;
|
columns?: number;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
stickyHeader?: boolean;
|
stickyHeader?: boolean;
|
||||||
showFooter?: boolean;
|
showFooter?: boolean;
|
||||||
footerProps?: { pageIndex?: number; pageSize?: number; totalItems?: number };
|
footerProps?: { pageIndex?: number; pageSize?: number; totalItems?: number };
|
||||||
}
|
};
|
||||||
|
|
||||||
// Componente Skeleton de tabla genérico
|
// Componente Skeleton de tabla genérico
|
||||||
export const SkeletonDataTable = ({
|
export const SkeletonDataTable = ({
|
||||||
columns = 6,
|
columns = 6,
|
||||||
rows = 10,
|
rows = 10,
|
||||||
stickyHeader = true,
|
stickyHeader = true,
|
||||||
showFooter = true,
|
showFooter = true,
|
||||||
footerProps,
|
footerProps,
|
||||||
}: SkeletonTableProps) => {
|
}: SkeletonTableProps) => {
|
||||||
// Genera arrays para mapear
|
// Genera arrays para mapear
|
||||||
const cols = React.useMemo(() => Array.from({ length: columns }), [columns]);
|
const cols = React.useMemo(() => Array.from({ length: columns }), [columns]);
|
||||||
const rws = React.useMemo(() => Array.from({ length: rows }), [rows]);
|
const rws = React.useMemo(() => Array.from({ length: rows }), [rows]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden rounded-md border bg-background" role="status" aria-busy="true">
|
<div aria-busy="true" className="overflow-hidden rounded-md border bg-background" role="status">
|
||||||
<TableComp className="w-full text-sm">
|
<TableComp className="w-full text-sm">
|
||||||
<TableHeader className={stickyHeader ? "sticky top-0 z-10 bg-muted" : ""}>
|
<TableHeader className={stickyHeader ? "sticky top-0 z-10 bg-muted" : ""}>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{cols.map((_, i) => (
|
{cols.map((_, i) => (
|
||||||
<TableHead key={`sk-th-${i}`}>
|
<TableHead key={`sk-th-${i}`}>
|
||||||
<div className="h-4 w-24 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none" />
|
<div className="h-4 w-24 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none" />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
|
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{rws.map((_, r) => (
|
{rws.map((_, r) => (
|
||||||
<TableRow key={`sk-tr-${r}`}>
|
<TableRow key={`sk-tr-${r}`}>
|
||||||
{cols.map((_, c) => (
|
{cols.map((_, c) => (
|
||||||
<TableCell key={`sk-td-${r}-${c}`} className="align-top">
|
<TableCell className="align-top" key={`sk-td-${r}-${c}`}>
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
"h-4 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none",
|
"h-4 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none",
|
||||||
c === 0 ? "w-36" : "w-full",
|
c === 0 ? "w-36" : "w-full",
|
||||||
"my-2",
|
"my-2",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
style={{ maxWidth: c % 3 === 0 ? "14rem" : c % 3 === 1 ? "10rem" : "100%" }}
|
style={{ maxWidth: c % 3 === 0 ? "14rem" : c % 3 === 1 ? "10rem" : "100%" }}
|
||||||
/>
|
/>
|
||||||
{c > 0 && (
|
{c > 0 && (
|
||||||
<div className="mt-2 h-3 w-1/2 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none" />
|
<div className="mt-2 h-3 w-1/2 rounded bg-foreground/10 animate-pulse motion-reduce:animate-none" />
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</TableComp>
|
</TableComp>
|
||||||
|
|
||||||
{showFooter && <SkeletonDataTableFooter {...footerProps} />}
|
{showFooter && <SkeletonDataTableFooter {...footerProps} />}
|
||||||
|
|
||||||
<span className="sr-only">Loading table…</span>
|
<span className="sr-only">Loading table…</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
AvatarFallback,
|
AvatarFallback,
|
||||||
AvatarImage,
|
AvatarImage,
|
||||||
Button,
|
|
||||||
Separator,
|
Separator,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
@ -22,12 +21,10 @@ export function AppTopbar() {
|
|||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<ProfileDropdown
|
<ProfileDropdown
|
||||||
trigger={
|
trigger={
|
||||||
<Button className="size-9.5" size="icon" variant="ghost">
|
<Avatar className="size-9.5 rounded-md">
|
||||||
<Avatar className="size-9.5 rounded-md">
|
<AvatarImage src="https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png" />
|
||||||
<AvatarImage src="https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png" />
|
<AvatarFallback>JD</AvatarFallback>
|
||||||
<AvatarFallback>JD</AvatarFallback>
|
</Avatar>
|
||||||
</Avatar>
|
|
||||||
</Button>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,7 +3,6 @@ export const LogoVerifactu = ({ color = "black", width = "100%", ...props }) =>
|
|||||||
<div {...props}>
|
<div {...props}>
|
||||||
<svg
|
<svg
|
||||||
{...props}
|
{...props}
|
||||||
height="auto"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
preserveAspectRatio="xMidYMid meet"
|
||||||
style={{
|
style={{
|
||||||
color,
|
color,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user