.
This commit is contained in:
parent
f9afa58b78
commit
fbb21c3fd3
@ -5,6 +5,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<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>
|
||||
</head>
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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">
|
||||
<article class="prose"> <h1>Garlic bread with cheese: What
|
||||
the science tells us</h1> <p> 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. </p> <p> But a recent study shows that the
|
||||
celebrated appetizer may be linked to a series of rabies cases
|
||||
springing up around the country. </p> <!-- ... -->
|
||||
</article>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@ -20,12 +20,13 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/ui";
|
||||
import { useToast } from "@/ui/use-toast";
|
||||
import { formatDateToYYYYMMDD } from "@shared/utilities/helpers";
|
||||
import { t } from "i18next";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuotes } from "../hooks";
|
||||
import { DownloadQuoteDialog } from "./DownloadQuoteDialog";
|
||||
import { QuoteStatusEditor } from "./editors";
|
||||
import { QuoteSentToEditor, QuoteStatusEditor } from "./editors";
|
||||
import { QuotePDFPreview } from "./QuotePDFPreview";
|
||||
|
||||
type QuoteResumeProps = {
|
||||
@ -37,9 +38,10 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { useOne, useSetStatus, useDownloader, getQuotePDFFilename } = useQuotes();
|
||||
const { useOne, useSetStatus, useSentTo, useDownloader, getQuotePDFFilename } = useQuotes();
|
||||
const { data, status } = useOne(quoteId);
|
||||
const { mutate: setStatusMutation } = useSetStatus(quoteId);
|
||||
const { mutate: sentToMutation } = useSentTo(quoteId);
|
||||
const { download, ...downloadProps } = useDownloader();
|
||||
|
||||
const { formatCurrency, formatNumber } = useCustomLocalization({
|
||||
@ -75,6 +77,9 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
|
||||
};
|
||||
}, [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) => {
|
||||
setStatusMutation(
|
||||
{ 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(() => {
|
||||
toast({
|
||||
description: t("quotes.downloading_dialog.toast_success"),
|
||||
@ -129,21 +147,33 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
|
||||
</CardTitle>
|
||||
<div className='flex mr-auto text-foreground'>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='default'
|
||||
className='h-8 gap-1'
|
||||
onClick={(e) => {
|
||||
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} />
|
||||
{allowToSent && !isSent && (
|
||||
<>
|
||||
<QuoteSentToEditor quote={data} onSentTo={handleOnSentTo} />
|
||||
<QuoteStatusEditor quote={data} onChangeStatus={handleOnChangeStatus} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!allowToSent && !isSent && (
|
||||
<>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='default'
|
||||
className='h-8 gap-1'
|
||||
onClick={(e) => {
|
||||
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>
|
||||
<TooltipTrigger asChild>
|
||||
@ -154,7 +184,9 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<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>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("quotes.list.resume.download_quote")}</TooltipContent>
|
||||
|
||||
@ -162,6 +162,22 @@ export const QuotesDataTable = ({
|
||||
),
|
||||
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,
|
||||
accessor: "total_price",
|
||||
@ -179,27 +195,44 @@ export const QuotesDataTable = ({
|
||||
{
|
||||
id: "row-actions",
|
||||
header: () => null,
|
||||
cell: ({ row }: { row: Row<IListQuotes_Response_DTO> }) => (
|
||||
cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => (
|
||||
<ButtonGroup>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outline'
|
||||
className='h-8 gap-1'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleEditQuote(row.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>
|
||||
{original.status === "accepted" && !original.date_sent ? (
|
||||
<Button
|
||||
size='sm'
|
||||
variant='default'
|
||||
className='h-8 gap-1'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
//handleSentToUecko(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.sent_to")}
|
||||
</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>
|
||||
<TooltipContent>
|
||||
<p>{t("quotes.list.columns.actions.edit")}</p>
|
||||
<p>{t("quotes.list.columns.actions.sent_to_uecko")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@ -213,7 +246,7 @@ export const QuotesDataTable = ({
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
download(row.original.id, getQuotePDFFilename(row.original));
|
||||
download(original.id, getQuotePDFFilename(original));
|
||||
}}
|
||||
>
|
||||
Download
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -1,4 +1,5 @@
|
||||
export * from "./QuoteDetailsCardEditor";
|
||||
export * from "./QuoteDocumentsCardEditor";
|
||||
export * from "./QuoteGeneralCardEditor";
|
||||
export * from "./QuoteSentToEditor";
|
||||
export * from "./QuoteStatusEditor";
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { TDataSourceError } from "@/lib/hooks/useDataSource/types";
|
||||
import { useDataSource } from "@/lib/hooks/useDataSource/useDataSource";
|
||||
@ -10,6 +10,7 @@ import {
|
||||
IGetQuote_Response_DTO,
|
||||
IListQuotes_Response_DTO,
|
||||
IListResponse_DTO,
|
||||
ISendQuote_Request_DTO,
|
||||
ISetStatusQuote_Request_DTO,
|
||||
IUpdateQuote_Request_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(),
|
||||
mutationFn: (data) => {
|
||||
const { newStatus } = data;
|
||||
const { sent_date } = data;
|
||||
|
||||
return dataSource.updateOne({
|
||||
resource: "quotes",
|
||||
id,
|
||||
return dataSource.custom({
|
||||
url: `${dataSource.getApiUrl()}/quotes/${id}/send`,
|
||||
method: "put",
|
||||
data: {
|
||||
newStatus,
|
||||
sent_date,
|
||||
},
|
||||
});
|
||||
},
|
||||
@ -192,7 +197,7 @@ export const useQuotes = () => {
|
||||
queryKey: ["data", "default", "quotes"],
|
||||
});
|
||||
},
|
||||
});*/
|
||||
});
|
||||
},
|
||||
|
||||
useOne: (id?: string, params?: UseQuotesGetParamsType) =>
|
||||
|
||||
@ -82,6 +82,7 @@
|
||||
"dealers": "Dealers",
|
||||
"catalog": "Catalog",
|
||||
"quotes": "Quotes",
|
||||
"orders": "Orders",
|
||||
"search_placeholder": "Type here for search quotes and articles",
|
||||
"user": {
|
||||
"user_menu": "User menu",
|
||||
@ -138,13 +139,15 @@
|
||||
},
|
||||
"columns": {
|
||||
"date": "Date",
|
||||
"date_sent": "Sent to Uecko",
|
||||
"reference": "Reference",
|
||||
"status": "Status",
|
||||
"customer_reference": "Customer Ref.",
|
||||
"customer_information": "Customer",
|
||||
"total_price": "Imp. total",
|
||||
"actions": {
|
||||
"edit": "Edit quote"
|
||||
"edit": "Edit quote",
|
||||
"sent_to": "Send to Uecko"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -82,6 +82,7 @@
|
||||
"dealers": "Distribuidores",
|
||||
"catalog": "Catálogo",
|
||||
"quotes": "Cotizaciones",
|
||||
"orders": "Pedidos",
|
||||
"search_placeholder": "Buscar productos, cotizaciones, etc...",
|
||||
"user": {
|
||||
"user_menu": "Menú del usuario",
|
||||
@ -138,13 +139,15 @@
|
||||
},
|
||||
"columns": {
|
||||
"date": "Fecha",
|
||||
"date_sent": "Enviado a Uecko",
|
||||
"reference": "Referencia",
|
||||
"status": "Estado",
|
||||
"customer_reference": "Ref. cliente",
|
||||
"customer_information": "Cliente",
|
||||
"total_price": "Imp. total",
|
||||
"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.",
|
||||
"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": {
|
||||
"trigger_button": "Cambiar el estado",
|
||||
"title": "Cambiar el estado de la cotización",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
import defaultTheme from "tailwindcss/defaultTheme";
|
||||
import plugin from "tailwindcss/plugin";
|
||||
|
||||
export default {
|
||||
@ -16,14 +17,14 @@ export default {
|
||||
},
|
||||
extend: {
|
||||
// https://tailwindcss.com/docs/font-family#font-families
|
||||
/*fontFamily: {
|
||||
sans: ['"Source Sans Pro"', ...defaultTheme.fontFamily.sans],
|
||||
},*/
|
||||
|
||||
fontFamily: {
|
||||
sans: ['"Poppins"', ...defaultTheme.fontFamily.sans],
|
||||
},
|
||||
|
||||
/*fontFamily: {
|
||||
display: "Public Sans, ui-sans-serif",
|
||||
heading: "Noto Serif, ui-serif",
|
||||
},
|
||||
},*/
|
||||
|
||||
colors: {
|
||||
border: "hsl(240, 5.9%, 90%)",
|
||||
|
||||
1
dist/client/assets/index-D1RJbR2g.css
vendored
Normal file
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
1
dist/client/assets/index-DSV01hTS.css
vendored
1
dist/client/assets/index-DSV01hTS.css
vendored
File diff suppressed because one or more lines are too long
5
dist/client/index.html
vendored
5
dist/client/index.html
vendored
@ -5,9 +5,10 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<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>
|
||||
<script type="module" crossorigin src="/assets/index-CPprBq9G.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DSV01hTS.css">
|
||||
<script type="module" crossorigin src="/assets/index-DA9apz9T.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D1RJbR2g.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@ -3,5 +3,5 @@ import { NextFunction, Request, Response } from "express";
|
||||
export const handleRequest =
|
||||
(controllerFactory: any) => (req: Request, res: Response, next: NextFunction) => {
|
||||
const context = res.locals["context"];
|
||||
return controllerFactory(context).execute(req, res, next);
|
||||
return controllerFactory(context, req, res, next).execute(req, res, next);
|
||||
};
|
||||
|
||||
@ -205,6 +205,13 @@ export class CreateQuoteUseCase
|
||||
return Result.fail(taxOrError.error);
|
||||
}
|
||||
|
||||
const dateSentOrError = UTCDateValue.create(null);
|
||||
if (dateSentOrError.isFailure) {
|
||||
return Result.fail(dateSentOrError.error);
|
||||
}
|
||||
|
||||
// Items
|
||||
|
||||
let items: Collection<QuoteItem>;
|
||||
|
||||
try {
|
||||
@ -283,6 +290,7 @@ export class CreateQuoteUseCase
|
||||
items,
|
||||
|
||||
dealerId,
|
||||
dateSent: dateSentOrError.object,
|
||||
},
|
||||
quoteId
|
||||
);
|
||||
|
||||
106
server/src/contexts/sales/application/Quote/SendQuote.useCase.ts
Normal file
106
server/src/contexts/sales/application/Quote/SendQuote.useCase.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
@ -48,6 +48,9 @@ export class SetStatusQuoteUseCase
|
||||
);
|
||||
}
|
||||
|
||||
// //
|
||||
// regla de negocio -> no poder modificar un quote enviado
|
||||
|
||||
// Comprobar el status
|
||||
const statusOrError = QuoteStatus.create(newStatus);
|
||||
if (statusOrError.isFailure) {
|
||||
|
||||
@ -83,6 +83,9 @@ export class UpdateQuoteUseCase
|
||||
);
|
||||
}
|
||||
|
||||
// //
|
||||
// regla de negocio -> no poder modificar un quote enviado
|
||||
|
||||
// Crear quote
|
||||
const quoteOrError = this._tryCreateQuoteInstance(quoteDTO, id, dealerId);
|
||||
|
||||
@ -194,6 +197,13 @@ export class UpdateQuoteUseCase
|
||||
return Result.fail(taxOrError.error);
|
||||
}
|
||||
|
||||
const dateSentOrError = UTCDateValue.create(null);
|
||||
if (dateSentOrError.isFailure) {
|
||||
return Result.fail(dateSentOrError.error);
|
||||
}
|
||||
|
||||
// Items
|
||||
|
||||
let items: Collection<QuoteItem>;
|
||||
|
||||
try {
|
||||
@ -272,6 +282,8 @@ export class UpdateQuoteUseCase
|
||||
items,
|
||||
|
||||
dealerId,
|
||||
|
||||
dateSent: dateSentOrError.object,
|
||||
},
|
||||
quoteId
|
||||
);
|
||||
|
||||
@ -2,5 +2,6 @@ export * from "./CreateQuote.useCase";
|
||||
export * from "./DeleteQuote.useCase";
|
||||
export * from "./GetQuote.useCase";
|
||||
export * from "./ListQuotes.useCase";
|
||||
export * from "./SendQuote.useCase";
|
||||
export * from "./SetStatusQuote.useCase";
|
||||
export * from "./UpdateQuote.useCase";
|
||||
|
||||
@ -36,6 +36,8 @@ export interface IQuoteProps {
|
||||
//totalPrice: MoneyValue;
|
||||
|
||||
dealerId: UniqueID;
|
||||
|
||||
dateSent: UTCDateValue;
|
||||
}
|
||||
|
||||
export interface IQuote {
|
||||
@ -67,6 +69,8 @@ export interface IQuote {
|
||||
items: ICollection<QuoteItem>;
|
||||
|
||||
dealerId: UniqueID;
|
||||
|
||||
dateSent: UTCDateValue;
|
||||
}
|
||||
|
||||
export class Quote extends AggregateRoot<IQuoteProps> implements IQuote {
|
||||
@ -183,4 +187,8 @@ export class Quote extends AggregateRoot<IQuoteProps> implements IQuote {
|
||||
get totalPrice(): MoneyValue {
|
||||
return this.beforeTaxPrice.add(this.taxPrice);
|
||||
}
|
||||
|
||||
get dateSent() {
|
||||
return this.props.dateSent;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
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";
|
||||
|
||||
export interface IQuoteRepository extends IRepository<Quote> {
|
||||
@ -19,4 +19,5 @@ export interface IQuoteRepository extends IRepository<Quote> {
|
||||
findLastReferenceByDealerId(dealerId: UniqueID): Promise<QuoteReference | null>;
|
||||
|
||||
updateStatusById(quoteId: UniqueID, newStatus: QuoteStatus): Promise<void>;
|
||||
updateSentDateById(quoteId: UniqueID, sentDate: UTCDateValue): Promise<void>;
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
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 { IQuoteRepository } from "../domain";
|
||||
import { Quote, QuoteReference, QuoteStatus } from "../domain/entities";
|
||||
import { ISalesContext } from "./Sales.context";
|
||||
import { IQuoteMapper, createQuoteMapper } from "./mappers/quote.mapper";
|
||||
import { createQuoteMapper, IQuoteMapper } from "./mappers/quote.mapper";
|
||||
|
||||
export type QueryParams = {
|
||||
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) => {
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
import { CreateQuoteUseCase } from "@/contexts/sales/application";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { registerQuoteRepository } from "../../../../Quote.repository";
|
||||
import { ISalesContext } from "../../../../Sales.context";
|
||||
import { CreateQuoteController } from "./CreateQuote.controller";
|
||||
import { CreateQuotePresenter } from "./presenter";
|
||||
|
||||
export const createQuoteController = (req: Request, res: Response, next: NextFunction) => {
|
||||
const context: ISalesContext = res.locals.context;
|
||||
|
||||
export const createQuoteController = (context: ISalesContext) => {
|
||||
registerQuoteRepository(context);
|
||||
return new CreateQuoteController(
|
||||
{
|
||||
@ -15,5 +12,5 @@ export const createQuoteController = (req: Request, res: Response, next: NextFun
|
||||
presenter: CreateQuotePresenter,
|
||||
},
|
||||
context
|
||||
).execute(req, res, next);
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
import { GetQuoteUseCase } from "@/contexts/sales/application";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { registerQuoteRepository } from "../../../../Quote.repository";
|
||||
import { ISalesContext } from "../../../../Sales.context";
|
||||
import { GetQuoteController } from "./GetQuote.controller";
|
||||
import { GetQuotePresenter } from "./presenter";
|
||||
|
||||
export const getQuoteController = (req: Request, res: Response, next: NextFunction) => {
|
||||
const context: ISalesContext = res.locals.context;
|
||||
|
||||
export const getQuoteController = (context: ISalesContext) => {
|
||||
registerQuoteRepository(context);
|
||||
|
||||
return new GetQuoteController(
|
||||
@ -16,5 +13,5 @@ export const getQuoteController = (req: Request, res: Response, next: NextFuncti
|
||||
presenter: GetQuotePresenter,
|
||||
},
|
||||
context
|
||||
).execute(req, res, next);
|
||||
);
|
||||
};
|
||||
|
||||
@ -41,6 +41,8 @@ export const GetQuotePresenter: IGetQuotePresenter = {
|
||||
|
||||
items: quoteItemPresenter(quote.items, context),
|
||||
dealer_id: quote.dealerId.toString(),
|
||||
|
||||
date_sent: quote.dateSent.toISO8601(),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
import { ListQuotesUseCase } from "@/contexts/sales/application";
|
||||
import { registerQuoteRepository } from "@/contexts/sales/infrastructure/Quote.repository";
|
||||
import { ISalesContext } from "@/contexts/sales/infrastructure/Sales.context";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { ListQuotesController } from "./ListQuotes.controller";
|
||||
import { ListQuotesPresenter } from "./presenter";
|
||||
|
||||
export const listQuotesController = (req: Request, res: Response, next: NextFunction) => {
|
||||
const context: ISalesContext = res.locals.context;
|
||||
|
||||
export const listQuotesController = (context: ISalesContext) => {
|
||||
registerQuoteRepository(context);
|
||||
|
||||
return new ListQuotesController(
|
||||
@ -16,5 +13,5 @@ export const listQuotesController = (req: Request, res: Response, next: NextFunc
|
||||
presenter: ListQuotesPresenter,
|
||||
},
|
||||
context
|
||||
).execute(req, res, next);
|
||||
);
|
||||
};
|
||||
|
||||
@ -39,6 +39,8 @@ export const ListQuotesPresenter: IListQuotesPresenter = {
|
||||
|
||||
total_price: quote.totalPrice.convertScale(2).toObject(),
|
||||
dealer_id: quote.dealerId.toString(),
|
||||
|
||||
date_sent: quote.dateSent.toISO8601(),
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
import { GetQuoteUseCase } from "@/contexts/sales/application";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { registerQuoteRepository } from "../../../../Quote.repository";
|
||||
import { ISalesContext } from "../../../../Sales.context";
|
||||
import { ReportQuotePresenter } from "./reporter/ReportQuote.reporter";
|
||||
import { ReportQuoteController } from "./ReportQuote.controller";
|
||||
|
||||
export const reportQuoteController = (req: Request, res: Response, next: NextFunction) => {
|
||||
const context: ISalesContext = res.locals.context;
|
||||
|
||||
export const reportQuoteController = (context: ISalesContext) => {
|
||||
registerQuoteRepository(context);
|
||||
|
||||
return new ReportQuoteController(
|
||||
@ -16,5 +13,5 @@ export const reportQuoteController = (req: Request, res: Response, next: NextFun
|
||||
reporter: ReportQuotePresenter,
|
||||
},
|
||||
context
|
||||
).execute(req, res, next);
|
||||
);
|
||||
};
|
||||
|
||||
@ -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
|
||||
);
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,14 @@
|
||||
import { SetStatusQuoteUseCase } from "@/contexts/sales/application";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { registerQuoteRepository } from "../../../../Quote.repository";
|
||||
import { ISalesContext } from "../../../../Sales.context";
|
||||
import { SetStatusQuoteController } from "./SetStatusQuote.controller";
|
||||
|
||||
export const setStatusQuoteController = (req: Request, res: Response, next: NextFunction) => {
|
||||
const context: ISalesContext = res.locals.context;
|
||||
|
||||
export const setStatusQuoteController = (context: ISalesContext) => {
|
||||
registerQuoteRepository(context);
|
||||
return new SetStatusQuoteController(
|
||||
{
|
||||
useCase: new SetStatusQuoteUseCase(context),
|
||||
},
|
||||
context
|
||||
).execute(req, res, next);
|
||||
);
|
||||
};
|
||||
|
||||
@ -95,6 +95,8 @@ class QuoteMapper
|
||||
),*/
|
||||
|
||||
dealerId: this.mapsValue(source, "dealer_id", UniqueID.create),
|
||||
|
||||
dateSent: this.mapsValue(source, "date_sent", UTCDateValue.create),
|
||||
};
|
||||
|
||||
const quoteOrError = Quote.create(props, id);
|
||||
@ -139,6 +141,8 @@ class QuoteMapper
|
||||
|
||||
items,
|
||||
dealer_id: source.dealerId.toPrimitive(),
|
||||
|
||||
date_sent: source.dateSent?.toPrimitive(),
|
||||
};
|
||||
|
||||
return quote;
|
||||
|
||||
@ -13,7 +13,7 @@ import { QuoteItemCreationAttributes, QuoteItem_Model } from "./quoteItem.model"
|
||||
|
||||
export type QuoteCreationAttributes = InferCreationAttributes<
|
||||
Quote_Model,
|
||||
{ omit: "items" | "dealer" }
|
||||
{ omit: "items" | "dealer" | "id_contract" }
|
||||
> & {
|
||||
items: QuoteItemCreationAttributes[];
|
||||
dealer_id: string;
|
||||
@ -65,6 +65,9 @@ export class Quote_Model extends Model<
|
||||
|
||||
declare items: NonAttribute<QuoteItem_Model[]>;
|
||||
declare dealer: NonAttribute<Dealer_Model>;
|
||||
|
||||
declare id_contract: CreationOptional<string | null>;
|
||||
declare date_sent: CreationOptional<string | null>;
|
||||
}
|
||||
|
||||
export default (sequelize: Sequelize) => {
|
||||
@ -155,6 +158,16 @@ export default (sequelize: Sequelize) => {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
},
|
||||
|
||||
id_contract: {
|
||||
type: DataTypes.BIGINT().UNSIGNED,
|
||||
allowNull: true,
|
||||
},
|
||||
|
||||
date_sent: {
|
||||
type: new DataTypes.DATE(),
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
@ -172,6 +185,8 @@ export default (sequelize: Sequelize) => {
|
||||
{ name: "status_idx", fields: ["status"] },
|
||||
{ name: "reference_idx", fields: ["reference"] },
|
||||
{ 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
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { checkUser } from "@/contexts/auth";
|
||||
import { handleRequest } from "@/contexts/common/infrastructure/express";
|
||||
import {
|
||||
createQuoteController,
|
||||
getQuoteController,
|
||||
@ -7,6 +8,7 @@ import {
|
||||
setStatusQuoteController,
|
||||
updateQuoteController,
|
||||
} 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 { Router } from "express";
|
||||
|
||||
@ -14,20 +16,30 @@ export const quoteRouter = (appRouter: Router): void => {
|
||||
const quoteRoutes: Router = Router({ mergeParams: true });
|
||||
|
||||
// Users CRUD
|
||||
quoteRoutes.get("/", checkUser, getDealerMiddleware, listQuotesController);
|
||||
quoteRoutes.get("/:quoteId", checkUser, getDealerMiddleware, getQuoteController);
|
||||
quoteRoutes.post("/", checkUser, getDealerMiddleware, createQuoteController);
|
||||
quoteRoutes.put("/:quoteId", checkUser, getDealerMiddleware, updateQuoteController);
|
||||
quoteRoutes.get("/", checkUser, getDealerMiddleware, handleRequest(listQuotesController));
|
||||
quoteRoutes.get("/:quoteId", checkUser, getDealerMiddleware, handleRequest(getQuoteController));
|
||||
quoteRoutes.post("/", checkUser, getDealerMiddleware, handleRequest(createQuoteController));
|
||||
quoteRoutes.put(
|
||||
"/:quoteId",
|
||||
checkUser,
|
||||
getDealerMiddleware,
|
||||
handleRequest(updateQuoteController)
|
||||
);
|
||||
|
||||
// Reports
|
||||
quoteRoutes.get("/:quoteId/report", checkUser, getDealerMiddleware, reportQuoteController);
|
||||
quoteRoutes.get(
|
||||
"/:quoteId/report",
|
||||
checkUser,
|
||||
getDealerMiddleware,
|
||||
handleRequest(reportQuoteController)
|
||||
);
|
||||
|
||||
// Status
|
||||
quoteRoutes.put(
|
||||
"/:quoteId/setStatus",
|
||||
checkUser,
|
||||
/*getDealerMiddleware, */ setStatusQuoteController
|
||||
);
|
||||
quoteRoutes.put("/:quoteId/setStatus", checkUser, handleRequest(setStatusQuoteController));
|
||||
|
||||
// Send to Uecko
|
||||
quoteRoutes.put("/:quoteId/send", checkUser, handleRequest(sendQuoteController));
|
||||
|
||||
/*
|
||||
quoteRoutes.post("/", isAdmin, createQuoteController);
|
||||
|
||||
|
||||
@ -25,6 +25,8 @@ export interface IGetQuote_Response_DTO {
|
||||
items: IGetQuote_QuoteItem_Response_DTO[];
|
||||
|
||||
dealer_id: string;
|
||||
|
||||
date_sent: string;
|
||||
}
|
||||
|
||||
export interface IGetQuote_QuoteItem_Response_DTO {
|
||||
|
||||
@ -19,4 +19,6 @@ export interface IListQuotes_Response_DTO {
|
||||
total_price: IMoney_DTO;
|
||||
|
||||
dealer_id: string;
|
||||
|
||||
date_sent: string;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./ISendQuote_Request.dto";
|
||||
@ -1,5 +1,6 @@
|
||||
export * from "./CreateQuote.dto";
|
||||
export * from "./GetQuote.dto";
|
||||
export * from "./ListQuotes.dto";
|
||||
export * from "./SendQuote.dto";
|
||||
export * from "./SetStatusQuote.dto";
|
||||
export * from "./UpdateQuote.dto";
|
||||
|
||||
@ -2,3 +2,11 @@ export const adjustPrecision = ({ amount, scale }: { amount: number; scale: numb
|
||||
const factor = 10 ** scale;
|
||||
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}`;
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user