This commit is contained in:
David Arranz 2024-08-18 22:39:06 +02:00
parent 0ff5c39023
commit d298754ee5
18 changed files with 369 additions and 40 deletions

View File

@ -44,6 +44,7 @@
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.51.23", "@tanstack/react-query": "^5.51.23",
"@tanstack/react-table": "^8.20.1", "@tanstack/react-table": "^8.20.1",
"@wojtekmaj/react-hooks": "^1.21.0",
"axios": "^1.7.3", "axios": "^1.7.3",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
@ -52,6 +53,7 @@
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.0",
"joi": "^17.13.1", "joi": "^17.13.1",
"lucide-react": "^0.427.0", "lucide-react": "^0.427.0",
"print-js": "^1.6.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
"react-currency-input-field": "^3.8.0", "react-currency-input-field": "^3.8.0",
@ -60,6 +62,7 @@
"react-hook-form": "^7.52.2", "react-hook-form": "^7.52.2",
"react-hook-form-persist": "^3.0.0", "react-hook-form-persist": "^3.0.0",
"react-i18next": "^15.0.1", "react-i18next": "^15.0.1",
"react-pdf": "^9.1.0",
"react-resizable-panels": "^2.0.23", "react-resizable-panels": "^2.0.23",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.0",
"react-secure-storage": "^1.3.2", "react-secure-storage": "^1.3.2",
@ -94,6 +97,7 @@
"ts-jest": "^29.2.4", "ts-jest": "^29.2.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vite": "^5.4.0" "vite": "^5.4.0",
"vite-plugin-static-copy": "^1.0.6"
} }
} }

View File

@ -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 { DataTableToolbar } from "@/components/DataTable/DataTableToolbar";
import { useDataTable, useDataTableContext } from "@/lib/hooks"; import { useDataTable, useDataTableContext } from "@/lib/hooks";
import { IListQuotes_Response_DTO, MoneyValue, UTCDateValue } from "@shared/contexts"; import { IListQuotes_Response_DTO, MoneyValue, UTCDateValue } from "@shared/contexts";
import { ColumnDef, Row, Table } from "@tanstack/react-table"; import { ColumnDef, Row, Table } from "@tanstack/react-table";
import { t } from "i18next"; import { t } from "i18next";
import { useMemo } from "react"; import { useMemo, useState } from "react";
import { Trans } from "react-i18next"; import { Trans } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useQuotes } from "../hooks"; 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 navigate = useNavigate();
const { pagination, globalFilter, isFiltered } = useDataTableContext(); const { pagination, globalFilter, isFiltered } = useDataTableContext();
const { useList } = useQuotes(); const [focusedRow, setFocusedRow] = useState<Row<IListQuotes_Response_DTO>>();
const { useList, useReport } = useQuotes();
const { data, isPending, isError, error } = useList({ const { data, isPending, isError, error } = useList({
pagination: { pagination: {
@ -26,6 +39,13 @@ export const QuotesDataTable = ({ status = "all" }: { status?: string }) => {
quickSearchTerm: globalFilter, quickSearchTerm: globalFilter,
}); });
const {
data: reportData,
isPending: reportIsPending,
isError: reportIsError,
error: errorReport,
} = useReport(focusedRow ? focusedRow.original.id : undefined);
const columns = useMemo<ColumnDef<IListQuotes_Response_DTO, any>[]>( const columns = useMemo<ColumnDef<IListQuotes_Response_DTO, any>[]>(
() => [ () => [
{ {
@ -119,23 +139,31 @@ export const QuotesDataTable = ({ status = "all" }: { status?: string }) => {
pageCount: data?.total_pages ?? -1, pageCount: data?.total_pages ?? -1,
}); });
const handleOnRowClick = (row: Row<IListQuotes_Response_DTO>) => {
console.log("setFocusedRow", row.id);
setFocusedRow(row);
};
if (isError) { if (isError) {
return <ErrorOverlay subtitle={(error as Error).message} />; return <ErrorOverlay subtitle={(error as Error).message} />;
} }
if (isPending) { if (isPending) {
return ( return (
<Card> <div className='grid items-start flex-1 gap-4 sm:py-0 md:gap-8 lg:grid-cols-3 xl:grid-cols-3'>
<CardContent> <Card className='grid items-start gap-4 auto-rows-max md:gap-8 lg:col-span-2'>
<DataTableSkeleton <CardContent>
columnCount={6} <DataTableSkeleton
searchableColumnCount={1} columnCount={6}
filterableColumnCount={2} searchableColumnCount={1}
//cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem"]} filterableColumnCount={2}
shrinkZero //cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem"]}
/> shrinkZero
</CardContent> />
</Card> </CardContent>
</Card>
<div></div>
</div>
); );
} }
@ -149,11 +177,29 @@ export const QuotesDataTable = ({ status = "all" }: { status?: string }) => {
); );
} }
console.log(reportData);
return ( return (
<> <div className='grid items-start flex-1 gap-4 sm:py-0 md:gap-8 lg:grid-cols-3 xl:grid-cols-3'>
<DataTable table={table} paginationOptions={{ visible: true }}> <DataTable
table={table}
paginationOptions={{ visible: true }}
className='grid items-start gap-4 auto-rows-max md:gap-8 lg:col-span-2'
onRowClick={handleOnRowClick}
>
<DataTableToolbar table={table} /> <DataTableToolbar table={table} />
</DataTable> </DataTable>
</> <div>
<Card className='overflow-hidden' x-chunk='dashboard-05-chunk-4'>
<CardHeader className='flex flex-row items-start bg-muted/50'></CardHeader>
<CardContent>
<PDFViewer
file={reportData}
className='aspect-[3/4] object-contain overflow-hidden border-2 border-dashed rounded-lg'
/>
</CardContent>
</Card>
</div>
</div>
); );
}; };

View File

@ -1,4 +1,4 @@
import { UseListQueryResult, useList, useOne, useSave } from "@/lib/hooks/useDataSource"; import { UseListQueryResult, useCustom, useList, useOne, useSave } from "@/lib/hooks/useDataSource";
import { import {
IFilterItemDataProviderParam, IFilterItemDataProviderParam,
IGetListDataProviderParams, IGetListDataProviderParams,
@ -12,6 +12,7 @@ import {
IGetQuote_Response_DTO, IGetQuote_Response_DTO,
IListQuotes_Response_DTO, IListQuotes_Response_DTO,
IListResponse_DTO, IListResponse_DTO,
IReportQuote_Response_DTO,
IUpdateQuote_Request_DTO, IUpdateQuote_Request_DTO,
IUpdateQuote_Response_DTO, IUpdateQuote_Response_DTO,
UniqueID, UniqueID,
@ -33,6 +34,11 @@ export type UseQuotesGetParamsType = {
queryOptions?: Record<string, unknown>; queryOptions?: Record<string, unknown>;
}; };
export type UseQuotesReportParamsType = {
enabled?: boolean;
queryOptions?: Record<string, unknown>;
};
const quoteStatusFilter: Record<string, IFilterItemDataProviderParam> = { const quoteStatusFilter: Record<string, IFilterItemDataProviderParam> = {
draft: { draft: {
field: "status", field: "status",
@ -85,7 +91,7 @@ export const useQuotes = () => {
useOne: (id?: string, params?: UseQuotesGetParamsType) => useOne: (id?: string, params?: UseQuotesGetParamsType) =>
useOne<IGetQuote_Response_DTO>({ useOne<IGetQuote_Response_DTO>({
queryKey: keys().data().resource("quotes").action("one").id("").params().get(), queryKey: keys().data().resource("quotes").action("one").id(id).params().get(),
queryFn: () => queryFn: () =>
dataSource.getOne({ dataSource.getOne({
resource: "quotes", resource: "quotes",
@ -94,6 +100,7 @@ export const useQuotes = () => {
enabled: !!id, enabled: !!id,
...params, ...params,
}), }),
useCreate: () => useCreate: () =>
useSave<ICreateQuote_Response_DTO, TDataSourceError, ICreateQuote_Request_DTO>({ useSave<ICreateQuote_Response_DTO, TDataSourceError, ICreateQuote_Request_DTO>({
//mutationKey: keys().data().resource("quotes").action("one").id("").params().get(), //mutationKey: keys().data().resource("quotes").action("one").id("").params().get(),
@ -131,5 +138,21 @@ export const useQuotes = () => {
}); });
}, },
}), }),
useReport: (id?: string, params?: UseQuotesReportParamsType) =>
useCustom<IReportQuote_Response_DTO>({
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,
}),
}; };
}; };

View File

@ -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 { PropsWithChildren, ReactNode } from "react";
import { import {
@ -45,6 +45,7 @@ export type DataTableProps<TData> = PropsWithChildren<{
footerClassName?: string; footerClassName?: string;
rowClassName?: string; rowClassName?: string;
cellClassName?: string; cellClassName?: string;
onRowClick?: (row: Row<TData>) => void;
}>; }>;
export function DataTable<TData>({ export function DataTable<TData>({
@ -60,6 +61,7 @@ export function DataTable<TData>({
footerClassName, footerClassName,
rowClassName, rowClassName,
cellClassName, cellClassName,
onRowClick,
}: DataTableProps<TData>) { }: DataTableProps<TData>) {
const headerVisible = headerOptions?.visible; const headerVisible = headerOptions?.visible;
@ -104,6 +106,8 @@ export function DataTable<TData>({
{table.getRowModel().rows?.length ? ( {table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<TableRow <TableRow
onClick={() => (onRowClick ? onRowClick(row) : null)}
tabIndex={0}
key={row.id} key={row.id}
data-state={row.getIsSelected() && "selected"} data-state={row.getIsSelected() && "selected"}
className={cn(row.getIsSelected() ? "bg-accent" : "", rowClassName)} className={cn(row.getIsSelected() ? "bg-accent" : "", rowClassName)}

View File

@ -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<HTMLElement | null>(null);
const [containerWidth, setContainerWidth] = useState<number>();
const onResize = useCallback<ResizeObserverCallback>((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 (
<div className='flex flex-col' ref={setContainerRef}>
<Document
options={options}
file={file}
onLoadSuccess={onDocumentLoadSuccess}
className={className}
>
{Array.from(new Array(numPages), (_el, index) => (
<Page
canvasBackground={"white"}
key={`page_${index + 1}`}
pageNumber={index + 1}
width={containerWidth ? Math.min(containerWidth, maxWidth) : maxWidth}
//onLoadSuccess={onPageLoadSuccess}
//onRenderSuccess={onPageRenderSuccess}
//onRenderError={onPageRenderError}
/>
))}
</Document>
<p className='text-center'>
Página {pageNumber} de {numPages}
</p>
<div>
<Button size={"icon"} variant='link' onClick={goToPrevPage}>
Prev
</Button>
<Button size={"icon"} variant='link' onClick={goToNextPage}>
Next
</Button>
<Button
size={"icon"}
variant='link'
onClick={(e) => {
e.preventDefault();
printJS({
printable: file,
type: "pdf",
showModal: false,
modalMessage: "Cargando...",
});
}}
>
Imprimir react
</Button>
</div>
</div>
);
};

View File

@ -0,0 +1 @@
export * from './PDFViewer';

View File

@ -10,6 +10,7 @@ export * from "./Forms";
export * from "./Layout"; export * from "./Layout";
export * from "./LoadingIndicator"; export * from "./LoadingIndicator";
export * from "./LoadingOverlay"; export * from "./LoadingOverlay";
export * from "./PDFViewer";
export * from "./ProtectedRoute"; export * from "./ProtectedRoute";
//export * from "./SorteableDataTable"; //export * from "./SorteableDataTable";
export * from "./TailwindIndicator"; export * from "./TailwindIndicator";

View File

@ -1,6 +1,7 @@
import { IListResponse_DTO, INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@shared/contexts"; import { IListResponse_DTO, INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@shared/contexts";
import { import {
ICreateOneDataProviderParams, ICreateOneDataProviderParams,
ICustomDataProviderParam,
IDataSource, IDataSource,
IFilterItemDataProviderParam, IFilterItemDataProviderParam,
IGetListDataProviderParams, IGetListDataProviderParams,
@ -18,6 +19,10 @@ export const createAxiosDataProvider = (
): IDataSource => ({ ): IDataSource => ({
name: () => "AxiosDataProvider", name: () => "AxiosDataProvider",
getApiUrl: () => {
return apiUrl;
},
getList: async <R>(params: IGetListDataProviderParams): Promise<IListResponse_DTO<R>> => { getList: async <R>(params: IGetListDataProviderParams): Promise<IListResponse_DTO<R>> => {
const { resource, quickSearchTerm, pagination, filters, sort } = params; const { resource, quickSearchTerm, pagination, filters, sort } = params;
@ -111,6 +116,56 @@ export const createAxiosDataProvider = (
return; return;
}, },
custom: async <R>(params: ICustomDataProviderParam): Promise<R> => {
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]<R>(url, payload);
break;
case "remove":
customResponse = await httpClient.delete<R>(url);
break;
default:
customResponse = await httpClient.get<R>(requestUrl);
break;
}
return customResponse.data;
},
/*getMany: async ({ resource }) => { /*getMany: async ({ resource }) => {
const { body } = await httpClient.request({ const { body } = await httpClient.request({
url: `${apiUrl}/${resource}`, url: `${apiUrl}/${resource}`,

View File

@ -1,4 +1,5 @@
import { IListResponse_DTO } from "@shared/contexts"; import { IListResponse_DTO } from "@shared/contexts";
import { AxiosHeaderValue } from "axios";
export interface IPaginationDataProviderParam { export interface IPaginationDataProviderParam {
pageIndex: number; pageIndex: number;
@ -51,11 +52,14 @@ export interface IRemoveOneDataProviderParams {
id: string; id: string;
} }
/*export interface ICustomDataProviderParam { export interface ICustomDataProviderParam {
resource: string; url: string;
method: string; method: "get" | "delete" | "head" | "options" | "post" | "put" | "patch";
params: any; headers?: {
}*/ [key: string]: AxiosHeaderValue;
};
payload?: unknown;
}
export interface IDataSource { export interface IDataSource {
name: () => string; name: () => string;
@ -66,9 +70,9 @@ export interface IDataSource {
updateOne: <P, R>(params: IUpdateOneDataProviderParams<P>) => Promise<R>; updateOne: <P, R>(params: IUpdateOneDataProviderParams<P>) => Promise<R>;
removeOne: (params: IRemoveOneDataProviderParams) => Promise<void>; removeOne: (params: IRemoveOneDataProviderParams) => Promise<void>;
//custom: <R>(params: ICustomDataProviderParam) => Promise<R>; custom: <R>(params: ICustomDataProviderParam) => Promise<R>;
//getApiUrl: () => string; getApiUrl: () => string;
//create: () => any; //create: () => any;
//createMany: () => any; //createMany: () => any;

View File

@ -1,5 +1,6 @@
// export * from './useApiUrl'; // export * from './useApiUrl';
// export * from './useCreateMany'; // export * from './useCreateMany';
export * from "./useCustom";
export * from "./useList"; export * from "./useList";
export * from "./useMany"; export * from "./useMany";
export * from "./useOne"; export * from "./useOne";

View File

@ -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<TQueryFnData, TError, TData>): UseQueryResult<TData, TError> {
return useQuery<TQueryFnData, TError, TData>({
placeholderData: keepPreviousData,
...options,
});
}

View File

@ -1,7 +1,7 @@
type BaseKey = string | number; type BaseKey = string | number;
type ParametrizedDataActions = "list" | "infinite"; type ParametrizedDataActions = "list" | "infinite";
type IdRequiredDataActions = "one"; type IdRequiredDataActions = "one" | "report";
type IdsRequiredDataActions = "many"; type IdsRequiredDataActions = "many";
type DataMutationActions = type DataMutationActions =
| "custom" | "custom"
@ -100,7 +100,7 @@ class DataResourceKeyBuilder extends BaseKeyBuilder {
action( action(
actionType: ParametrizedDataActions | IdRequiredDataActions | IdsRequiredDataActions actionType: ParametrizedDataActions | IdRequiredDataActions | IdsRequiredDataActions
): ParamsKeyBuilder | DataIdRequiringKeyBuilder | DataIdsRequiringKeyBuilder { ): ParamsKeyBuilder | DataIdRequiringKeyBuilder | DataIdsRequiringKeyBuilder {
if (actionType === "one") { if (["one", "report"].includes(actionType)) {
return new DataIdRequiringKeyBuilder([...this.segments, actionType]); return new DataIdRequiringKeyBuilder([...this.segments, actionType]);
} }
if (actionType === "many") { if (actionType === "many") {

View File

@ -1,7 +1,13 @@
import react from "@vitejs/plugin-react"; 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); //const require = createRequire(import.meta.url);
@ -17,17 +23,28 @@ const standardFontsDir = normalizePath(
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [
react(),
viteStaticCopy({
targets: [
{
src: cMapsDir,
dest: "",
},
],
}),
],
css: { postcss: "./postcss.config.js" }, css: { postcss: "./postcss.config.js" },
resolve: { resolve: {
alias: [ alias: [
{ {
find: "@", find: "@",
replacement: resolve(__dirname, "./src"), replacement: path.resolve(__dirname, "./src"),
}, },
{ {
find: "@shared", find: "@shared",
replacement: resolve(__dirname, "../shared/lib/"), replacement: path.resolve(__dirname, "../shared/lib/"),
}, },
], ],
}, },

View File

@ -62,7 +62,7 @@ export abstract class ExpressController implements IController {
} }
public downloadPDF(pdfBuffer: Buffer, filename: string) { 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) { public clientError(message?: string) {
@ -125,7 +125,7 @@ export abstract class ExpressController implements IController {
this.res.set({ this.res.set({
"Content-Type": contentType, "Content-Type": contentType,
"Content-Disposition": `attachment; filename=${filename}`, "Content-Disposition": `attachment; filename=${filename}`,
"Content-Length": buffer.length, //"Content-Length": buffer.length,
}); });
return this.res.send(buffer); return this.res.send(buffer);

View File

@ -55,7 +55,7 @@ export class ReportQuoteController extends ExpressController {
const quote = <Quote>result.object; const quote = <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) { } catch (e: unknown) {
return this.fail(e as IServerError); return this.fail(e as IServerError);
} }

View File

@ -0,0 +1 @@
export type IReportQuote_Response_DTO = Uint8Array;

View File

@ -0,0 +1 @@
export * from "./IReportQuote_Response.dto";

View File

@ -1,4 +1,5 @@
export * from "./CreateQuote.dto"; export * from "./CreateQuote.dto";
export * from "./GetQuote.dto"; export * from "./GetQuote.dto";
export * from "./ListQuotes.dto"; export * from "./ListQuotes.dto";
export * from "./ReportQuote.dto";
export * from "./UpdateQuote.dto"; export * from "./UpdateQuote.dto";