Facturas de cliente
This commit is contained in:
parent
f39e55ca92
commit
6e0900afae
@ -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(() =>
|
||||||
|
|||||||
@ -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,53 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -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,24 @@
|
|||||||
|
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,
|
||||||
|
|
||||||
|
handleDownloadPDF,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -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>*/}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -14,21 +14,20 @@ import {
|
|||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import { DownloadIcon, MailIcon, MoreVerticalIcon, QrCodeIcon } from "lucide-react";
|
import { DownloadIcon, 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;
|
onSendEmail?: (issuedInvoice: IssuedInvoiceSummaryData) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
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 +100,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 +131,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,7 +320,7 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
size: 64,
|
size: 64,
|
||||||
minSize: 64,
|
minSize: 64,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const proforma = row.original;
|
const issuedInvoice = row.original;
|
||||||
const stop = (e: React.MouseEvent | React.KeyboardEvent) => e.stopPropagation();
|
const stop = (e: React.MouseEvent | React.KeyboardEvent) => e.stopPropagation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -333,8 +332,8 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
aria-label={t("common.download_pdf")}
|
aria-label={t("common.download_pdf")}
|
||||||
className="cursor-pointer text-muted-foreground hover:text-primary"
|
className="cursor-pointer text-muted-foreground hover:text-primary"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
stop(e);
|
||||||
onDownloadPdf?.(proforma);
|
actionHandlers.onDownloadPdf?.(issuedInvoice);
|
||||||
}}
|
}}
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
type="button"
|
type="button"
|
||||||
@ -366,14 +365,14 @@ 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"
|
className="cursor-pointer"
|
||||||
onClick={() => onSendEmail?.(proforma)}
|
onClick={() => actionHandlers.onSendEmail?.(issuedInvoice)}
|
||||||
>
|
>
|
||||||
<MailIcon className="mr-2 size-4" />
|
<MailIcon className="mr-2 size-4" />
|
||||||
{t("common.send_email")}
|
{t("common.send_email")}
|
||||||
@ -389,6 +388,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,123 @@
|
|||||||
|
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 } = useIssuedInvoiceListPageController();
|
||||||
|
|
||||||
|
const columns = useIssuedInvoicesGridColumns({
|
||||||
|
onDownloadPdf: handleDownloadPDF,
|
||||||
|
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user