From d298754ee515a56fffbbc22824d1736171815525 Mon Sep 17 00:00:00 2001 From: David Arranz Date: Sun, 18 Aug 2024 22:39:06 +0200 Subject: [PATCH] . --- client/package.json | 6 +- .../app/quotes/components/QuotesDataTable.tsx | 84 +++++++--- client/src/app/quotes/hooks/useQuotes.tsx | 27 ++- client/src/components/DataTable/DataTable.tsx | 6 +- client/src/components/PDFViewer/PDFViewer.tsx | 156 ++++++++++++++++++ client/src/components/PDFViewer/index.ts | 1 + client/src/components/index.ts | 1 + .../src/lib/axios/createAxiosDataProvider.ts | 55 ++++++ .../src/lib/hooks/useDataSource/DataSource.ts | 18 +- client/src/lib/hooks/useDataSource/index.ts | 1 + .../src/lib/hooks/useDataSource/useCustom.tsx | 14 ++ .../src/lib/hooks/useQueryKey/KeyBuilder.ts | 4 +- client/vite.config.ts | 27 ++- .../express/ExpressController.ts | 4 +- .../reportQuote/ReportQuote.controller.ts | 2 +- .../IReportQuote_Response.dto.ts | 1 + .../dto/Quote/ReportQuote.dto/index.ts | 1 + .../sales/application/dto/Quote/index.ts | 1 + 18 files changed, 369 insertions(+), 40 deletions(-) create mode 100644 client/src/components/PDFViewer/PDFViewer.tsx create mode 100644 client/src/components/PDFViewer/index.ts create mode 100644 client/src/lib/hooks/useDataSource/useCustom.tsx create mode 100644 shared/lib/contexts/sales/application/dto/Quote/ReportQuote.dto/IReportQuote_Response.dto.ts create mode 100644 shared/lib/contexts/sales/application/dto/Quote/ReportQuote.dto/index.ts diff --git a/client/package.json b/client/package.json index 5509b0f..f552964 100644 --- a/client/package.json +++ b/client/package.json @@ -44,6 +44,7 @@ "@radix-ui/react-tooltip": "^1.0.7", "@tanstack/react-query": "^5.51.23", "@tanstack/react-table": "^8.20.1", + "@wojtekmaj/react-hooks": "^1.21.0", "axios": "^1.7.3", "class-variance-authority": "^0.7.0", "cmdk": "^1.0.0", @@ -52,6 +53,7 @@ "i18next-browser-languagedetector": "^8.0.0", "joi": "^17.13.1", "lucide-react": "^0.427.0", + "print-js": "^1.6.0", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-currency-input-field": "^3.8.0", @@ -60,6 +62,7 @@ "react-hook-form": "^7.52.2", "react-hook-form-persist": "^3.0.0", "react-i18next": "^15.0.1", + "react-pdf": "^9.1.0", "react-resizable-panels": "^2.0.23", "react-router-dom": "^6.26.0", "react-secure-storage": "^1.3.2", @@ -94,6 +97,7 @@ "ts-jest": "^29.2.4", "ts-node": "^10.9.2", "typescript": "^5.5.4", - "vite": "^5.4.0" + "vite": "^5.4.0", + "vite-plugin-static-copy": "^1.0.6" } } diff --git a/client/src/app/quotes/components/QuotesDataTable.tsx b/client/src/app/quotes/components/QuotesDataTable.tsx index f87bec1..adda6d3 100644 --- a/client/src/app/quotes/components/QuotesDataTable.tsx +++ b/client/src/app/quotes/components/QuotesDataTable.tsx @@ -1,21 +1,34 @@ -import { Badge, Button, Card, CardContent } from "@/ui"; +import { Badge, Button, Card, CardContent, CardHeader } from "@/ui"; -import { DataTable, DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components"; +import { + DataTable, + DataTableSkeleton, + ErrorOverlay, + PDFViewer, + SimpleEmptyState, +} from "@/components"; import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar"; import { useDataTable, useDataTableContext } from "@/lib/hooks"; import { IListQuotes_Response_DTO, MoneyValue, UTCDateValue } from "@shared/contexts"; import { ColumnDef, Row, Table } from "@tanstack/react-table"; import { t } from "i18next"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { Trans } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { useQuotes } from "../hooks"; -export const QuotesDataTable = ({ status = "all" }: { status?: string }) => { +export const QuotesDataTable = ({ + status = "all", + className, +}: { + status?: string; + className?: string; +}) => { const navigate = useNavigate(); const { pagination, globalFilter, isFiltered } = useDataTableContext(); - const { useList } = useQuotes(); + const [focusedRow, setFocusedRow] = useState>(); + const { useList, useReport } = useQuotes(); const { data, isPending, isError, error } = useList({ pagination: { @@ -26,6 +39,13 @@ export const QuotesDataTable = ({ status = "all" }: { status?: string }) => { quickSearchTerm: globalFilter, }); + const { + data: reportData, + isPending: reportIsPending, + isError: reportIsError, + error: errorReport, + } = useReport(focusedRow ? focusedRow.original.id : undefined); + const columns = useMemo[]>( () => [ { @@ -119,23 +139,31 @@ export const QuotesDataTable = ({ status = "all" }: { status?: string }) => { pageCount: data?.total_pages ?? -1, }); + const handleOnRowClick = (row: Row) => { + console.log("setFocusedRow", row.id); + setFocusedRow(row); + }; + if (isError) { return ; } if (isPending) { return ( - - - - - +
+ + + + + +
+
); } @@ -149,11 +177,29 @@ export const QuotesDataTable = ({ status = "all" }: { status?: string }) => { ); } + console.log(reportData); + return ( - <> - +
+ - +
+ + + + + + +
+
); }; diff --git a/client/src/app/quotes/hooks/useQuotes.tsx b/client/src/app/quotes/hooks/useQuotes.tsx index fdfc6cf..e75ac18 100644 --- a/client/src/app/quotes/hooks/useQuotes.tsx +++ b/client/src/app/quotes/hooks/useQuotes.tsx @@ -1,4 +1,4 @@ -import { UseListQueryResult, useList, useOne, useSave } from "@/lib/hooks/useDataSource"; +import { UseListQueryResult, useCustom, useList, useOne, useSave } from "@/lib/hooks/useDataSource"; import { IFilterItemDataProviderParam, IGetListDataProviderParams, @@ -12,6 +12,7 @@ import { IGetQuote_Response_DTO, IListQuotes_Response_DTO, IListResponse_DTO, + IReportQuote_Response_DTO, IUpdateQuote_Request_DTO, IUpdateQuote_Response_DTO, UniqueID, @@ -33,6 +34,11 @@ export type UseQuotesGetParamsType = { queryOptions?: Record; }; +export type UseQuotesReportParamsType = { + enabled?: boolean; + queryOptions?: Record; +}; + const quoteStatusFilter: Record = { draft: { field: "status", @@ -85,7 +91,7 @@ export const useQuotes = () => { useOne: (id?: string, params?: UseQuotesGetParamsType) => useOne({ - queryKey: keys().data().resource("quotes").action("one").id("").params().get(), + queryKey: keys().data().resource("quotes").action("one").id(id).params().get(), queryFn: () => dataSource.getOne({ resource: "quotes", @@ -94,6 +100,7 @@ export const useQuotes = () => { enabled: !!id, ...params, }), + useCreate: () => useSave({ //mutationKey: keys().data().resource("quotes").action("one").id("").params().get(), @@ -131,5 +138,21 @@ export const useQuotes = () => { }); }, }), + + 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", + headers: { + responseType: "arraybuffer", + }, + }), + enabled: !!id, + select: (data) => new Uint8Array(data), + ...params, + }), }; }; diff --git a/client/src/components/DataTable/DataTable.tsx b/client/src/components/DataTable/DataTable.tsx index 2d7aeb1..6905127 100644 --- a/client/src/components/DataTable/DataTable.tsx +++ b/client/src/components/DataTable/DataTable.tsx @@ -1,4 +1,4 @@ -import { ColumnDef, Table as ReactTable, flexRender } from "@tanstack/react-table"; +import { ColumnDef, Table as ReactTable, Row, flexRender } from "@tanstack/react-table"; import { PropsWithChildren, ReactNode } from "react"; import { @@ -45,6 +45,7 @@ export type DataTableProps = PropsWithChildren<{ footerClassName?: string; rowClassName?: string; cellClassName?: string; + onRowClick?: (row: Row) => void; }>; export function DataTable({ @@ -60,6 +61,7 @@ export function DataTable({ footerClassName, rowClassName, cellClassName, + onRowClick, }: DataTableProps) { const headerVisible = headerOptions?.visible; @@ -104,6 +106,8 @@ export function DataTable({ {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( (onRowClick ? onRowClick(row) : null)} + tabIndex={0} key={row.id} data-state={row.getIsSelected() && "selected"} className={cn(row.getIsSelected() ? "bg-accent" : "", rowClassName)} diff --git a/client/src/components/PDFViewer/PDFViewer.tsx b/client/src/components/PDFViewer/PDFViewer.tsx new file mode 100644 index 0000000..18799a1 --- /dev/null +++ b/client/src/components/PDFViewer/PDFViewer.tsx @@ -0,0 +1,156 @@ +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 { Document, Page, pdfjs } from "react-pdf"; +import "react-pdf/dist/esm/Page/AnnotationLayer.css"; +import "react-pdf/dist/esm/Page/TextLayer.css"; + +pdfjs.GlobalWorkerOptions.workerSrc = new URL( + "pdfjs-dist/build/pdf.worker.min.mjs", + import.meta.url +).toString(); + +const options = { + cMapUrl: "/cmaps/", + standardFontDataUrl: "/standard_fonts/", +}; + +const maxWidth = 800; +const resizeObserverOptions = {}; + +export interface PDFViewerProps { + file?: Uint8Array; + className?: string; +} + +export const PDFViewer = ({ file, className }: PDFViewerProps): JSX.Element => { + const [eventResize, setEventResize] = useState(false); + + const [numPages, setNumPages] = useState(0); + const [pageNumber, setPageNumber] = useState(1); + + const [width, setWidth] = useState(undefined); + //const parentRef = useRef(null); + //const canvasRef = useRef(null); + + const [containerRef, setContainerRef] = useState(null); + const [containerWidth, setContainerWidth] = useState(); + + const onResize = useCallback((entries) => { + const [entry] = entries; + + if (entry) { + setContainerWidth(entry.contentRect.width); + } + }, []); + + 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]); + + const onPageRenderError = useCallback(() => { + showPageCanvas(); + }, [showPageCanvas]); + */ + + function onDocumentLoadSuccess({ numPages: nextNumPages }: PDFDocumentProxy): void { + setNumPages(nextNumPages); + } + + /*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) + ); + + const goToNextPage = () => changePage(1); + const goToPrevPage = () => changePage(-1); + 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)`} + + return ( +
+ + {Array.from(new Array(numPages), (_el, index) => ( + + ))} + +

+ Página {pageNumber} de {numPages} +

+
+ + + +
+
+ ); +}; diff --git a/client/src/components/PDFViewer/index.ts b/client/src/components/PDFViewer/index.ts new file mode 100644 index 0000000..8e76c42 --- /dev/null +++ b/client/src/components/PDFViewer/index.ts @@ -0,0 +1 @@ +export * from './PDFViewer'; diff --git a/client/src/components/index.ts b/client/src/components/index.ts index 1398e96..09aeea2 100644 --- a/client/src/components/index.ts +++ b/client/src/components/index.ts @@ -10,6 +10,7 @@ export * from "./Forms"; export * from "./Layout"; export * from "./LoadingIndicator"; export * from "./LoadingOverlay"; +export * from "./PDFViewer"; export * from "./ProtectedRoute"; //export * from "./SorteableDataTable"; export * from "./TailwindIndicator"; diff --git a/client/src/lib/axios/createAxiosDataProvider.ts b/client/src/lib/axios/createAxiosDataProvider.ts index 8a6e1fb..979dfc9 100644 --- a/client/src/lib/axios/createAxiosDataProvider.ts +++ b/client/src/lib/axios/createAxiosDataProvider.ts @@ -1,6 +1,7 @@ import { IListResponse_DTO, INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@shared/contexts"; import { ICreateOneDataProviderParams, + ICustomDataProviderParam, IDataSource, IFilterItemDataProviderParam, IGetListDataProviderParams, @@ -18,6 +19,10 @@ export const createAxiosDataProvider = ( ): IDataSource => ({ name: () => "AxiosDataProvider", + getApiUrl: () => { + return apiUrl; + }, + getList: async (params: IGetListDataProviderParams): Promise> => { const { resource, quickSearchTerm, pagination, filters, sort } = params; @@ -111,6 +116,56 @@ export const createAxiosDataProvider = ( return; }, + custom: async (params: ICustomDataProviderParam): Promise => { + const { url, method, headers, payload } = params; + const requestUrl = `${url}?`; + + /*if (sort) { + const generatedSort = extractSortParams(sort); + if (generatedSort) { + const { _sort, _order } = generatedSort; + const sortQuery = { + _sort: _sort.join(","), + _order: _order.join(","), + }; + requestUrl = `${requestUrl}&${queryString.stringify(sortQuery)}`; + } + } + + if (filters) { + const filterQuery = extractFilterParams(filters); + requestUrl = `${requestUrl}&${queryString.stringify(filterQuery)}`; + }*/ + + /*if (query) { + requestUrl = `${requestUrl}&${queryString.stringify(query)}`; + }*/ + + if (headers) { + httpClient.defaults.headers = { + ...httpClient.defaults.headers, + ...headers, + }; + } + + let customResponse; + switch (method) { + case "put": + case "post": + case "patch": + customResponse = await httpClient[method](url, payload); + break; + case "remove": + customResponse = await httpClient.delete(url); + break; + default: + customResponse = await httpClient.get(requestUrl); + break; + } + + return customResponse.data; + }, + /*getMany: async ({ resource }) => { const { body } = await httpClient.request({ url: `${apiUrl}/${resource}`, diff --git a/client/src/lib/hooks/useDataSource/DataSource.ts b/client/src/lib/hooks/useDataSource/DataSource.ts index 1cebc92..d402d62 100644 --- a/client/src/lib/hooks/useDataSource/DataSource.ts +++ b/client/src/lib/hooks/useDataSource/DataSource.ts @@ -1,4 +1,5 @@ import { IListResponse_DTO } from "@shared/contexts"; +import { AxiosHeaderValue } from "axios"; export interface IPaginationDataProviderParam { pageIndex: number; @@ -51,11 +52,14 @@ export interface IRemoveOneDataProviderParams { id: string; } -/*export interface ICustomDataProviderParam { - resource: string; - method: string; - params: any; -}*/ +export interface ICustomDataProviderParam { + url: string; + method: "get" | "delete" | "head" | "options" | "post" | "put" | "patch"; + headers?: { + [key: string]: AxiosHeaderValue; + }; + payload?: unknown; +} export interface IDataSource { name: () => string; @@ -66,9 +70,9 @@ export interface IDataSource { updateOne: (params: IUpdateOneDataProviderParams

) => Promise; removeOne: (params: IRemoveOneDataProviderParams) => Promise; - //custom: (params: ICustomDataProviderParam) => Promise; + custom: (params: ICustomDataProviderParam) => Promise; - //getApiUrl: () => string; + getApiUrl: () => string; //create: () => any; //createMany: () => any; diff --git a/client/src/lib/hooks/useDataSource/index.ts b/client/src/lib/hooks/useDataSource/index.ts index b140c77..bfa342b 100644 --- a/client/src/lib/hooks/useDataSource/index.ts +++ b/client/src/lib/hooks/useDataSource/index.ts @@ -1,5 +1,6 @@ // export * from './useApiUrl'; // export * from './useCreateMany'; +export * from "./useCustom"; export * from "./useList"; export * from "./useMany"; export * from "./useOne"; diff --git a/client/src/lib/hooks/useDataSource/useCustom.tsx b/client/src/lib/hooks/useDataSource/useCustom.tsx new file mode 100644 index 0000000..5b900bc --- /dev/null +++ b/client/src/lib/hooks/useDataSource/useCustom.tsx @@ -0,0 +1,14 @@ +import { UseQueryOptions, UseQueryResult, keepPreviousData, useQuery } from "@tanstack/react-query"; + +import { TDataSourceError, TDataSourceRecord } from "./types"; + +export function useCustom< + TQueryFnData extends TDataSourceRecord = TDataSourceRecord, + TError = TDataSourceError, + TData extends TDataSourceRecord = TQueryFnData +>(options: UseQueryOptions): UseQueryResult { + return useQuery({ + placeholderData: keepPreviousData, + ...options, + }); +} diff --git a/client/src/lib/hooks/useQueryKey/KeyBuilder.ts b/client/src/lib/hooks/useQueryKey/KeyBuilder.ts index 6152614..b8c81d3 100644 --- a/client/src/lib/hooks/useQueryKey/KeyBuilder.ts +++ b/client/src/lib/hooks/useQueryKey/KeyBuilder.ts @@ -1,7 +1,7 @@ type BaseKey = string | number; type ParametrizedDataActions = "list" | "infinite"; -type IdRequiredDataActions = "one"; +type IdRequiredDataActions = "one" | "report"; type IdsRequiredDataActions = "many"; type DataMutationActions = | "custom" @@ -100,7 +100,7 @@ class DataResourceKeyBuilder extends BaseKeyBuilder { action( actionType: ParametrizedDataActions | IdRequiredDataActions | IdsRequiredDataActions ): ParamsKeyBuilder | DataIdRequiringKeyBuilder | DataIdsRequiringKeyBuilder { - if (actionType === "one") { + if (["one", "report"].includes(actionType)) { return new DataIdRequiringKeyBuilder([...this.segments, actionType]); } if (actionType === "many") { diff --git a/client/vite.config.ts b/client/vite.config.ts index 8c09cf7..7acf55b 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,7 +1,13 @@ import react from "@vitejs/plugin-react"; -import { defineConfig } from "vite"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { defineConfig, normalizePath } from "vite"; +import { viteStaticCopy } from "vite-plugin-static-copy"; -import { resolve } from "node:path"; +const require = createRequire(import.meta.url); + +const pdfjsDistPath = path.dirname(require.resolve("pdfjs-dist/package.json")); +const cMapsDir = normalizePath(path.join(pdfjsDistPath, "cmaps")); //const require = createRequire(import.meta.url); @@ -17,17 +23,28 @@ const standardFontsDir = normalizePath( // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [ + react(), + viteStaticCopy({ + targets: [ + { + src: cMapsDir, + dest: "", + }, + ], + }), + ], + css: { postcss: "./postcss.config.js" }, resolve: { alias: [ { find: "@", - replacement: resolve(__dirname, "./src"), + replacement: path.resolve(__dirname, "./src"), }, { find: "@shared", - replacement: resolve(__dirname, "../shared/lib/"), + replacement: path.resolve(__dirname, "../shared/lib/"), }, ], }, diff --git a/server/src/contexts/common/infrastructure/express/ExpressController.ts b/server/src/contexts/common/infrastructure/express/ExpressController.ts index 5ecda83..c1b6b8d 100644 --- a/server/src/contexts/common/infrastructure/express/ExpressController.ts +++ b/server/src/contexts/common/infrastructure/express/ExpressController.ts @@ -62,7 +62,7 @@ export abstract class ExpressController implements IController { } public downloadPDF(pdfBuffer: Buffer, filename: string) { - return this._download(pdfBuffer, "application/pdf", `${filename}.pdf`); + return this._download(pdfBuffer, "application/pdf", `${filename}`); } public clientError(message?: string) { @@ -125,7 +125,7 @@ export abstract class ExpressController implements IController { this.res.set({ "Content-Type": contentType, "Content-Disposition": `attachment; filename=${filename}`, - "Content-Length": buffer.length, + //"Content-Length": buffer.length, }); return this.res.send(buffer); diff --git a/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/ReportQuote.controller.ts b/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/ReportQuote.controller.ts index c0efd02..1aae147 100644 --- a/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/ReportQuote.controller.ts +++ b/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/ReportQuote.controller.ts @@ -55,7 +55,7 @@ export class ReportQuoteController extends ExpressController { const quote = result.object; - return this.downloadPDF(await this.reporter.toPDF(quote, this.context), "prueba.pdf"); + return this.downloadPDF(await this.reporter.toPDF(quote, this.context), "quote.pdf"); } catch (e: unknown) { return this.fail(e as IServerError); } diff --git a/shared/lib/contexts/sales/application/dto/Quote/ReportQuote.dto/IReportQuote_Response.dto.ts b/shared/lib/contexts/sales/application/dto/Quote/ReportQuote.dto/IReportQuote_Response.dto.ts new file mode 100644 index 0000000..a540bfa --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/Quote/ReportQuote.dto/IReportQuote_Response.dto.ts @@ -0,0 +1 @@ +export type IReportQuote_Response_DTO = Uint8Array; diff --git a/shared/lib/contexts/sales/application/dto/Quote/ReportQuote.dto/index.ts b/shared/lib/contexts/sales/application/dto/Quote/ReportQuote.dto/index.ts new file mode 100644 index 0000000..32d9865 --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/Quote/ReportQuote.dto/index.ts @@ -0,0 +1 @@ +export * from "./IReportQuote_Response.dto"; diff --git a/shared/lib/contexts/sales/application/dto/Quote/index.ts b/shared/lib/contexts/sales/application/dto/Quote/index.ts index 6cbb1f3..d3a08c1 100644 --- a/shared/lib/contexts/sales/application/dto/Quote/index.ts +++ b/shared/lib/contexts/sales/application/dto/Quote/index.ts @@ -1,4 +1,5 @@ export * from "./CreateQuote.dto"; export * from "./GetQuote.dto"; export * from "./ListQuotes.dto"; +export * from "./ReportQuote.dto"; export * from "./UpdateQuote.dto";