.
This commit is contained in:
parent
f85f40311a
commit
e2938594d2
@ -16,7 +16,8 @@ import {
|
||||
} from "@/ui";
|
||||
import { IListQuotes_Response_DTO } from "@shared/contexts";
|
||||
import { t } from "i18next";
|
||||
import { CopyIcon, MoreVerticalIcon, TruckIcon } from "lucide-react";
|
||||
import { DownloadIcon, MoreVerticalIcon, PrinterIcon } from "lucide-react";
|
||||
import printJS from "print-js";
|
||||
import { useMemo } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@ -54,17 +55,17 @@ export const QuotePDFPreview = ({
|
||||
|
||||
if (reportIsLoading || reportIsPending || reportIsFetching) {
|
||||
return (
|
||||
<Card className={cn("overflow-hidden", className)}>
|
||||
<CardHeader className='flex flex-row items-start'>
|
||||
<CardTitle className='flex items-center gap-2 text-lg group'>
|
||||
<Skeleton className='w-[250px] h-8 bg-card' />
|
||||
<Card className={cn("overflow-hidden flex flex-col", className)}>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Skeleton className='w-full h-8' />
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<Skeleton className='w-full h-8' />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='flex content-center px-4 py-6 text-center'>
|
||||
<Skeleton className='object-contain w-full overflow-hidden bg-white border-2 border-dashed rounded-lg min-h-min' />
|
||||
<CardContent className='py-4'>
|
||||
<Skeleton className='w-full aspect-[3/4] relative bg-white shadow flex-1' />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@ -74,20 +75,33 @@ export const QuotePDFPreview = ({
|
||||
<Card className={cn("overflow-hidden", className)}>
|
||||
<CardHeader className='bg-muted/50'>
|
||||
<CardTitle className='flex items-center gap-2 text-lg group'>
|
||||
{t("quotes.list.quote")}
|
||||
<Button
|
||||
size='icon'
|
||||
variant='outline'
|
||||
className='w-6 h-6 transition-opacity opacity-0 group-hover:opacity-100'
|
||||
>
|
||||
<CopyIcon className='w-3 h-3' />
|
||||
<span className='sr-only'>Copy Order ID</span>
|
||||
</Button>
|
||||
{t("quotes.list.preview.quote")}
|
||||
|
||||
<div className='flex items-center gap-1 ml-auto'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outline'
|
||||
className='h-8 gap-1'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
printJS({
|
||||
printable: file,
|
||||
type: "pdf",
|
||||
showModal: false,
|
||||
modalMessage: "Cargando...",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<PrinterIcon className='h-3.5 w-3.5' />
|
||||
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>
|
||||
{t("common.print")}
|
||||
</span>
|
||||
</Button>
|
||||
<Button size='sm' variant='outline' className='h-8 gap-1'>
|
||||
<TruckIcon className='h-3.5 w-3.5' />
|
||||
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>Track Order</span>
|
||||
<DownloadIcon className='h-3.5 w-3.5' />
|
||||
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>
|
||||
{t("quotes.list.preview.download_quote")}
|
||||
</span>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@ -108,11 +122,11 @@ export const QuotePDFPreview = ({
|
||||
<Trans i18nKey={"common.edit"} />
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Trans i18nKey={"common.download"} />
|
||||
<Trans i18nKey={"common.duplicate"} />
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Trans i18nKey={"common.remove"} />
|
||||
<Trans i18nKey={"common.archive"} />
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@ -123,19 +137,8 @@ export const QuotePDFPreview = ({
|
||||
{quote?.date.toString()}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PDFViewer
|
||||
file={file}
|
||||
className='object-contain my-6 overflow-hidden bg-white border-dashed rounded-none border-1'
|
||||
/>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
cancelQuery();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<CardContent className='py-4'>
|
||||
<PDFViewer file={file} className='object-contain' />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@ -169,7 +169,7 @@ export const QuotesDataTable = ({ status = "all" }: { status?: string }) => {
|
||||
<DataTableToolbar table={table} />
|
||||
</DataTable>
|
||||
|
||||
<QuotePDFPreview quote={focusedQuote} className='flex-1' />
|
||||
<QuotePDFPreview quote={focusedQuote} className='flex-1 ' />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
18
client/src/components/LoadingSpinner/LoadingSpinner.tsx
Normal file
18
client/src/components/LoadingSpinner/LoadingSpinner.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LoaderIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
const spinnerVariants = "w-6 h-6 rounded-full animate-spin";
|
||||
|
||||
interface LoadingSpinnerProps extends React.HTMLAttributes<SVGSVGElement> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const LoadingSpinner = React.forwardRef<SVGSVGElement, LoadingSpinnerProps>((props, ref) => {
|
||||
const { className, ...rest } = props;
|
||||
return <LoaderIcon ref={ref} className={cn(spinnerVariants, className)} {...rest} />;
|
||||
});
|
||||
|
||||
LoadingSpinner.displayName = "LoadingSpinner";
|
||||
|
||||
export { LoadingSpinner };
|
||||
1
client/src/components/LoadingSpinner/index.ts
Normal file
1
client/src/components/LoadingSpinner/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./LoadingSpinner";
|
||||
@ -1,9 +1,12 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/ui";
|
||||
import { useResizeObserver } from "@wojtekmaj/react-hooks";
|
||||
import type { PDFDocumentProxy } from "pdfjs-dist";
|
||||
import printJS from "print-js";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { t } from "i18next";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Document, Page, pdfjs } from "react-pdf";
|
||||
import { LoadingSpinner } from "../LoadingSpinner";
|
||||
|
||||
import "react-pdf/dist/esm/Page/AnnotationLayer.css";
|
||||
import "react-pdf/dist/esm/Page/TextLayer.css";
|
||||
|
||||
@ -28,15 +31,9 @@ export interface PDFViewerProps {
|
||||
}
|
||||
|
||||
export const PDFViewer = ({ file, className }: PDFViewerProps): JSX.Element => {
|
||||
const [eventResize, setEventResize] = useState(false);
|
||||
|
||||
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 [numPages, setNumPages] = useState<number>(0);
|
||||
const [pageNumber, setPageNumber] = useState<number>(1);
|
||||
const [renderedPageNumber, setRenderedPageNumber] = useState<number | undefined>(undefined);
|
||||
|
||||
const [containerRef, setContainerRef] = useState<HTMLElement | null>(null);
|
||||
const [containerWidth, setContainerWidth] = useState<number>();
|
||||
@ -51,38 +48,15 @@ export const PDFViewer = ({ file, className }: PDFViewerProps): JSX.Element => {
|
||||
|
||||
useResizeObserver(containerRef, resizeObserverOptions, onResize);
|
||||
|
||||
const hidePageCanvas = useCallback(() => {
|
||||
const canvas = containerRef?.current?.querySelector("canvas");
|
||||
if (canvas) canvas.style.visibility = "hidden";
|
||||
}, [containerRef]);
|
||||
|
||||
const showPageCanvas = useCallback(() => {
|
||||
const canvas = containerRef?.current?.querySelector("canvas");
|
||||
if (canvas) canvas.style.visibility = "visible";
|
||||
}, [containerRef]);
|
||||
|
||||
const onPageLoadSuccess = useCallback(() => {
|
||||
hidePageCanvas();
|
||||
}, [hidePageCanvas]);
|
||||
|
||||
const onPageRenderSuccess = useCallback(() => {
|
||||
showPageCanvas();
|
||||
}, [showPageCanvas]);
|
||||
setRenderedPageNumber(pageNumber);
|
||||
}, [setRenderedPageNumber, pageNumber]);
|
||||
|
||||
const onPageRenderError = useCallback(() => {
|
||||
showPageCanvas();
|
||||
}, [showPageCanvas]);
|
||||
|
||||
function onDocumentLoadSuccess({ numPages: nextNumPages }: PDFDocumentProxy): void {
|
||||
function onDocumentLoadSuccess({ numPages }: { numPages: number }) {
|
||||
setPageNumber(1);
|
||||
setNumPages(nextNumPages);
|
||||
setNumPages(numPages);
|
||||
}
|
||||
|
||||
/*function onDocumentLoadSuccess({ numPages: nextNumPages }: PDFDocumentProxy): void {
|
||||
setPageNumber(1);
|
||||
setNumPages(nextNumPages);
|
||||
}*/
|
||||
|
||||
const changePage = (offset: number) =>
|
||||
setPageNumber((prevPage) =>
|
||||
offset > 0 ? Math.min(prevPage + offset, numPages) : Math.max(prevPage + offset, 1)
|
||||
@ -93,77 +67,82 @@ export const PDFViewer = ({ file, className }: PDFViewerProps): JSX.Element => {
|
||||
const goToFirstPage = () => setPageNumber(1);
|
||||
const goToLastPage = () => setPageNumber(numPages);
|
||||
|
||||
useEffect(() => {
|
||||
if (numPages > 0 || eventResize) {
|
||||
const parentWidth = parentRef.current ? parentRef.current.offsetWidth : 0;
|
||||
const canvasWidth = canvasRef.current ? canvasRef.current.width : 0;
|
||||
|
||||
console.log("Document => ", parentWidth);
|
||||
console.log("Canvas => ", canvasWidth);
|
||||
|
||||
setWidth(parentWidth);
|
||||
setEventResize(false);
|
||||
}
|
||||
}, [eventResize, numPages]);
|
||||
|
||||
//file={`data:application/pdf;base64,$(pdfBase64String)`}
|
||||
|
||||
const isLoading = renderedPageNumber !== pageNumber;
|
||||
const isLoading = useMemo(
|
||||
() => renderedPageNumber !== pageNumber,
|
||||
[renderedPageNumber, pageNumber]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col' ref={setContainerRef}>
|
||||
<div
|
||||
className={cn("flex flex-col cursor-default text-center", className)}
|
||||
ref={setContainerRef}
|
||||
>
|
||||
<Document
|
||||
options={options}
|
||||
renderMode='canvas'
|
||||
file={file}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
className={className}
|
||||
renderMode='canvas'
|
||||
loading={<LoadingSpinner className='w-full mx-auto mt-32' />}
|
||||
options={options}
|
||||
className={`w-full aspect-[3/4] relative bg-white shadow w-[${
|
||||
containerWidth ? Math.min(containerWidth, maxWidth) : maxWidth
|
||||
}]`}
|
||||
>
|
||||
{isLoading && renderedPageNumber ? (
|
||||
<Page
|
||||
key={`page_${renderedPageNumber}`}
|
||||
className='prevPage'
|
||||
pageNumber={renderedPageNumber}
|
||||
width={width}
|
||||
/>
|
||||
) : null}
|
||||
{/**
|
||||
* IMPORTANT: Keys are necessary so that React will know which Page component
|
||||
* instances to use.
|
||||
* Without keys, on page number update, React would replace the page number
|
||||
* in 1st and 2nd page components. This may cause previously rendered page
|
||||
* to render again, thus causing a flash.
|
||||
* With keys, React, will add prevPage className to previously rendered page,
|
||||
* and mount new Page component instance for the new page.
|
||||
*/}
|
||||
|
||||
<Page
|
||||
className={isLoading && renderedPageNumber ? "visible" : "hidden"}
|
||||
key={`page_${renderedPageNumber}`}
|
||||
pageNumber={renderedPageNumber}
|
||||
canvasBackground={"white"}
|
||||
key={`page_${pageNumber}`}
|
||||
width={containerWidth ? Math.min(containerWidth, maxWidth) : maxWidth}
|
||||
/>
|
||||
|
||||
<Page
|
||||
className={cn(isLoading && renderedPageNumber ? "hidden" : "visible", "text-center")}
|
||||
key={pageNumber}
|
||||
pageNumber={pageNumber}
|
||||
canvasRef={canvasRef}
|
||||
width={width}
|
||||
onLoadSuccess={onPageLoadSuccess}
|
||||
canvasBackground={"white"}
|
||||
onRenderSuccess={onPageRenderSuccess}
|
||||
onRenderError={onPageRenderError}
|
||||
width={containerWidth ? Math.min(containerWidth, maxWidth) : maxWidth}
|
||||
/>
|
||||
</Document>
|
||||
<p className='text-sm font-medium text-center'>
|
||||
Página {pageNumber} de {numPages}
|
||||
</p>
|
||||
<div className='flex justify-between'>
|
||||
<Button size={"icon"} variant='link' onClick={goToPrevPage}>
|
||||
Prev
|
||||
</Button>
|
||||
<Button size={"icon"} variant='link' onClick={goToNextPage}>
|
||||
Next
|
||||
|
||||
<div className='flex flex-row justify-center w-full mt-4 space-x-4'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
className='w-8 h-8 p-0'
|
||||
onClick={goToPrevPage}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<span className='sr-only'>{t("common.go_to_prev_page")}</span>
|
||||
<ChevronLeftIcon className='w-4 h-4' />
|
||||
</Button>
|
||||
<Button
|
||||
size={"icon"}
|
||||
variant='link'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
printJS({
|
||||
printable: file,
|
||||
type: "pdf",
|
||||
showModal: false,
|
||||
modalMessage: "Cargando...",
|
||||
});
|
||||
}}
|
||||
type='button'
|
||||
variant='outline'
|
||||
className='w-8 h-8 p-0'
|
||||
onClick={goToNextPage}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Imprimir react
|
||||
<span className='sr-only'>{t("common.go_to_next_page")}</span>
|
||||
<ChevronRightIcon className='w-4 h-4' />
|
||||
</Button>
|
||||
</div>
|
||||
<p className='mt-4 text-sm font-medium text-center'>
|
||||
{t("common.num_page_of_total", {
|
||||
count: pageNumber,
|
||||
total: numPages,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -8,8 +8,8 @@ export * from "./EmptyState";
|
||||
export * from "./ErrorOverlay";
|
||||
export * from "./Forms";
|
||||
export * from "./Layout";
|
||||
export * from "./LoadingIndicator";
|
||||
export * from "./LoadingOverlay";
|
||||
export * from "./LoadingSpinner";
|
||||
export * from "./PDFViewer";
|
||||
export * from "./ProtectedRoute";
|
||||
//export * from "./SorteableDataTable";
|
||||
|
||||
@ -46,7 +46,11 @@
|
||||
"pick_date": "Select a date",
|
||||
"required_field": "This field is required",
|
||||
"unsaved_changes_prompt": "There are unsaved changes. If you leave, you'll lose your changes.",
|
||||
"edit": "Edit"
|
||||
"edit": "Edit",
|
||||
"remove": "Remove",
|
||||
"archive": "Archive",
|
||||
"duplicate": "Duplicate",
|
||||
"print": "Print"
|
||||
},
|
||||
"main_menu": {
|
||||
"home": "Home",
|
||||
@ -111,7 +115,10 @@
|
||||
"customer_information": "Customer",
|
||||
"total_price": "Imp. total"
|
||||
},
|
||||
"quote": "Quote"
|
||||
"preview": {
|
||||
"quote": "Quote",
|
||||
"download_quote": "Download quote"
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
"title": "New quote",
|
||||
|
||||
@ -46,7 +46,11 @@
|
||||
"pick_date": "Elige una fecha",
|
||||
"required_field": "Este campo es obligatorio",
|
||||
"unsaved_changes_prompt": "Los últimos cambios no se han guardado. Si continúas, se perderán.",
|
||||
"edit": "Editar"
|
||||
"edit": "Editar",
|
||||
"remove": "Eliminar",
|
||||
"archive": "Archivar",
|
||||
"duplicate": "Duplicar",
|
||||
"print": "Imprimir"
|
||||
},
|
||||
"main_menu": {
|
||||
"home": "Inicio",
|
||||
@ -110,6 +114,10 @@
|
||||
"status": "Estado",
|
||||
"customer_information": "Cliente",
|
||||
"total_price": "Imp. total"
|
||||
},
|
||||
"preview": {
|
||||
"quote": "Cotización",
|
||||
"download_quote": "Descargar"
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user