.
This commit is contained in:
parent
5016b8e5bd
commit
033d2363df
236
client/src/app/quotes/components/QuoteResume.tsx
Normal file
236
client/src/app/quotes/components/QuoteResume.tsx
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
import {
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
CreditCard,
|
||||||
|
DownloadIcon,
|
||||||
|
FilePenLineIcon,
|
||||||
|
MoreVertical,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationItem,
|
||||||
|
Separator,
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
} from "@/ui";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useQuotes } from "../hooks";
|
||||||
|
import { QuoteStatusEditor } from "./editors";
|
||||||
|
|
||||||
|
type QuoteResumeProps = {
|
||||||
|
quoteId: string;
|
||||||
|
className: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
|
||||||
|
const { useOne, useUpdate } = useQuotes();
|
||||||
|
const { data, status, error: queryError } = useOne(quoteId);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
if (status === "error") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status !== "success") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<Card className={cn("overflow-hidden", className)}>
|
||||||
|
<CardContent className='px-4 py-6 text-center'>
|
||||||
|
<p className='mx-auto'>Select a quote</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs defaultValue='resume'>
|
||||||
|
<Card className='w-full overflow-hidden'>
|
||||||
|
<CardHeader className='flex flex-row items-start border-b'>
|
||||||
|
<div className='grid gap-0.5'>
|
||||||
|
<CardTitle className='flex items-center gap-2 text-lg group'>
|
||||||
|
{`${t("quotes.list.preview.quote")} #${data.reference}`}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>assaasassa</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-1 ml-auto'>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
variant='outline'
|
||||||
|
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='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>
|
||||||
|
{t("quotes.list.columns.actions.edit")}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
<Button size='sm' variant='outline' className='h-8 gap-1' onClick={() => null}>
|
||||||
|
<DownloadIcon className='h-3.5 w-3.5' />
|
||||||
|
<span className='xl:sr-only 2xl:not-sr-only 2xl:whitespace-nowrap'>
|
||||||
|
{t("quotes.list.preview.download_quote")}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button size='icon' variant='outline' className='w-8 h-8'>
|
||||||
|
<MoreVertical className='h-3.5 w-3.5' />
|
||||||
|
<span className='sr-only'>More</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align='end'>
|
||||||
|
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Export</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>Trash</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardHeader className='flex flex-row items-start border-b'>
|
||||||
|
<QuoteStatusEditor quote={data} onChangeStatus={() => null} />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='p-6 text-sm'>
|
||||||
|
<TabsList className='grid w-full grid-cols-2'>
|
||||||
|
<TabsTrigger value='resume'>Resume</TabsTrigger>
|
||||||
|
<TabsTrigger value='preview'>Preview</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value='resume' className='pt-4'>
|
||||||
|
<div className='grid gap-3'>
|
||||||
|
<div className='grid gap-3'>
|
||||||
|
<div className='font-semibold'>Customer Information</div>
|
||||||
|
Date: {new Date(data.date).toLocaleDateString()}
|
||||||
|
<div>{data.customer_information}</div>
|
||||||
|
<dl className='grid gap-3'>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<dt className='text-muted-foreground'>Customer</dt>
|
||||||
|
<dd>Liam Johnson</dd>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<dt className='text-muted-foreground'>Email</dt>
|
||||||
|
<dd>
|
||||||
|
<a href='mailto:'>liam@acme.com</a>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<dt className='text-muted-foreground'>Phone</dt>
|
||||||
|
<dd>
|
||||||
|
<a href='tel:'>+1 234 567 890</a>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<Separator className='my-4' />
|
||||||
|
<div className='font-semibold'>Order Details</div>
|
||||||
|
<ul className='grid gap-3'>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>
|
||||||
|
Glimmer Lamps x <span>2</span>
|
||||||
|
</span>
|
||||||
|
<span>$250.00</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>
|
||||||
|
Aqua Filters x <span>1</span>
|
||||||
|
</span>
|
||||||
|
<span>$49.00</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<Separator className='my-2' />
|
||||||
|
<ul className='grid gap-3'>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>Subtotal</span>
|
||||||
|
<span>$299.00</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>Shipping</span>
|
||||||
|
<span>$5.00</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>Tax</span>
|
||||||
|
<span>$25.00</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between font-semibold'>
|
||||||
|
<span className='text-muted-foreground'>Total</span>
|
||||||
|
<span>$329.00</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<Separator className='my-4' />
|
||||||
|
<div className='grid grid-cols-2 gap-4'>
|
||||||
|
<div className='grid gap-3'>
|
||||||
|
<div className='font-semibold'>Shipping Information</div>
|
||||||
|
<address className='grid gap-0.5 not-italic text-muted-foreground'>
|
||||||
|
<span>Liam Johnson</span>
|
||||||
|
<span>1234 Main St.</span>
|
||||||
|
<span>Anytown, CA 12345</span>
|
||||||
|
</address>
|
||||||
|
</div>
|
||||||
|
<div className='grid gap-3 auto-rows-max'>
|
||||||
|
<div className='font-semibold'>Billing Information</div>
|
||||||
|
<div className='text-muted-foreground'>Same as shipping address</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator className='my-4' />
|
||||||
|
|
||||||
|
<div className='grid gap-3'>
|
||||||
|
<div className='font-semibold'>Payment Information</div>
|
||||||
|
<dl className='grid gap-3'>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<dt className='flex items-center gap-1 text-muted-foreground'>
|
||||||
|
<CreditCard className='w-4 h-4' />
|
||||||
|
Visa
|
||||||
|
</dt>
|
||||||
|
<dd>**** **** **** 4532</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value='preview'></TabsContent>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className='flex flex-row items-center px-6 py-3 border-t'>
|
||||||
|
<div className='text-xs text-muted-foreground'>Updated...</div>
|
||||||
|
<Pagination className='w-auto ml-auto mr-0'>
|
||||||
|
<PaginationContent>
|
||||||
|
<PaginationItem>
|
||||||
|
<Button size='icon' variant='outline' className='w-6 h-6'>
|
||||||
|
<ChevronLeft className='h-3.5 w-3.5' />
|
||||||
|
<span className='sr-only'>Previous Order</span>
|
||||||
|
</Button>
|
||||||
|
</PaginationItem>
|
||||||
|
<PaginationItem>
|
||||||
|
<Button size='icon' variant='outline' className='w-6 h-6'>
|
||||||
|
<ChevronRight className='h-3.5 w-3.5' />
|
||||||
|
<span className='sr-only'>Next Order</span>
|
||||||
|
</Button>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -25,7 +25,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/ui";
|
} from "@/ui";
|
||||||
import { useToast } from "@/ui/use-toast";
|
import { useToast } from "@/ui/use-toast";
|
||||||
import { IListQuotes_Response_DTO, MoneyValue, UTCDateValue } from "@shared/contexts";
|
import { IListQuotes_Response_DTO, UTCDateValue } from "@shared/contexts";
|
||||||
import { ColumnDef, Row } from "@tanstack/react-table";
|
import { ColumnDef, Row } from "@tanstack/react-table";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { FilePenLineIcon, MoreVerticalIcon } from "lucide-react";
|
import { FilePenLineIcon, MoreVerticalIcon } from "lucide-react";
|
||||||
@ -33,7 +33,7 @@ import { useCallback, useEffect, useId, useMemo, useState } 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 { QuotePDFPreview } from "./QuotePDFPreview";
|
import { QuoteResume } from "./QuoteResume";
|
||||||
|
|
||||||
export const QuotesDataTable = ({
|
export const QuotesDataTable = ({
|
||||||
status = "all",
|
status = "all",
|
||||||
@ -137,8 +137,7 @@ export const QuotesDataTable = ({
|
|||||||
),
|
),
|
||||||
size: 600,
|
size: 600,
|
||||||
},
|
},
|
||||||
|
/*{
|
||||||
{
|
|
||||||
id: "total_price" as const,
|
id: "total_price" as const,
|
||||||
accessor: "total_price",
|
accessor: "total_price",
|
||||||
header: () => <div className='text-right'>{t("quotes.list.columns.total_price")}</div>,
|
header: () => <div className='text-right'>{t("quotes.list.columns.total_price")}</div>,
|
||||||
@ -148,7 +147,7 @@ export const QuotesDataTable = ({
|
|||||||
<div className='text-right'>{price.isSuccess ? price.object.toFormat() : "-"}</div>
|
<div className='text-right'>{price.isSuccess ? price.object.toFormat() : "-"}</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},*/
|
||||||
{
|
{
|
||||||
id: "row-actions",
|
id: "row-actions",
|
||||||
header: () => null,
|
header: () => null,
|
||||||
@ -294,7 +293,8 @@ export const QuotesDataTable = ({
|
|||||||
defaultSize={35}
|
defaultSize={35}
|
||||||
className='flex items-stretch flex-1'
|
className='flex items-stretch flex-1'
|
||||||
>
|
>
|
||||||
<QuotePDFPreview quote={activeRow?.original} className='flex-1' />
|
<QuoteResume quoteId={activeRow?.original.id} />
|
||||||
|
{/*<QuotePDFPreview quote={activeRow?.original} className='flex-1' />*/}
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
)}
|
)}
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
|
|||||||
@ -0,0 +1,50 @@
|
|||||||
|
import { Button } from "@/ui/button";
|
||||||
|
import { Select, SelectContent, SelectTrigger, SelectValue } from "@/ui/select";
|
||||||
|
import { SelectItem } from "@radix-ui/react-select";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const QUOTE_STATUS = ["draft", "ready", "delivered", "accepted", "rejected", "archived"];
|
||||||
|
|
||||||
|
const QUOTE_NEXT_STATUS = {
|
||||||
|
draft: ["ready", "archived"],
|
||||||
|
ready: ["delivered", "archived"],
|
||||||
|
delivered: ["accepted", "rejected", "archived"],
|
||||||
|
accepted: ["archived"],
|
||||||
|
rejected: ["archived"],
|
||||||
|
archived: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QuoteStatusEditor = ({ quote, onChangeStatus }) => {
|
||||||
|
const [status, changeStatus] = useState(quote.status);
|
||||||
|
|
||||||
|
const handleChangeStatus = () => {
|
||||||
|
if (status !== quote.status) {
|
||||||
|
onChangeStatus(quote.id, status);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-center space-x-2 '>
|
||||||
|
<Select value={status} onValueChange={changeStatus}>
|
||||||
|
<SelectTrigger className='w-[180px]'>
|
||||||
|
<SelectValue placeholder='Seleccionar estado' />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{QUOTE_NEXT_STATUS[quote.status].map((_status) => (
|
||||||
|
<SelectItem key={_status} value={_status}>
|
||||||
|
{_status}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
variant='secondary'
|
||||||
|
size='sm'
|
||||||
|
onClick={handleChangeStatus}
|
||||||
|
disabled={status === quote.status}
|
||||||
|
>
|
||||||
|
Cambiar estado
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,3 +1,4 @@
|
|||||||
export * from "./QuoteDetailsCardEditor";
|
export * from "./QuoteDetailsCardEditor";
|
||||||
export * from "./QuoteDocumentsCardEditor";
|
export * from "./QuoteDocumentsCardEditor";
|
||||||
export * from "./QuoteGeneralCardEditor";
|
export * from "./QuoteGeneralCardEditor";
|
||||||
|
export * from "./QuoteStatusEditor";
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./QuotePricesResume";
|
export * from "./QuotePricesResume";
|
||||||
|
export * from "./QuoteResume";
|
||||||
export * from "./QuotesDataTable";
|
export * from "./QuotesDataTable";
|
||||||
|
|||||||
@ -255,7 +255,7 @@ export const QuoteEdit = () => {
|
|||||||
|
|
||||||
<div className='items-center hidden gap-2 md:ml-auto md:flex'>
|
<div className='items-center hidden gap-2 md:ml-auto md:flex'>
|
||||||
<CancelButton
|
<CancelButton
|
||||||
label={t("common.cancel")}
|
label={t("common.close")}
|
||||||
variant='secondary'
|
variant='secondary'
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={() => navigate("/quotes")}
|
onClick={() => navigate("/quotes")}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
|
|
||||||
export const QuotesList = () => {
|
export const QuotesList = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [enabledPreview, toggleEnabledPreview] = useToggle(false);
|
const [enabledPreview, toggleEnabledPreview] = useToggle(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTableProvider>
|
<DataTableProvider>
|
||||||
|
|||||||
@ -0,0 +1,96 @@
|
|||||||
|
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, ISetStatusQuote_Request_DTO, Result, UniqueID } from "@shared/contexts";
|
||||||
|
import { IQuoteRepository, QuoteStatus } from "../../domain";
|
||||||
|
|
||||||
|
export interface ISetStatusQuoteUseCaseRequest extends IUseCaseRequest {
|
||||||
|
id: UniqueID;
|
||||||
|
newStatusDTO: ISetStatusQuote_Request_DTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SetStatusQuoteResponseOrError =
|
||||||
|
| Result<never, IUseCaseError> // Misc errors (value objects)
|
||||||
|
| Result<void, never>; // Success!
|
||||||
|
|
||||||
|
export class SetStatusQuoteUseCase
|
||||||
|
implements IUseCase<ISetStatusQuoteUseCaseRequest, Promise<SetStatusQuoteResponseOrError>>
|
||||||
|
{
|
||||||
|
private _adapter: ISequelizeAdapter;
|
||||||
|
private _repositoryManager: IRepositoryManager;
|
||||||
|
|
||||||
|
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
|
||||||
|
this._adapter = props.adapter;
|
||||||
|
this._repositoryManager = props.repositoryManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(request: ISetStatusQuoteUseCaseRequest): Promise<SetStatusQuoteResponseOrError> {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
newStatusDTO: { newStatus },
|
||||||
|
} = 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",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprobar el status
|
||||||
|
const statusOrError = QuoteStatus.create(newStatus);
|
||||||
|
if (statusOrError.isFailure) {
|
||||||
|
const { error: domainError } = statusOrError;
|
||||||
|
let errorCode = "";
|
||||||
|
let message = "";
|
||||||
|
|
||||||
|
switch (domainError.code) {
|
||||||
|
// Errores manuales
|
||||||
|
case DomainError.INVALID_INPUT_DATA:
|
||||||
|
errorCode = UseCaseError.INVALID_INPUT_DATA;
|
||||||
|
message = "Es nuevo estado no es correcto";
|
||||||
|
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.updateStatusById(id, statusOrError.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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,4 +2,5 @@ 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 "./SetStatusQuote.useCase";
|
||||||
export * from "./UpdateQuote.useCase";
|
export * from "./UpdateQuote.useCase";
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/* 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 } from "@shared/contexts";
|
||||||
import { Quote, QuoteReference } from "../entities";
|
import { Quote, QuoteReference, QuoteStatus } from "../entities";
|
||||||
|
|
||||||
export interface IQuoteRepository extends IRepository<Quote> {
|
export interface IQuoteRepository extends IRepository<Quote> {
|
||||||
exists(id: UniqueID): Promise<boolean>;
|
exists(id: UniqueID): Promise<boolean>;
|
||||||
@ -17,4 +17,6 @@ export interface IQuoteRepository extends IRepository<Quote> {
|
|||||||
|
|
||||||
findLastQuoteByDealerId(dealerId: UniqueID): Promise<Quote | null>;
|
findLastQuoteByDealerId(dealerId: UniqueID): Promise<Quote | null>;
|
||||||
findLastReferenceByDealerId(dealerId: UniqueID): Promise<QuoteReference | null>;
|
findLastReferenceByDealerId(dealerId: UniqueID): Promise<QuoteReference | null>;
|
||||||
|
|
||||||
|
updateStatusById(quoteId: UniqueID, newStatus: QuoteStatus): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts";
|
|||||||
import { ModelDefined, Transaction } from "sequelize";
|
import { ModelDefined, Transaction } from "sequelize";
|
||||||
|
|
||||||
import { IQuoteRepository } from "../domain";
|
import { IQuoteRepository } from "../domain";
|
||||||
import { Quote, QuoteReference } 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 { IQuoteMapper, createQuoteMapper } from "./mappers/quote.mapper";
|
||||||
|
|
||||||
@ -33,29 +33,29 @@ export class QuoteRepository extends SequelizeRepository<Quote> implements IQuot
|
|||||||
return this._exists("Quote_Model", "reference", reference.toPrimitive());
|
return this._exists("Quote_Model", "reference", reference.toPrimitive());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async create(user: Quote): Promise<void> {
|
public async create(quote: Quote): Promise<void> {
|
||||||
const userData = this.mapper.mapToPersistence(user);
|
const quoteData = this.mapper.mapToPersistence(quote);
|
||||||
await this._save("Quote_Model", user.id, userData);
|
await this._save("Quote_Model", quote.id, quoteData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async update(user: Quote): Promise<void> {
|
public async update(quote: Quote): Promise<void> {
|
||||||
console.time("update");
|
console.time("update");
|
||||||
const userData = this.mapper.mapToPersistence(user);
|
const quoteData = this.mapper.mapToPersistence(quote);
|
||||||
|
|
||||||
const QuoteItem_Model: ModelDefined<any, any> = this._adapter.getModel("QuoteItem_Model");
|
const QuoteItem_Model: ModelDefined<any, any> = this._adapter.getModel("QuoteItem_Model");
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this._save("Quote_Model", user.id, userData, {}),
|
this._save("Quote_Model", quote.id, quoteData, {}),
|
||||||
QuoteItem_Model.destroy({
|
QuoteItem_Model.destroy({
|
||||||
where: {
|
where: {
|
||||||
quote_id: userData.id,
|
quote_id: quoteData.id,
|
||||||
},
|
},
|
||||||
transaction: this._transaction,
|
transaction: this._transaction,
|
||||||
force: true,
|
force: true,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await QuoteItem_Model.bulkCreate(userData.items, {
|
await QuoteItem_Model.bulkCreate(quoteData.items, {
|
||||||
transaction: this._transaction,
|
transaction: this._transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -143,6 +143,21 @@ export class QuoteRepository extends SequelizeRepository<Quote> implements IQuot
|
|||||||
const quote = await this.findLastQuoteByDealerId(dealerId);
|
const quote = await this.findLastQuoteByDealerId(dealerId);
|
||||||
return quote ? quote.reference : null;
|
return quote ? quote.reference : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updateStatusById(quoteId: UniqueID, newStatus: QuoteStatus): Promise<void> {
|
||||||
|
const quoteStatusData = newStatus.toPrimitive(); //this.mapper.mapToPersistence(quote);
|
||||||
|
const _model = this._adapter.getModel("Quote_Model");
|
||||||
|
|
||||||
|
await _model.update(
|
||||||
|
{
|
||||||
|
status: quoteStatusData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
where: { id: quoteId.toPrimitive() },
|
||||||
|
transaction: this._transaction,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const registerQuoteRepository = (context: ISalesContext) => {
|
export const registerQuoteRepository = (context: ISalesContext) => {
|
||||||
|
|||||||
@ -3,4 +3,5 @@ export * from "./deleteQuote";
|
|||||||
export * from "./getQuote";
|
export * from "./getQuote";
|
||||||
export * from "./listQuotes";
|
export * from "./listQuotes";
|
||||||
export * from "./reportQuote";
|
export * from "./reportQuote";
|
||||||
|
export * from "./setStatusQuote";
|
||||||
export * from "./updateQuote";
|
export * from "./updateQuote";
|
||||||
|
|||||||
@ -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 { SetStatusQuoteUseCase } from "@/contexts/sales/application";
|
||||||
|
import {
|
||||||
|
ensureIdIsValid,
|
||||||
|
ensureSetStatusQuote_Request_DTOIsValid,
|
||||||
|
ISetStatusQuote_Request_DTO,
|
||||||
|
} from "@shared/contexts";
|
||||||
|
import { ISalesContext } from "../../../../Sales.context";
|
||||||
|
|
||||||
|
export class SetStatusQuoteController extends ExpressController {
|
||||||
|
private useCase: SetStatusQuoteUseCase;
|
||||||
|
private context: ISalesContext;
|
||||||
|
|
||||||
|
constructor(props: { useCase: SetStatusQuoteUseCase }, context: ISalesContext) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const { useCase } = props;
|
||||||
|
this.useCase = useCase;
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeImpl(): Promise<any> {
|
||||||
|
try {
|
||||||
|
const { quoteId } = this.req.params;
|
||||||
|
const newStatusDTO: ISetStatusQuote_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 newStatusDTOOrError = ensureSetStatusQuote_Request_DTOIsValid(newStatusDTO);
|
||||||
|
|
||||||
|
if (newStatusDTOOrError.isFailure) {
|
||||||
|
const errorMessage = "New quote status is not valid";
|
||||||
|
const infraError = InfrastructureError.create(
|
||||||
|
InfrastructureError.INVALID_INPUT_DATA,
|
||||||
|
errorMessage,
|
||||||
|
newStatusDTOOrError.error
|
||||||
|
);
|
||||||
|
return this.invalidInputError(errorMessage, infraError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llamar al caso de uso
|
||||||
|
const result = await this.useCase.execute({
|
||||||
|
id: quoteIdOrError.object,
|
||||||
|
newStatusDTO,
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { SetStatusQuoteUseCase } from "@/contexts/sales/application";
|
||||||
|
import Express from "express";
|
||||||
|
import { registerQuoteRepository } from "../../../../Quote.repository";
|
||||||
|
import { ISalesContext } from "../../../../Sales.context";
|
||||||
|
import { SetStatusQuoteController } from "./SetStatusQuote.controller";
|
||||||
|
|
||||||
|
export const setStatusQuoteController = (
|
||||||
|
req: Express.Request,
|
||||||
|
res: Express.Response,
|
||||||
|
next: Express.NextFunction
|
||||||
|
) => {
|
||||||
|
const context: ISalesContext = res.locals.context;
|
||||||
|
|
||||||
|
registerQuoteRepository(context);
|
||||||
|
return new SetStatusQuoteController(
|
||||||
|
{
|
||||||
|
useCase: new SetStatusQuoteUseCase(context),
|
||||||
|
},
|
||||||
|
context
|
||||||
|
).execute(req, res, next);
|
||||||
|
};
|
||||||
@ -4,6 +4,7 @@ import {
|
|||||||
getQuoteController,
|
getQuoteController,
|
||||||
listQuotesController,
|
listQuotesController,
|
||||||
reportQuoteController,
|
reportQuoteController,
|
||||||
|
setStatusQuoteController,
|
||||||
updateQuoteController,
|
updateQuoteController,
|
||||||
} from "@/contexts/sales/infrastructure/express/controllers";
|
} from "@/contexts/sales/infrastructure/express/controllers";
|
||||||
import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/dealerMiddleware";
|
import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/dealerMiddleware";
|
||||||
@ -21,6 +22,12 @@ export const QuoteRouter = (appRouter: Express.Router) => {
|
|||||||
// Reports
|
// Reports
|
||||||
quoteRoutes.get("/:quoteId/report", checkUser, getDealerMiddleware, reportQuoteController);
|
quoteRoutes.get("/:quoteId/report", checkUser, getDealerMiddleware, reportQuoteController);
|
||||||
|
|
||||||
|
// Status
|
||||||
|
quoteRoutes.put(
|
||||||
|
"/:quoteId/setStatus",
|
||||||
|
checkUser,
|
||||||
|
/*getDealerMiddleware, */ setStatusQuoteController
|
||||||
|
);
|
||||||
/*
|
/*
|
||||||
quoteRoutes.post("/", isAdmin, createQuoteController);
|
quoteRoutes.post("/", isAdmin, createQuoteController);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
import Joi from "joi";
|
||||||
|
import { Result, RuleValidator } from "../../../../../common";
|
||||||
|
|
||||||
|
export interface ISetStatusQuote_Request_DTO {
|
||||||
|
newStatus: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureSetStatusQuote_Request_DTOIsValid(quoteDTO: ISetStatusQuote_Request_DTO) {
|
||||||
|
const schema = Joi.object({
|
||||||
|
newStatus: Joi.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = RuleValidator.validate<ISetStatusQuote_Request_DTO>(schema, quoteDTO);
|
||||||
|
|
||||||
|
if (result.isFailure) {
|
||||||
|
return Result.fail(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(true);
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./ISetStatusQuote_Request.dto";
|
||||||
@ -1,4 +1,5 @@
|
|||||||
export * from "./CreateQuote.dto";
|
export * from "./CreateQuote.dto";
|
||||||
export * from "./GetQuote.dto";
|
export * from "./GetQuote.dto";
|
||||||
export * from "./ListQuotes.dto";
|
export * from "./ListQuotes.dto";
|
||||||
|
export * from "./SetStatusQuote.dto";
|
||||||
export * from "./UpdateQuote.dto";
|
export * from "./UpdateQuote.dto";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user