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