Compare commits
2 Commits
f39e55ca92
...
c1399cd67d
| Author | SHA1 | Date | |
|---|---|---|---|
| c1399cd67d | |||
| 6e0900afae |
@ -1,3 +0,0 @@
|
|||||||
export * from "../proformas/adapters/proforma-dto.adapter";
|
|
||||||
|
|
||||||
export * from "./invoice-resume-dto.adapter";
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core";
|
|
||||||
|
|
||||||
import type { InvoiceSummaryFormData } from "../schemas/invoice-resume.form.schema";
|
|
||||||
import type { CustomerInvoiceSummary } from "../schemas/invoices.api.schema";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convierte el DTO completo de API a datos numéricos para el formulario.
|
|
||||||
*/
|
|
||||||
export const invoiceResumeDtoToFormAdapter = {
|
|
||||||
fromDto(dtos: CustomerInvoiceSummary[], context?: any) {
|
|
||||||
return dtos.map(
|
|
||||||
(dto) =>
|
|
||||||
({
|
|
||||||
...dto,
|
|
||||||
|
|
||||||
subtotal_amount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
|
|
||||||
subtotal_amount_fmt: formatCurrency(
|
|
||||||
MoneyDTOHelper.toNumber(dto.subtotal_amount),
|
|
||||||
Number(dto.total_amount.scale || 2),
|
|
||||||
dto.currency_code,
|
|
||||||
dto.language_code
|
|
||||||
),
|
|
||||||
|
|
||||||
discount_percentage: PercentageDTOHelper.toNumber(dto.discount_percentage),
|
|
||||||
discount_percentage_fmt: PercentageDTOHelper.toNumericString(dto.discount_percentage),
|
|
||||||
|
|
||||||
discount_amount: MoneyDTOHelper.toNumber(dto.discount_amount),
|
|
||||||
discount_amount_fmt: formatCurrency(
|
|
||||||
MoneyDTOHelper.toNumber(dto.discount_amount),
|
|
||||||
Number(dto.total_amount.scale || 2),
|
|
||||||
dto.currency_code,
|
|
||||||
dto.language_code
|
|
||||||
),
|
|
||||||
|
|
||||||
taxable_amount: MoneyDTOHelper.toNumber(dto.taxable_amount),
|
|
||||||
taxable_amount_fmt: formatCurrency(
|
|
||||||
MoneyDTOHelper.toNumber(dto.taxable_amount),
|
|
||||||
Number(dto.total_amount.scale || 2),
|
|
||||||
dto.currency_code,
|
|
||||||
dto.language_code
|
|
||||||
),
|
|
||||||
|
|
||||||
taxes_amount: MoneyDTOHelper.toNumber(dto.taxes_amount),
|
|
||||||
taxes_amount_fmt: formatCurrency(
|
|
||||||
MoneyDTOHelper.toNumber(dto.taxes_amount),
|
|
||||||
Number(dto.total_amount.scale || 2),
|
|
||||||
dto.currency_code,
|
|
||||||
dto.language_code
|
|
||||||
),
|
|
||||||
|
|
||||||
total_amount: MoneyDTOHelper.toNumber(dto.total_amount),
|
|
||||||
total_amount_fmt: formatCurrency(
|
|
||||||
MoneyDTOHelper.toNumber(dto.total_amount),
|
|
||||||
Number(dto.total_amount.scale || 2),
|
|
||||||
dto.currency_code,
|
|
||||||
dto.language_code
|
|
||||||
),
|
|
||||||
|
|
||||||
//taxes: dto.taxes,
|
|
||||||
}) as unknown as InvoiceSummaryFormData
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -15,7 +15,7 @@ const ProformasListPage = lazy(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const IssuedInvoiceListPage = lazy(() =>
|
const IssuedInvoiceListPage = lazy(() =>
|
||||||
import("./issued-invoices/pages").then((m) => ({ default: m.IssuedInvoiceListPage }))
|
import("./issued-invoices/list").then((m) => ({ default: m.IssuedInvoiceListPage }))
|
||||||
);
|
);
|
||||||
|
|
||||||
/*const CustomerInvoiceAdd = lazy(() =>
|
/*const CustomerInvoiceAdd = lazy(() =>
|
||||||
|
|||||||
@ -18,9 +18,8 @@ export const useInvoiceQuery = (invoiceId?: string, options?: InvoiceQueryOption
|
|||||||
queryKey: CUSTOMER_INVOICE_QUERY_KEY(invoiceId ?? "unknown"),
|
queryKey: CUSTOMER_INVOICE_QUERY_KEY(invoiceId ?? "unknown"),
|
||||||
queryFn: async (context) => {
|
queryFn: async (context) => {
|
||||||
const { signal } = context;
|
const { signal } = context;
|
||||||
if (!invoiceId) {
|
if (!invoiceId) throw new Error("invoiceId is required");
|
||||||
if (!invoiceId) throw new Error("invoiceId is required");
|
|
||||||
}
|
|
||||||
return await dataSource.getOne<Proforma>("customer-invoices", invoiceId, {
|
return await dataSource.getOne<Proforma>("customer-invoices", invoiceId, {
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
import type { IDataSource } from "@erp/core/client";
|
||||||
|
|
||||||
|
export async function downloadInvoicePDFApi(
|
||||||
|
dataSource: IDataSource,
|
||||||
|
invoiceId: string,
|
||||||
|
params?: Record<string, unknown>
|
||||||
|
): Promise<Blob> {
|
||||||
|
return dataSource.custom<Blob>({
|
||||||
|
...params,
|
||||||
|
path: `issued-invoices/${invoiceId}/report`,
|
||||||
|
method: "get",
|
||||||
|
responseType: "blob",
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./download-invoice-pdf.api";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./use-download-invoice-pdf.controller";
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { useDownloadInvoicePDFQuery } from "../hooks";
|
||||||
|
|
||||||
|
interface PendingDownload {
|
||||||
|
id: string;
|
||||||
|
invoice_number: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDownloadInvoicePDFController() {
|
||||||
|
const [pending, setPending] = React.useState<PendingDownload | null>(null);
|
||||||
|
|
||||||
|
const { data, isFetching, refetch } = useDownloadInvoicePDFQuery(pending?.id);
|
||||||
|
|
||||||
|
// Efecto: cuando hay blob + pending, disparamos la descarga
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!(pending && data)) return;
|
||||||
|
|
||||||
|
const blob = data;
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `invoice-${pending.invoice_number}.pdf`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
showSuccessToast(
|
||||||
|
"Descarga iniciada",
|
||||||
|
`La factura ${pending.invoice_number} se está descargando.`
|
||||||
|
);
|
||||||
|
|
||||||
|
setPending(null);
|
||||||
|
}, [data, pending]);
|
||||||
|
|
||||||
|
const download = (id: string, invoice_number: string) => {
|
||||||
|
setPending({ id, invoice_number });
|
||||||
|
|
||||||
|
// refetch se dispara en un efecto, o aquí si prefieres:
|
||||||
|
refetch().catch((err) => {
|
||||||
|
showErrorToast(
|
||||||
|
"Error al descargar",
|
||||||
|
err instanceof Error ? err.message : "Error desconocido"
|
||||||
|
);
|
||||||
|
setPending(null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
download, // (id, reference) => void
|
||||||
|
isLoading: isFetching,
|
||||||
|
loadingId: pending?.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./use-download-invoice-pdf-query";
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
// features/invoices/view-pdf/hooks/use-download-invoice-pdf.ts
|
||||||
|
|
||||||
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
|
import { type QueryKey, useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { downloadInvoicePDFApi } from "../api";
|
||||||
|
|
||||||
|
export const ISSUED_INVOICE_QUERY_KEY = (id: string): QueryKey => ["issued_invoice", id] as const;
|
||||||
|
|
||||||
|
type DownloadInvoicePDFOptions = {
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useDownloadInvoicePDFQuery(
|
||||||
|
invoiceId?: string,
|
||||||
|
options?: DownloadInvoicePDFOptions
|
||||||
|
) {
|
||||||
|
const dataSource = useDataSource();
|
||||||
|
const enabled = (options?.enabled ?? true) && !!invoiceId;
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ISSUED_INVOICE_QUERY_KEY(invoiceId ?? "unknown"),
|
||||||
|
queryFn: async (context) => {
|
||||||
|
if (!invoiceId) throw new Error("invoiceId is required");
|
||||||
|
|
||||||
|
const { signal } = context;
|
||||||
|
return await downloadInvoicePDFApi(dataSource, invoiceId, {
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enabled,
|
||||||
|
staleTime: 0,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
export function useQuery<
|
||||||
|
TQueryFnData = unknown,
|
||||||
|
TError = unknown,
|
||||||
|
TData = TQueryFnData,
|
||||||
|
TQueryKey extends QueryKey = QueryKey
|
||||||
|
>
|
||||||
|
|
||||||
|
TQueryFnData: the type returned from the queryFn.
|
||||||
|
TError: the type of Errors to expect from the queryFn.
|
||||||
|
TData: the type our data property will eventually have.
|
||||||
|
Only relevant if you use the select option,
|
||||||
|
because then the data property can be different
|
||||||
|
from what the queryFn returns.
|
||||||
|
Otherwise, it will default to whatever the queryFn returns.
|
||||||
|
TQueryKey: the type of our queryKey, only relevant
|
||||||
|
if you use the queryKey that is passed to your queryFn.
|
||||||
|
|
||||||
|
*/
|
||||||
@ -4,13 +4,14 @@ import type {
|
|||||||
IssuedInvoiceSummaryData,
|
IssuedInvoiceSummaryData,
|
||||||
IssuedInvoiceSummaryPage,
|
IssuedInvoiceSummaryPage,
|
||||||
IssuedInvoiceSummaryPageData,
|
IssuedInvoiceSummaryPageData,
|
||||||
} from "../schema";
|
} from "../../types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convierte el DTO completo de API a datos numéricos para el formulario.
|
* Convierte el DTO completo de API a datos numéricos para el formulario.
|
||||||
*/
|
*/
|
||||||
export const IssuedInvoiceSummaryDtoAdapter = {
|
export const IssuedInvoiceSummaryDtoAdapter = {
|
||||||
fromDto(pageDto: IssuedInvoiceSummaryPage, context?: unknown): IssuedInvoiceSummaryPageData {
|
fromDto(pageDto: IssuedInvoiceSummaryPage, context?: unknown): IssuedInvoiceSummaryPageData {
|
||||||
|
console.log(pageDto);
|
||||||
return {
|
return {
|
||||||
...pageDto,
|
...pageDto,
|
||||||
items: pageDto.items.map(
|
items: pageDto.items.map(
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import type { CriteriaDTO } from "@erp/core";
|
||||||
|
import type { IDataSource } from "@erp/core/client";
|
||||||
|
|
||||||
|
import type { IssuedInvoiceSummaryPage } from "../../types";
|
||||||
|
|
||||||
|
export async function getIssuedInvoiceListApi(
|
||||||
|
dataSource: IDataSource,
|
||||||
|
signal: AbortSignal,
|
||||||
|
criteria: CriteriaDTO
|
||||||
|
) {
|
||||||
|
const response = dataSource.getList<IssuedInvoiceSummaryPage>("issued-invoices", {
|
||||||
|
signal,
|
||||||
|
...criteria,
|
||||||
|
});
|
||||||
|
|
||||||
|
//return mapIssuedInvoiceList(raw);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./get-issued-invoice-list.api";
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./use-issued-invoice-list.controller";
|
||||||
|
export * from "./use-issued-invoice-list-page.controller.ts";
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useDownloadInvoicePDFController } from "../../download-pdf/controller";
|
||||||
|
import type { IssuedInvoiceSummaryData } from "../../types";
|
||||||
|
|
||||||
|
import { useIssuedInvoiceListController } from "./use-issued-invoice-list.controller";
|
||||||
|
|
||||||
|
export function useIssuedInvoiceListPageController() {
|
||||||
|
const listCtrl = useIssuedInvoiceListController();
|
||||||
|
const downloadPDFCtrl = useDownloadInvoicePDFController();
|
||||||
|
|
||||||
|
const handleDownloadPDF = React.useCallback(
|
||||||
|
(issuedInvoice: IssuedInvoiceSummaryData) => {
|
||||||
|
downloadPDFCtrl.download(issuedInvoice.id, issuedInvoice.invoice_number);
|
||||||
|
},
|
||||||
|
[downloadPDFCtrl]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
listCtrl,
|
||||||
|
|
||||||
|
downloadPDFCtrl,
|
||||||
|
|
||||||
|
handleDownloadPDF,
|
||||||
|
pdfDownloadingId: downloadPDFCtrl.loadingId,
|
||||||
|
isPDFDownloading: downloadPDFCtrl.isLoading,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
export const useIssuedInvoiceListController = () => {
|
||||||
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [status, setStatus] = useState("all");
|
||||||
|
|
||||||
|
const debouncedQ = useDebounce(search, 300);
|
||||||
|
|
||||||
|
const criteria = useMemo<CriteriaDTO>(() => {
|
||||||
|
const baseFilters =
|
||||||
|
status !== "all" ? [{ field: "status", operator: "CONTAINS", value: status }] : [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
q: debouncedQ || "",
|
||||||
|
pageSize,
|
||||||
|
pageNumber: pageIndex,
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setSearchValue = (value: string) => setSearch(value.trim().replace(/\s+/g, " "));
|
||||||
|
|
||||||
|
const setStatusFilter = (newStatus: string) => setStatus(newStatus);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
data,
|
||||||
|
pageIndex,
|
||||||
|
pageSize,
|
||||||
|
search,
|
||||||
|
setPageIndex,
|
||||||
|
setPageSize,
|
||||||
|
setSearchValue,
|
||||||
|
setStatusFilter,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./use-issued-invoice-list-query";
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
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 { IssuedInvoiceSummaryPage } from "../../types";
|
||||||
|
import { getIssuedInvoiceListApi } from "../api";
|
||||||
|
|
||||||
|
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 ?? "",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type IssuedInvoicesQueryOptions = {
|
||||||
|
enabled?: boolean;
|
||||||
|
criteria?: CriteriaDTO;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Obtener todas las facturas
|
||||||
|
export const useIssuedInvoiceListQuery = (options?: IssuedInvoicesQueryOptions) => {
|
||||||
|
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),
|
||||||
|
enabled,
|
||||||
|
placeholderData: (previousData, _previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./ui";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./issued-invoices-grid";
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./issued-invoices-grid";
|
||||||
|
export * from "./use-issued-invoices-grid-columns";
|
||||||
@ -0,0 +1,125 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
export type InvoiceUpdateCompProps = {
|
||||||
|
data: IssuedInvoiceSummaryPageData;
|
||||||
|
loading?: boolean;
|
||||||
|
|
||||||
|
columns: ColumnDef<IssuedInvoiceSummaryData, unknown>[];
|
||||||
|
|
||||||
|
pageIndex: number;
|
||||||
|
pageSize: number;
|
||||||
|
onPageChange?: (pageNumber: number) => void;
|
||||||
|
onPageSizeChange?: (pageSize: number) => void;
|
||||||
|
|
||||||
|
onRowClick?: (
|
||||||
|
row: IssuedInvoiceSummaryPageData,
|
||||||
|
index: number,
|
||||||
|
event: React.MouseEvent<HTMLTableRowElement>
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create new GridExample component
|
||||||
|
export const IssuedInvoicesGrid = ({
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
pageIndex,
|
||||||
|
pageSize,
|
||||||
|
onPageChange,
|
||||||
|
onPageSizeChange,
|
||||||
|
onRowClick,
|
||||||
|
}: InvoiceUpdateCompProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { items, total_items } = data;
|
||||||
|
|
||||||
|
// Navegación accesible (click o teclado)
|
||||||
|
/* const goToRow = useCallback(
|
||||||
|
(id: string, newTab = false) => {
|
||||||
|
const url = `/customer-invoices/${id}/edit`;
|
||||||
|
newTab ? window.open(url, "_blank", "noopener,noreferrer") : navigate(url);
|
||||||
|
},
|
||||||
|
[navigate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onRowClicked = useCallback(
|
||||||
|
(e: RowClickedEvent<any>) => {
|
||||||
|
if (!e.data) return;
|
||||||
|
const newTab = e.event instanceof MouseEvent && (e.event.metaKey || e.event.ctrlKey);
|
||||||
|
goToRow(e.data.id, newTab);
|
||||||
|
},
|
||||||
|
[goToRow]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onCellKeyDown = useCallback(
|
||||||
|
(e: CellKeyDownEvent<any>) => {
|
||||||
|
if (!e.data) return;
|
||||||
|
|
||||||
|
const ev = e.event;
|
||||||
|
if (!(ev && ev instanceof KeyboardEvent)) return;
|
||||||
|
|
||||||
|
const key = ev.key;
|
||||||
|
if (key === "Enter" || key === " ") {
|
||||||
|
ev.preventDefault();
|
||||||
|
goToRow(e.data.id);
|
||||||
|
}
|
||||||
|
if ((ev.ctrlKey || ev.metaKey) && key === "Enter") {
|
||||||
|
ev.preventDefault();
|
||||||
|
goToRow(e.data.id, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[goToRow]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRowClick = useCallback(
|
||||||
|
(invoice: IssuedInvoiceSummaryPageData, _i: number, e: React.MouseEvent) => {
|
||||||
|
const url = `/customer-invoices/${invoice.id}/edit`;
|
||||||
|
if (e.metaKey || e.ctrlKey) {
|
||||||
|
window.open(url, "_blank", "noopener,noreferrer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
preview.open(invoice);
|
||||||
|
},
|
||||||
|
[preview]
|
||||||
|
); */
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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>*/}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -7,28 +7,29 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
|
Spinner,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import { DownloadIcon, MailIcon, MoreVerticalIcon, QrCodeIcon } from "lucide-react";
|
import { DownloadIcon, FileDownIcon, MailIcon, MoreVerticalIcon, QrCodeIcon } from "lucide-react";
|
||||||
import * as React from "react";
|
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 "../../../schema";
|
import type { IssuedInvoiceSummaryData } from "../../../../types";
|
||||||
|
|
||||||
type GridActionHandlers = {
|
type GridActionHandlers = {
|
||||||
onDownloadPdf?: (proforma: IssuedInvoiceSummaryData) => void;
|
onDownloadPdf?: (issuedInvoice: IssuedInvoiceSummaryData) => void;
|
||||||
onSendEmail?: (proforma: IssuedInvoiceSummaryData) => void;
|
pdfDownloadingId?: string | null;
|
||||||
|
isPdfDownloading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useIssuedInvoicesGridColumns(
|
export function useIssuedInvoicesGridColumns(
|
||||||
actionHandlers: GridActionHandlers = {}
|
actionHandlers: GridActionHandlers = {}
|
||||||
): ColumnDef<IssuedInvoiceSummaryData, unknown>[] {
|
): ColumnDef<IssuedInvoiceSummaryData, unknown>[] {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { onDownloadPdf, onSendEmail } = actionHandlers;
|
|
||||||
|
|
||||||
return React.useMemo<ColumnDef<IssuedInvoiceSummaryData>[]>(
|
return React.useMemo<ColumnDef<IssuedInvoiceSummaryData>[]>(
|
||||||
() => [
|
() => [
|
||||||
@ -101,7 +102,7 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
</a>
|
</a>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="m-0 p-3">
|
<TooltipContent className="m-0 p-3">
|
||||||
<QRCode className="bg-white p-8" value={verifactu.url} />
|
<QrCode className="bg-white p-8" value={verifactu.url} />
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@ -132,15 +133,15 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
size: 140,
|
size: 140,
|
||||||
minSize: 120,
|
minSize: 120,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const c = row.original.recipient;
|
const r = row.original.recipient;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start gap-1">
|
<div className="flex items-start gap-1">
|
||||||
<div className="min-w-0 grid gap-1">
|
<div className="min-w-0 grid gap-1">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="font-semibold truncate text-primary">{c.name}</span>
|
<span className="font-semibold truncate text-primary">{r.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{c.tin && <span className="font-base truncate">{c.tin}</span>}
|
{r.tin && <span className="font-base truncate">{r.tin}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -321,29 +322,38 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
size: 64,
|
size: 64,
|
||||||
minSize: 64,
|
minSize: 64,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const proforma = row.original;
|
const issuedInvoice = row.original;
|
||||||
|
const isCompleted = issuedInvoice.verifactu.status === "Correcto";
|
||||||
|
const isPDFLoading =
|
||||||
|
actionHandlers.isPdfDownloading && actionHandlers.pdfDownloadingId === issuedInvoice.id;
|
||||||
const stop = (e: React.MouseEvent | React.KeyboardEvent) => e.stopPropagation();
|
const stop = (e: React.MouseEvent | React.KeyboardEvent) => e.stopPropagation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
{/* Descargar en PDF */}
|
{/* Descargar en PDF */}
|
||||||
|
{/* Descargar en PDF */}
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t("common.download_pdf")}
|
className="size-8"
|
||||||
className="cursor-pointer text-muted-foreground hover:text-primary"
|
disabled={isPDFLoading || !isCompleted}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
stop(e);
|
||||||
onDownloadPdf?.(proforma);
|
isCompleted ? actionHandlers.onDownloadPdf?.(issuedInvoice) : null;
|
||||||
}}
|
}}
|
||||||
size="icon-sm"
|
size="icon"
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
<DownloadIcon aria-hidden="true" className="size-4" />
|
{isPDFLoading ? (
|
||||||
|
<Spinner className="size-4" />
|
||||||
|
) : (
|
||||||
|
<FileDownIcon className="size-4" />
|
||||||
|
)}
|
||||||
|
<span className="sr-only">Descargar PDF</span>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{t("common.download_pdf")}</TooltipContent>
|
<TooltipContent>Descargar PDF</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* Menú demás acciones */}
|
{/* Menú demás acciones */}
|
||||||
@ -366,15 +376,12 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
<DropdownMenuContent align="end" className="w-48">
|
<DropdownMenuContent align="end" className="w-48">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => onDownloadPdf?.(proforma)}
|
onClick={() => actionHandlers.onDownloadPdf?.(issuedInvoice)}
|
||||||
>
|
>
|
||||||
<DownloadIcon className="mr-2 size-4" />
|
<DownloadIcon className="mr-2 size-4" />
|
||||||
{t("common.download_pdf")}
|
{t("common.download_pdf")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem className="cursor-pointer" onClick={stop}>
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => onSendEmail?.(proforma)}
|
|
||||||
>
|
|
||||||
<MailIcon className="mr-2 size-4" />
|
<MailIcon className="mr-2 size-4" />
|
||||||
{t("common.send_email")}
|
{t("common.send_email")}
|
||||||
</DropdownMenuItem>{" "}
|
</DropdownMenuItem>{" "}
|
||||||
@ -389,6 +396,6 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[t, onDownloadPdf, onSendEmail]
|
[t, actionHandlers]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./pages";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./issued-invoice-list-page";
|
||||||
@ -0,0 +1,131 @@
|
|||||||
|
import { PageHeader, SimpleSearchInput } from "@erp/core/components";
|
||||||
|
import { ErrorAlert } from "@erp/customers/components";
|
||||||
|
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
AlertDescription,
|
||||||
|
AlertTitle,
|
||||||
|
Button,
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import { FilterIcon, LockIcon, PlusIcon } from "lucide-react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../../i18n";
|
||||||
|
import { useIssuedInvoiceListPageController } from "../../controllers";
|
||||||
|
import { IssuedInvoicesGrid, useIssuedInvoicesGridColumns } from "../blocks";
|
||||||
|
|
||||||
|
export const IssuedInvoiceListPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const {
|
||||||
|
listCtrl,
|
||||||
|
handleDownloadPDF,
|
||||||
|
isPDFDownloading: isPdfDownloading,
|
||||||
|
pdfDownloadingId,
|
||||||
|
} = useIssuedInvoiceListPageController();
|
||||||
|
|
||||||
|
const columns = useIssuedInvoicesGridColumns({
|
||||||
|
onDownloadPdf: handleDownloadPDF,
|
||||||
|
pdfDownloadingId,
|
||||||
|
isPdfDownloading,
|
||||||
|
|
||||||
|
//onSendEmail: () => null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hook con Sheet de shadcn
|
||||||
|
/*const preview = usePinnedPreviewSheet<IssuedInvoiceSummaryPageData>({
|
||||||
|
persistKey: "invoice-preview-pin",
|
||||||
|
widthClass: "w-[500px]",
|
||||||
|
});*/
|
||||||
|
|
||||||
|
if (listCtrl.isError || !listCtrl.data) {
|
||||||
|
return (
|
||||||
|
<AppContent>
|
||||||
|
<ErrorAlert
|
||||||
|
message={(listCtrl.error as Error)?.message || "Error al cargar el listado"}
|
||||||
|
title={t("pages.issued_invoices.list.loadErrorTitle")}
|
||||||
|
/>
|
||||||
|
<BackHistoryButton />
|
||||||
|
</AppContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppHeader>
|
||||||
|
<PageHeader
|
||||||
|
description={t("pages.issued_invoices.list.description")}
|
||||||
|
rightSlot={
|
||||||
|
<Button
|
||||||
|
aria-label={t("pages.issued_invoices.create.title")}
|
||||||
|
onClick={() => navigate("/issued-invoices/create")}
|
||||||
|
>
|
||||||
|
<PlusIcon aria-hidden className="mr-2 size-4" />
|
||||||
|
{t("pages.issued_invoices.create.title")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
title={t("pages.issued_invoices.list.title")}
|
||||||
|
/>
|
||||||
|
</AppHeader>
|
||||||
|
<AppContent>
|
||||||
|
<Alert className="bg-green-50 text-green-800">
|
||||||
|
<LockIcon />
|
||||||
|
<AlertTitle className="font-semibold text-green-800">¡Atención!</AlertTitle>
|
||||||
|
<AlertDescription className="text-green-800">
|
||||||
|
Las facturas de esta pantalla son de solo lectura. Se generan automáticamente al emitir
|
||||||
|
una proforma aprobada y no se pueden modificar ni eliminar. Solo puedes descargar el PDF
|
||||||
|
de cada factura.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{/* Search and filters */}
|
||||||
|
<div className="flex items-center justify-between gap-16">
|
||||||
|
<SimpleSearchInput
|
||||||
|
loading={listCtrl.isLoading}
|
||||||
|
onSearchChange={listCtrl.setSearchValue}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select defaultValue="all" onValueChange={listCtrl.setStatusFilter}>
|
||||||
|
<SelectTrigger className="w-full sm:w-48">
|
||||||
|
<FilterIcon aria-hidden className="mr-2 size-4" />
|
||||||
|
<SelectValue placeholder={t("filters.status")} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">{t("catalog.issuedInvoices.status.all.label")}</SelectItem>
|
||||||
|
<SelectItem value="draft">
|
||||||
|
{t("catalog.issuedInvoices.status.draft.label")}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="sent">{t("catalog.issuedInvoices.status.sent.label")}</SelectItem>
|
||||||
|
<SelectItem value="approved">
|
||||||
|
{t("catalog.issuedInvoices.status.approved.label")}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="rejected">
|
||||||
|
{t("catalog.issuedInvoices.status.rejected.label")}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="issued">
|
||||||
|
{t("catalog.issuedInvoices.status.issued.label")}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IssuedInvoicesGrid
|
||||||
|
columns={columns}
|
||||||
|
data={listCtrl.data}
|
||||||
|
loading={listCtrl.isLoading}
|
||||||
|
onPageChange={listCtrl.setPageIndex}
|
||||||
|
onPageSizeChange={listCtrl.setPageSize}
|
||||||
|
// acciones rápidas del grid → page controller
|
||||||
|
//onRowClick={(id) => navigate(`/issuedInvoices/${id}`)}
|
||||||
|
pageIndex={listCtrl.pageIndex}
|
||||||
|
pageSize={listCtrl.pageSize}
|
||||||
|
/>
|
||||||
|
</AppContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./use-issued-invoices-grid-columns";
|
export * from "../../../list/ui/blocks/issued-invoices-grid/use-issued-invoices-grid-columns";
|
||||||
|
|
||||||
export * from "./use-issued-invoices-list";
|
export * from "./use-issued-invoices-list";
|
||||||
|
|||||||
@ -1,61 +0,0 @@
|
|||||||
import { PageHeader } from "@erp/core/components";
|
|
||||||
import { ErrorAlert } from "@erp/customers/components";
|
|
||||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
|
||||||
import { Button } from "@repo/shadcn-ui/components";
|
|
||||||
import { PlusIcon } from "lucide-react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
|
||||||
|
|
||||||
import { useIssuedInvoicesList } from "./hooks";
|
|
||||||
import { IssuedInvoicesGrid } from "./ui/issued-invoices-grid";
|
|
||||||
|
|
||||||
export const IssuedInvoiceListPage = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const list = useIssuedInvoicesList();
|
|
||||||
|
|
||||||
if (list.isError || !list.data) {
|
|
||||||
return (
|
|
||||||
<AppContent>
|
|
||||||
<ErrorAlert
|
|
||||||
message={(list.error as Error)?.message || "Error al cargar el listado"}
|
|
||||||
title={t("pages.issued_invoices.list.loadErrorTitle")}
|
|
||||||
/>
|
|
||||||
<BackHistoryButton />
|
|
||||||
</AppContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AppHeader>
|
|
||||||
<PageHeader
|
|
||||||
description={t("pages.issued_invoices.list.description")}
|
|
||||||
rightSlot={
|
|
||||||
<Button
|
|
||||||
aria-label={t("pages.issued_invoices.create.title")}
|
|
||||||
onClick={() => navigate("/issued-invoices/create")}
|
|
||||||
>
|
|
||||||
<PlusIcon aria-hidden className="mr-2 size-4" />
|
|
||||||
{t("pages.issued_invoices.create.title")}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
title={t("pages.issued_invoices.list.title")}
|
|
||||||
/>
|
|
||||||
</AppHeader>
|
|
||||||
<AppContent>
|
|
||||||
<IssuedInvoicesGrid
|
|
||||||
data={list.data}
|
|
||||||
loading={list.isLoading}
|
|
||||||
onPageChange={list.setPageIndex}
|
|
||||||
onPageSizeChange={list.setPageSize}
|
|
||||||
onSearchChange={list.setSearchValue}
|
|
||||||
pageIndex={list.pageIndex}
|
|
||||||
pageSize={list.pageSize}
|
|
||||||
searchValue={list.search}
|
|
||||||
/>
|
|
||||||
</AppContent>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,178 +0,0 @@
|
|||||||
import { SimpleSearchInput } from "@erp/core/components";
|
|
||||||
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
import { usePinnedPreviewSheet } from "../../hooks";
|
|
||||||
import { useTranslation } from "../../i18n";
|
|
||||||
import type { InvoiceSummaryFormData, InvoicesPageFormData } from "../../schemas";
|
|
||||||
|
|
||||||
export type InvoiceUpdateCompProps = {
|
|
||||||
invoicesPage: InvoicesPageFormData;
|
|
||||||
loading?: boolean;
|
|
||||||
|
|
||||||
pageIndex: number;
|
|
||||||
pageSize: number;
|
|
||||||
onPageChange?: (pageNumber: number) => void;
|
|
||||||
onPageSizeChange?: (pageSize: number) => void;
|
|
||||||
|
|
||||||
searchValue: string;
|
|
||||||
onSearchChange: (value: string) => void;
|
|
||||||
|
|
||||||
onRowClick?: (
|
|
||||||
row: InvoiceSummaryFormData,
|
|
||||||
index: number,
|
|
||||||
event: React.MouseEvent<HTMLTableRowElement>
|
|
||||||
) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create new GridExample component
|
|
||||||
export const InvoicesListGrid = ({
|
|
||||||
invoicesPage,
|
|
||||||
loading,
|
|
||||||
pageIndex,
|
|
||||||
pageSize,
|
|
||||||
onPageChange,
|
|
||||||
onPageSizeChange,
|
|
||||||
searchValue,
|
|
||||||
onSearchChange,
|
|
||||||
onRowClick,
|
|
||||||
}: InvoiceUpdateCompProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { items, total_items } = invoicesPage;
|
|
||||||
|
|
||||||
// Hook con Sheet de shadcn
|
|
||||||
const preview = usePinnedPreviewSheet<InvoiceSummaryFormData>({
|
|
||||||
persistKey: "invoice-preview-pin",
|
|
||||||
widthClass: "w-[500px]",
|
|
||||||
});
|
|
||||||
|
|
||||||
const [statusFilter, setStatusFilter] = useState("todas");
|
|
||||||
|
|
||||||
const columns = useProformasGridColumns({
|
|
||||||
onEdit: (invoice) => navigate(`/customer-invoices/${invoice.id}/edit`),
|
|
||||||
onDuplicate: (invoice) => null, //duplicateInvoice(inv.id),
|
|
||||||
onDownloadPdf: (invoice) => null, //downloadInvoicePdf(inv.id),
|
|
||||||
onSendEmail: (invoice) => null, //sendInvoiceEmail(inv.id),
|
|
||||||
onDelete: (invoice) => null, //confirmDelete(inv.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navegación accesible (click o teclado)
|
|
||||||
const goToRow = useCallback(
|
|
||||||
(id: string, newTab = false) => {
|
|
||||||
const url = `/customer-invoices/${id}/edit`;
|
|
||||||
newTab ? window.open(url, "_blank", "noopener,noreferrer") : navigate(url);
|
|
||||||
},
|
|
||||||
[navigate]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onRowClicked = useCallback(
|
|
||||||
(e: RowClickedEvent<any>) => {
|
|
||||||
if (!e.data) return;
|
|
||||||
const newTab = e.event instanceof MouseEvent && (e.event.metaKey || e.event.ctrlKey);
|
|
||||||
goToRow(e.data.id, newTab);
|
|
||||||
},
|
|
||||||
[goToRow]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onCellKeyDown = useCallback(
|
|
||||||
(e: CellKeyDownEvent<any>) => {
|
|
||||||
if (!e.data) return;
|
|
||||||
|
|
||||||
const ev = e.event;
|
|
||||||
if (!(ev && ev instanceof KeyboardEvent)) return;
|
|
||||||
|
|
||||||
const key = ev.key;
|
|
||||||
if (key === "Enter" || key === " ") {
|
|
||||||
ev.preventDefault();
|
|
||||||
goToRow(e.data.id);
|
|
||||||
}
|
|
||||||
if ((ev.ctrlKey || ev.metaKey) && key === "Enter") {
|
|
||||||
ev.preventDefault();
|
|
||||||
goToRow(e.data.id, true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[goToRow]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRowClick = useCallback(
|
|
||||||
(invoice: InvoiceSummaryFormData, _i: number, e: React.MouseEvent) => {
|
|
||||||
const url = `/customer-invoices/${invoice.id}/edit`;
|
|
||||||
if (e.metaKey || e.ctrlKey) {
|
|
||||||
window.open(url, "_blank", "noopener,noreferrer");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
preview.open(invoice);
|
|
||||||
},
|
|
||||||
[preview]
|
|
||||||
);
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render principal
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
{/* Barra de filtros */}
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
|
||||||
<SimpleSearchInput loading={loading} onSearchChange={onSearchChange} />
|
|
||||||
{/*<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
||||||
<SelectTrigger className="w-full sm:w-48 bg-white border-gray-200 shadow-sm">
|
|
||||||
<FilterIcon className="mr-2 h-4 w-4" />
|
|
||||||
<SelectValue placeholder="Estado" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="todas">Todas</SelectItem>
|
|
||||||
<SelectItem value="pagada">Pagadas</SelectItem>
|
|
||||||
<SelectItem value="pendiente">Pendientes</SelectItem>
|
|
||||||
<SelectItem value="vencida">Vencidas</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Button variant="outline" className="border-blue-200 text-blue-600 hover:bg-blue-50 bg-transparent">
|
|
||||||
<FileDownIcon className="mr-2 h-4 w-4" />
|
|
||||||
Exportar
|
|
||||||
</Button>*/}
|
|
||||||
</div>
|
|
||||||
<div className="relative flex">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/*<preview.Preview>
|
|
||||||
{({ item, isPinned, close, togglePin }) => (
|
|
||||||
<InvoicePreviewPanel
|
|
||||||
invoice={item}
|
|
||||||
isPinned={isPinned}
|
|
||||||
onClose={close}
|
|
||||||
onTogglePin={togglePin}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</preview.Preview>*/}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -6,9 +6,9 @@ import { PlusIcon } from "lucide-react";
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { invoiceResumeDtoToFormAdapter } from "../../adapters/invoice-resume-dto.adapter";
|
|
||||||
import { useInvoicesQuery } from "../../hooks";
|
import { useInvoicesQuery } from "../../hooks";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
|
import { issuedInvoiceResumeDtoToFormAdapter } from "../../issued-invoices/adapters/issued-invoice-resume-dto.adapter";
|
||||||
|
|
||||||
export const InvoiceListPage = () => {
|
export const InvoiceListPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -37,7 +37,7 @@ export const InvoiceListPage = () => {
|
|||||||
if (!data) return undefined;
|
if (!data) return undefined;
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
items: invoiceResumeDtoToFormAdapter.fromDto(data.items),
|
items: issuedInvoiceResumeDtoToFormAdapter.fromDto(data.items),
|
||||||
};
|
};
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
|||||||
@ -138,7 +138,7 @@ export function useProformasGridColumns(
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<a
|
<a
|
||||||
className="text-primary hover:underline"
|
className="text-primary hover:underline font-semibold"
|
||||||
href={`/customers/${proforma.customer_id}`}
|
href={`/customers/${proforma.customer_id}`}
|
||||||
>
|
>
|
||||||
{proforma.recipient.name}
|
{proforma.recipient.name}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export * from "../adapters/invoice-resume-dto.adapter";
|
export * from "../issued-invoices/adapters/issued-invoice-resume-dto.adapter";
|
||||||
export * from "../proformas/adapters/proforma-dto.adapter";
|
export * from "../proformas/adapters/proforma-dto.adapter";
|
||||||
|
|
||||||
export * from "./invoice.form.schema";
|
export * from "./invoice.form.schema";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user