diff --git a/client/package.json b/client/package.json index f552964..afb072e 100644 --- a/client/package.json +++ b/client/package.json @@ -67,6 +67,7 @@ "react-router-dom": "^6.26.0", "react-secure-storage": "^1.3.2", "react-toastify": "^10.0.5", + "react-use-downloader": "^1.2.8", "react-wrap-balancer": "^1.1.1", "recharts": "^2.12.7" }, diff --git a/client/src/app/quotes/components/QuotePDFPreview.tsx b/client/src/app/quotes/components/QuotePDFPreview.tsx index 1d626b1..b0a38d4 100644 --- a/client/src/app/quotes/components/QuotePDFPreview.tsx +++ b/client/src/app/quotes/components/QuotePDFPreview.tsx @@ -31,7 +31,7 @@ export const QuotePDFPreview = ({ className: string; }) => { const navigate = useNavigate(); - const { useReport } = useQuotes(); + const { useReport, useDownload } = useQuotes(); const { cancelQuery, @@ -41,7 +41,14 @@ export const QuotePDFPreview = ({ isFetching: reportIsFetching, } = useReport(quote?.id); - const file = useMemo(() => (reportData ? { data: reportData } : undefined), [reportData]); + const { + download, + error: downloadError, + isFetching: downloadIsFetching, + isError: downloadIsError, + } = useDownload(quote?.id); + + const file = useMemo(() => (reportData ? { data: reportData.data } : undefined), [reportData]); if (!quote) { return ( @@ -81,7 +88,7 @@ export const QuotePDFPreview = ({ onClick={(e) => { e.preventDefault(); printJS({ - printable: file?.data, + printable: reportData?.original, type: "pdf", showModal: false, modalMessage: "Cargando...", @@ -93,9 +100,17 @@ export const QuotePDFPreview = ({ {t("common.print")} - diff --git a/client/src/app/quotes/components/QuotesDataTable.tsx b/client/src/app/quotes/components/QuotesDataTable.tsx index f3df88d..756cb9c 100644 --- a/client/src/app/quotes/components/QuotesDataTable.tsx +++ b/client/src/app/quotes/components/QuotesDataTable.tsx @@ -2,6 +2,7 @@ import { ColorBadge, DataTable, DataTableSkeleton, + DownloadDialog, ErrorOverlay, SimpleEmptyState, } from "@/components"; @@ -29,6 +30,7 @@ import { t } from "i18next"; import { FilePenLineIcon, MoreVerticalIcon } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; +import useDownloader from "react-use-downloader"; import { useQuotes } from "../hooks"; import { QuotePDFPreview } from "./QuotePDFPreview"; @@ -44,7 +46,7 @@ export const QuotesDataTable = ({ const [activeRow, setActiveRow] = useState | undefined>(undefined); - const { useList } = useQuotes(); + const { useList, getQuotePDFFilename } = useQuotes(); const { data, isPending, isError, error } = useList({ pagination: { @@ -55,6 +57,8 @@ export const QuotesDataTable = ({ quickSearchTerm: globalFilter, }); + const { download, ...downloadProps } = useDownloader(); + const columns = useMemo[]>( () => [ { @@ -158,7 +162,14 @@ export const QuotesDataTable = ({ Edit - Export + { + e.preventDefault(); + download(row.original.id, getQuotePDFFilename(row.original)); + }} + > + Download + Trash @@ -227,24 +238,27 @@ export const QuotesDataTable = ({ } return ( - - - - - - - {preview && } - {preview && ( - - + <> + + + + + - )} - + {preview && } + {preview && ( + + + + )} + + + ); }; diff --git a/client/src/app/quotes/hooks/useQuotes.tsx b/client/src/app/quotes/hooks/useQuotes.tsx index e6755c9..f5c3774 100644 --- a/client/src/app/quotes/hooks/useQuotes.tsx +++ b/client/src/app/quotes/hooks/useQuotes.tsx @@ -1,5 +1,6 @@ import { UseListQueryResult, useCustom, useList, useOne, useSave } from "@/lib/hooks/useDataSource"; import { + IDownloadPDFDataProviderResponse, IFilterItemDataProviderParam, IGetListDataProviderParams, } from "@/lib/hooks/useDataSource/DataSource"; @@ -19,6 +20,7 @@ import { } from "@shared/contexts"; import { useQueryClient } from "@tanstack/react-query"; import { useCallback, useMemo } from "react"; +import useDownloader from "react-use-downloader"; export type UseQuotesListParams = Omit & { status?: string; @@ -63,7 +65,7 @@ export const useQuotes = () => { const dataSource = useDataSource(); const keys = useQueryKey(); - return { + const actions = { useList: (params: UseQuotesListParams): UseQuotesListResponse => { const dataSource = useDataSource(); const keys = useQueryKey(); @@ -160,11 +162,85 @@ export const useQuotes = () => { }), enabled: !!id, - select: useCallback((data: ArrayBuffer) => new Uint8Array(data), []), + select: useCallback( + (data: ArrayBuffer) => ({ + original: data, + data: new Uint8Array(data), + }), + [] + ), ...params, }), cancelQuery: () => queryClient.cancelQueries({ queryKey }), }; }, + + getQuotePDFDownloadURL: (id: string) => `${dataSource.getApiUrl()}/quotes/${id}/report`, + + getQuotePDFFilename: (quote: IListQuotes_Response_DTO | IGetQuote_Response_DTO) => + "filename-quote.pdf", + + useDownload2: (id?: string, params?: UseQuotesReportParamsType) => { + const queryKey = useMemo( + () => keys().data().resource("quotes").action("report").id(id).params().get(), + [id] + ); + + const { data, error, refetch, isFetching, isError } = useCustom< + IDownloadPDFDataProviderResponse, + TDataSourceError + >({ + queryKey, + queryFn: () => + dataSource.downloadPDF({ + url: `${dataSource.getApiUrl()}/quotes/${id}/report`, + }), + + enabled: false, + refetchInterval: false, + ...params, + }); + + const download = () => { + console.log("pido"); + refetch().then((result) => { + console.log("termino"); + if (result.isSuccess) { + const blob = data!.filedata; + const link = document.createElement("a"); + const url = window.URL.createObjectURL(blob); + link.href = url; + link.setAttribute("download", data!.filename); // Nombre del archivo + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + } + }); + }; + + return { download, error, isFetching, isError }; + }, + + useDownloader: () => { + const auth = dataSource.getApiAuthorization(); + const downloader = useDownloader({ + credentials: "include", + headers: { + Authorization: auth, + }, + }); + + const download = (id: string, filename?: string) => { + console.log(actions.getQuotePDFDownloadURL(id)); + //return downloader.download(actions.getQuotePDFDownloadURL(id), filename ?? "ssaas"); + }; + + return { + ...downloader, + download, + }; + }, }; + return actions; }; diff --git a/client/src/components/DownloadDialog/DownloadDialog.tsx b/client/src/components/DownloadDialog/DownloadDialog.tsx new file mode 100644 index 0000000..b2a7e29 --- /dev/null +++ b/client/src/components/DownloadDialog/DownloadDialog.tsx @@ -0,0 +1,44 @@ +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + Label, +} from "@/ui"; +import { UseDownloader } from "react-use-downloader/dist/types"; + +export const DownloadDialog = (props: Omit) => { + const { size, elapsed, percentage, cancel, error, isInProgress } = props; + + return ( + + + + Share link + Anyone who has this link will be able to view this. + +
+

Download is in {isInProgress ? "in progress" : "stopped"}

+ + +

Download size in bytes {size}

+ + +

Elapsed time in seconds {elapsed}

+ {error &&

possible error {JSON.stringify(error)}

} +
+ + + + + +
+
+ ); +}; diff --git a/client/src/components/DownloadDialog/index.ts b/client/src/components/DownloadDialog/index.ts new file mode 100644 index 0000000..6c079a3 --- /dev/null +++ b/client/src/components/DownloadDialog/index.ts @@ -0,0 +1 @@ +export * from "./DownloadDialog"; diff --git a/client/src/components/index.ts b/client/src/components/index.ts index ce9e226..45c4c6b 100644 --- a/client/src/components/index.ts +++ b/client/src/components/index.ts @@ -5,6 +5,7 @@ export * from "./Container"; export * from "./CustomButtons"; export * from "./CustomDialog"; export * from "./DataTable"; +export * from "./DownloadDialog"; export * from "./EmptyState"; export * from "./ErrorOverlay"; export * from "./Forms"; diff --git a/client/src/lib/api/apiAuthorization.ts b/client/src/lib/api/apiAuthorization.ts new file mode 100644 index 0000000..fa2ebf1 --- /dev/null +++ b/client/src/lib/api/apiAuthorization.ts @@ -0,0 +1,10 @@ +import { ILogin_Response_DTO } from "@shared/contexts"; +import secureLocalStorage from "react-secure-storage"; + +export const getApiAuthorization = () => { + const authInfo: ILogin_Response_DTO = secureLocalStorage.getItem( + "uecko.auth" + ) as ILogin_Response_DTO; + + return authInfo && authInfo.token ? `Bearer ${authInfo.token}` : ""; +}; diff --git a/client/src/lib/api/index.ts b/client/src/lib/api/index.ts new file mode 100644 index 0000000..a4367a2 --- /dev/null +++ b/client/src/lib/api/index.ts @@ -0,0 +1 @@ +export * from "./apiAuthorization"; diff --git a/client/src/lib/axios/createAxiosDataProvider.ts b/client/src/lib/axios/createAxiosDataProvider.ts index 8b460d5..9eb0e4e 100644 --- a/client/src/lib/axios/createAxiosDataProvider.ts +++ b/client/src/lib/axios/createAxiosDataProvider.ts @@ -1,8 +1,11 @@ import { IListResponse_DTO, INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@shared/contexts"; +import { getApiAuthorization as getApiAuthLib } from "../api"; import { ICreateOneDataProviderParams, ICustomDataProviderParam, IDataSource, + IDownloadPDFDataProviderParams, + IDownloadPDFDataProviderResponse, IFilterItemDataProviderParam, IGetListDataProviderParams, IGetOneDataProviderParams, @@ -19,9 +22,9 @@ export const createAxiosDataProvider = ( ): IDataSource => ({ name: () => "AxiosDataProvider", - getApiUrl: () => { - return apiUrl; - }, + getApiUrl: () => apiUrl, + + getApiAuthorization: getApiAuthLib, getList: async (params: IGetListDataProviderParams): Promise> => { const { resource, quickSearchTerm, pagination, filters, sort } = params; @@ -116,6 +119,36 @@ export const createAxiosDataProvider = ( return; }, + downloadPDF: async ( + params: IDownloadPDFDataProviderParams + ): Promise => { + const { url, config } = params; + + const response = await httpClient.get(url, { + responseType: "arraybuffer", // Esto es necesario para recibir los datos en formato ArrayBuffer + ...config, + }); + + // Extraer el nombre del archivo de la cabecera Content-Disposition + const contentDisposition = response.headers["content-disposition"]; + let filename: string = "downloaded-file.pdf"; // Valor por defecto si no se encuentra el nombre en la cabecera + + if (contentDisposition) { + const match = contentDisposition.match(/filename="?(.+)"?/); + if (match && match[1]) { + filename = match[1]; + } + } + + // Crear un Blob con los datos descargados + const filedata = new Blob([response.data], { type: "application/pdf" }); + + return { + filename, + filedata, + }; + }, + custom: async (params: ICustomDataProviderParam): Promise => { const { url, method, responseType, headers, signal, ...payload } = params; const requestUrl = `${url}?`; diff --git a/client/src/lib/axios/setupInterceptors.ts b/client/src/lib/axios/setupInterceptors.ts index 4e38d49..6332da2 100644 --- a/client/src/lib/axios/setupInterceptors.ts +++ b/client/src/lib/axios/setupInterceptors.ts @@ -1,14 +1,8 @@ -import { ILogin_Response_DTO } from "@shared/contexts"; import { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from "axios"; -import secureLocalStorage from "react-secure-storage"; +import { getApiAuthorization } from "../api"; const onRequest = (request: InternalAxiosRequestConfig): InternalAxiosRequestConfig => { - const authInfo: ILogin_Response_DTO = secureLocalStorage.getItem( - "uecko.auth" - ) as ILogin_Response_DTO; - if (authInfo && authInfo.token && request.headers) { - request.headers.Authorization = `Bearer ${authInfo.token}`; - } + request.headers.Authorization = getApiAuthorization(); return request; }; diff --git a/client/src/lib/hooks/useDataSource/DataSource.ts b/client/src/lib/hooks/useDataSource/DataSource.ts index 45fc7e2..b35bcc4 100644 --- a/client/src/lib/hooks/useDataSource/DataSource.ts +++ b/client/src/lib/hooks/useDataSource/DataSource.ts @@ -52,6 +52,18 @@ export interface IRemoveOneDataProviderParams { id: string; } +export interface IDownloadPDFDataProviderParams { + url: string; + config?: { + [key: string]: unknown; + }; +} + +export interface IDownloadPDFDataProviderResponse { + filename: string; + filedata: Blob; +} + export interface ICustomDataProviderParam { url: string; method: "get" | "delete" | "head" | "options" | "post" | "put" | "patch"; @@ -71,10 +83,13 @@ export interface IDataSource { createOne: (params: ICreateOneDataProviderParams

) => Promise; updateOne: (params: IUpdateOneDataProviderParams

) => Promise; removeOne: (params: IRemoveOneDataProviderParams) => Promise; - + downloadPDF: ( + params: IDownloadPDFDataProviderParams + ) => Promise; custom: (params: ICustomDataProviderParam) => Promise; getApiUrl: () => string; + getApiAuthorization: () => string; //create: () => any; //createMany: () => any; diff --git a/shared/lib/contexts/sales/application/dto/Quote/ReportQuote.dto/IReportQuote_Response.dto.ts b/shared/lib/contexts/sales/application/dto/Quote/ReportQuote.dto/IReportQuote_Response.dto.ts index a540bfa..8f46a4b 100644 --- a/shared/lib/contexts/sales/application/dto/Quote/ReportQuote.dto/IReportQuote_Response.dto.ts +++ b/shared/lib/contexts/sales/application/dto/Quote/ReportQuote.dto/IReportQuote_Response.dto.ts @@ -1 +1,4 @@ -export type IReportQuote_Response_DTO = Uint8Array; +export interface IReportQuote_Response_DTO { + data: Uint8Array; + original: ArrayBuffer; +}