This commit is contained in:
David Arranz 2024-08-20 23:40:37 +02:00
parent 9edd351d2e
commit 2b6a8cf204
13 changed files with 248 additions and 40 deletions

View File

@ -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"
},

View File

@ -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")}
</span>
</Button>
<Button size='sm' variant='outline' className='h-8 gap-1'>
<Button
size='sm'
variant='outline'
className='h-8 gap-1'
disabled={downloadIsFetching || downloadIsError}
onClick={(e) => {
download();
}}
>
<DownloadIcon className='h-3.5 w-3.5' />
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>
<span className='xl:sr-only 2xl:not-sr-only 2xl:whitespace-nowrap'>
{t("quotes.list.preview.download_quote")}
</span>
</Button>

View File

@ -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<Row<IListQuotes_Response_DTO> | 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<ColumnDef<IListQuotes_Response_DTO, any>[]>(
() => [
{
@ -158,7 +162,14 @@ export const QuotesDataTable = ({
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Export</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
download(row.original.id, getQuotePDFFilename(row.original));
}}
>
Download
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Trash</DropdownMenuItem>
</DropdownMenuContent>
@ -227,24 +238,27 @@ export const QuotesDataTable = ({
}
return (
<ResizablePanelGroup direction='horizontal' className='flex items-stretch flex-1 gap-4'>
<ResizablePanel defaultSize={preview ? 75 : 100} className='flex items-stretch flex-1'>
<DataTable
table={table}
paginationOptions={{ visible: true }}
className='grid items-start flex-1 gap-4 auto-rows-max md:gap-8 lg:col-span-2'
onRowClick={handleOnRowClick}
activeRowIndex={activeRow?.index}
>
<DataTableToolbar table={table} />
</DataTable>
</ResizablePanel>
{preview && <ResizableHandle withHandle />}
{preview && (
<ResizablePanel defaultSize={25} className='flex items-stretch flex-1'>
<QuotePDFPreview quote={activeRow?.original} className='flex-1' />
<>
<ResizablePanelGroup direction='horizontal' className='flex items-stretch flex-1 gap-4'>
<ResizablePanel defaultSize={preview ? 66 : 100} className='flex items-stretch flex-1'>
<DataTable
table={table}
paginationOptions={{ visible: true }}
className='grid items-start flex-1 gap-4 auto-rows-max md:gap-8 lg:col-span-2'
onRowClick={handleOnRowClick}
activeRowIndex={activeRow?.index}
>
<DataTableToolbar table={table} />
</DataTable>
</ResizablePanel>
)}
</ResizablePanelGroup>
{preview && <ResizableHandle withHandle />}
{preview && (
<ResizablePanel defaultSize={33} className='flex items-stretch flex-1'>
<QuotePDFPreview quote={activeRow?.original} className='flex-1' />
</ResizablePanel>
)}
</ResizablePanelGroup>
<DownloadDialog {...downloadProps} />
</>
);
};

View File

@ -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<IGetListDataProviderParams, "filters" | "resource"> & {
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;
};

View File

@ -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<UseDownloader, "download">) => {
const { size, elapsed, percentage, cancel, error, isInProgress } = props;
return (
<Dialog defaultOpen>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle>Share link</DialogTitle>
<DialogDescription>Anyone who has this link will be able to view this.</DialogDescription>
</DialogHeader>
<div>
<p>Download is in {isInProgress ? "in progress" : "stopped"}</p>
<button onClick={() => cancel()}>Cancel the download</button>
<p>Download size in bytes {size}</p>
<Label>Downloading progress:</Label>
<progress id='file' value={percentage} max='100' />
<p>Elapsed time in seconds {elapsed}</p>
{error && <p>possible error {JSON.stringify(error)}</p>}
</div>
<DialogFooter className='sm:justify-start'>
<DialogClose asChild>
<Button type='button' variant='secondary'>
Close
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1 @@
export * from "./DownloadDialog";

View File

@ -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";

View File

@ -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}` : "";
};

View File

@ -0,0 +1 @@
export * from "./apiAuthorization";

View File

@ -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 <R>(params: IGetListDataProviderParams): Promise<IListResponse_DTO<R>> => {
const { resource, quickSearchTerm, pagination, filters, sort } = params;
@ -116,6 +119,36 @@ export const createAxiosDataProvider = (
return;
},
downloadPDF: async (
params: IDownloadPDFDataProviderParams
): Promise<IDownloadPDFDataProviderResponse> => {
const { url, config } = params;
const response = await httpClient.get<ArrayBuffer>(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 <R>(params: ICustomDataProviderParam): Promise<R> => {
const { url, method, responseType, headers, signal, ...payload } = params;
const requestUrl = `${url}?`;

View File

@ -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;
};

View File

@ -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: <P, R>(params: ICreateOneDataProviderParams<P>) => Promise<R>;
updateOne: <P, R>(params: IUpdateOneDataProviderParams<P>) => Promise<R>;
removeOne: (params: IRemoveOneDataProviderParams) => Promise<void>;
downloadPDF: (
params: IDownloadPDFDataProviderParams
) => Promise<IDownloadPDFDataProviderResponse>;
custom: <R>(params: ICustomDataProviderParam) => Promise<R>;
getApiUrl: () => string;
getApiAuthorization: () => string;
//create: () => any;
//createMany: () => any;

View File

@ -1 +1,4 @@
export type IReportQuote_Response_DTO = Uint8Array;
export interface IReportQuote_Response_DTO {
data: Uint8Array;
original: ArrayBuffer;
}