Descarga de proformas en PDF

This commit is contained in:
David Arranz 2026-07-08 19:53:56 +02:00
parent 00c647fe20
commit 9d7e18d73d
14 changed files with 163 additions and 9 deletions

View File

@ -5,9 +5,9 @@ export async function downloadInvoicePDFApi(
invoiceId: string,
params?: Record<string, unknown>
): Promise<Blob> {
return dataSource.custom<Blob>({
return dataSource.custom<unknown, Blob>({
...params,
path: `issued-invoices/${invoiceId}/report`,
path: `issued-invoices/${invoiceId}/report/pdf`,
method: "get",
responseType: "blob",
});

View File

@ -1,7 +1,7 @@
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
import * as React from "react";
import { useDownloadInvoicePDFQuery } from "../../shared";
import { useDownloadInvoicePDFQuery } from "../hooks";
interface PendingDownload {
id: string;
@ -37,7 +37,7 @@ export function useDownloadInvoicePDFController() {
setPending({ id, invoice_number });
// refetch se dispara en un efecto, o aquí si prefieres:
refetch().catch((err) => {
refetch().catch((err: unknown) => {
showErrorToast(
"Error al descargar",
err instanceof Error ? err.message : "Error desconocido"

View File

@ -0,0 +1 @@
export * from "./use-download-invoice-pdf-query";

View File

@ -3,7 +3,7 @@
import { useDataSource } from "@erp/core/hooks";
import { type QueryKey, useQuery } from "@tanstack/react-query";
import { downloadInvoicePDFApi } from "../../download-pdf/api";
import { downloadInvoicePDFApi } from "../api";
export const ISSUED_INVOICE_QUERY_KEY = (id: string): QueryKey => ["issued_invoice", id] as const;

View File

@ -1,2 +1 @@
export * from "./use-download-invoice-pdf-query";
export * from "./use-issued-invoice-list-query";

View File

@ -0,0 +1,14 @@
import type { IDataSource } from "@erp/core/client";
export async function downloadProformaPDFApi(
dataSource: IDataSource,
proformaId: string,
params?: Record<string, unknown>
): Promise<Blob> {
return dataSource.custom<Blob>({
...params,
path: `proformas/${proformaId}/report/pdf`,
method: "get",
responseType: "blob",
});
}

View File

@ -0,0 +1 @@
export * from "./download-proforma-pdf.api";

View File

@ -0,0 +1 @@
export * from "./use-download-proforma-pdf.controller";

View File

@ -0,0 +1,54 @@
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
import * as React from "react";
import { useDownloadProformaPDFQuery } from "../hooks";
interface PendingDownload {
id: string;
proforma_number: string;
}
export function useDownloadProformaPDFController() {
const [pending, setPending] = React.useState<PendingDownload | null>(null);
const { data, isFetching, refetch } = useDownloadProformaPDFQuery(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 = `proforma-${pending.proforma_number}.pdf`;
a.click();
URL.revokeObjectURL(url);
showSuccessToast(
"Descarga iniciada",
`La proforma ${pending.proforma_number} se está descargando.`
);
setPending(null);
}, [data, pending]);
const download = (id: string, proforma_number: string) => {
setPending({ id, proforma_number });
// refetch se dispara en un efecto, o aquí si prefieres:
refetch().catch((err: unknown) => {
showErrorToast(
"Error al descargar",
err instanceof Error ? err.message : "Error desconocido"
);
setPending(null);
});
};
return {
download, // (id, reference) => void
isLoading: isFetching,
loadingId: pending?.id,
};
}

View File

@ -0,0 +1 @@
export * from "./use-download-proforma-pdf-query";

View File

@ -0,0 +1,53 @@
import { useDataSource } from "@erp/core/hooks";
import { type QueryKey, useQuery } from "@tanstack/react-query";
import { downloadProformaPDFApi } from "../api";
export const PROFORMA_QUERY_KEY = (id: string): QueryKey => ["proforma", id] as const;
type DownloadProformaPDFOptions = {
enabled?: boolean;
};
export function useDownloadProformaPDFQuery(
proformaId?: string,
options?: DownloadProformaPDFOptions
) {
const dataSource = useDataSource();
const enabled = (options?.enabled ?? true) && !!proformaId;
return useQuery({
queryKey: PROFORMA_QUERY_KEY(proformaId ?? "unknown"),
queryFn: async (context) => {
if (!proformaId) throw new Error("proformaId is required");
const { signal } = context;
return await downloadProformaPDFApi(dataSource, proformaId, {
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.
*/

View File

@ -0,0 +1,3 @@
export * from "./api";
export * from "./controller";
export * from "./hooks";

View File

@ -1,9 +1,12 @@
import type { RightPanelMode } from "@repo/rdx-ui/hooks";
import { useCallback } from "react";
import { useSearchParams } from "react-router-dom";
import { useChangeProformaStatusDialogController } from "../../change-status";
import { useDeleteProformaDialogController } from "../../delete";
import { useDownloadProformaPDFController } from "../../download-pdf/controller";
import { useIssueProformaDialogController } from "../../issue-proforma";
import type { ProformaListRow } from "../../shared";
import { useListProformasController } from "./use-list-proformas.controller";
import { useProformaSummaryPanelController } from "./use-proforma-summary-panel.controller";
@ -14,6 +17,15 @@ export const useListProformasPageController = () => {
const issueDialogCtrl = useIssueProformaDialogController();
const changeStatusDialogCtrl = useChangeProformaStatusDialogController();
const downloadPDFCtrl = useDownloadProformaPDFController();
const handleDownloadPDF = useCallback(
(proforma: ProformaListRow) => {
downloadPDFCtrl.download(proforma.id, proforma.invoiceNumber);
},
[downloadPDFCtrl]
);
const [searchParams] = useSearchParams();
const proformaId = searchParams.get("proformaId") ?? "";
@ -32,5 +44,10 @@ export const useListProformasPageController = () => {
deleteDialogCtrl,
issueDialogCtrl,
changeStatusDialogCtrl,
downloadPDFCtrl,
handleDownloadPDF,
pdfDownloadingId: downloadPDFCtrl.loadingId,
isPDFDownloading: downloadPDFCtrl.isLoading,
};
};

View File

@ -44,8 +44,16 @@ export const ListProformasPage = () => {
fallbackPath: "/proformas",
});
const { listCtrl, panelCtrl, deleteDialogCtrl, issueDialogCtrl, changeStatusDialogCtrl } =
useListProformasPageController();
const {
listCtrl,
panelCtrl,
deleteDialogCtrl,
issueDialogCtrl,
changeStatusDialogCtrl,
handleDownloadPDF,
isPDFDownloading: isPdfDownloading,
pdfDownloadingId,
} = useListProformasPageController();
const handleEditClick = (proformaId: string) => {
navigate({
@ -65,7 +73,9 @@ export const ListProformasPage = () => {
onChangeStatusClick: (proforma) =>
changeStatusDialogCtrl.openDialog(prepareChangeProformaStatusTargets(proforma)),
onDownloadPdf: (proforma) => null,
onDownloadPdf: handleDownloadPDF,
pdfDownloadingId,
isPdfDownloading,
});
const isPanelOpen = panelCtrl.panelState.isOpen;