.
This commit is contained in:
parent
9edd351d2e
commit
2b6a8cf204
@ -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"
|
||||
},
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
44
client/src/components/DownloadDialog/DownloadDialog.tsx
Normal file
44
client/src/components/DownloadDialog/DownloadDialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
1
client/src/components/DownloadDialog/index.ts
Normal file
1
client/src/components/DownloadDialog/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./DownloadDialog";
|
||||
@ -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";
|
||||
|
||||
10
client/src/lib/api/apiAuthorization.ts
Normal file
10
client/src/lib/api/apiAuthorization.ts
Normal 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}` : "";
|
||||
};
|
||||
1
client/src/lib/api/index.ts
Normal file
1
client/src/lib/api/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./apiAuthorization";
|
||||
@ -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}?`;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1 +1,4 @@
|
||||
export type IReportQuote_Response_DTO = Uint8Array;
|
||||
export interface IReportQuote_Response_DTO {
|
||||
data: Uint8Array;
|
||||
original: ArrayBuffer;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user