diff --git a/client/src/app/quotes/components/QuoteResume.tsx b/client/src/app/quotes/components/QuoteResume.tsx
new file mode 100644
index 0000000..4fd100f
--- /dev/null
+++ b/client/src/app/quotes/components/QuoteResume.tsx
@@ -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 (
+ Select a quote
+
+
+
+
+
+
+
+
{ exists(id: UniqueID): Promise; @@ -17,4 +17,6 @@ export interface IQuoteRepository extends IRepository { findLastQuoteByDealerId(dealerId: UniqueID): Promise; findLastReferenceByDealerId(dealerId: UniqueID): Promise; + + updateStatusById(quoteId: UniqueID, newStatus: QuoteStatus): Promise ; } diff --git a/server/src/contexts/sales/infrastructure/Quote.repository.ts b/server/src/contexts/sales/infrastructure/Quote.repository.ts index 85e4cec..abe00de 100644 --- a/server/src/contexts/sales/infrastructure/Quote.repository.ts +++ b/server/src/contexts/sales/infrastructure/Quote.repository.ts @@ -3,7 +3,7 @@ import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts"; import { ModelDefined, Transaction } from "sequelize"; import { IQuoteRepository } from "../domain"; -import { Quote, QuoteReference } from "../domain/entities"; +import { Quote, QuoteReference, QuoteStatus } from "../domain/entities"; import { ISalesContext } from "./Sales.context"; import { IQuoteMapper, createQuoteMapper } from "./mappers/quote.mapper"; @@ -33,29 +33,29 @@ export class QuoteRepository extends SequelizeRepository implements IQuot return this._exists("Quote_Model", "reference", reference.toPrimitive()); } - public async create(user: Quote): Promise{ - const userData = this.mapper.mapToPersistence(user); - await this._save("Quote_Model", user.id, userData); + public async create(quote: Quote): Promise { + const quoteData = this.mapper.mapToPersistence(quote); + await this._save("Quote_Model", quote.id, quoteData); } - public async update(user: Quote): Promise { + public async update(quote: Quote): Promise { console.time("update"); - const userData = this.mapper.mapToPersistence(user); + const quoteData = this.mapper.mapToPersistence(quote); const QuoteItem_Model: ModelDefined = this._adapter.getModel("QuoteItem_Model"); await Promise.all([ - this._save("Quote_Model", user.id, userData, {}), + this._save("Quote_Model", quote.id, quoteData, {}), QuoteItem_Model.destroy({ where: { - quote_id: userData.id, + quote_id: quoteData.id, }, transaction: this._transaction, force: true, }), ]); - await QuoteItem_Model.bulkCreate(userData.items, { + await QuoteItem_Model.bulkCreate(quoteData.items, { transaction: this._transaction, }); @@ -143,6 +143,21 @@ export class QuoteRepository extends SequelizeRepository implements IQuot const quote = await this.findLastQuoteByDealerId(dealerId); return quote ? quote.reference : null; } + + public async updateStatusById(quoteId: UniqueID, newStatus: QuoteStatus): Promise{ + 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) => { diff --git a/server/src/contexts/sales/infrastructure/express/controllers/quotes/index.ts b/server/src/contexts/sales/infrastructure/express/controllers/quotes/index.ts index 5adf695..cfdf99b 100644 --- a/server/src/contexts/sales/infrastructure/express/controllers/quotes/index.ts +++ b/server/src/contexts/sales/infrastructure/express/controllers/quotes/index.ts @@ -3,4 +3,5 @@ export * from "./deleteQuote"; export * from "./getQuote"; export * from "./listQuotes"; export * from "./reportQuote"; +export * from "./setStatusQuote"; export * from "./updateQuote"; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/quotes/setStatusQuote/SetStatusQuote.controller.ts b/server/src/contexts/sales/infrastructure/express/controllers/quotes/setStatusQuote/SetStatusQuote.controller.ts new file mode 100644 index 0000000..77c1c6f --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/quotes/setStatusQuote/SetStatusQuote.controller.ts @@ -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 { + 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); + } + } +} diff --git a/server/src/contexts/sales/infrastructure/express/controllers/quotes/setStatusQuote/index.ts b/server/src/contexts/sales/infrastructure/express/controllers/quotes/setStatusQuote/index.ts new file mode 100644 index 0000000..26cbdee --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/quotes/setStatusQuote/index.ts @@ -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); +}; diff --git a/server/src/infrastructure/express/api/routes/quote.routes.ts b/server/src/infrastructure/express/api/routes/quote.routes.ts index a31802d..eccb78c 100644 --- a/server/src/infrastructure/express/api/routes/quote.routes.ts +++ b/server/src/infrastructure/express/api/routes/quote.routes.ts @@ -4,6 +4,7 @@ import { getQuoteController, listQuotesController, reportQuoteController, + setStatusQuoteController, updateQuoteController, } from "@/contexts/sales/infrastructure/express/controllers"; import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/dealerMiddleware"; @@ -21,6 +22,12 @@ export const QuoteRouter = (appRouter: Express.Router) => { // Reports quoteRoutes.get("/:quoteId/report", checkUser, getDealerMiddleware, reportQuoteController); + // Status + quoteRoutes.put( + "/:quoteId/setStatus", + checkUser, + /*getDealerMiddleware, */ setStatusQuoteController + ); /* quoteRoutes.post("/", isAdmin, createQuoteController); diff --git a/shared/lib/contexts/sales/application/dto/Quote/SetStatusQuote.dto/ISetStatusQuote_Request.dto.ts b/shared/lib/contexts/sales/application/dto/Quote/SetStatusQuote.dto/ISetStatusQuote_Request.dto.ts new file mode 100644 index 0000000..8bb23fe --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/Quote/SetStatusQuote.dto/ISetStatusQuote_Request.dto.ts @@ -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 (schema, quoteDTO); + + if (result.isFailure) { + return Result.fail(result.error); + } + + return Result.ok(true); +} diff --git a/shared/lib/contexts/sales/application/dto/Quote/SetStatusQuote.dto/index.ts b/shared/lib/contexts/sales/application/dto/Quote/SetStatusQuote.dto/index.ts new file mode 100644 index 0000000..338fd44 --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/Quote/SetStatusQuote.dto/index.ts @@ -0,0 +1 @@ +export * from "./ISetStatusQuote_Request.dto"; diff --git a/shared/lib/contexts/sales/application/dto/Quote/index.ts b/shared/lib/contexts/sales/application/dto/Quote/index.ts index 6cbb1f3..b8ce619 100644 --- a/shared/lib/contexts/sales/application/dto/Quote/index.ts +++ b/shared/lib/contexts/sales/application/dto/Quote/index.ts @@ -1,4 +1,5 @@ export * from "./CreateQuote.dto"; export * from "./GetQuote.dto"; export * from "./ListQuotes.dto"; +export * from "./SetStatusQuote.dto"; export * from "./UpdateQuote.dto";