This commit is contained in:
David Arranz 2024-10-03 16:23:46 +02:00
parent f9afa58b78
commit fbb21c3fd3
42 changed files with 660 additions and 369 deletions

View File

@ -5,6 +5,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://fonts.upset.dev/css2?family=Poppins&display=swap" rel="stylesheet" />
<title>Uecko</title> <title>Uecko</title>
</head> </head>

View File

@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@ -1,130 +0,0 @@
import { Container } from "./components/Container";
export function TypographyDemo() {
return (
<Container className="mx-auto mt-8 prose prose-slate lg:prose-lg">
<h1>The Joke Tax Chronicles</h1>
<p className="leading-7">
Once upon a time, in a far-off land, there was a very lazy king who
spent all day lounging on his throne. One day, his advisors came to him
with a problem: the kingdom was running out of money.
</p>
<pre>
<code className="language-html">
&lt;article class="prose"&gt; &lt;h1&gt;Garlic bread with cheese: What
the science tells us&lt;/h1&gt; &lt;p&gt; For years parents have
espoused the health benefits of eating garlic bread with cheese to
their children, with the food earning such an iconic status in our
culture that kids will often dress up as warm, cheesy loaf for
Halloween. &lt;/p&gt; &lt;p&gt; But a recent study shows that the
celebrated appetizer may be linked to a series of rabies cases
springing up around the country. &lt;/p&gt; &lt;!-- ... --&gt;
&lt;/article&gt;
</code>
</pre>
<h2>The King's Plan</h2>
<p>
The king thought long and hard, and finally came up with{" "}
<a
href="#"
className="font-medium underline text-primary underline-offset-4"
>
a brilliant plan
</a>
: he would tax the jokes in the kingdom.
</p>
<blockquote className="pl-6 mt-6 italic border-l-2">
"After all," he said, "everyone enjoys a good joke, so it's only fair
that they should pay for the privilege."
</blockquote>
<h3 className="mt-8 text-2xl font-semibold tracking-tight scroll-m-20">
The Joke Tax
</h3>
<p className="leading-7 [&:not(:first-child)]:mt-6">
The king's subjects were not amused. They grumbled and complained, but
the king was firm:
</p>
<ul className="my-6 ml-6 list-disc [&>li]:mt-2">
<li>1st level of puns: 5 gold coins</li>
<li>2nd level of jokes: 10 gold coins</li>
<li>3rd level of one-liners : 20 gold coins</li>
</ul>
<p className="leading-7 [&:not(:first-child)]:mt-6">
As a result, people stopped telling jokes, and the kingdom fell into a
gloom. But there was one person who refused to let the king's
foolishness get him down: a court jester named Jokester.
</p>
<h3 className="mt-8 text-2xl font-semibold tracking-tight scroll-m-20">
Jokester's Revolt
</h3>
<p className="leading-7 [&:not(:first-child)]:mt-6">
Jokester began sneaking into the castle in the middle of the night and
leaving jokes all over the place: under the king's pillow, in his soup,
even in the royal toilet. The king was furious, but he couldn't seem to
stop Jokester.
</p>
<p className="leading-7 [&:not(:first-child)]:mt-6">
And then, one day, the people of the kingdom discovered that the jokes
left by Jokester were so funny that they couldn't help but laugh. And
once they started laughing, they couldn't stop.
</p>
<h3 className="mt-8 text-2xl font-semibold tracking-tight scroll-m-20">
The People's Rebellion
</h3>
<p className="leading-7 [&:not(:first-child)]:mt-6">
The people of the kingdom, feeling uplifted by the laughter, started to
tell jokes and puns again, and soon the entire kingdom was in on the
joke.
</p>
<div className="w-full my-6 overflow-y-auto">
<table className="w-full">
<thead>
<tr className="p-0 m-0 border-t even:bg-muted">
<th className="border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right">
King's Treasury
</th>
<th className="border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right">
People's happiness
</th>
</tr>
</thead>
<tbody>
<tr className="p-0 m-0 border-t even:bg-muted">
<td className="border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right">
Empty
</td>
<td className="border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right">
Overflowing
</td>
</tr>
<tr className="p-0 m-0 border-t even:bg-muted">
<td className="border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right">
Modest
</td>
<td className="border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right">
Satisfied
</td>
</tr>
<tr className="p-0 m-0 border-t even:bg-muted">
<td className="border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right">
Full
</td>
<td className="border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right">
Ecstatic
</td>
</tr>
</tbody>
</table>
</div>
<p className="leading-7 [&:not(:first-child)]:mt-6">
The king, seeing how much happier his subjects were, realized the error
of his ways and repealed the joke tax. Jokester was declared a hero, and
the kingdom lived happily ever after.
</p>
<p className="leading-7 [&:not(:first-child)]:mt-6">
The moral of the story is: never underestimate the power of a good laugh
and always be careful of bad ideas.
</p>
</Container>
);
}

View File

@ -20,12 +20,13 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/ui"; } from "@/ui";
import { useToast } from "@/ui/use-toast"; import { useToast } from "@/ui/use-toast";
import { formatDateToYYYYMMDD } from "@shared/utilities/helpers";
import { t } from "i18next"; import { t } from "i18next";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useQuotes } from "../hooks"; import { useQuotes } from "../hooks";
import { DownloadQuoteDialog } from "./DownloadQuoteDialog"; import { DownloadQuoteDialog } from "./DownloadQuoteDialog";
import { QuoteStatusEditor } from "./editors"; import { QuoteSentToEditor, QuoteStatusEditor } from "./editors";
import { QuotePDFPreview } from "./QuotePDFPreview"; import { QuotePDFPreview } from "./QuotePDFPreview";
type QuoteResumeProps = { type QuoteResumeProps = {
@ -37,9 +38,10 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { toast } = useToast(); const { toast } = useToast();
const { useOne, useSetStatus, useDownloader, getQuotePDFFilename } = useQuotes(); const { useOne, useSetStatus, useSentTo, useDownloader, getQuotePDFFilename } = useQuotes();
const { data, status } = useOne(quoteId); const { data, status } = useOne(quoteId);
const { mutate: setStatusMutation } = useSetStatus(quoteId); const { mutate: setStatusMutation } = useSetStatus(quoteId);
const { mutate: sentToMutation } = useSentTo(quoteId);
const { download, ...downloadProps } = useDownloader(); const { download, ...downloadProps } = useDownloader();
const { formatCurrency, formatNumber } = useCustomLocalization({ const { formatCurrency, formatNumber } = useCustomLocalization({
@ -75,6 +77,9 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
}; };
}, [data]); }, [data]);
const allowToSent = useMemo(() => data?.status === "accepted" && !data?.date_sent, [data]);
const isSent = useMemo(() => data?.status === "accepted" && data?.date_sent, [data]);
const handleOnChangeStatus = (_: string, newStatus: string) => { const handleOnChangeStatus = (_: string, newStatus: string) => {
setStatusMutation( setStatusMutation(
{ newStatus }, { newStatus },
@ -89,6 +94,19 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
); );
}; };
const handleOnSentTo = (_: string) => {
sentToMutation(
{ sent_date: formatDateToYYYYMMDD(new Date()) },
{
onSuccess: () => {
toast({
description: t("quotes.quote_sent_to_editor.toast_status_changed"),
});
},
}
);
};
const handleFinishDownload = useCallback(() => { const handleFinishDownload = useCallback(() => {
toast({ toast({
description: t("quotes.downloading_dialog.toast_success"), description: t("quotes.downloading_dialog.toast_success"),
@ -129,21 +147,33 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
</CardTitle> </CardTitle>
<div className='flex mr-auto text-foreground'> <div className='flex mr-auto text-foreground'>
<div className='flex items-center gap-1'> <div className='flex items-center gap-1'>
<Button {allowToSent && !isSent && (
size='sm' <>
variant='default' <QuoteSentToEditor quote={data} onSentTo={handleOnSentTo} />
className='h-8 gap-1' <QuoteStatusEditor quote={data} onChangeStatus={handleOnChangeStatus} />
onClick={(e) => { </>
e.preventDefault(); )}
navigate(`/quotes/edit/${data.id}`, { relative: "path" });
}} {!allowToSent && !isSent && (
> <>
<FilePenLineIcon className='h-3.5 w-3.5' /> <Button
<span className='sr-only md:not-sr-only md:whitespace-nowrap'> size='sm'
{t("quotes.list.columns.actions.edit")} variant='default'
</span> className='h-8 gap-1'
</Button> onClick={(e) => {
<QuoteStatusEditor quote={data} onChangeStatus={handleOnChangeStatus} /> e.preventDefault();
navigate(`/quotes/edit/${data.id}`, { relative: "path" });
}}
>
<FilePenLineIcon className='h-3.5 w-3.5' />
<span className='sr-only md:not-sr-only md:whitespace-nowrap'>
{t("quotes.list.columns.actions.edit")}
</span>
</Button>
<QuoteStatusEditor quote={data} onChangeStatus={handleOnChangeStatus} />
</>
)}
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@ -154,7 +184,9 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
onClick={handleDownload} onClick={handleDownload}
> >
<DownloadIcon className='h-3.5 w-3.5 ' /> <DownloadIcon className='h-3.5 w-3.5 ' />
<span className='sr-only'>{t("quotes.list.resume.download_quote")}</span> <span className={isSent ? "" : "sr-only"}>
{t("quotes.list.resume.download_quote")}
</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{t("quotes.list.resume.download_quote")}</TooltipContent> <TooltipContent>{t("quotes.list.resume.download_quote")}</TooltipContent>

View File

@ -162,6 +162,22 @@ export const QuotesDataTable = ({
), ),
size: 600, size: 600,
}, },
{
id: "date_sent" as const,
accessor: "date_sent",
header: () => (
<div className='text-right text-ellipsis'>{t("quotes.list.columns.date_sent")}</div>
),
cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => {
const quoteDate = UTCDateValue.create(original.date_sent);
return (
<div className='text-right text-ellipsis'>
{quoteDate.isSuccess}
{quoteDate.isSuccess ? quoteDate.object.toLocaleDateString("es-ES") : "-"}
</div>
);
},
},
/*{ /*{
id: "total_price" as const, id: "total_price" as const,
accessor: "total_price", accessor: "total_price",
@ -179,27 +195,44 @@ export const QuotesDataTable = ({
{ {
id: "row-actions", id: "row-actions",
header: () => null, header: () => null,
cell: ({ row }: { row: Row<IListQuotes_Response_DTO> }) => ( cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => (
<ButtonGroup> <ButtonGroup>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button {original.status === "accepted" && !original.date_sent ? (
size='sm' <Button
variant='outline' size='sm'
className='h-8 gap-1' variant='default'
onClick={(e) => { className='h-8 gap-1'
e.preventDefault(); onClick={(e) => {
handleEditQuote(row.original); e.preventDefault();
}} //handleSentToUecko(original);
> }}
<FilePenLineIcon className='h-3.5 w-3.5' /> >
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'> <FilePenLineIcon className='h-3.5 w-3.5' />
{t("quotes.list.columns.actions.edit")} <span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>
</span> {t("quotes.list.columns.actions.sent_to")}
</Button> </span>
</Button>
) : (
<Button
size='sm'
variant='outline'
className='h-8 gap-1'
onClick={(e) => {
e.preventDefault();
handleEditQuote(original);
}}
>
<FilePenLineIcon className='h-3.5 w-3.5' />
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>
{t("quotes.list.columns.actions.edit")}
</span>
</Button>
)}
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>{t("quotes.list.columns.actions.edit")}</p> <p>{t("quotes.list.columns.actions.sent_to_uecko")}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@ -213,7 +246,7 @@ export const QuotesDataTable = ({
<DropdownMenuContent align='end'> <DropdownMenuContent align='end'>
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
download(row.original.id, getQuotePDFFilename(row.original)); download(original.id, getQuotePDFFilename(original));
}} }}
> >
Download Download

View File

@ -0,0 +1,50 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
Button,
} from "@/ui";
import { IGetQuote_Response_DTO } from "@shared/contexts";
import { t } from "i18next";
export const QuoteSentToEditor = ({
quote,
onSentTo,
}: {
quote: IGetQuote_Response_DTO;
onSentTo: (quoteId: string) => void;
}) => {
const handleSentTo = () => {
onSentTo(quote.id);
};
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button size='sm' variant='default' className='h-8 gap-1'>
{t("quotes.quote_sent_to_editor.trigger_button")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("quotes.quote_sent_to_editor.title")}</AlertDialogTitle>
<AlertDialogDescription>
{t("quotes.quote_sent_to_editor.description")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction asChild>
<Button onClick={handleSentTo}>{t("common.continue")}</Button>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@ -1,4 +1,5 @@
export * from "./QuoteDetailsCardEditor"; export * from "./QuoteDetailsCardEditor";
export * from "./QuoteDocumentsCardEditor"; export * from "./QuoteDocumentsCardEditor";
export * from "./QuoteGeneralCardEditor"; export * from "./QuoteGeneralCardEditor";
export * from "./QuoteSentToEditor";
export * from "./QuoteStatusEditor"; export * from "./QuoteStatusEditor";

View File

@ -1,5 +1,5 @@
import { useDownloader } from "@/lib/hooks"; import { useDownloader } from "@/lib/hooks";
import { UseListQueryResult, useList, useOne, useSave } from "@/lib/hooks/useDataSource"; import { useList, UseListQueryResult, useOne, useSave } from "@/lib/hooks/useDataSource";
import { IGetListDataProviderParams } from "@/lib/hooks/useDataSource/DataSource"; import { IGetListDataProviderParams } from "@/lib/hooks/useDataSource/DataSource";
import { TDataSourceError } from "@/lib/hooks/useDataSource/types"; import { TDataSourceError } from "@/lib/hooks/useDataSource/types";
import { useDataSource } from "@/lib/hooks/useDataSource/useDataSource"; import { useDataSource } from "@/lib/hooks/useDataSource/useDataSource";
@ -10,6 +10,7 @@ import {
IGetQuote_Response_DTO, IGetQuote_Response_DTO,
IListQuotes_Response_DTO, IListQuotes_Response_DTO,
IListResponse_DTO, IListResponse_DTO,
ISendQuote_Request_DTO,
ISetStatusQuote_Request_DTO, ISetStatusQuote_Request_DTO,
IUpdateQuote_Request_DTO, IUpdateQuote_Request_DTO,
IUpdateQuote_Response_DTO, IUpdateQuote_Response_DTO,
@ -173,17 +174,21 @@ export const useQuotes = () => {
}); });
}, },
}); });
},
/*return useMutation<void, TDataSourceError, ISetStatusQuote_Request_DTO>({ useSentTo: (id?: string) => {
const queryClient = useQueryClient();
return useMutation<void, TDataSourceError, ISendQuote_Request_DTO>({
mutationKey: keys().data().resource("quotes").action("one").id(id).params().get(), mutationKey: keys().data().resource("quotes").action("one").id(id).params().get(),
mutationFn: (data) => { mutationFn: (data) => {
const { newStatus } = data; const { sent_date } = data;
return dataSource.updateOne({ return dataSource.custom({
resource: "quotes", url: `${dataSource.getApiUrl()}/quotes/${id}/send`,
id, method: "put",
data: { data: {
newStatus, sent_date,
}, },
}); });
}, },
@ -192,7 +197,7 @@ export const useQuotes = () => {
queryKey: ["data", "default", "quotes"], queryKey: ["data", "default", "quotes"],
}); });
}, },
});*/ });
}, },
useOne: (id?: string, params?: UseQuotesGetParamsType) => useOne: (id?: string, params?: UseQuotesGetParamsType) =>

View File

@ -82,6 +82,7 @@
"dealers": "Dealers", "dealers": "Dealers",
"catalog": "Catalog", "catalog": "Catalog",
"quotes": "Quotes", "quotes": "Quotes",
"orders": "Orders",
"search_placeholder": "Type here for search quotes and articles", "search_placeholder": "Type here for search quotes and articles",
"user": { "user": {
"user_menu": "User menu", "user_menu": "User menu",
@ -138,13 +139,15 @@
}, },
"columns": { "columns": {
"date": "Date", "date": "Date",
"date_sent": "Sent to Uecko",
"reference": "Reference", "reference": "Reference",
"status": "Status", "status": "Status",
"customer_reference": "Customer Ref.", "customer_reference": "Customer Ref.",
"customer_information": "Customer", "customer_information": "Customer",
"total_price": "Imp. total", "total_price": "Imp. total",
"actions": { "actions": {
"edit": "Edit quote" "edit": "Edit quote",
"sent_to": "Send to Uecko"
} }
}, },

View File

@ -82,6 +82,7 @@
"dealers": "Distribuidores", "dealers": "Distribuidores",
"catalog": "Catálogo", "catalog": "Catálogo",
"quotes": "Cotizaciones", "quotes": "Cotizaciones",
"orders": "Pedidos",
"search_placeholder": "Buscar productos, cotizaciones, etc...", "search_placeholder": "Buscar productos, cotizaciones, etc...",
"user": { "user": {
"user_menu": "Menú del usuario", "user_menu": "Menú del usuario",
@ -138,13 +139,15 @@
}, },
"columns": { "columns": {
"date": "Fecha", "date": "Fecha",
"date_sent": "Enviado a Uecko",
"reference": "Referencia", "reference": "Referencia",
"status": "Estado", "status": "Estado",
"customer_reference": "Ref. cliente", "customer_reference": "Ref. cliente",
"customer_information": "Cliente", "customer_information": "Cliente",
"total_price": "Imp. total", "total_price": "Imp. total",
"actions": { "actions": {
"edit": "Editar" "edit": "Editar",
"sent_to": "Enviar a Uecko"
} }
}, },
@ -217,6 +220,14 @@
"description": "Para rellenar su cotización, puede añadir artículos del catálogo.", "description": "Para rellenar su cotización, puede añadir artículos del catálogo.",
"toast_article_added": "Artículo del catálogo añadido:" "toast_article_added": "Artículo del catálogo añadido:"
}, },
"quote_sent_to_editor": {
"trigger_button": "Enviar a Uecko",
"title": "Enviar la cotización a Uecko",
"description": "¿Desea enviar esta cotización a Uecko? Esta acción no se puede deshacer.",
"submit_button": "Enviar",
"toast_status_changed": "Cotización enviada a Uecko"
},
"quote_status_editor": { "quote_status_editor": {
"trigger_button": "Cambiar el estado", "trigger_button": "Cambiar el estado",
"title": "Cambiar el estado de la cotización", "title": "Cambiar el estado de la cotización",

View File

@ -1,5 +1,6 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
import defaultTheme from "tailwindcss/defaultTheme";
import plugin from "tailwindcss/plugin"; import plugin from "tailwindcss/plugin";
export default { export default {
@ -16,14 +17,14 @@ export default {
}, },
extend: { extend: {
// https://tailwindcss.com/docs/font-family#font-families // https://tailwindcss.com/docs/font-family#font-families
/*fontFamily: {
sans: ['"Source Sans Pro"', ...defaultTheme.fontFamily.sans],
},*/
fontFamily: { fontFamily: {
sans: ['"Poppins"', ...defaultTheme.fontFamily.sans],
},
/*fontFamily: {
display: "Public Sans, ui-sans-serif", display: "Public Sans, ui-sans-serif",
heading: "Noto Serif, ui-serif", heading: "Noto Serif, ui-serif",
}, },*/
colors: { colors: {
border: "hsl(240, 5.9%, 90%)", border: "hsl(240, 5.9%, 90%)",

1
dist/client/assets/index-D1RJbR2g.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,9 +5,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://fonts.upset.dev/css2?family=Poppins&display=swap" rel="stylesheet" />
<title>Uecko</title> <title>Uecko</title>
<script type="module" crossorigin src="/assets/index-CPprBq9G.js"></script> <script type="module" crossorigin src="/assets/index-DA9apz9T.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DSV01hTS.css"> <link rel="stylesheet" crossorigin href="/assets/index-D1RJbR2g.css">
</head> </head>
<body> <body>

View File

@ -3,5 +3,5 @@ import { NextFunction, Request, Response } from "express";
export const handleRequest = export const handleRequest =
(controllerFactory: any) => (req: Request, res: Response, next: NextFunction) => { (controllerFactory: any) => (req: Request, res: Response, next: NextFunction) => {
const context = res.locals["context"]; const context = res.locals["context"];
return controllerFactory(context).execute(req, res, next); return controllerFactory(context, req, res, next).execute(req, res, next);
}; };

View File

@ -205,6 +205,13 @@ export class CreateQuoteUseCase
return Result.fail(taxOrError.error); return Result.fail(taxOrError.error);
} }
const dateSentOrError = UTCDateValue.create(null);
if (dateSentOrError.isFailure) {
return Result.fail(dateSentOrError.error);
}
// Items
let items: Collection<QuoteItem>; let items: Collection<QuoteItem>;
try { try {
@ -283,6 +290,7 @@ export class CreateQuoteUseCase
items, items,
dealerId, dealerId,
dateSent: dateSentOrError.object,
}, },
quoteId quoteId
); );

View File

@ -0,0 +1,106 @@
import {
IUseCase,
IUseCaseError,
IUseCaseRequest,
UseCaseError,
} from "@/contexts/common/application/useCases";
import { IRepositoryManager } from "@/contexts/common/domain";
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import {
DomainError,
ISendQuote_Request_DTO,
Result,
UniqueID,
UTCDateValue,
} from "@shared/contexts";
import { IQuoteRepository } from "../../domain";
export interface ISendQuoteUseCaseRequest extends IUseCaseRequest {
id: UniqueID;
sentQuoteDTO: ISendQuote_Request_DTO;
}
export type SendQuoteResponseOrError =
| Result<never, IUseCaseError> // Misc errors (value objects)
| Result<void, never>; // Success!
export class SendQuoteUseCase
implements IUseCase<ISendQuoteUseCaseRequest, Promise<SendQuoteResponseOrError>>
{
private _adapter: ISequelizeAdapter;
private _repositoryManager: IRepositoryManager;
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
this._adapter = props.adapter;
this._repositoryManager = props.repositoryManager;
}
async execute(request: ISendQuoteUseCaseRequest): Promise<SendQuoteResponseOrError> {
const {
id,
sentQuoteDTO: { sent_date },
} = request;
const quoteRepository = this._getQuoteRepository();
// Comprobar que existe el Quote
const idExists = await quoteRepository().exists(id);
if (!idExists) {
const message = `Quote ID not found`;
return Result.fail(
UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, message, {
path: "id",
})
);
}
// //
// regla de negocio -> no poder enviar un quote enviado
// regla de negocio -> no poder enviar un quote que no esté aceptado
// Comprobar el status
const sentDateOrError = UTCDateValue.create(sent_date);
if (sentDateOrError.isFailure) {
const { error: domainError } = sentDateOrError;
let errorCode = "";
let message = "";
switch (domainError.code) {
// Errores manuales
case DomainError.INVALID_INPUT_DATA:
errorCode = UseCaseError.INVALID_INPUT_DATA;
message = "La fecha de envío no es correcta";
break;
default:
errorCode = UseCaseError.UNEXCEPTED_ERROR;
message = domainError.message;
break;
}
return Result.fail(UseCaseError.create(errorCode, message, domainError));
}
const transaction = this._adapter.startTransaction();
try {
await transaction.complete(async (t) => {
const quoteRepo = quoteRepository({ transaction: t });
await quoteRepo.updateSentDateById(id, sentDateOrError.object);
});
return Result.ok<void>();
} catch (error: unknown) {
//const _error = error as IInfrastructureError;
return Result.fail(
UseCaseError.create(
UseCaseError.REPOSITORY_ERROR,
"Error al establecer el nuevo estado en la cotización"
)
);
}
}
private _getQuoteRepository() {
return this._repositoryManager.getRepository<IQuoteRepository>("Quote");
}
}

View File

@ -48,6 +48,9 @@ export class SetStatusQuoteUseCase
); );
} }
// //
// regla de negocio -> no poder modificar un quote enviado
// Comprobar el status // Comprobar el status
const statusOrError = QuoteStatus.create(newStatus); const statusOrError = QuoteStatus.create(newStatus);
if (statusOrError.isFailure) { if (statusOrError.isFailure) {

View File

@ -83,6 +83,9 @@ export class UpdateQuoteUseCase
); );
} }
// //
// regla de negocio -> no poder modificar un quote enviado
// Crear quote // Crear quote
const quoteOrError = this._tryCreateQuoteInstance(quoteDTO, id, dealerId); const quoteOrError = this._tryCreateQuoteInstance(quoteDTO, id, dealerId);
@ -194,6 +197,13 @@ export class UpdateQuoteUseCase
return Result.fail(taxOrError.error); return Result.fail(taxOrError.error);
} }
const dateSentOrError = UTCDateValue.create(null);
if (dateSentOrError.isFailure) {
return Result.fail(dateSentOrError.error);
}
// Items
let items: Collection<QuoteItem>; let items: Collection<QuoteItem>;
try { try {
@ -272,6 +282,8 @@ export class UpdateQuoteUseCase
items, items,
dealerId, dealerId,
dateSent: dateSentOrError.object,
}, },
quoteId quoteId
); );

View File

@ -2,5 +2,6 @@ export * from "./CreateQuote.useCase";
export * from "./DeleteQuote.useCase"; export * from "./DeleteQuote.useCase";
export * from "./GetQuote.useCase"; export * from "./GetQuote.useCase";
export * from "./ListQuotes.useCase"; export * from "./ListQuotes.useCase";
export * from "./SendQuote.useCase";
export * from "./SetStatusQuote.useCase"; export * from "./SetStatusQuote.useCase";
export * from "./UpdateQuote.useCase"; export * from "./UpdateQuote.useCase";

View File

@ -36,6 +36,8 @@ export interface IQuoteProps {
//totalPrice: MoneyValue; //totalPrice: MoneyValue;
dealerId: UniqueID; dealerId: UniqueID;
dateSent: UTCDateValue;
} }
export interface IQuote { export interface IQuote {
@ -67,6 +69,8 @@ export interface IQuote {
items: ICollection<QuoteItem>; items: ICollection<QuoteItem>;
dealerId: UniqueID; dealerId: UniqueID;
dateSent: UTCDateValue;
} }
export class Quote extends AggregateRoot<IQuoteProps> implements IQuote { export class Quote extends AggregateRoot<IQuoteProps> implements IQuote {
@ -183,4 +187,8 @@ export class Quote extends AggregateRoot<IQuoteProps> implements IQuote {
get totalPrice(): MoneyValue { get totalPrice(): MoneyValue {
return this.beforeTaxPrice.add(this.taxPrice); return this.beforeTaxPrice.add(this.taxPrice);
} }
get dateSent() {
return this.props.dateSent;
}
} }

View File

@ -1,6 +1,6 @@
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
import { IRepository } from "@/contexts/common/domain/repositories"; import { IRepository } from "@/contexts/common/domain/repositories";
import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts"; import { ICollection, IQueryCriteria, UniqueID, UTCDateValue } from "@shared/contexts";
import { Quote, QuoteReference, QuoteStatus } from "../entities"; import { Quote, QuoteReference, QuoteStatus } from "../entities";
export interface IQuoteRepository extends IRepository<Quote> { export interface IQuoteRepository extends IRepository<Quote> {
@ -19,4 +19,5 @@ export interface IQuoteRepository extends IRepository<Quote> {
findLastReferenceByDealerId(dealerId: UniqueID): Promise<QuoteReference | null>; findLastReferenceByDealerId(dealerId: UniqueID): Promise<QuoteReference | null>;
updateStatusById(quoteId: UniqueID, newStatus: QuoteStatus): Promise<void>; updateStatusById(quoteId: UniqueID, newStatus: QuoteStatus): Promise<void>;
updateSentDateById(quoteId: UniqueID, sentDate: UTCDateValue): Promise<void>;
} }

View File

@ -1,11 +1,11 @@
import { ISequelizeAdapter, SequelizeRepository } from "@/contexts/common/infrastructure/sequelize"; import { ISequelizeAdapter, SequelizeRepository } from "@/contexts/common/infrastructure/sequelize";
import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts"; import { ICollection, IQueryCriteria, UniqueID, UTCDateValue } from "@shared/contexts";
import { ModelDefined, Transaction } from "sequelize"; import { ModelDefined, Transaction } from "sequelize";
import { IQuoteRepository } from "../domain"; import { IQuoteRepository } from "../domain";
import { Quote, QuoteReference, QuoteStatus } from "../domain/entities"; import { Quote, QuoteReference, QuoteStatus } from "../domain/entities";
import { ISalesContext } from "./Sales.context"; import { ISalesContext } from "./Sales.context";
import { IQuoteMapper, createQuoteMapper } from "./mappers/quote.mapper"; import { createQuoteMapper, IQuoteMapper } from "./mappers/quote.mapper";
export type QueryParams = { export type QueryParams = {
pagination: Record<string, any>; pagination: Record<string, any>;
@ -158,6 +158,21 @@ export class QuoteRepository extends SequelizeRepository<Quote> implements IQuot
} }
); );
} }
public async updateSentDateById(quoteId: UniqueID, sentDate: UTCDateValue): Promise<void> {
const quoteSentData = sentDate.toPrimitive(); //this.mapper.mapToPersistence(quote);
const _model = this._adapter.getModel("Quote_Model");
await _model.update(
{
date_sent: quoteSentData,
},
{
where: { id: quoteId.toPrimitive() },
transaction: this._transaction,
}
);
}
} }
export const registerQuoteRepository = (context: ISalesContext) => { export const registerQuoteRepository = (context: ISalesContext) => {

View File

@ -1,13 +1,10 @@
import { CreateQuoteUseCase } from "@/contexts/sales/application"; import { CreateQuoteUseCase } from "@/contexts/sales/application";
import { NextFunction, Request, Response } from "express";
import { registerQuoteRepository } from "../../../../Quote.repository"; import { registerQuoteRepository } from "../../../../Quote.repository";
import { ISalesContext } from "../../../../Sales.context"; import { ISalesContext } from "../../../../Sales.context";
import { CreateQuoteController } from "./CreateQuote.controller"; import { CreateQuoteController } from "./CreateQuote.controller";
import { CreateQuotePresenter } from "./presenter"; import { CreateQuotePresenter } from "./presenter";
export const createQuoteController = (req: Request, res: Response, next: NextFunction) => { export const createQuoteController = (context: ISalesContext) => {
const context: ISalesContext = res.locals.context;
registerQuoteRepository(context); registerQuoteRepository(context);
return new CreateQuoteController( return new CreateQuoteController(
{ {
@ -15,5 +12,5 @@ export const createQuoteController = (req: Request, res: Response, next: NextFun
presenter: CreateQuotePresenter, presenter: CreateQuotePresenter,
}, },
context context
).execute(req, res, next); );
}; };

View File

@ -1,13 +1,10 @@
import { GetQuoteUseCase } from "@/contexts/sales/application"; import { GetQuoteUseCase } from "@/contexts/sales/application";
import { NextFunction, Request, Response } from "express";
import { registerQuoteRepository } from "../../../../Quote.repository"; import { registerQuoteRepository } from "../../../../Quote.repository";
import { ISalesContext } from "../../../../Sales.context"; import { ISalesContext } from "../../../../Sales.context";
import { GetQuoteController } from "./GetQuote.controller"; import { GetQuoteController } from "./GetQuote.controller";
import { GetQuotePresenter } from "./presenter"; import { GetQuotePresenter } from "./presenter";
export const getQuoteController = (req: Request, res: Response, next: NextFunction) => { export const getQuoteController = (context: ISalesContext) => {
const context: ISalesContext = res.locals.context;
registerQuoteRepository(context); registerQuoteRepository(context);
return new GetQuoteController( return new GetQuoteController(
@ -16,5 +13,5 @@ export const getQuoteController = (req: Request, res: Response, next: NextFuncti
presenter: GetQuotePresenter, presenter: GetQuotePresenter,
}, },
context context
).execute(req, res, next); );
}; };

View File

@ -41,6 +41,8 @@ export const GetQuotePresenter: IGetQuotePresenter = {
items: quoteItemPresenter(quote.items, context), items: quoteItemPresenter(quote.items, context),
dealer_id: quote.dealerId.toString(), dealer_id: quote.dealerId.toString(),
date_sent: quote.dateSent.toISO8601(),
}; };
}, },
}; };

View File

@ -1,13 +1,10 @@
import { ListQuotesUseCase } from "@/contexts/sales/application"; import { ListQuotesUseCase } from "@/contexts/sales/application";
import { registerQuoteRepository } from "@/contexts/sales/infrastructure/Quote.repository"; import { registerQuoteRepository } from "@/contexts/sales/infrastructure/Quote.repository";
import { ISalesContext } from "@/contexts/sales/infrastructure/Sales.context"; import { ISalesContext } from "@/contexts/sales/infrastructure/Sales.context";
import { NextFunction, Request, Response } from "express";
import { ListQuotesController } from "./ListQuotes.controller"; import { ListQuotesController } from "./ListQuotes.controller";
import { ListQuotesPresenter } from "./presenter"; import { ListQuotesPresenter } from "./presenter";
export const listQuotesController = (req: Request, res: Response, next: NextFunction) => { export const listQuotesController = (context: ISalesContext) => {
const context: ISalesContext = res.locals.context;
registerQuoteRepository(context); registerQuoteRepository(context);
return new ListQuotesController( return new ListQuotesController(
@ -16,5 +13,5 @@ export const listQuotesController = (req: Request, res: Response, next: NextFunc
presenter: ListQuotesPresenter, presenter: ListQuotesPresenter,
}, },
context context
).execute(req, res, next); );
}; };

View File

@ -39,6 +39,8 @@ export const ListQuotesPresenter: IListQuotesPresenter = {
total_price: quote.totalPrice.convertScale(2).toObject(), total_price: quote.totalPrice.convertScale(2).toObject(),
dealer_id: quote.dealerId.toString(), dealer_id: quote.dealerId.toString(),
date_sent: quote.dateSent.toISO8601(),
}; };
}, },

View File

@ -1,13 +1,10 @@
import { GetQuoteUseCase } from "@/contexts/sales/application"; import { GetQuoteUseCase } from "@/contexts/sales/application";
import { NextFunction, Request, Response } from "express";
import { registerQuoteRepository } from "../../../../Quote.repository"; import { registerQuoteRepository } from "../../../../Quote.repository";
import { ISalesContext } from "../../../../Sales.context"; import { ISalesContext } from "../../../../Sales.context";
import { ReportQuotePresenter } from "./reporter/ReportQuote.reporter"; import { ReportQuotePresenter } from "./reporter/ReportQuote.reporter";
import { ReportQuoteController } from "./ReportQuote.controller"; import { ReportQuoteController } from "./ReportQuote.controller";
export const reportQuoteController = (req: Request, res: Response, next: NextFunction) => { export const reportQuoteController = (context: ISalesContext) => {
const context: ISalesContext = res.locals.context;
registerQuoteRepository(context); registerQuoteRepository(context);
return new ReportQuoteController( return new ReportQuoteController(
@ -16,5 +13,5 @@ export const reportQuoteController = (req: Request, res: Response, next: NextFun
reporter: ReportQuotePresenter, reporter: ReportQuotePresenter,
}, },
context context
).execute(req, res, next); );
}; };

View File

@ -0,0 +1,14 @@
import { SendQuoteUseCase } from "@/contexts/sales/application";
import { registerQuoteRepository } from "../../../../Quote.repository";
import { ISalesContext } from "../../../../Sales.context";
import { SendQuoteController } from "./sendQuote.controller";
export const sendQuoteController = (context: ISalesContext) => {
registerQuoteRepository(context);
return new SendQuoteController(
{
useCase: new SendQuoteUseCase(context),
},
context
);
};

View File

@ -0,0 +1,103 @@
import { IUseCaseError, UseCaseError } from "@/contexts/common/application/useCases";
import { IServerError } from "@/contexts/common/domain/errors";
import { IInfrastructureError, InfrastructureError } from "@/contexts/common/infrastructure";
import { ExpressController } from "@/contexts/common/infrastructure/express";
import { SendQuoteUseCase } from "@/contexts/sales/application";
import {
ensureIdIsValid,
ensureSendQuote_Request_DTOIsValid,
ISendQuote_Request_DTO,
} from "@shared/contexts";
import { ISalesContext } from "../../../../Sales.context";
export class SendQuoteController extends ExpressController {
private useCase: SendQuoteUseCase;
private context: ISalesContext;
constructor(props: { useCase: SendQuoteUseCase }, context: ISalesContext) {
super();
const { useCase } = props;
this.useCase = useCase;
this.context = context;
}
async executeImpl(): Promise<any> {
try {
const { quoteId } = this.req.params;
const newStatusDTO: ISendQuote_Request_DTO = this.req.body;
// Validar ID
const quoteIdOrError = ensureIdIsValid(quoteId);
if (quoteIdOrError.isFailure) {
const errorMessage = "Quote ID is not valid";
const infraError = InfrastructureError.create(
InfrastructureError.INVALID_INPUT_DATA,
errorMessage,
quoteIdOrError.error
);
return this.invalidInputError(errorMessage, infraError);
}
// Validar DTO de datos
const sentQuoteDTOOrError = ensureSendQuote_Request_DTOIsValid(newStatusDTO);
if (sentQuoteDTOOrError.isFailure) {
const errorMessage = "New quote status is not valid";
const infraError = InfrastructureError.create(
InfrastructureError.INVALID_INPUT_DATA,
errorMessage,
sentQuoteDTOOrError.error
);
return this.invalidInputError(errorMessage, infraError);
}
// Llamar al caso de uso
const result = await this.useCase.execute({
id: quoteIdOrError.object,
sentQuoteDTO: sentQuoteDTOOrError.object,
});
if (result.isFailure) {
return this._handleExecuteError(result.error);
}
return this.noContent();
} catch (e: unknown) {
return this.fail(e as IServerError);
}
}
private _handleExecuteError(error: IUseCaseError) {
let errorMessage: string;
let infraError: IInfrastructureError;
switch (error.code) {
case UseCaseError.NOT_FOUND_ERROR:
errorMessage = "Quote not found";
infraError = InfrastructureError.create(
InfrastructureError.RESOURCE_NOT_FOUND_ERROR,
errorMessage,
error
);
return this.notFoundError(errorMessage, infraError);
break;
case UseCaseError.UNEXCEPTED_ERROR:
errorMessage = error.message;
infraError = InfrastructureError.create(
InfrastructureError.UNEXCEPTED_ERROR,
errorMessage,
error
);
return this.internalServerError(errorMessage, infraError);
break;
default:
errorMessage = error.message;
return this.clientError(errorMessage);
}
}
}

View File

@ -1,17 +1,14 @@
import { SetStatusQuoteUseCase } from "@/contexts/sales/application"; import { SetStatusQuoteUseCase } from "@/contexts/sales/application";
import { NextFunction, Request, Response } from "express";
import { registerQuoteRepository } from "../../../../Quote.repository"; import { registerQuoteRepository } from "../../../../Quote.repository";
import { ISalesContext } from "../../../../Sales.context"; import { ISalesContext } from "../../../../Sales.context";
import { SetStatusQuoteController } from "./SetStatusQuote.controller"; import { SetStatusQuoteController } from "./SetStatusQuote.controller";
export const setStatusQuoteController = (req: Request, res: Response, next: NextFunction) => { export const setStatusQuoteController = (context: ISalesContext) => {
const context: ISalesContext = res.locals.context;
registerQuoteRepository(context); registerQuoteRepository(context);
return new SetStatusQuoteController( return new SetStatusQuoteController(
{ {
useCase: new SetStatusQuoteUseCase(context), useCase: new SetStatusQuoteUseCase(context),
}, },
context context
).execute(req, res, next); );
}; };

View File

@ -95,6 +95,8 @@ class QuoteMapper
),*/ ),*/
dealerId: this.mapsValue(source, "dealer_id", UniqueID.create), dealerId: this.mapsValue(source, "dealer_id", UniqueID.create),
dateSent: this.mapsValue(source, "date_sent", UTCDateValue.create),
}; };
const quoteOrError = Quote.create(props, id); const quoteOrError = Quote.create(props, id);
@ -139,6 +141,8 @@ class QuoteMapper
items, items,
dealer_id: source.dealerId.toPrimitive(), dealer_id: source.dealerId.toPrimitive(),
date_sent: source.dateSent?.toPrimitive(),
}; };
return quote; return quote;

View File

@ -13,7 +13,7 @@ import { QuoteItemCreationAttributes, QuoteItem_Model } from "./quoteItem.model"
export type QuoteCreationAttributes = InferCreationAttributes< export type QuoteCreationAttributes = InferCreationAttributes<
Quote_Model, Quote_Model,
{ omit: "items" | "dealer" } { omit: "items" | "dealer" | "id_contract" }
> & { > & {
items: QuoteItemCreationAttributes[]; items: QuoteItemCreationAttributes[];
dealer_id: string; dealer_id: string;
@ -65,6 +65,9 @@ export class Quote_Model extends Model<
declare items: NonAttribute<QuoteItem_Model[]>; declare items: NonAttribute<QuoteItem_Model[]>;
declare dealer: NonAttribute<Dealer_Model>; declare dealer: NonAttribute<Dealer_Model>;
declare id_contract: CreationOptional<string | null>;
declare date_sent: CreationOptional<string | null>;
} }
export default (sequelize: Sequelize) => { export default (sequelize: Sequelize) => {
@ -155,6 +158,16 @@ export default (sequelize: Sequelize) => {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(),
allowNull: true, allowNull: true,
}, },
id_contract: {
type: DataTypes.BIGINT().UNSIGNED,
allowNull: true,
},
date_sent: {
type: new DataTypes.DATE(),
allowNull: true,
},
}, },
{ {
sequelize, sequelize,
@ -172,6 +185,8 @@ export default (sequelize: Sequelize) => {
{ name: "status_idx", fields: ["status"] }, { name: "status_idx", fields: ["status"] },
{ name: "reference_idx", fields: ["reference"] }, { name: "reference_idx", fields: ["reference"] },
{ name: "deleted_at_idx", fields: ["deleted_at"] }, { name: "deleted_at_idx", fields: ["deleted_at"] },
{ name: "date_sent_idx", fields: ["date_sent"] },
{ name: "id_contract_idx", fields: ["id_contract"] },
], ],
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope whereMergeStrategy: "and", // <- cómo tratar el merge de un scope

View File

@ -1,4 +1,5 @@
import { checkUser } from "@/contexts/auth"; import { checkUser } from "@/contexts/auth";
import { handleRequest } from "@/contexts/common/infrastructure/express";
import { import {
createQuoteController, createQuoteController,
getQuoteController, getQuoteController,
@ -7,6 +8,7 @@ import {
setStatusQuoteController, setStatusQuoteController,
updateQuoteController, updateQuoteController,
} from "@/contexts/sales/infrastructure/express/controllers"; } from "@/contexts/sales/infrastructure/express/controllers";
import { sendQuoteController } from "@/contexts/sales/infrastructure/express/controllers/quotes/sendQuote";
import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/Dealer.middleware"; import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/Dealer.middleware";
import { Router } from "express"; import { Router } from "express";
@ -14,20 +16,30 @@ export const quoteRouter = (appRouter: Router): void => {
const quoteRoutes: Router = Router({ mergeParams: true }); const quoteRoutes: Router = Router({ mergeParams: true });
// Users CRUD // Users CRUD
quoteRoutes.get("/", checkUser, getDealerMiddleware, listQuotesController); quoteRoutes.get("/", checkUser, getDealerMiddleware, handleRequest(listQuotesController));
quoteRoutes.get("/:quoteId", checkUser, getDealerMiddleware, getQuoteController); quoteRoutes.get("/:quoteId", checkUser, getDealerMiddleware, handleRequest(getQuoteController));
quoteRoutes.post("/", checkUser, getDealerMiddleware, createQuoteController); quoteRoutes.post("/", checkUser, getDealerMiddleware, handleRequest(createQuoteController));
quoteRoutes.put("/:quoteId", checkUser, getDealerMiddleware, updateQuoteController); quoteRoutes.put(
"/:quoteId",
checkUser,
getDealerMiddleware,
handleRequest(updateQuoteController)
);
// Reports // Reports
quoteRoutes.get("/:quoteId/report", checkUser, getDealerMiddleware, reportQuoteController); quoteRoutes.get(
"/:quoteId/report",
checkUser,
getDealerMiddleware,
handleRequest(reportQuoteController)
);
// Status // Status
quoteRoutes.put( quoteRoutes.put("/:quoteId/setStatus", checkUser, handleRequest(setStatusQuoteController));
"/:quoteId/setStatus",
checkUser, // Send to Uecko
/*getDealerMiddleware, */ setStatusQuoteController quoteRoutes.put("/:quoteId/send", checkUser, handleRequest(sendQuoteController));
);
/* /*
quoteRoutes.post("/", isAdmin, createQuoteController); quoteRoutes.post("/", isAdmin, createQuoteController);

View File

@ -25,6 +25,8 @@ export interface IGetQuote_Response_DTO {
items: IGetQuote_QuoteItem_Response_DTO[]; items: IGetQuote_QuoteItem_Response_DTO[];
dealer_id: string; dealer_id: string;
date_sent: string;
} }
export interface IGetQuote_QuoteItem_Response_DTO { export interface IGetQuote_QuoteItem_Response_DTO {

View File

@ -19,4 +19,6 @@ export interface IListQuotes_Response_DTO {
total_price: IMoney_DTO; total_price: IMoney_DTO;
dealer_id: string; dealer_id: string;
date_sent: string;
} }

View File

@ -0,0 +1,20 @@
import Joi from "joi";
import { Result, RuleValidator } from "../../../../../common";
export interface ISendQuote_Request_DTO {
sent_date: string;
}
export function ensureSendQuote_Request_DTOIsValid(quoteDTO: ISendQuote_Request_DTO) {
const schema = Joi.object({
sent_date: Joi.string(),
});
const result = RuleValidator.validate<ISendQuote_Request_DTO>(schema, quoteDTO);
if (result.isFailure) {
return Result.fail(result.error);
}
return Result.ok(result.object);
}

View File

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

View File

@ -1,5 +1,6 @@
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 "./SendQuote.dto";
export * from "./SetStatusQuote.dto"; export * from "./SetStatusQuote.dto";
export * from "./UpdateQuote.dto"; export * from "./UpdateQuote.dto";

View File

@ -2,3 +2,11 @@ export const adjustPrecision = ({ amount, scale }: { amount: number; scale: numb
const factor = 10 ** scale; const factor = 10 ** scale;
return Number(amount) / factor; return Number(amount) / factor;
}; };
export const formatDateToYYYYMMDD = (date: Date): string => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0"); // Los meses van de 0 a 11, por lo que sumamos 1
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};