From 9d7e18d73dc4b634c7a11f040c25a01427c3102e Mon Sep 17 00:00:00 2001 From: david Date: Wed, 8 Jul 2026 19:53:56 +0200 Subject: [PATCH] Descarga de proformas en PDF --- .../api/download-invoice-pdf.api.ts | 4 +- .../use-download-invoice-pdf.controller.ts | 4 +- .../download-pdf/hooks/index.ts | 1 + .../hooks/use-download-invoice-pdf-query.ts | 2 +- .../web/issued-invoices/shared/hooks/index.ts | 1 - .../api/download-proforma-pdf.api.ts | 14 +++++ .../web/proformas/download-pdf/api/index.ts | 1 + .../download-pdf/controller/index.ts | 1 + .../use-download-proforma-pdf.controller.ts | 54 +++++++++++++++++++ .../web/proformas/download-pdf/hooks/index.ts | 1 + .../hooks/use-download-proforma-pdf-query.ts | 53 ++++++++++++++++++ .../src/web/proformas/download-pdf/index.ts | 3 ++ .../use-list-proformas-page.controller.ts | 17 ++++++ .../list/ui/pages/list-proformas-page.tsx | 16 ++++-- 14 files changed, 163 insertions(+), 9 deletions(-) rename modules/customer-invoices/src/web/issued-invoices/{shared => download-pdf}/hooks/use-download-invoice-pdf-query.ts (96%) create mode 100644 modules/customer-invoices/src/web/proformas/download-pdf/api/download-proforma-pdf.api.ts create mode 100644 modules/customer-invoices/src/web/proformas/download-pdf/api/index.ts create mode 100644 modules/customer-invoices/src/web/proformas/download-pdf/controller/index.ts create mode 100644 modules/customer-invoices/src/web/proformas/download-pdf/controller/use-download-proforma-pdf.controller.ts create mode 100644 modules/customer-invoices/src/web/proformas/download-pdf/hooks/index.ts create mode 100644 modules/customer-invoices/src/web/proformas/download-pdf/hooks/use-download-proforma-pdf-query.ts create mode 100644 modules/customer-invoices/src/web/proformas/download-pdf/index.ts diff --git a/modules/customer-invoices/src/web/issued-invoices/download-pdf/api/download-invoice-pdf.api.ts b/modules/customer-invoices/src/web/issued-invoices/download-pdf/api/download-invoice-pdf.api.ts index ef728c68..6cc494e6 100644 --- a/modules/customer-invoices/src/web/issued-invoices/download-pdf/api/download-invoice-pdf.api.ts +++ b/modules/customer-invoices/src/web/issued-invoices/download-pdf/api/download-invoice-pdf.api.ts @@ -5,9 +5,9 @@ export async function downloadInvoicePDFApi( invoiceId: string, params?: Record ): Promise { - return dataSource.custom({ + return dataSource.custom({ ...params, - path: `issued-invoices/${invoiceId}/report`, + path: `issued-invoices/${invoiceId}/report/pdf`, method: "get", responseType: "blob", }); diff --git a/modules/customer-invoices/src/web/issued-invoices/download-pdf/controller/use-download-invoice-pdf.controller.ts b/modules/customer-invoices/src/web/issued-invoices/download-pdf/controller/use-download-invoice-pdf.controller.ts index e19010f9..a14d2fb1 100644 --- a/modules/customer-invoices/src/web/issued-invoices/download-pdf/controller/use-download-invoice-pdf.controller.ts +++ b/modules/customer-invoices/src/web/issued-invoices/download-pdf/controller/use-download-invoice-pdf.controller.ts @@ -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" diff --git a/modules/customer-invoices/src/web/issued-invoices/download-pdf/hooks/index.ts b/modules/customer-invoices/src/web/issued-invoices/download-pdf/hooks/index.ts index e69de29b..fba7aefc 100644 --- a/modules/customer-invoices/src/web/issued-invoices/download-pdf/hooks/index.ts +++ b/modules/customer-invoices/src/web/issued-invoices/download-pdf/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-download-invoice-pdf-query"; diff --git a/modules/customer-invoices/src/web/issued-invoices/shared/hooks/use-download-invoice-pdf-query.ts b/modules/customer-invoices/src/web/issued-invoices/download-pdf/hooks/use-download-invoice-pdf-query.ts similarity index 96% rename from modules/customer-invoices/src/web/issued-invoices/shared/hooks/use-download-invoice-pdf-query.ts rename to modules/customer-invoices/src/web/issued-invoices/download-pdf/hooks/use-download-invoice-pdf-query.ts index 10c7be4f..0bef7f2d 100644 --- a/modules/customer-invoices/src/web/issued-invoices/shared/hooks/use-download-invoice-pdf-query.ts +++ b/modules/customer-invoices/src/web/issued-invoices/download-pdf/hooks/use-download-invoice-pdf-query.ts @@ -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; diff --git a/modules/customer-invoices/src/web/issued-invoices/shared/hooks/index.ts b/modules/customer-invoices/src/web/issued-invoices/shared/hooks/index.ts index 780a37a8..6903c330 100644 --- a/modules/customer-invoices/src/web/issued-invoices/shared/hooks/index.ts +++ b/modules/customer-invoices/src/web/issued-invoices/shared/hooks/index.ts @@ -1,2 +1 @@ -export * from "./use-download-invoice-pdf-query"; export * from "./use-issued-invoice-list-query"; diff --git a/modules/customer-invoices/src/web/proformas/download-pdf/api/download-proforma-pdf.api.ts b/modules/customer-invoices/src/web/proformas/download-pdf/api/download-proforma-pdf.api.ts new file mode 100644 index 00000000..bd6fbeaa --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/download-pdf/api/download-proforma-pdf.api.ts @@ -0,0 +1,14 @@ +import type { IDataSource } from "@erp/core/client"; + +export async function downloadProformaPDFApi( + dataSource: IDataSource, + proformaId: string, + params?: Record +): Promise { + return dataSource.custom({ + ...params, + path: `proformas/${proformaId}/report/pdf`, + method: "get", + responseType: "blob", + }); +} diff --git a/modules/customer-invoices/src/web/proformas/download-pdf/api/index.ts b/modules/customer-invoices/src/web/proformas/download-pdf/api/index.ts new file mode 100644 index 00000000..267dead0 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/download-pdf/api/index.ts @@ -0,0 +1 @@ +export * from "./download-proforma-pdf.api"; diff --git a/modules/customer-invoices/src/web/proformas/download-pdf/controller/index.ts b/modules/customer-invoices/src/web/proformas/download-pdf/controller/index.ts new file mode 100644 index 00000000..258ea72d --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/download-pdf/controller/index.ts @@ -0,0 +1 @@ +export * from "./use-download-proforma-pdf.controller"; diff --git a/modules/customer-invoices/src/web/proformas/download-pdf/controller/use-download-proforma-pdf.controller.ts b/modules/customer-invoices/src/web/proformas/download-pdf/controller/use-download-proforma-pdf.controller.ts new file mode 100644 index 00000000..c498521a --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/download-pdf/controller/use-download-proforma-pdf.controller.ts @@ -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(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, + }; +} diff --git a/modules/customer-invoices/src/web/proformas/download-pdf/hooks/index.ts b/modules/customer-invoices/src/web/proformas/download-pdf/hooks/index.ts new file mode 100644 index 00000000..06b98fe7 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/download-pdf/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-download-proforma-pdf-query"; diff --git a/modules/customer-invoices/src/web/proformas/download-pdf/hooks/use-download-proforma-pdf-query.ts b/modules/customer-invoices/src/web/proformas/download-pdf/hooks/use-download-proforma-pdf-query.ts new file mode 100644 index 00000000..e0d7265d --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/download-pdf/hooks/use-download-proforma-pdf-query.ts @@ -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. + +*/ diff --git a/modules/customer-invoices/src/web/proformas/download-pdf/index.ts b/modules/customer-invoices/src/web/proformas/download-pdf/index.ts new file mode 100644 index 00000000..d3c51b14 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/download-pdf/index.ts @@ -0,0 +1,3 @@ +export * from "./api"; +export * from "./controller"; +export * from "./hooks"; diff --git a/modules/customer-invoices/src/web/proformas/list/controllers/use-list-proformas-page.controller.ts b/modules/customer-invoices/src/web/proformas/list/controllers/use-list-proformas-page.controller.ts index 05cd2195..ccd271ac 100644 --- a/modules/customer-invoices/src/web/proformas/list/controllers/use-list-proformas-page.controller.ts +++ b/modules/customer-invoices/src/web/proformas/list/controllers/use-list-proformas-page.controller.ts @@ -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, }; }; diff --git a/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx b/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx index da1a1def..e46867be 100644 --- a/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx +++ b/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx @@ -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;