diff --git a/client/src/app/quotes/components/QuotePDFPreview.tsx b/client/src/app/quotes/components/QuotePDFPreview.tsx new file mode 100644 index 0000000..b6eccb7 --- /dev/null +++ b/client/src/app/quotes/components/QuotePDFPreview.tsx @@ -0,0 +1,142 @@ +import { PDFViewer } from "@/components"; +import { cn } from "@/lib/utils"; +import { + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, + Skeleton, +} from "@/ui"; +import { IListQuotes_Response_DTO } from "@shared/contexts"; +import { t } from "i18next"; +import { CopyIcon, MoreVerticalIcon, TruckIcon } from "lucide-react"; +import { useMemo } from "react"; +import { Trans } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { useQuotes } from "../hooks"; + +export const QuotePDFPreview = ({ + quote, + className, +}: { + quote?: IListQuotes_Response_DTO; + className: string; +}) => { + const navigate = useNavigate(); + const { useReport } = useQuotes(); + + const { + cancelQuery, + data: reportData, + isLoading: reportIsLoading, + isPending: reportIsPending, + isFetching: reportIsFetching, + } = useReport(quote?.id); + + const file = useMemo(() => (reportData ? { data: reportData } : undefined), [reportData]); + + if (!quote) { + return ( + + +

Select a quote

+
+
+ ); + } + + if (reportIsLoading || reportIsPending || reportIsFetching) { + return ( + + + + + + + + + + + + + + ); + } + + return ( + + + + {t("quotes.list.quote")} + + +
+ + + + + + + { + e.preventDefault(); + navigate(`/quotes/edit/${quote.id}`, { relative: "path" }); + }} + > + + + + + + + + + + + +
+
+ + {quote?.reference} + {quote?.date.toString()} + +
+ + + + +
+ ); +}; diff --git a/client/src/app/quotes/components/QuotesDataTable.tsx b/client/src/app/quotes/components/QuotesDataTable.tsx index 8740ad4..8f4e133 100644 --- a/client/src/app/quotes/components/QuotesDataTable.tsx +++ b/client/src/app/quotes/components/QuotesDataTable.tsx @@ -1,13 +1,7 @@ -import { - DataTable, - DataTableSkeleton, - ErrorOverlay, - PDFViewer, - SimpleEmptyState, -} from "@/components"; +import { DataTable, DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components"; import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar"; import { useDataTable, useDataTableContext } from "@/lib/hooks"; -import { Badge, Button, Card, CardContent, CardHeader } from "@/ui"; +import { Badge, Button, Card, CardContent } from "@/ui"; import { IListQuotes_Response_DTO, MoneyValue, UTCDateValue } from "@shared/contexts"; import { ColumnDef, Row } from "@tanstack/react-table"; import { t } from "i18next"; @@ -15,12 +9,13 @@ import { useMemo, useState } from "react"; import { Trans } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { useQuotes } from "../hooks"; +import { QuotePDFPreview } from "./QuotePDFPreview"; export const QuotesDataTable = ({ status = "all" }: { status?: string }) => { const navigate = useNavigate(); const { pagination, globalFilter, isFiltered } = useDataTableContext(); - const [focusedQuote, setFocusedQuote] = useState(undefined); - const { useList, useReport } = useQuotes(); + const [focusedQuote, setFocusedQuote] = useState(undefined); + const { useList } = useQuotes(); const { data, isPending, isError, error } = useList({ pagination: { @@ -31,14 +26,6 @@ export const QuotesDataTable = ({ status = "all" }: { status?: string }) => { quickSearchTerm: globalFilter, }); - const { - data: reportData, - isLoading: reportIsLoading, - isPending: reportIsPending, - isError: reportIsError, - error: errorReport, - } = useReport(focusedQuote); - const columns = useMemo[]>( () => [ { @@ -135,8 +122,7 @@ export const QuotesDataTable = ({ status = "all" }: { status?: string }) => { }); const handleOnRowClick = (row: Row) => { - console.log("setFocusedRow", row.id); - setFocusedQuote(row.original.id); + setFocusedQuote(row.original); }; if (isError) { @@ -172,10 +158,8 @@ export const QuotesDataTable = ({ status = "all" }: { status?: string }) => { ); } - console.log(reportData?.toString()); - return ( -
+
{ > -
- - - - - - -
+ +
); }; diff --git a/client/src/app/quotes/hooks/useQuotes.tsx b/client/src/app/quotes/hooks/useQuotes.tsx index f9089ec..e6755c9 100644 --- a/client/src/app/quotes/hooks/useQuotes.tsx +++ b/client/src/app/quotes/hooks/useQuotes.tsx @@ -17,7 +17,8 @@ import { IUpdateQuote_Response_DTO, UniqueID, } from "@shared/contexts"; -import { useCallback } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { useCallback, useMemo } from "react"; export type UseQuotesListParams = Omit & { status?: string; @@ -140,18 +141,30 @@ export const useQuotes = () => { ...params, }), - useReport: (id?: string, params?: UseQuotesReportParamsType) => - useCustom({ - queryKey: keys().data().resource("quotes").action("report").id(id).params().get(), - queryFn: () => - dataSource.custom({ - url: `${dataSource.getApiUrl()}/quotes/${id}/report`, - method: "get", - responseType: "arraybuffer", - }), - enabled: !!id, - select: useCallback((data: ArrayBuffer) => new Uint8Array(data), []), - ...params, - }), + useReport: (id?: string, params?: UseQuotesReportParamsType) => { + const queryClient = useQueryClient(); + const queryKey = useMemo( + () => keys().data().resource("quotes").action("report").id(id).params().get(), + [id] + ); + + return { + ...useCustom({ + queryKey, + queryFn: ({ signal }) => + dataSource.custom({ + url: `${dataSource.getApiUrl()}/quotes/${id}/report`, + method: "get", + responseType: "arraybuffer", + signal, + }), + + enabled: !!id, + select: useCallback((data: ArrayBuffer) => new Uint8Array(data), []), + ...params, + }), + cancelQuery: () => queryClient.cancelQueries({ queryKey }), + }; + }, }; }; diff --git a/client/src/components/PDFViewer/PDFViewer.tsx b/client/src/components/PDFViewer/PDFViewer.tsx index 18799a1..9ba3b9c 100644 --- a/client/src/components/PDFViewer/PDFViewer.tsx +++ b/client/src/components/PDFViewer/PDFViewer.tsx @@ -2,7 +2,7 @@ import { Button } from "@/ui"; import { useResizeObserver } from "@wojtekmaj/react-hooks"; import type { PDFDocumentProxy } from "pdfjs-dist"; import printJS from "print-js"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { Document, Page, pdfjs } from "react-pdf"; import "react-pdf/dist/esm/Page/AnnotationLayer.css"; import "react-pdf/dist/esm/Page/TextLayer.css"; @@ -21,7 +21,9 @@ const maxWidth = 800; const resizeObserverOptions = {}; export interface PDFViewerProps { - file?: Uint8Array; + file?: { + data: Uint8Array; + }; className?: string; } @@ -30,10 +32,11 @@ export const PDFViewer = ({ file, className }: PDFViewerProps): JSX.Element => { const [numPages, setNumPages] = useState(0); const [pageNumber, setPageNumber] = useState(1); + const [renderedPageNumber, setRenderedPageNumber] = useState(null); const [width, setWidth] = useState(undefined); - //const parentRef = useRef(null); - //const canvasRef = useRef(null); + const parentRef = useRef(null); + const canvasRef = useRef(null); const [containerRef, setContainerRef] = useState(null); const [containerWidth, setContainerWidth] = useState(); @@ -48,7 +51,7 @@ export const PDFViewer = ({ file, className }: PDFViewerProps): JSX.Element => { useResizeObserver(containerRef, resizeObserverOptions, onResize); - /*const hidePageCanvas = useCallback(() => { + const hidePageCanvas = useCallback(() => { const canvas = containerRef?.current?.querySelector("canvas"); if (canvas) canvas.style.visibility = "hidden"; }, [containerRef]); @@ -69,9 +72,9 @@ export const PDFViewer = ({ file, className }: PDFViewerProps): JSX.Element => { const onPageRenderError = useCallback(() => { showPageCanvas(); }, [showPageCanvas]); - */ function onDocumentLoadSuccess({ numPages: nextNumPages }: PDFDocumentProxy): void { + setPageNumber(1); setNumPages(nextNumPages); } @@ -90,7 +93,7 @@ export const PDFViewer = ({ file, className }: PDFViewerProps): JSX.Element => { const goToFirstPage = () => setPageNumber(1); const goToLastPage = () => setPageNumber(numPages); - /*useEffect(() => { + useEffect(() => { if (numPages > 0 || eventResize) { const parentWidth = parentRef.current ? parentRef.current.offsetWidth : 0; const canvasWidth = canvasRef.current ? canvasRef.current.width : 0; @@ -101,10 +104,12 @@ export const PDFViewer = ({ file, className }: PDFViewerProps): JSX.Element => { setWidth(parentWidth); setEventResize(false); } - }, [eventResize, numPages]);**/ + }, [eventResize, numPages]); //file={`data:application/pdf;base64,$(pdfBase64String)`} + const isLoading = renderedPageNumber !== pageNumber; + return (
{ file={file} onLoadSuccess={onDocumentLoadSuccess} className={className} + renderMode='canvas' > - {Array.from(new Array(numPages), (_el, index) => ( + {isLoading && renderedPageNumber ? ( - ))} + ) : null} + -

+

Página {pageNumber} de {numPages}

-
+
diff --git a/client/src/lib/axios/createAxiosDataProvider.ts b/client/src/lib/axios/createAxiosDataProvider.ts index 4c69ab4..8b460d5 100644 --- a/client/src/lib/axios/createAxiosDataProvider.ts +++ b/client/src/lib/axios/createAxiosDataProvider.ts @@ -117,7 +117,7 @@ export const createAxiosDataProvider = ( }, custom: async (params: ICustomDataProviderParam): Promise => { - const { url, method, responseType, headers, ...payload } = params; + const { url, method, responseType, headers, signal, ...payload } = params; const requestUrl = `${url}?`; /*if (sort) { @@ -168,6 +168,7 @@ export const createAxiosDataProvider = ( default: customResponse = await httpClient.get(requestUrl, { responseType, + signal, headers, }); break; diff --git a/client/src/lib/hooks/useDataSource/DataSource.ts b/client/src/lib/hooks/useDataSource/DataSource.ts index c4026ee..45fc7e2 100644 --- a/client/src/lib/hooks/useDataSource/DataSource.ts +++ b/client/src/lib/hooks/useDataSource/DataSource.ts @@ -55,6 +55,7 @@ export interface IRemoveOneDataProviderParams { export interface ICustomDataProviderParam { url: string; method: "get" | "delete" | "head" | "options" | "post" | "put" | "patch"; + signal?: AbortSignal; responseType?: ResponseType; headers?: { [key: string]: AxiosHeaderValue; diff --git a/client/src/lib/hooks/useDataSource/useCustom.tsx b/client/src/lib/hooks/useDataSource/useCustom.tsx index 77b7a33..5b27491 100644 --- a/client/src/lib/hooks/useDataSource/useCustom.tsx +++ b/client/src/lib/hooks/useDataSource/useCustom.tsx @@ -1,4 +1,4 @@ -import { UseQueryOptions, UseQueryResult, keepPreviousData, useQuery } from "@tanstack/react-query"; +import { UseQueryOptions, UseQueryResult, useQuery } from "@tanstack/react-query"; import { TDataSourceError, TDataSourceRecord } from "./types"; export const useCustom = < @@ -9,7 +9,6 @@ export const useCustom = < options: UseQueryOptions ): UseQueryResult => { return useQuery({ - placeholderData: keepPreviousData, ...options, }); }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index cd7aea2..84efdd8 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -110,7 +110,8 @@ "status": "Status", "customer_information": "Customer", "total_price": "Imp. total" - } + }, + "quote": "Quote" }, "create": { "title": "New quote",