From 1093bcceff22416f1c068afaf0257204dd5d3131 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 14 Apr 2026 19:51:07 -0300 Subject: [PATCH 1/9] feat: add domain layer --- .../domain/models/input-use-products-model.ts | 21 +++++++++++++++++++ .../create-input-use-product-use-case.ts | 9 ++++++++ .../delete-input-use-product-use-case.ts | 6 ++++++ .../get-input-use-product-use-case.ts | 9 ++++++++ .../get-input-use-products-use-case.ts | 11 ++++++++++ .../input-use-products-use-cases/index.ts | 5 +++++ .../update-input-use-product-use-case.ts | 9 ++++++++ 7 files changed, 70 insertions(+) create mode 100644 src/app/modules/input-uses/domain/models/input-use-products-model.ts create mode 100644 src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/create-input-use-product-use-case.ts create mode 100644 src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/delete-input-use-product-use-case.ts create mode 100644 src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/get-input-use-product-use-case.ts create mode 100644 src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/get-input-use-products-use-case.ts create mode 100644 src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/index.ts create mode 100644 src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/update-input-use-product-use-case.ts diff --git a/src/app/modules/input-uses/domain/models/input-use-products-model.ts b/src/app/modules/input-uses/domain/models/input-use-products-model.ts new file mode 100644 index 00000000..7b7ac204 --- /dev/null +++ b/src/app/modules/input-uses/domain/models/input-use-products-model.ts @@ -0,0 +1,21 @@ +import type { Option, WithId } from '@/core/domain/types' + +export type InputUseProductDetailsModel = { + name: string + category: Option +} + +export type InputUseProductDetailsApiResponse = { + name: string + category: Option +} + +export type InputUseProductModel = WithId<{ + name: string + category: string +}> + +export type InputUseProductApiResponse = WithId<{ + name: string + category: string +}> diff --git a/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/create-input-use-product-use-case.ts b/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/create-input-use-product-use-case.ts new file mode 100644 index 00000000..41d1f098 --- /dev/null +++ b/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/create-input-use-product-use-case.ts @@ -0,0 +1,9 @@ +import type { InputUseProductDetailsModel } from '../../models/input-use-products-model' +import type { RequestInterface } from '@/core/domain/types' + +export type CreateInputUseProductUseCase = RequestInterface< + { + inputUseProduct: InputUseProductDetailsModel + }, + void +> diff --git a/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/delete-input-use-product-use-case.ts b/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/delete-input-use-product-use-case.ts new file mode 100644 index 00000000..208137f3 --- /dev/null +++ b/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/delete-input-use-product-use-case.ts @@ -0,0 +1,6 @@ +import type { RequestInterface } from '@/core/domain/types' + +export type DeleteInputUseProductUseCase = RequestInterface< + { id: number }, + void +> diff --git a/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/get-input-use-product-use-case.ts b/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/get-input-use-product-use-case.ts new file mode 100644 index 00000000..dd54f082 --- /dev/null +++ b/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/get-input-use-product-use-case.ts @@ -0,0 +1,9 @@ +import type { InputUseProductDetailsModel } from '../../models/input-use-products-model' +import type { RequestInterface } from '@/core/domain/types' + +export type GetInputUseProductUseCase = RequestInterface< + { + id: number + }, + InputUseProductDetailsModel +> diff --git a/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/get-input-use-products-use-case.ts b/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/get-input-use-products-use-case.ts new file mode 100644 index 00000000..08484d85 --- /dev/null +++ b/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/get-input-use-products-use-case.ts @@ -0,0 +1,11 @@ +import type { InputUseProductModel } from '../../models/input-use-products-model' +import type { + ListParams, + ListResponse, + RequestInterface, +} from '@/core/domain/types' + +export type GetInputUseProductsUseCase = RequestInterface< + ListParams, + ListResponse +> diff --git a/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/index.ts b/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/index.ts new file mode 100644 index 00000000..9d88b6d6 --- /dev/null +++ b/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/index.ts @@ -0,0 +1,5 @@ +export * from './create-input-use-product-use-case' +export * from './delete-input-use-product-use-case' +export * from './get-input-use-product-use-case' +export * from './get-input-use-products-use-case' +export * from './update-input-use-product-use-case' diff --git a/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/update-input-use-product-use-case.ts b/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/update-input-use-product-use-case.ts new file mode 100644 index 00000000..7d29fb92 --- /dev/null +++ b/src/app/modules/input-uses/domain/use-cases/input-use-products-use-cases/update-input-use-product-use-case.ts @@ -0,0 +1,9 @@ +import type { InputUseProductDetailsModel } from '../../models/input-use-products-model' +import type { RequestInterface, WithId } from '@/core/domain/types' + +export type UpdateInputUseProductUseCase = RequestInterface< + { + inputUseProduct: WithId + }, + void +> From 1ed40a73e578a1065617f1f25a79f58fa92d736f Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 14 Apr 2026 19:55:29 -0300 Subject: [PATCH 2/9] feat: add data layer --- .../input-use-products-use-cases/index.ts | 5 ++ ...emote-create-input-use-product-use-case.ts | 35 +++++++++ ...emote-delete-input-use-product-use-case.ts | 36 +++++++++ .../remote-get-input-use-product-use-case.ts | 47 ++++++++++++ .../remote-get-input-use-products-use-case.ts | 75 +++++++++++++++++++ ...emote-update-input-use-product-use-case.ts | 39 ++++++++++ 6 files changed, 237 insertions(+) create mode 100644 src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/index.ts create mode 100644 src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-create-input-use-product-use-case.ts create mode 100644 src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-delete-input-use-product-use-case.ts create mode 100644 src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-get-input-use-product-use-case.ts create mode 100644 src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-get-input-use-products-use-case.ts create mode 100644 src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-update-input-use-product-use-case.ts diff --git a/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/index.ts b/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/index.ts new file mode 100644 index 00000000..69ef9767 --- /dev/null +++ b/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/index.ts @@ -0,0 +1,5 @@ +export * from './remote-create-input-use-product-use-case' +export * from './remote-delete-input-use-product-use-case' +export * from './remote-get-input-use-product-use-case' +export * from './remote-get-input-use-products-use-case' +export * from './remote-update-input-use-product-use-case' diff --git a/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-create-input-use-product-use-case.ts b/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-create-input-use-product-use-case.ts new file mode 100644 index 00000000..3413cbde --- /dev/null +++ b/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-create-input-use-product-use-case.ts @@ -0,0 +1,35 @@ +import { type HttpClient, HttpStatusCode } from '@/core/data/protocols/http' +import { + BadRequestError, + ForbiddenError, + UnexpectedError, +} from '@/core/domain/errors' + +import type { CreateInputUseProductUseCase } from '../../../domain/use-cases/input-use-products-use-cases' + +export class RemoteCreateInputUseProductUseCase + implements CreateInputUseProductUseCase +{ + constructor( + private readonly url: string, + private readonly httpClient: HttpClient + ) {} + + execute: CreateInputUseProductUseCase['execute'] = async (params) => { + const { statusCode } = await this.httpClient.request({ + url: this.url, + method: 'post', + body: params, + }) + + if (statusCode === HttpStatusCode.created) return + + if (statusCode === HttpStatusCode.badRequest) throw new BadRequestError() + + if (statusCode === HttpStatusCode.forbidden) { + throw new ForbiddenError('Você não tem permissão para criar um produto.') + } + + throw new UnexpectedError() + } +} diff --git a/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-delete-input-use-product-use-case.ts b/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-delete-input-use-product-use-case.ts new file mode 100644 index 00000000..5f4003e2 --- /dev/null +++ b/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-delete-input-use-product-use-case.ts @@ -0,0 +1,36 @@ +import { type HttpClient, HttpStatusCode } from '@/core/data/protocols/http' +import { + BadRequestError, + ForbiddenError, + UnexpectedError, +} from '@/core/domain/errors' + +import type { DeleteInputUseProductUseCase } from '../../../domain/use-cases/input-use-products-use-cases' + +export class RemoteDeleteInputUseProductUseCase + implements DeleteInputUseProductUseCase +{ + constructor( + private readonly url: string, + private readonly httpClient: HttpClient + ) {} + + execute: DeleteInputUseProductUseCase['execute'] = async ({ id }) => { + const { statusCode } = await this.httpClient.request({ + url: `${this.url}/${id}`, + method: 'delete', + }) + + if (statusCode === HttpStatusCode.noContent) return + + if (statusCode === HttpStatusCode.badRequest) throw new BadRequestError() + + if (statusCode === HttpStatusCode.forbidden) { + throw new ForbiddenError( + 'Você não tem permissão para remover um produto.' + ) + } + + throw new UnexpectedError() + } +} diff --git a/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-get-input-use-product-use-case.ts b/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-get-input-use-product-use-case.ts new file mode 100644 index 00000000..3074128a --- /dev/null +++ b/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-get-input-use-product-use-case.ts @@ -0,0 +1,47 @@ +import { type HttpClient, HttpStatusCode } from '@/core/data/protocols/http' +import { + UnexpectedError, + NotFoundError, + ForbiddenError, +} from '@/core/domain/errors' + +import type { + InputUseProductDetailsApiResponse, + InputUseProductDetailsModel, +} from '../../../domain/models/input-use-products-model' +import type { GetInputUseProductUseCase } from '../../../domain/use-cases/input-use-products-use-cases' + +export class RemoteGetInputUseProductUseCase + implements GetInputUseProductUseCase +{ + constructor( + private readonly url: string, + private readonly httpClient: HttpClient< + InputUseProductDetailsModel, + InputUseProductDetailsApiResponse + > + ) {} + + execute: GetInputUseProductUseCase['execute'] = async ({ id }) => { + const { statusCode, body } = await this.httpClient.request({ + url: `${this.url}/${id}`, + method: 'get', + }) + + if (statusCode === HttpStatusCode.ok && !!body) { + return { + name: body.name, + category: body.category, + } + } + + if (statusCode === HttpStatusCode.notFound) + throw new NotFoundError('Produto') + + if (statusCode === HttpStatusCode.forbidden) { + throw new ForbiddenError('Você não tem permissão para buscar um produto') + } + + throw new UnexpectedError() + } +} diff --git a/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-get-input-use-products-use-case.ts b/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-get-input-use-products-use-case.ts new file mode 100644 index 00000000..781ba6ae --- /dev/null +++ b/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-get-input-use-products-use-case.ts @@ -0,0 +1,75 @@ +import { type HttpClient, HttpStatusCode } from '@/core/data/protocols/http' +import { + UnexpectedError, + NotFoundError, + ForbiddenError, +} from '@/core/domain/errors' + +import type { + InputUseProductApiResponse, + InputUseProductModel, +} from '../../../domain/models/input-use-products-model' +import type { GetInputUseProductsUseCase } from '../../../domain/use-cases/input-use-products-use-cases' +import type { ListApiResponse, MapApiProperties } from '@/core/domain/types' + +export class RemoteGetInputUseProductsUseCase + implements GetInputUseProductsUseCase +{ + constructor( + private readonly url: string, + private readonly httpClient: HttpClient< + InputUseProductModel, + InputUseProductApiResponse, + ListApiResponse + > + ) {} + + execute: GetInputUseProductsUseCase['execute'] = async ({ + filters, + pagination, + sort, + }) => { + const mapApiProperties: MapApiProperties< + InputUseProductModel, + InputUseProductApiResponse + > = { + id: 'id', + name: 'name', + category: 'category', + } + + const { statusCode, body } = await this.httpClient.request({ + url: `${this.url}/search`, + method: 'post', + filters, + pagination, + sort, + mapApiProperties, + }) + + if (statusCode === HttpStatusCode.ok && !!body) { + return { + resources: body.content.map((item) => { + return { + id: item.id, + name: item.name, + category: item.category, + } + }), + totalPages: body.totalPages, + } + } + + if (statusCode === HttpStatusCode.notFound) { + throw new NotFoundError('Produtos') + } + + if (statusCode === HttpStatusCode.forbidden) { + throw new ForbiddenError( + 'Você não tem permissão para buscar os produtos.' + ) + } + + throw new UnexpectedError() + } +} diff --git a/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-update-input-use-product-use-case.ts b/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-update-input-use-product-use-case.ts new file mode 100644 index 00000000..9be527f9 --- /dev/null +++ b/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-update-input-use-product-use-case.ts @@ -0,0 +1,39 @@ +import { type HttpClient, HttpStatusCode } from '@/core/data/protocols/http' +import { + BadRequestError, + ForbiddenError, + UnexpectedError, +} from '@/core/domain/errors' + +import type { UpdateInputUseProductUseCase } from '../../../domain/use-cases/input-use-products-use-cases' + +export class RemoteUpdateInputUseProductUseCase + implements UpdateInputUseProductUseCase +{ + constructor( + private readonly url: string, + private readonly httpClient: HttpClient + ) {} + + execute: UpdateInputUseProductUseCase['execute'] = async ({ + inputUseProduct: { id, ...inputUseProduct }, + }) => { + const { statusCode } = await this.httpClient.request({ + url: `${this.url}/${id}`, + method: 'patch', + body: inputUseProduct, + }) + + if (statusCode === HttpStatusCode.noContent) return + + if (statusCode === HttpStatusCode.badRequest) throw new BadRequestError() + + if (statusCode === HttpStatusCode.forbidden) { + throw new ForbiddenError( + 'Você não tem permissão para atualizar um produto.' + ) + } + + throw new UnexpectedError() + } +} From 5d2ae2279c8d3d918d8b5c604c2f0f378d4ca760 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 14 Apr 2026 20:03:19 -0300 Subject: [PATCH 3/9] feat: add main with factories layer --- .../use-cases/input-use-products-use-cases/index.ts | 5 +++++ ...mote-create-input-use-product-use-case-factory.ts | 12 ++++++++++++ .../remote-delete-input-use-product-use-factory.ts | 12 ++++++++++++ .../remote-get-input-use-product-use-factory.ts | 12 ++++++++++++ ...remote-get-input-use-products-use-case-factory.ts | 12 ++++++++++++ .../remote-update-input-use-product-use-factory.ts | 12 ++++++++++++ 6 files changed, 65 insertions(+) create mode 100644 src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/index.ts create mode 100644 src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-create-input-use-product-use-case-factory.ts create mode 100644 src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-delete-input-use-product-use-factory.ts create mode 100644 src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-get-input-use-product-use-factory.ts create mode 100644 src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-get-input-use-products-use-case-factory.ts create mode 100644 src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-update-input-use-product-use-factory.ts diff --git a/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/index.ts b/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/index.ts new file mode 100644 index 00000000..aaaf2e0e --- /dev/null +++ b/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/index.ts @@ -0,0 +1,5 @@ +export * from './remote-create-input-use-product-use-case-factory' +export * from './remote-delete-input-use-product-use-factory' +export * from './remote-get-input-use-product-use-factory' +export * from './remote-get-input-use-products-use-case-factory' +export * from './remote-update-input-use-product-use-factory' diff --git a/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-create-input-use-product-use-case-factory.ts b/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-create-input-use-product-use-case-factory.ts new file mode 100644 index 00000000..e7d3d2ea --- /dev/null +++ b/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-create-input-use-product-use-case-factory.ts @@ -0,0 +1,12 @@ +import { makeApiHttpClient } from '@/core/main/factories/http' + +import { RemoteCreateInputUseProductUseCase } from '../../../../data/use-cases/input-use-products-use-cases' + +import type { CreateInputUseProductUseCase } from '../../../../domain/use-cases/input-use-products-use-cases' + +export function makeRemoteCreateInputUseProductUseCase(): CreateInputUseProductUseCase { + return new RemoteCreateInputUseProductUseCase( + '/input-uses/products', + makeApiHttpClient() + ) +} diff --git a/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-delete-input-use-product-use-factory.ts b/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-delete-input-use-product-use-factory.ts new file mode 100644 index 00000000..9b177861 --- /dev/null +++ b/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-delete-input-use-product-use-factory.ts @@ -0,0 +1,12 @@ +import { makeApiHttpClient } from '@/core/main/factories/http' + +import { RemoteDeleteInputUseProductUseCase } from '../../../../data/use-cases/input-use-products-use-cases' + +import type { DeleteInputUseProductUseCase } from '../../../../domain/use-cases/input-use-products-use-cases' + +export function makeRemoteDeleteInputUseProductUseCase(): DeleteInputUseProductUseCase { + return new RemoteDeleteInputUseProductUseCase( + '/input-uses/products', + makeApiHttpClient() + ) +} diff --git a/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-get-input-use-product-use-factory.ts b/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-get-input-use-product-use-factory.ts new file mode 100644 index 00000000..5ad15908 --- /dev/null +++ b/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-get-input-use-product-use-factory.ts @@ -0,0 +1,12 @@ +import { makeApiHttpClient } from '@/core/main/factories/http' + +import { RemoteGetInputUseProductUseCase } from '../../../../data/use-cases/input-use-products-use-cases' + +import type { GetInputUseProductUseCase } from '../../../../domain/use-cases/input-use-products-use-cases' + +export function makeRemoteGetInputUseProductUseCase(): GetInputUseProductUseCase { + return new RemoteGetInputUseProductUseCase( + '/input-uses/products', + makeApiHttpClient() + ) +} diff --git a/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-get-input-use-products-use-case-factory.ts b/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-get-input-use-products-use-case-factory.ts new file mode 100644 index 00000000..dcb524ee --- /dev/null +++ b/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-get-input-use-products-use-case-factory.ts @@ -0,0 +1,12 @@ +import { makeApiHttpClient } from '@/core/main/factories/http' + +import { RemoteGetInputUseProductsUseCase } from '../../../../data/use-cases/input-use-products-use-cases' + +import type { GetInputUseProductsUseCase } from '../../../../domain/use-cases/input-use-products-use-cases' + +export function makeRemoteGetInputUseProductsUseCase(): GetInputUseProductsUseCase { + return new RemoteGetInputUseProductsUseCase( + '/input-uses/products', + makeApiHttpClient() + ) +} diff --git a/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-update-input-use-product-use-factory.ts b/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-update-input-use-product-use-factory.ts new file mode 100644 index 00000000..5360d059 --- /dev/null +++ b/src/app/modules/input-uses/main/factories/use-cases/input-use-products-use-cases/remote-update-input-use-product-use-factory.ts @@ -0,0 +1,12 @@ +import { makeApiHttpClient } from '@/core/main/factories/http' + +import { RemoteUpdateInputUseProductUseCase } from '../../../../data/use-cases/input-use-products-use-cases' + +import type { UpdateInputUseProductUseCase } from '../../../../domain/use-cases/input-use-products-use-cases' + +export function makeRemoteUpdateInputUseProductUseCase(): UpdateInputUseProductUseCase { + return new RemoteUpdateInputUseProductUseCase( + '/input-uses/products', + makeApiHttpClient() + ) +} From 37a4c386323d804e508f57d012a45338cb2c20ad Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 14 Apr 2026 20:03:30 -0300 Subject: [PATCH 4/9] feat: add mocks layer --- .../create-input-use-product-handler.ts | 20 +++++ .../delete-input-use-product-handler.ts | 20 +++++ .../get-input-use-product-handler.ts | 45 ++++++++++ .../get-input-use-products-handler.ts | 82 +++++++++++++++++++ .../input-use-products-handlers/index.ts | 5 ++ .../update-input-use-product-handler.ts | 20 +++++ 6 files changed, 192 insertions(+) create mode 100644 src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/create-input-use-product-handler.ts create mode 100644 src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/delete-input-use-product-handler.ts create mode 100644 src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-product-handler.ts create mode 100644 src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-products-handler.ts create mode 100644 src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/index.ts create mode 100644 src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/update-input-use-product-handler.ts diff --git a/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/create-input-use-product-handler.ts b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/create-input-use-product-handler.ts new file mode 100644 index 00000000..cbb621e9 --- /dev/null +++ b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/create-input-use-product-handler.ts @@ -0,0 +1,20 @@ +import { HttpResponse, type PathParams } from 'msw' + +import { HttpStatusCode } from '@/core/data/protocols/http' +import { httpWithMiddleware } from '@/core/mocks/lib' +import { withAuth, withDelay } from '@/core/mocks/middleware' + +import type { CreateInputUseProductUseCase } from '../../../domain/use-cases/input-use-products-use-cases' + +export const createInputUseProductHandler = httpWithMiddleware< + PathParams, + Parameters[0], + undefined +>({ + routePath: '/api/input-uses/products', + method: 'post', + middlewares: [withDelay(), withAuth], + resolver: async () => { + return HttpResponse.json({}, { status: HttpStatusCode.created }) + }, +}) diff --git a/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/delete-input-use-product-handler.ts b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/delete-input-use-product-handler.ts new file mode 100644 index 00000000..15cc6b4d --- /dev/null +++ b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/delete-input-use-product-handler.ts @@ -0,0 +1,20 @@ +import { HttpResponse } from 'msw' + +import { HttpStatusCode } from '@/core/data/protocols/http' +import { httpWithMiddleware } from '@/core/mocks/lib' +import { withAuth, withDelay } from '@/core/mocks/middleware' + +import type { DeleteInputUseProductUseCase } from '../../../domain/use-cases/input-use-products-use-cases' + +export const deleteInputUseProductHandler = httpWithMiddleware< + { id: string }, + Parameters[0], + undefined +>({ + routePath: '/api/input-uses/products/:id', + method: 'delete', + middlewares: [withDelay(), withAuth], + resolver: async () => { + return HttpResponse.json(undefined, { status: HttpStatusCode.noContent }) + }, +}) diff --git a/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-product-handler.ts b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-product-handler.ts new file mode 100644 index 00000000..00a8b258 --- /dev/null +++ b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-product-handler.ts @@ -0,0 +1,45 @@ +import { HttpResponse } from 'msw' + +import { HttpStatusCode } from '@/core/data/protocols/http' +import { httpWithMiddleware } from '@/core/mocks/lib' +import { withAuth, withDelay } from '@/core/mocks/middleware' + +import inputUseProductCategoriesData from '@database/inputUseProductCategoriesData.json' +import inputUseProductsData from '@database/inputUseProductsData.json' + +import type { InputUseProductDetailsApiResponse } from '@/app/modules/input-uses/domain/models/input-use-products-model' + +export const getInputUseProductHandler = httpWithMiddleware< + { id: string }, + undefined, + InputUseProductDetailsApiResponse +>({ + routePath: '/api/input-uses/products/:id', + method: 'get', + middlewares: [withDelay(), withAuth], + resolver: async ({ params }) => { + const { id } = params + + const inputUseProduct = inputUseProductsData.find( + (item) => item.id === Number(id) + ) + + if (!inputUseProduct) { + return HttpResponse.json(null, { status: HttpStatusCode.notFound }) + } + + const category = inputUseProductCategoriesData.find( + (cat) => cat.id === inputUseProduct.productCategoryId + ) + + return HttpResponse.json( + { + name: inputUseProduct.name, + category: category + ? { value: category.id, label: category.name } + : { value: 0, label: '' }, + }, + { status: HttpStatusCode.ok } + ) + }, +}) diff --git a/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-products-handler.ts b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-products-handler.ts new file mode 100644 index 00000000..ee840fc5 --- /dev/null +++ b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-products-handler.ts @@ -0,0 +1,82 @@ +import { HttpResponse, type PathParams } from 'msw' + +import { HttpStatusCode } from '@/core/data/protocols/http' +import { httpWithMiddleware } from '@/core/mocks/lib' +import { withAuth, withDelay } from '@/core/mocks/middleware' +import { filterData, paginateData, sortData } from '@/core/mocks/utils' + +import inputUseProductCategoriesData from '@database/inputUseProductCategoriesData.json' +import inputUseProductsData from '@database/inputUseProductsData.json' + +import type { InputUseProductApiResponse } from '@/app/modules/input-uses/domain/models/input-use-products-model' +import type { MockParams } from '@/core/mocks/types/mock-params-type' +import type { MockResponse } from '@/core/mocks/types/mock-response-type' + +export const getInputUseProductsHandler = httpWithMiddleware< + PathParams, + MockParams, + MockResponse +>({ + routePath: '/api/input-uses/products/search', + method: 'post', + middlewares: [withDelay(), withAuth], + resolver: async ({ request }) => { + const { filters, page, rows, sort } = await request.json() + + if (!inputUseProductsData.length) { + return HttpResponse.json( + { + content: [], + numberOfElements: 0, + pageable: { + pageSize: 0, + }, + }, + { + status: 404, + } + ) + } + + let inputUseProducts = inputUseProductsData.map((product) => { + const category = inputUseProductCategoriesData.find( + (cat) => cat.id === product.productCategoryId + ) + return { + id: product.id, + name: product.name, + category: category?.name ?? 'Não informada', + } + }) + + if (filters) { + inputUseProducts = filterData( + filters, + inputUseProducts + ) + } + if (sort) { + inputUseProducts = sortData( + sort, + inputUseProducts + ) + } + + const numberOfElements = inputUseProducts.length + inputUseProducts = paginateData( + { page, perPage: rows }, + inputUseProducts + ) + + return HttpResponse.json( + { + content: inputUseProducts, + numberOfElements, + pageable: { + pageSize: rows, + }, + }, + { status: HttpStatusCode.ok } + ) + }, +}) diff --git a/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/index.ts b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/index.ts new file mode 100644 index 00000000..47863975 --- /dev/null +++ b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/index.ts @@ -0,0 +1,5 @@ +export * from './create-input-use-product-handler' +export * from './delete-input-use-product-handler' +export * from './get-input-use-product-handler' +export * from './get-input-use-products-handler' +export * from './update-input-use-product-handler' diff --git a/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/update-input-use-product-handler.ts b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/update-input-use-product-handler.ts new file mode 100644 index 00000000..9b83658e --- /dev/null +++ b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/update-input-use-product-handler.ts @@ -0,0 +1,20 @@ +import { HttpResponse } from 'msw' + +import { HttpStatusCode } from '@/core/data/protocols/http' +import { httpWithMiddleware } from '@/core/mocks/lib' +import { withAuth, withDelay } from '@/core/mocks/middleware' + +import type { UpdateInputUseProductUseCase } from '../../../domain/use-cases/input-use-products-use-cases' + +export const updateInputUseProductHandler = httpWithMiddleware< + { id: string }, + Parameters[0], + undefined +>({ + routePath: '/api/input-uses/products/:id', + method: 'patch', + middlewares: [withDelay(), withAuth], + resolver: async () => { + return HttpResponse.json(null, { status: HttpStatusCode.noContent }) + }, +}) From d1f98cc5834eb6c17edf330c4939638426cd0a50 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 14 Apr 2026 20:26:52 -0300 Subject: [PATCH 5/9] feat: add presentation layer --- .../input-use-product-data-table/index.ts | 1 + .../input-use-product-data-table.hook.tsx | 92 ++++++++++++ .../input-use-product-data-table.tsx | 27 ++++ .../input-use-product-delete-dialog/index.ts | 1 + .../input-use-product-delete-dialog.tsx | 78 ++++++++++ .../contexts/input-use-product-context.tsx | 133 +++++++++++++++++ .../create-input-use-product-form.tsx | 108 ++++++++++++++ .../edit-input-use-product-form.tsx | 137 ++++++++++++++++++ .../forms/input-use-product-form/index.ts | 1 + .../input-use-product-form-inputs.tsx | 85 +++++++++++ .../input-use-product-form.tsx | 18 +++ .../input-use-product-initial-form-data.ts | 9 ++ .../hooks/input-use-product-context.hook.ts | 15 ++ ...input-use-product-categories-query.hook.ts | 49 +++++++ .../queries/input-use-product-query.hook.ts | 35 +++++ .../queries/input-use-products-query.hook.ts | 50 +++++++ .../screens/input-use-products-screen.tsx | 67 +++++++++ .../types/input-use-product-types.ts | 6 + .../input-use-product-form-schema.ts | 20 +++ 19 files changed, 932 insertions(+) create mode 100644 src/app/modules/input-uses/presentation/components/input-use-product-data-table/index.ts create mode 100644 src/app/modules/input-uses/presentation/components/input-use-product-data-table/input-use-product-data-table.hook.tsx create mode 100644 src/app/modules/input-uses/presentation/components/input-use-product-data-table/input-use-product-data-table.tsx create mode 100644 src/app/modules/input-uses/presentation/components/input-use-product-delete-dialog/index.ts create mode 100644 src/app/modules/input-uses/presentation/components/input-use-product-delete-dialog/input-use-product-delete-dialog.tsx create mode 100644 src/app/modules/input-uses/presentation/contexts/input-use-product-context.tsx create mode 100644 src/app/modules/input-uses/presentation/forms/input-use-product-form/create-input-use-product-form.tsx create mode 100644 src/app/modules/input-uses/presentation/forms/input-use-product-form/edit-input-use-product-form.tsx create mode 100644 src/app/modules/input-uses/presentation/forms/input-use-product-form/index.ts create mode 100644 src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-form-inputs.tsx create mode 100644 src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-form.tsx create mode 100644 src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-initial-form-data.ts create mode 100644 src/app/modules/input-uses/presentation/hooks/input-use-product-context.hook.ts create mode 100644 src/app/modules/input-uses/presentation/hooks/queries/all-input-use-product-categories-query.hook.ts create mode 100644 src/app/modules/input-uses/presentation/hooks/queries/input-use-product-query.hook.ts create mode 100644 src/app/modules/input-uses/presentation/hooks/queries/input-use-products-query.hook.ts create mode 100644 src/app/modules/input-uses/presentation/screens/input-use-products-screen.tsx create mode 100644 src/app/modules/input-uses/presentation/types/input-use-product-types.ts create mode 100644 src/app/modules/input-uses/presentation/validations/input-use-product-form-schema.ts diff --git a/src/app/modules/input-uses/presentation/components/input-use-product-data-table/index.ts b/src/app/modules/input-uses/presentation/components/input-use-product-data-table/index.ts new file mode 100644 index 00000000..3d6c8dab --- /dev/null +++ b/src/app/modules/input-uses/presentation/components/input-use-product-data-table/index.ts @@ -0,0 +1 @@ +export * from './input-use-product-data-table' diff --git a/src/app/modules/input-uses/presentation/components/input-use-product-data-table/input-use-product-data-table.hook.tsx b/src/app/modules/input-uses/presentation/components/input-use-product-data-table/input-use-product-data-table.hook.tsx new file mode 100644 index 00000000..429c3a20 --- /dev/null +++ b/src/app/modules/input-uses/presentation/components/input-use-product-data-table/input-use-product-data-table.hook.tsx @@ -0,0 +1,92 @@ +import { useMemo, useState } from 'react' + +import { MoreHorizontalIcon, PencilIcon, Trash2Icon } from 'lucide-react' + +import { DropdownMenu } from '@/core/presentation/components/ui' +import { useDebounce } from '@/core/presentation/hooks' + +import { useInputUseProductContext } from '../../hooks/input-use-product-context.hook' +import { useInputUseProductsQuery } from '../../hooks/queries/input-use-products-query.hook' + +import type { InputUseProductModel } from '../../../domain/models/input-use-products-model' +import type { InputUseProductSort } from '../../types/input-use-product-types' +import type { ColumnDef } from '@tanstack/react-table' + +export function useInputUseProductDataTable() { + const { + filters, + openEditInputUseProductForm, + openDeleteInputUseProductContainer, + } = useInputUseProductContext() + + const [page, setPage] = useState(1) + const [sort, setSort] = useState() + + const debouncedFilters = useDebounce({ value: filters }) + + const { isLoading, inputUseProducts } = useInputUseProductsQuery({ + filters: debouncedFilters, + page, + sort, + }) + + const columns = useMemo[]>( + () => [ + { + accessorKey: 'name', + header: 'Produto', + }, + { + accessorKey: 'productCategory', + header: 'Categoria', + }, + { + id: 'row-actions', + header: '', + cell: ({ row }) => { + const { original: inputUseProduct } = row + + return ( + + + + + + { + event.stopPropagation() + openEditInputUseProductForm(inputUseProduct) + }} + > + Editar + + + { + event.stopPropagation() + openDeleteInputUseProductContainer(inputUseProduct) + }} + > + Excluir + + + + ) + }, + }, + ], + [openDeleteInputUseProductContainer, openEditInputUseProductForm] + ) + + return { + columns, + inputUseProducts, + isLoading, + page, + sort, + setSort, + setPage, + } +} diff --git a/src/app/modules/input-uses/presentation/components/input-use-product-data-table/input-use-product-data-table.tsx b/src/app/modules/input-uses/presentation/components/input-use-product-data-table/input-use-product-data-table.tsx new file mode 100644 index 00000000..bb5fd335 --- /dev/null +++ b/src/app/modules/input-uses/presentation/components/input-use-product-data-table/input-use-product-data-table.tsx @@ -0,0 +1,27 @@ +import { DataTable } from '@/core/presentation/components/ui' + +import { useInputUseProductDataTable } from './input-use-product-data-table.hook' + +export function InputUseProductDataTable() { + const { columns, inputUseProducts, isLoading, page, sort, setPage, setSort } = + useInputUseProductDataTable() + + return ( + + ) +} + +InputUseProductDataTable.displayName = 'InputUseProductDataTable' diff --git a/src/app/modules/input-uses/presentation/components/input-use-product-delete-dialog/index.ts b/src/app/modules/input-uses/presentation/components/input-use-product-delete-dialog/index.ts new file mode 100644 index 00000000..b9992f90 --- /dev/null +++ b/src/app/modules/input-uses/presentation/components/input-use-product-delete-dialog/index.ts @@ -0,0 +1 @@ +export * from './input-use-product-delete-dialog' diff --git a/src/app/modules/input-uses/presentation/components/input-use-product-delete-dialog/input-use-product-delete-dialog.tsx b/src/app/modules/input-uses/presentation/components/input-use-product-delete-dialog/input-use-product-delete-dialog.tsx new file mode 100644 index 00000000..8bc223e7 --- /dev/null +++ b/src/app/modules/input-uses/presentation/components/input-use-product-delete-dialog/input-use-product-delete-dialog.tsx @@ -0,0 +1,78 @@ +import { useCallback } from 'react' + +import { useMutation, useQueryClient } from '@tanstack/react-query' +import toast from 'react-hot-toast' + +import { AlertDialog } from '@/core/presentation/components/ui' + +import { makeRemoteDeleteInputUseProductUseCase } from '../../../main/factories/use-cases/input-use-products-use-cases' +import { useInputUseProductContext } from '../../hooks/input-use-product-context.hook' + +export function InputUseProductDeleteDialog() { + const { + isOpenDeleteInputUseProductContainer, + closeDeleteInputUseProductContainer, + selectedInputUseProduct, + } = useInputUseProductContext() + + const deleteInputUseProductUseCase = makeRemoteDeleteInputUseProductUseCase() + + const queryClient = useQueryClient() + + const { mutateAsync: mutateHandleDeleteInputUseProduct } = useMutation({ + mutationFn: deleteInputUseProductUseCase.execute, + }) + + const handleDeleteInputUseProduct = useCallback(async () => { + if (!selectedInputUseProduct?.id) { + toast.error('Erro ao remover produto') + return + } + + try { + await mutateHandleDeleteInputUseProduct({ + id: selectedInputUseProduct.id, + }) + + queryClient.invalidateQueries({ + queryKey: ['input-use-products'], + exact: false, + }) + + toast.success('Produto removido com sucesso') + } catch { + toast.error('Erro ao remover produto') + } finally { + closeDeleteInputUseProductContainer() + } + }, [ + closeDeleteInputUseProductContainer, + mutateHandleDeleteInputUseProduct, + queryClient, + selectedInputUseProduct, + ]) + + return ( + + + + {`Deseja remover o produto ${selectedInputUseProduct?.name}?`} + + Não será possível desfazer essa ação! + + + + Cancelar + + Remover + + + + + ) +} + +InputUseProductDeleteDialog.displayName = 'InputUseProductDeleteDialog' diff --git a/src/app/modules/input-uses/presentation/contexts/input-use-product-context.tsx b/src/app/modules/input-uses/presentation/contexts/input-use-product-context.tsx new file mode 100644 index 00000000..602845f2 --- /dev/null +++ b/src/app/modules/input-uses/presentation/contexts/input-use-product-context.tsx @@ -0,0 +1,133 @@ +import { + createContext, + useCallback, + useMemo, + useState, + type PropsWithChildren, +} from 'react' + +import type { InputUseProductModel } from '../../domain/models/input-use-products-model' +import type { InputUseProductFilters } from '../types/input-use-product-types' + +type InputUseProductContextValue = { + filters: InputUseProductFilters + handleChangeFilters: (newFilters: InputUseProductFilters) => void + selectedInputUseProduct?: InputUseProductModel + isOpenNewInputUseProductForm: boolean + isOpenEditInputUseProductForm: boolean + isOpenDeleteInputUseProductContainer: boolean + openNewInputUseProductForm: () => void + closeNewInputUseProductForm: () => void + openEditInputUseProductForm: (inputUseProduct: InputUseProductModel) => void + closeEditInputUseProductForm: () => void + openDeleteInputUseProductContainer: ( + inputUseProduct: InputUseProductModel + ) => void + closeDeleteInputUseProductContainer: () => void +} + +export const InputUseProductContext = + createContext({} as InputUseProductContextValue) + +export function InputUseProductProvider({ + children, +}: Readonly) { + const [filters, setFilters] = useState({}) + + const handleChangeFilters = useCallback( + (newFilters: InputUseProductFilters) => { + setFilters((prevState: InputUseProductFilters) => ({ + ...prevState, + ...newFilters, + })) + }, + [] + ) + + const [isOpenNewInputUseProductForm, setIsOpenNewInputUseProductForm] = + useState(false) + + const [isOpenEditInputUseProductForm, setIsOpenEditInputUseProductForm] = + useState(false) + + const [ + isOpenDeleteInputUseProductContainer, + setIsOpenDeleteInputUseProductContainer, + ] = useState(false) + + const [selectedInputUseProduct, setSelectedInputUseProduct] = + useState() + + const openNewInputUseProductForm = useCallback(() => { + setIsOpenNewInputUseProductForm(true) + }, []) + + const closeNewInputUseProductForm = useCallback(() => { + setIsOpenNewInputUseProductForm(false) + }, []) + + const openEditInputUseProductForm = useCallback( + (inputUseProduct: InputUseProductModel) => { + setSelectedInputUseProduct(inputUseProduct) + setIsOpenEditInputUseProductForm(true) + }, + [] + ) + + const closeEditInputUseProductForm = useCallback(() => { + setSelectedInputUseProduct(undefined) + setIsOpenEditInputUseProductForm(false) + }, []) + + const openDeleteInputUseProductContainer = useCallback( + (inputUseProduct: InputUseProductModel) => { + setSelectedInputUseProduct(inputUseProduct) + setIsOpenDeleteInputUseProductContainer(true) + }, + [] + ) + + const closeDeleteInputUseProductContainer = useCallback(() => { + setSelectedInputUseProduct(undefined) + setIsOpenDeleteInputUseProductContainer(false) + }, []) + + const providerValues = useMemo( + () => ({ + filters, + handleChangeFilters, + selectedInputUseProduct, + isOpenNewInputUseProductForm, + isOpenEditInputUseProductForm, + isOpenDeleteInputUseProductContainer, + openNewInputUseProductForm, + closeNewInputUseProductForm, + openEditInputUseProductForm, + closeEditInputUseProductForm, + openDeleteInputUseProductContainer, + closeDeleteInputUseProductContainer, + }), + [ + filters, + handleChangeFilters, + selectedInputUseProduct, + isOpenNewInputUseProductForm, + isOpenEditInputUseProductForm, + isOpenDeleteInputUseProductContainer, + openNewInputUseProductForm, + closeNewInputUseProductForm, + openEditInputUseProductForm, + closeEditInputUseProductForm, + openDeleteInputUseProductContainer, + closeDeleteInputUseProductContainer, + ] + ) + + return ( + + {children} + + ) +} + +InputUseProductProvider.displayName = 'InputUseProductProvider' diff --git a/src/app/modules/input-uses/presentation/forms/input-use-product-form/create-input-use-product-form.tsx b/src/app/modules/input-uses/presentation/forms/input-use-product-form/create-input-use-product-form.tsx new file mode 100644 index 00000000..d148553f --- /dev/null +++ b/src/app/modules/input-uses/presentation/forms/input-use-product-form/create-input-use-product-form.tsx @@ -0,0 +1,108 @@ +import { useCallback } from 'react' + +import { zodResolver } from '@hookform/resolvers/zod' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import toast from 'react-hot-toast' + +import { + Button, + Form, + ScrollArea, + Sheet, +} from '@/core/presentation/components/ui' +import { useHookForm } from '@/core/presentation/hooks' + +import { makeRemoteCreateInputUseProductUseCase } from '../../../main/factories/use-cases/input-use-products-use-cases' +import { useInputUseProductContext } from '../../hooks/input-use-product-context.hook' +import { + inputUseProductFormSchema, + type InputUseProductFormSchema, +} from '../../validations/input-use-product-form-schema' + +import { InputUseProductFormInputs } from './input-use-product-form-inputs' +import { INPUT_USE_PRODUCT_INITIAL_FORM_DATA } from './input-use-product-initial-form-data' + +export function CreateInputUseProductForm() { + const { isOpenNewInputUseProductForm, closeNewInputUseProductForm } = + useInputUseProductContext() + + const createInputUseProductUseCase = makeRemoteCreateInputUseProductUseCase() + + const queryClient = useQueryClient() + + const form = useHookForm({ + defaultValues: INPUT_USE_PRODUCT_INITIAL_FORM_DATA, + resolver: zodResolver(inputUseProductFormSchema), + }) + + const { mutateAsync: mutateHandleCreateInputUseProduct } = useMutation({ + mutationFn: createInputUseProductUseCase.execute, + }) + + const handleCreateInputUseProduct = useCallback( + async (data: InputUseProductFormSchema) => { + try { + await mutateHandleCreateInputUseProduct({ + inputUseProduct: data, + }) + + queryClient.invalidateQueries({ + queryKey: ['input-use-products'], + exact: false, + }) + + toast.success('Produto criado com sucesso') + form.reset(INPUT_USE_PRODUCT_INITIAL_FORM_DATA) + closeNewInputUseProductForm() + } catch { + toast.error('Erro ao criar produto') + } + }, + [ + closeNewInputUseProductForm, + form, + mutateHandleCreateInputUseProduct, + queryClient, + ] + ) + + return ( + + + + Novo Produto + + Preencha o formulário para criar um novo produto + + + + +
+ + +
+
+ + + + +
+
+ ) +} + +CreateInputUseProductForm.displayName = 'CreateInputUseProductForm' diff --git a/src/app/modules/input-uses/presentation/forms/input-use-product-form/edit-input-use-product-form.tsx b/src/app/modules/input-uses/presentation/forms/input-use-product-form/edit-input-use-product-form.tsx new file mode 100644 index 00000000..4b816291 --- /dev/null +++ b/src/app/modules/input-uses/presentation/forms/input-use-product-form/edit-input-use-product-form.tsx @@ -0,0 +1,137 @@ +import { useCallback } from 'react' + +import { zodResolver } from '@hookform/resolvers/zod' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import toast from 'react-hot-toast' + +import { + Button, + Form, + Loading, + ScrollArea, + Sheet, +} from '@/core/presentation/components/ui' +import { useHookForm } from '@/core/presentation/hooks' + +import { makeRemoteUpdateInputUseProductUseCase } from '../../../main/factories/use-cases/input-use-products-use-cases' +import { useInputUseProductContext } from '../../hooks/input-use-product-context.hook' +import { useInputUseProductQuery } from '../../hooks/queries/input-use-product-query.hook' +import { + inputUseProductFormSchema, + type InputUseProductFormSchema, +} from '../../validations/input-use-product-form-schema' + +import { InputUseProductFormInputs } from './input-use-product-form-inputs' +import { INPUT_USE_PRODUCT_INITIAL_FORM_DATA } from './input-use-product-initial-form-data' + +export function EditInputUseProductForm() { + const { + isOpenEditInputUseProductForm, + closeEditInputUseProductForm, + selectedInputUseProduct, + } = useInputUseProductContext() + + const { isLoading, inputUseProduct } = useInputUseProductQuery({ + id: selectedInputUseProduct!.id, + }) + + const updateInputUseProductUseCase = makeRemoteUpdateInputUseProductUseCase() + + const queryClient = useQueryClient() + + const form = useHookForm({ + defaultValues: INPUT_USE_PRODUCT_INITIAL_FORM_DATA, + ...(inputUseProduct && { + values: { + ...inputUseProduct, + }, + }), + resolver: zodResolver(inputUseProductFormSchema), + }) + + const { mutateAsync: mutateHandleUpdateInputUseProduct } = useMutation({ + mutationFn: updateInputUseProductUseCase.execute, + }) + + const handleUpdateInputUseProduct = useCallback( + async (data: InputUseProductFormSchema) => { + try { + if (!selectedInputUseProduct) { + toast.error('Erro ao atualizar produto') + return + } + + await mutateHandleUpdateInputUseProduct({ + inputUseProduct: { + ...data, + id: selectedInputUseProduct.id, + }, + }) + + queryClient.invalidateQueries({ + queryKey: ['input-use-products'], + exact: false, + }) + + toast.success('Produto editado com sucesso') + form.reset(INPUT_USE_PRODUCT_INITIAL_FORM_DATA) + closeEditInputUseProductForm() + } catch { + toast.error('Erro ao salvar alterações') + } + }, + [ + closeEditInputUseProductForm, + form, + mutateHandleUpdateInputUseProduct, + queryClient, + selectedInputUseProduct, + ] + ) + + return ( + + + + {`Editar Produto ${selectedInputUseProduct?.name}`} + + Preencha o formulário para editar o produto + + + + +
+ {isLoading ? ( +
+ +
+ ) : ( + + )} + +
+
+ + + + +
+
+ ) +} + +EditInputUseProductForm.displayName = 'EditInputUseProductForm' diff --git a/src/app/modules/input-uses/presentation/forms/input-use-product-form/index.ts b/src/app/modules/input-uses/presentation/forms/input-use-product-form/index.ts new file mode 100644 index 00000000..c7c9f1ce --- /dev/null +++ b/src/app/modules/input-uses/presentation/forms/input-use-product-form/index.ts @@ -0,0 +1 @@ +export * from './input-use-product-form' diff --git a/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-form-inputs.tsx b/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-form-inputs.tsx new file mode 100644 index 00000000..e2d94195 --- /dev/null +++ b/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-form-inputs.tsx @@ -0,0 +1,85 @@ +import { useState } from 'react' + +import { useFormContext } from 'react-hook-form' + +import { Combobox, Form, Input } from '@/core/presentation/components/ui' +import { useDebounce } from '@/core/presentation/hooks' + +import { useAllInputUseProductCategoriesQuery } from '../../hooks/queries/all-input-use-product-categories-query.hook' + +import type { InputUseProductFormSchema } from '../../validations/input-use-product-form-schema' + +export function InputUseProductFormInputs() { + const form = useFormContext() + + const [searchCategory, setSearchCategory] = useState('') + + const debouncedCategory = useDebounce({ value: searchCategory }) + + const { allInputUseProductCategories, isLoading } = + useAllInputUseProductCategoriesQuery({ + filters: { + name: { + value: debouncedCategory, + type: 'LIKE', + }, + }, + }) + + return ( + <> + { + const { error } = fieldState + + return ( + + Nome do Produto* + + + + + + ) + }} + /> + + { + const { error } = fieldState + + return ( + + Categoria* + + + + + + ) + }} + /> + + ) +} + +InputUseProductFormInputs.displayName = 'InputUseProductFormInputs' diff --git a/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-form.tsx b/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-form.tsx new file mode 100644 index 00000000..49de2b77 --- /dev/null +++ b/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-form.tsx @@ -0,0 +1,18 @@ +import { CreateInputUseProductForm } from './create-input-use-product-form' +import { EditInputUseProductForm } from './edit-input-use-product-form' + +type InputUseProductFormProps = { + readonly id?: number +} + +export function InputUseProductForm({ + id, +}: Readonly) { + if (id) { + return + } + + return +} + +InputUseProductForm.displayName = 'InputUseProductForm' diff --git a/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-initial-form-data.ts b/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-initial-form-data.ts new file mode 100644 index 00000000..d8b740dc --- /dev/null +++ b/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-initial-form-data.ts @@ -0,0 +1,9 @@ +import type { InputUseProductFormSchema } from '../../validations/input-use-product-form-schema' + +export const INPUT_USE_PRODUCT_INITIAL_FORM_DATA: InputUseProductFormSchema = { + name: '', + category: { + label: '', + value: 0, + }, +} diff --git a/src/app/modules/input-uses/presentation/hooks/input-use-product-context.hook.ts b/src/app/modules/input-uses/presentation/hooks/input-use-product-context.hook.ts new file mode 100644 index 00000000..ccb5f6bf --- /dev/null +++ b/src/app/modules/input-uses/presentation/hooks/input-use-product-context.hook.ts @@ -0,0 +1,15 @@ +import { useContext } from 'react' + +import { InputUseProductContext } from '../contexts/input-use-product-context' + +export function useInputUseProductContext() { + const context = useContext(InputUseProductContext) + + if (!context) { + throw new Error( + 'useInputUseProductContext must be used within a InputUseProductProvider' + ) + } + + return context +} diff --git a/src/app/modules/input-uses/presentation/hooks/queries/all-input-use-product-categories-query.hook.ts b/src/app/modules/input-uses/presentation/hooks/queries/all-input-use-product-categories-query.hook.ts new file mode 100644 index 00000000..5d4cf4d1 --- /dev/null +++ b/src/app/modules/input-uses/presentation/hooks/queries/all-input-use-product-categories-query.hook.ts @@ -0,0 +1,49 @@ +import { useEffect } from 'react' + +import { useQuery } from '@tanstack/react-query' +import toast from 'react-hot-toast' + +import { toOption } from '@/core/utils/object/to-option' + +import { makeRemoteGetInputUseProductCategoriesUseCase } from '../../../main/factories/use-cases/input-use-product-categories-use-cases' + +import type { InputUseProductCategoryFilters } from '../../types/input-use-product-category-types' + +type Props = { + filters: InputUseProductCategoryFilters +} + +export function useAllInputUseProductCategoriesQuery({ filters }: Props) { + const getInputUseProductCategoriesUseCase = + makeRemoteGetInputUseProductCategoriesUseCase() + + const { + data, + isError, + error, + isLoading, + refetch: refetchAllInputUseProductCategories, + } = useQuery({ + queryKey: ['all-input-use-product-categories', { filters }], + queryFn: () => + getInputUseProductCategoriesUseCase.execute({ + filters, + pagination: { + page: 1, + perPage: 30, + }, + }), + }) + + useEffect(() => { + if (isError) + toast.error(error?.message ?? 'Erro ao buscar categorias de produtos') + }, [error, isError]) + + return { + allInputUseProductCategories: + data?.resources.map((category) => toOption(category, 'name')) ?? [], + isLoading, + refetchAllInputUseProductCategories, + } +} diff --git a/src/app/modules/input-uses/presentation/hooks/queries/input-use-product-query.hook.ts b/src/app/modules/input-uses/presentation/hooks/queries/input-use-product-query.hook.ts new file mode 100644 index 00000000..bdea2f77 --- /dev/null +++ b/src/app/modules/input-uses/presentation/hooks/queries/input-use-product-query.hook.ts @@ -0,0 +1,35 @@ +import { useEffect } from 'react' + +import { useQuery } from '@tanstack/react-query' +import toast from 'react-hot-toast' + +import { makeRemoteGetInputUseProductUseCase } from '../../../main/factories/use-cases/input-use-products-use-cases' + +type Props = { + id: number +} + +export function useInputUseProductQuery({ id }: Props) { + const getInputUseProductUseCase = makeRemoteGetInputUseProductUseCase() + + const { + data: inputUseProduct, + isError, + error, + isLoading, + } = useQuery({ + queryKey: ['input-use-product', id], + queryFn: () => getInputUseProductUseCase.execute({ id }), + enabled: !!id, + }) + + useEffect(() => { + if (isError) + toast.error(error?.message ?? 'Erro ao buscar detalhes do produto') + }, [error, isError]) + + return { + inputUseProduct, + isLoading, + } +} diff --git a/src/app/modules/input-uses/presentation/hooks/queries/input-use-products-query.hook.ts b/src/app/modules/input-uses/presentation/hooks/queries/input-use-products-query.hook.ts new file mode 100644 index 00000000..68e5dc37 --- /dev/null +++ b/src/app/modules/input-uses/presentation/hooks/queries/input-use-products-query.hook.ts @@ -0,0 +1,50 @@ +import { useEffect } from 'react' + +import { useQuery } from '@tanstack/react-query' +import toast from 'react-hot-toast' + +import { makeRemoteGetInputUseProductsUseCase } from '../../../main/factories/use-cases/input-use-products-use-cases' + +import type { + InputUseProductFilters, + InputUseProductSort, +} from '../../types/input-use-product-types' + +type Props = { + filters?: InputUseProductFilters + page: number + sort?: InputUseProductSort +} + +export function useInputUseProductsQuery({ filters, page, sort }: Props) { + const getInputUseProductsUseCase = makeRemoteGetInputUseProductsUseCase() + + const { + data, + isError, + error, + isLoading, + refetch: refetchInputUseProducts, + } = useQuery({ + queryKey: ['input-use-products', { page, sort, filters }], + queryFn: () => + getInputUseProductsUseCase.execute({ + pagination: { page }, + sort, + filters, + }), + }) + + useEffect(() => { + if (isError) toast.error(error?.message ?? 'Erro ao buscar produtos') + }, [error, isError]) + + return { + inputUseProducts: data ?? { + resources: [], + totalPages: 1, + }, + isLoading, + refetchInputUseProducts, + } +} diff --git a/src/app/modules/input-uses/presentation/screens/input-use-products-screen.tsx b/src/app/modules/input-uses/presentation/screens/input-use-products-screen.tsx new file mode 100644 index 00000000..992411e9 --- /dev/null +++ b/src/app/modules/input-uses/presentation/screens/input-use-products-screen.tsx @@ -0,0 +1,67 @@ +import { Button, Input } from '@/core/presentation/components/ui' + +import { InputUseProductDataTable } from '../components/input-use-product-data-table' +import { InputUseProductDeleteDialog } from '../components/input-use-product-delete-dialog' +import { + InputUseProductContext, + InputUseProductProvider, +} from '../contexts/input-use-product-context' +import { InputUseProductForm } from '../forms/input-use-product-form' + +export function InputUseProductsScreen() { + return ( + + + {({ + filters, + handleChangeFilters, + selectedInputUseProduct, + isOpenDeleteInputUseProductContainer, + isOpenNewInputUseProductForm, + isOpenEditInputUseProductForm, + openNewInputUseProductForm, + }) => ( +
+
+ + + { + handleChangeFilters({ + name: { + value: target.value, + type: 'LIKE', + }, + }) + }} + placeholder="Procurar produto por nome" + /> +
+ + + {selectedInputUseProduct && + isOpenDeleteInputUseProductContainer && ( + + )} + + {(isOpenNewInputUseProductForm || + isOpenEditInputUseProductForm) && ( + + )} +
+ )} +
+
+ ) +} + +InputUseProductsScreen.displayName = 'InputUseProductsScreen' diff --git a/src/app/modules/input-uses/presentation/types/input-use-product-types.ts b/src/app/modules/input-uses/presentation/types/input-use-product-types.ts new file mode 100644 index 00000000..27149e51 --- /dev/null +++ b/src/app/modules/input-uses/presentation/types/input-use-product-types.ts @@ -0,0 +1,6 @@ +import type { InputUseProductModel } from '../../domain/models/input-use-products-model' +import type { Filters, Sort } from '@/core/domain/types' + +export type InputUseProductFilters = Filters + +export type InputUseProductSort = Sort diff --git a/src/app/modules/input-uses/presentation/validations/input-use-product-form-schema.ts b/src/app/modules/input-uses/presentation/validations/input-use-product-form-schema.ts new file mode 100644 index 00000000..61b1450c --- /dev/null +++ b/src/app/modules/input-uses/presentation/validations/input-use-product-form-schema.ts @@ -0,0 +1,20 @@ +import { z } from 'zod' + +import { optionSchema } from '@/core/validation/schemas' + +export const inputUseProductFormSchema = z.object({ + name: z + .string() + .min(3, 'O nome deve ter no mínimo 3 caracteres') + .max(255, 'O nome deve ter no máximo 255 caracteres'), + category: optionSchema.refine( + ({ label, value }) => label !== '' && value !== 0, + { + message: 'Categoria é obrigatória', + } + ), +}) + +export type InputUseProductFormSchema = z.infer< + typeof inputUseProductFormSchema +> From a89aff86e1fb453bf2e56e116fe42c773c7da964 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 14 Apr 2026 20:28:13 -0300 Subject: [PATCH 6/9] feat: register input use products module --- src/app/pages/general-registrations-page.tsx | 6 ++++++ src/core/mocks/browser.ts | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/app/pages/general-registrations-page.tsx b/src/app/pages/general-registrations-page.tsx index 40378aa8..3e7ad3f7 100644 --- a/src/app/pages/general-registrations-page.tsx +++ b/src/app/pages/general-registrations-page.tsx @@ -7,6 +7,7 @@ import { GeneralCultivationPestsScreen } from '../modules/general-cultivations/p import { GeneralCultivationsScreen } from '../modules/general-cultivations/presentation/screens/general-cultivations-screen' import { InputUseLocationsScreen } from '../modules/input-uses/presentation/screens/input-use-locations-screen' import { InputUseProductCategoriesScreen } from '../modules/input-uses/presentation/screens/input-use-product-categories-screen' +import { InputUseProductsScreen } from '../modules/input-uses/presentation/screens/input-use-products-screen' type Tab = | { @@ -64,6 +65,11 @@ export function GeneralRegistrationsPage() { name: 'Categorias de Produtos', component: , }, + { + key: 'input-use-products', + name: 'Produtos', + component: , + }, ], }, ], diff --git a/src/core/mocks/browser.ts b/src/core/mocks/browser.ts index a63c0c27..ec1d66e9 100644 --- a/src/core/mocks/browser.ts +++ b/src/core/mocks/browser.ts @@ -144,6 +144,13 @@ import { getInputUseProductCategoriesHandler, updateInputUseProductCategoryHandler, } from '@/app/modules/input-uses/mocks/handlers/input-use-product-categories-handlers' +import { + createInputUseProductHandler, + deleteInputUseProductHandler, + getInputUseProductHandler, + getInputUseProductsHandler, + updateInputUseProductHandler, +} from '@/app/modules/input-uses/mocks/handlers/input-use-products-handlers' import { createMachineHandler, deleteMachineHandler, @@ -318,6 +325,12 @@ const handlers: HttpHandler[] = [ getInputUseProductCategoriesHandler, updateInputUseProductCategoryHandler, + createInputUseProductHandler, + deleteInputUseProductHandler, + getInputUseProductHandler, + getInputUseProductsHandler, + updateInputUseProductHandler, + createNutritionalBalancingHandler, deleteNutritionalBalancingHandler, getLastVisitNutritionalBalancingsHandler, From dda279601070dda64db6fb376bfa9d68ee7abfe7 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Thu, 16 Apr 2026 19:13:11 -0300 Subject: [PATCH 7/9] feat: remove older product code --- scripts/seed/data/index.mjs | 2 +- scripts/seed/data/input-use-products.mjs | 23 +++++++ scripts/seed/data/products.mjs | 23 ------- .../animal-medication-form-inputs.tsx | 19 +++--- .../domain/models/input-use-products-model.ts | 4 ++ .../get-input-use-product-handler.ts | 1 + .../get-input-use-products-handler.ts | 1 + .../all-input-use-products-query.hook.ts | 51 ++++++++++++++ .../use-cases/products-use-cases/index.ts | 1 - .../remote-get-all-products-use-case.ts | 68 ------------------- src/core/domain/models/products-model.ts | 16 ----- .../get-all-products-use-case.ts | 11 --- .../use-cases/products-use-cases/index.ts | 1 - .../use-cases/products-use-cases/index.ts | 1 - ...emote-get-all-products-use-case-factory.ts | 20 ------ src/core/mocks/browser.ts | 3 - .../get-all-products-handler.ts | 66 ------------------ .../mocks/handlers/products-handlers/index.ts | 1 - .../hooks/queries/all-products-query.hook.ts | 52 -------------- 19 files changed, 91 insertions(+), 273 deletions(-) create mode 100644 scripts/seed/data/input-use-products.mjs delete mode 100644 scripts/seed/data/products.mjs create mode 100644 src/app/modules/input-uses/presentation/hooks/queries/all-input-use-products-query.hook.ts delete mode 100644 src/core/data/use-cases/products-use-cases/index.ts delete mode 100644 src/core/data/use-cases/products-use-cases/remote-get-all-products-use-case.ts delete mode 100644 src/core/domain/models/products-model.ts delete mode 100644 src/core/domain/use-cases/products-use-cases/get-all-products-use-case.ts delete mode 100644 src/core/domain/use-cases/products-use-cases/index.ts delete mode 100644 src/core/main/factories/use-cases/products-use-cases/index.ts delete mode 100644 src/core/main/factories/use-cases/products-use-cases/remote-get-all-products-use-case-factory.ts delete mode 100644 src/core/mocks/handlers/products-handlers/get-all-products-handler.ts delete mode 100644 src/core/mocks/handlers/products-handlers/index.ts delete mode 100644 src/core/presentation/hooks/queries/all-products-query.hook.ts diff --git a/scripts/seed/data/index.mjs b/scripts/seed/data/index.mjs index 64e14103..b2db7148 100644 --- a/scripts/seed/data/index.mjs +++ b/scripts/seed/data/index.mjs @@ -17,7 +17,7 @@ export * from './animal-purchases.mjs' export * from './animal-sales.mjs' export * from './active-ingredients.mjs' export * from './input-use-product-categories.mjs' -export * from './products.mjs' +export * from './input-use-products.mjs' export * from './animal-medications.mjs' export * from './animal-mastitides.mjs' export * from './cultivation-diseases.mjs' diff --git a/scripts/seed/data/input-use-products.mjs b/scripts/seed/data/input-use-products.mjs new file mode 100644 index 00000000..5027fc38 --- /dev/null +++ b/scripts/seed/data/input-use-products.mjs @@ -0,0 +1,23 @@ +import { faker } from '@faker-js/faker/locale/pt_BR' + +import { inputUseProductCategoriesData } from './input-use-product-categories.mjs' + +export const inputUseProductsDependencies = ['inputUseProductCategories'] + +export const inputUseProductsData = [] + +inputUseProductCategoriesData.forEach((category) => { + const numberOfProducts = faker.number.int({ min: 5, max: 15 }) + + for (let i = 0; i < numberOfProducts; i += 1) { + inputUseProductsData.push({ + id: inputUseProductsData.length + 1, + name: `${faker.commerce.productName()} (${category.name})`, + productCategoryId: category.id, + activeIngredient: { + value: faker.number.int({ min: 1, max: 100 }), + label: faker.science.chemicalElement().name, + }, + }) + } +}) diff --git a/scripts/seed/data/products.mjs b/scripts/seed/data/products.mjs deleted file mode 100644 index cf5212d5..00000000 --- a/scripts/seed/data/products.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import { faker } from '@faker-js/faker/locale/pt_BR' - -export const allProductsData = Array.from( - { - length: faker.number.int({ - min: 50, - max: 150, - }), - }, - (_, index) => ({ - id: index + 1, - name: faker.commerce.productName(), - description: faker.commerce.productDescription(), - category: { - value: faker.number.int({ min: 1, max: 150 }), - label: faker.commerce.department(), - }, - activeIngredient: { - value: faker.number.int({ min: 1, max: 150 }), - label: faker.science.chemicalElement().name, - }, - }) -) diff --git a/src/app/modules/animals/presentation/forms/animal-medication-form/animal-medication-form-inputs.tsx b/src/app/modules/animals/presentation/forms/animal-medication-form/animal-medication-form-inputs.tsx index 5e7bfeff..df284823 100644 --- a/src/app/modules/animals/presentation/forms/animal-medication-form/animal-medication-form-inputs.tsx +++ b/src/app/modules/animals/presentation/forms/animal-medication-form/animal-medication-form-inputs.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { useFormContext } from 'react-hook-form' +import { useAllInputUseProductsQuery } from '@/app/modules/input-uses/presentation/hooks/queries/all-input-use-products-query.hook' import { floatMask } from '@/core/masker' import { DatePicker, @@ -12,7 +13,6 @@ import { } from '@/core/presentation/components/ui' import { Grouper } from '@/core/presentation/components/utils' import { useDebounce } from '@/core/presentation/hooks' -import { useAllProductsQuery } from '@/core/presentation/hooks/queries/all-products-query.hook' import { AnimalMedicationFormSchema } from '../../validations/animal-medication-form-schema' @@ -25,14 +25,15 @@ export function AnimalMedicationFormInputs() { const debouncedProduct = useDebounce({ value: searchProduct }) - const { allProducts, isLoading: isLoadingAllProducts } = useAllProductsQuery({ - filters: { - name: { - value: debouncedProduct, - type: 'LIKE', + const { allInputUseProducts, isLoading: isLoadingAllProducts } = + useAllInputUseProductsQuery({ + filters: { + name: { + value: debouncedProduct, + type: 'LIKE', + }, }, - }, - }) + }) return ( <> @@ -71,7 +72,7 @@ export function AnimalMedicationFormInputs() { search={searchProduct} - items={allProducts} + items={allInputUseProducts} loading={isLoadingAllProducts} selected={field.value} handleSearch={setSearchProduct} diff --git a/src/app/modules/input-uses/domain/models/input-use-products-model.ts b/src/app/modules/input-uses/domain/models/input-use-products-model.ts index 7b7ac204..68e67e3c 100644 --- a/src/app/modules/input-uses/domain/models/input-use-products-model.ts +++ b/src/app/modules/input-uses/domain/models/input-use-products-model.ts @@ -3,19 +3,23 @@ import type { Option, WithId } from '@/core/domain/types' export type InputUseProductDetailsModel = { name: string category: Option + activeIngredient: Option } export type InputUseProductDetailsApiResponse = { name: string category: Option + activeIngredient: Option } export type InputUseProductModel = WithId<{ name: string category: string + activeIngredient: Option }> export type InputUseProductApiResponse = WithId<{ name: string category: string + activeIngredient: Option }> diff --git a/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-product-handler.ts b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-product-handler.ts index 00a8b258..a830e325 100644 --- a/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-product-handler.ts +++ b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-product-handler.ts @@ -38,6 +38,7 @@ export const getInputUseProductHandler = httpWithMiddleware< category: category ? { value: category.id, label: category.name } : { value: 0, label: '' }, + activeIngredient: inputUseProduct.activeIngredient, }, { status: HttpStatusCode.ok } ) diff --git a/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-products-handler.ts b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-products-handler.ts index ee840fc5..7e4e0992 100644 --- a/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-products-handler.ts +++ b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-products-handler.ts @@ -46,6 +46,7 @@ export const getInputUseProductsHandler = httpWithMiddleware< id: product.id, name: product.name, category: category?.name ?? 'Não informada', + activeIngredient: product.activeIngredient, } }) diff --git a/src/app/modules/input-uses/presentation/hooks/queries/all-input-use-products-query.hook.ts b/src/app/modules/input-uses/presentation/hooks/queries/all-input-use-products-query.hook.ts new file mode 100644 index 00000000..55cc6514 --- /dev/null +++ b/src/app/modules/input-uses/presentation/hooks/queries/all-input-use-products-query.hook.ts @@ -0,0 +1,51 @@ +import { useEffect } from 'react' + +import { useQuery } from '@tanstack/react-query' +import toast from 'react-hot-toast' + +import { toOption } from '@/core/utils/object/to-option' + +import { makeRemoteGetInputUseProductsUseCase } from '../../../main/factories/use-cases/input-use-products-use-cases' + +import type { InputUseProductFilters } from '../../types/input-use-product-types' + +type Props = { + filters: InputUseProductFilters +} + +export function useAllInputUseProductsQuery({ filters }: Props) { + const getInputUseProductsUseCase = makeRemoteGetInputUseProductsUseCase() + + const { + data, + isError, + error, + isLoading, + refetch: refetchAllInputUseProducts, + } = useQuery({ + queryKey: ['all-input-use-products', { filters }], + queryFn: () => + getInputUseProductsUseCase.execute({ + filters, + pagination: { + page: 1, + perPage: 30, + }, + }), + }) + + useEffect(() => { + if (isError) toast.error(error?.message ?? 'Erro ao buscar produtos') + }, [error, isError]) + + return { + allInputUseProducts: + data?.resources.map((product) => + toOption(product, 'name', { + activeIngredient: product.activeIngredient, + }) + ) ?? [], + isLoading, + refetchAllInputUseProducts, + } +} diff --git a/src/core/data/use-cases/products-use-cases/index.ts b/src/core/data/use-cases/products-use-cases/index.ts deleted file mode 100644 index 9851184c..00000000 --- a/src/core/data/use-cases/products-use-cases/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './remote-get-all-products-use-case' diff --git a/src/core/data/use-cases/products-use-cases/remote-get-all-products-use-case.ts b/src/core/data/use-cases/products-use-cases/remote-get-all-products-use-case.ts deleted file mode 100644 index 2ef9d360..00000000 --- a/src/core/data/use-cases/products-use-cases/remote-get-all-products-use-case.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { type HttpClient, HttpStatusCode } from '@/core/data/protocols/http' -import { - BadRequestError, - ForbiddenError, - NotFoundError, - UnexpectedError, -} from '@/core/domain/errors' - -import type { - ProductApiResponse, - ProductModel, -} from '@/core/domain/models/products-model' -import type { ListApiResponse, MapApiProperties } from '@/core/domain/types' -import type { GetAllProductsUseCase } from '@/core/domain/use-cases/products-use-cases' - -export class RemoteGetAllProductsUseCase implements GetAllProductsUseCase { - constructor( - private readonly url: string, - private readonly httpClient: HttpClient< - ProductModel, - ProductApiResponse, - ListApiResponse - > - ) {} - - execute: GetAllProductsUseCase['execute'] = async ({ filters }) => { - const mapApiProperties: MapApiProperties = - { - id: 'id', - name: 'name', - description: 'description', - activeIngredient: 'activeIngredient', - category: 'category', - } - - const { statusCode, body } = await this.httpClient.request({ - url: `${this.url}/search`, - method: 'post', - filters, - mapApiProperties, - }) - - if (statusCode === HttpStatusCode.ok && !!body) { - return { - resources: body.content.map((item) => ({ - id: item.id, - name: item.name, - description: item.description, - activeIngredient: item.activeIngredient, - category: item.category, - })), - totalPages: Math.ceil(body.numberOfElements / body.pageable.pageSize), - } - } - - if (statusCode === HttpStatusCode.forbidden) { - throw new ForbiddenError() - } - - if (statusCode === HttpStatusCode.notFound) { - throw new NotFoundError('Produtos') - } - - if (statusCode === HttpStatusCode.badRequest) throw new BadRequestError() - - throw new UnexpectedError() - } -} diff --git a/src/core/domain/models/products-model.ts b/src/core/domain/models/products-model.ts deleted file mode 100644 index c49a2aea..00000000 --- a/src/core/domain/models/products-model.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Option, WithId } from '../types' - -// todo: quando for implementado a parte de registros globais de produtos(products), mover esse model e tudo referente a products para la -export type ProductModel = WithId<{ - name: string - description: string - category: Option - activeIngredient: Option -}> - -export type ProductApiResponse = WithId<{ - name: string - description: string - category: Option - activeIngredient: Option -}> diff --git a/src/core/domain/use-cases/products-use-cases/get-all-products-use-case.ts b/src/core/domain/use-cases/products-use-cases/get-all-products-use-case.ts deleted file mode 100644 index 912bc3c8..00000000 --- a/src/core/domain/use-cases/products-use-cases/get-all-products-use-case.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ProductModel } from '../../models/products-model' -import type { - RequestInterface, - ListParams, - ListResponse, -} from '@/core/domain/types' - -export type GetAllProductsUseCase = RequestInterface< - ListParams, - ListResponse -> diff --git a/src/core/domain/use-cases/products-use-cases/index.ts b/src/core/domain/use-cases/products-use-cases/index.ts deleted file mode 100644 index bf496d6d..00000000 --- a/src/core/domain/use-cases/products-use-cases/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './get-all-products-use-case' diff --git a/src/core/main/factories/use-cases/products-use-cases/index.ts b/src/core/main/factories/use-cases/products-use-cases/index.ts deleted file mode 100644 index 9d683a18..00000000 --- a/src/core/main/factories/use-cases/products-use-cases/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './remote-get-all-products-use-case-factory' diff --git a/src/core/main/factories/use-cases/products-use-cases/remote-get-all-products-use-case-factory.ts b/src/core/main/factories/use-cases/products-use-cases/remote-get-all-products-use-case-factory.ts deleted file mode 100644 index f12cdca3..00000000 --- a/src/core/main/factories/use-cases/products-use-cases/remote-get-all-products-use-case-factory.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { RemoteGetAllProductsUseCase } from '@/core/data/use-cases/products-use-cases' -import { makeApiHttpClient } from '@/core/main/factories/http' - -import type { - ProductApiResponse, - ProductModel, -} from '@/core/domain/models/products-model' -import type { ListApiResponse } from '@/core/domain/types' -import type { GetAllProductsUseCase } from '@/core/domain/use-cases/products-use-cases' - -export function makeRemoteGetAllProductsUseCase(): GetAllProductsUseCase { - return new RemoteGetAllProductsUseCase( - 'products', - makeApiHttpClient< - ProductModel, - ProductApiResponse, - ListApiResponse - >() - ) -} diff --git a/src/core/mocks/browser.ts b/src/core/mocks/browser.ts index ec1d66e9..c44aa1a2 100644 --- a/src/core/mocks/browser.ts +++ b/src/core/mocks/browser.ts @@ -176,7 +176,6 @@ import { import { getAllActiveIngredientsHandler } from './handlers/active-ingredients-handlers' import { getAllBreedsHandler } from './handlers/breeds-handlers' -import { getAllProductsHandler } from './handlers/products-handlers' import { getAllUsersHandler, getMeHandler } from './handlers/users-handlers' const handlers: HttpHandler[] = [ @@ -186,8 +185,6 @@ const handlers: HttpHandler[] = [ getAllActiveIngredientsHandler, - getAllProductsHandler, - getAllUsersHandler, getMeHandler, diff --git a/src/core/mocks/handlers/products-handlers/get-all-products-handler.ts b/src/core/mocks/handlers/products-handlers/get-all-products-handler.ts deleted file mode 100644 index fb55d9a5..00000000 --- a/src/core/mocks/handlers/products-handlers/get-all-products-handler.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { HttpResponse } from 'msw' - -import { HttpStatusCode } from '@/core/data/protocols/http' - -import allProductsData from '@database/allProductsData.json' - -import { httpWithMiddleware } from '../../lib' -import { withDelay, withAuth } from '../../middleware' -import { filterData } from '../../utils' - -import type { MockParams } from '../../types/mock-params-type' -import type { MockResponse } from '../../types/mock-response-type' -import type { ProductApiResponse } from '@/core/domain/models/products-model' - -export const getAllProductsHandler = httpWithMiddleware< - never, - MockParams, - MockResponse ->({ - routePath: '/api/products/search', - method: 'post', - middlewares: [withDelay(), withAuth], - resolver: async ({ request }) => { - const { filters, rows } = await request.json() - - if (!allProductsData.length) { - return HttpResponse.json( - { - content: [], - numberOfElements: 0, - pageable: { - pageSize: 0, - }, - }, - { - status: 404, - } - ) - } - - if (filters) { - const products = filterData(filters, allProductsData) - return HttpResponse.json( - { - content: products, - numberOfElements: products.length, - pageable: { - pageSize: rows, - }, - }, - { status: HttpStatusCode.ok } - ) - } - - return HttpResponse.json( - { - content: allProductsData, - numberOfElements: allProductsData.length, - pageable: { - pageSize: rows, - }, - }, - { status: HttpStatusCode.ok } - ) - }, -}) diff --git a/src/core/mocks/handlers/products-handlers/index.ts b/src/core/mocks/handlers/products-handlers/index.ts deleted file mode 100644 index 92b82b08..00000000 --- a/src/core/mocks/handlers/products-handlers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './get-all-products-handler' diff --git a/src/core/presentation/hooks/queries/all-products-query.hook.ts b/src/core/presentation/hooks/queries/all-products-query.hook.ts deleted file mode 100644 index b7b7176e..00000000 --- a/src/core/presentation/hooks/queries/all-products-query.hook.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { useEffect } from 'react' - -import { useQuery } from '@tanstack/react-query' -import toast from 'react-hot-toast' - -import { makeRemoteGetAllProductsUseCase } from '@/core/main/factories/use-cases/products-use-cases' -import { toOption } from '@/core/utils/object/to-option' - -import type { ProductModel } from '@/core/domain/models/products-model' -import type { Filters } from '@/core/domain/types' - -type Props = { - filters: Filters -} - -export function useAllProductsQuery({ filters }: Props) { - const getAllProducts = makeRemoteGetAllProductsUseCase() - - const { - data, - isError, - error, - isLoading, - refetch: refetchAllProducts, - } = useQuery({ - queryKey: ['all-products', { filters }], - queryFn: () => - getAllProducts.execute({ - filters, - pagination: { - page: 1, - perPage: 30, - }, - }), - enabled: !!filters, - }) - - useEffect(() => { - if (isError) toast.error(error?.message ?? 'Erro ao buscar produtos') - }, [error, isError]) - - return { - allProducts: - data?.resources.map((resource) => - toOption(resource, 'name', { - activeIngredient: resource.activeIngredient, - }) - ) ?? [], - isLoading, - refetchAllProducts, - } -} From f072c3e8a515f64cf782652448628014a29c0fd3 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Thu, 16 Apr 2026 19:16:10 -0300 Subject: [PATCH 8/9] chore: improve skills md --- .../idr-web-module-generator/SKILL.md | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/.agents/skills/idr-web-module-generator/idr-web-module-generator/SKILL.md b/.agents/skills/idr-web-module-generator/idr-web-module-generator/SKILL.md index 96adf7ec..12f738d3 100644 --- a/.agents/skills/idr-web-module-generator/idr-web-module-generator/SKILL.md +++ b/.agents/skills/idr-web-module-generator/idr-web-module-generator/SKILL.md @@ -72,6 +72,7 @@ This layer requires a highly specific file organization: #### Queries Hooks - Create `hooks/queries/{entities}-query.hook.ts` using `@tanstack/react-query` to fetch lists. Handle `toast.error` for errors. +- Create `hooks/queries/all-{entities}-query.hook.ts` (following the `all-breeds` pattern) to fetch a list of items mapped to `Option` (using `toOption`). - Create `hooks/queries/{entity}-query.hook.ts` for fetching single items. - Always use the factories from the `main` layer. @@ -85,24 +86,32 @@ This layer requires a highly specific file organization: #### UI Components - **Data Table**: `components/{entity}-data-table/` - - Implement a hook (`.hook.tsx`) mapping columns and returning `react-table` configuration. + - Needs 3 files: + - `index.ts` (exporting `{entity}-data-table.tsx`) + - `{entity}-data-table.hook.tsx` mapping columns and returning `react-table` configuration. + - `{entity}-data-table.tsx` implementing the table using the generic `` component from `@/core/presentation/components/ui`. - Include an action column with a `DropdownMenu` for "Editar" and "Excluir". - - Implement the table (`.tsx`) using the generic `` component from `@/core/presentation/components/ui`. - **Delete Dialog**: `components/{entity}-delete-dialog/` - - Implement `.tsx` with `` and a `useMutation` calling the delete factory. + - Needs 2 files: + - `index.ts` (exporting `{entity}-delete-dialog.tsx`) + - `{entity}-delete-dialog.tsx` (Implement with ``, use `finally` block to close the dialog, and follow the Title pattern: `{`Deseja remover o [entity] ${item?.name}?`}`) - Invalidates react-query cache on success. - **Forms**: `forms/{entity}-form/` - - Needs 5 files: - - `create-{entity}-form.tsx` (using ``) - - `edit-{entity}-form.tsx` (fetches the item via Get One hook, displays `` while fetching) - - `{entity}-form-inputs.tsx` (UI inputs mapped to `react-hook-form` via `useFormContext`) + - Needs 6 files: + - `index.ts` (exporting `{entity}-form.tsx`) + - `{entity}-form.tsx` (Wrapper rendering `Create` or `Edit` based on `id` prop presence). + - `create-{entity}-form.tsx` (using ``, form className `flex flex-col gap-4`, button text "Criar") + - `edit-{entity}-form.tsx` (fetches the item via Get One hook, displays `` while fetching, form className `flex flex-col gap-4`, button text "Salvar") + - `{entity}-form-inputs.tsx` (UI inputs mapped to `react-hook-form` via `useFormContext`. Use `useAll{Entities}Query` for Comboboxes/Selects) - `{entity}-initial-form-data.ts` (Empty initial data object) - - `{entity}-form.tsx` (Wrapper rendering Create or Edit based on `id` prop presence). #### Screens - Create `screens/{entities}-screen.tsx` which glues everything together: - Wraps content in `{Entity}Provider` and `{Entity}Context.Consumer`. - - Header with a "Add" button and a search ``. + - Destructure values in this exact order: `filters, handleChangeFilters, selected{Entity}, isOpenDelete{Entity}Container, isOpenNew{Entity}Form, isOpenEdit{Entity}Form, openNew{Entity}Form`. + - Header (or `div` for sub-screens) with an "Add" button and a search ``. + - `Input` value should be `filters.field?.value` (without `?? ''`). + - `handleChangeFilters` inside `onChange` should be multi-line. - Renders `<{Entity}DataTable />`. - Conditionally renders `<{Entity}DeleteDialog />` and `<{Entity}Form />`. From bdf2ab3465d520ed2979149a7b35c875dad2c272 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Fri, 17 Apr 2026 20:08:16 -0300 Subject: [PATCH 9/9] fix: active ingredient on products --- scripts/seed/data/input-use-products.mjs | 9 ++-- .../remote-get-animal-medication-use-case.ts | 1 - .../domain/models/animal-medications-model.ts | 2 - .../get-animal-medication-handler.ts | 4 -- .../animal-medication-form-inputs.tsx | 52 +++++-------------- .../animal-medication-initial-form-data.ts | 4 -- .../edit-animal-medication-form.tsx | 8 +++ .../animal-medication-form-schema.ts | 19 +++---- .../remote-get-input-use-product-use-case.ts | 21 +++++--- .../remote-get-input-use-products-use-case.ts | 24 ++++----- .../domain/models/input-use-products-model.ts | 4 +- .../get-input-use-product-handler.ts | 24 ++++----- .../get-input-use-products-handler.ts | 8 +-- .../update-input-use-product-handler.ts | 16 +++--- .../input-use-product-data-table.hook.tsx | 6 ++- .../input-use-product-form-inputs.tsx | 48 ++++++++++++++++- .../input-use-product-initial-form-data.ts | 4 ++ .../input-use-product-form-schema.ts | 6 +++ src/core/validation/schemas/index.ts | 2 +- src/core/validation/schemas/option-schema.ts | 7 +++ 20 files changed, 150 insertions(+), 119 deletions(-) diff --git a/scripts/seed/data/input-use-products.mjs b/scripts/seed/data/input-use-products.mjs index 5027fc38..3f019559 100644 --- a/scripts/seed/data/input-use-products.mjs +++ b/scripts/seed/data/input-use-products.mjs @@ -12,12 +12,9 @@ inputUseProductCategoriesData.forEach((category) => { for (let i = 0; i < numberOfProducts; i += 1) { inputUseProductsData.push({ id: inputUseProductsData.length + 1, - name: `${faker.commerce.productName()} (${category.name})`, - productCategoryId: category.id, - activeIngredient: { - value: faker.number.int({ min: 1, max: 100 }), - label: faker.science.chemicalElement().name, - }, + name: faker.commerce.productName(), + category: category.name, + activeIngredient: faker.science.chemicalElement().name, }) } }) diff --git a/src/app/modules/animals/data/use-cases/animal-medications-use-cases/remote-get-animal-medication-use-case.ts b/src/app/modules/animals/data/use-cases/animal-medications-use-cases/remote-get-animal-medication-use-case.ts index aa3f12c2..c01bca11 100644 --- a/src/app/modules/animals/data/use-cases/animal-medications-use-cases/remote-get-animal-medication-use-case.ts +++ b/src/app/modules/animals/data/use-cases/animal-medications-use-cases/remote-get-animal-medication-use-case.ts @@ -42,7 +42,6 @@ export class RemoteGetAnimalMedicationUseCase date: new Date(body.date), product: body.product, appliedDose: body.appliedDose, - activeIngredient: body.activeIngredient, applicationMethod: body.applicationMethod as AnimalMedicationApplicationMethod, } diff --git a/src/app/modules/animals/domain/models/animal-medications-model.ts b/src/app/modules/animals/domain/models/animal-medications-model.ts index 51c4b150..10146d8e 100644 --- a/src/app/modules/animals/domain/models/animal-medications-model.ts +++ b/src/app/modules/animals/domain/models/animal-medications-model.ts @@ -10,7 +10,6 @@ export type AnimalMedicationApplicationMethod = export type AnimalMedicationDetailsModel = { date: Date product: Option - activeIngredient: Option appliedDose: string applicationMethod: AnimalMedicationApplicationMethod } @@ -18,7 +17,6 @@ export type AnimalMedicationDetailsModel = { export type AnimalMedicationDetailsApiResponse = { date: string product: Option - activeIngredient: Option appliedDose: string applicationMethod: string } diff --git a/src/app/modules/animals/mocks/handlers/animal-medications-handlers/get-animal-medication-handler.ts b/src/app/modules/animals/mocks/handlers/animal-medications-handlers/get-animal-medication-handler.ts index 46e2a02e..e6de1144 100644 --- a/src/app/modules/animals/mocks/handlers/animal-medications-handlers/get-animal-medication-handler.ts +++ b/src/app/modules/animals/mocks/handlers/animal-medications-handlers/get-animal-medication-handler.ts @@ -43,10 +43,6 @@ export const getAnimalMedicationHandler = httpWithMiddleware< }, applicationMethod: animalMedicationFound.applicationMethod, appliedDose: animalMedicationFound.appliedDose, - activeIngredient: { - label: animalMedicationFound.activeIngredient, - value: faker.number.int({ min: 1, max: 1000 }), - }, }, { status: HttpStatusCode.ok } ) diff --git a/src/app/modules/animals/presentation/forms/animal-medication-form/animal-medication-form-inputs.tsx b/src/app/modules/animals/presentation/forms/animal-medication-form/animal-medication-form-inputs.tsx index df284823..5f4b0e03 100644 --- a/src/app/modules/animals/presentation/forms/animal-medication-form/animal-medication-form-inputs.tsx +++ b/src/app/modules/animals/presentation/forms/animal-medication-form/animal-medication-form-inputs.tsx @@ -16,8 +16,6 @@ import { useDebounce } from '@/core/presentation/hooks' import { AnimalMedicationFormSchema } from '../../validations/animal-medication-form-schema' -import type { Option } from '@/core/domain/types' - export function AnimalMedicationFormInputs() { const form = useFormContext() @@ -35,6 +33,8 @@ export function AnimalMedicationFormInputs() { }, }) + const selectedProduct = form.watch('product') + return ( <> Produto* - + search={searchProduct} items={allInputUseProducts} loading={isLoadingAllProducts} selected={field.value} handleSearch={setSearchProduct} - handleSelect={(item) => { - field.onChange(item) - if (item.extraData?.activeIngredient) { - form.setValue( - 'activeIngredient', - item.extraData?.activeIngredient - ) - } - }} + handleSelect={field.onChange} isError={!!error} placeholder="Selecione um produto" emptyMessage="Nenhum produto encontrado" @@ -97,32 +89,16 @@ export function AnimalMedicationFormInputs() { }} /> - { - const { error } = fieldState - - return ( - - Princípio Ativo* - - null} - handleSelect={field.onChange} - isError={!!error} - placeholder="Princípio ativo do produto" - /> - - - - ) - }} - /> + + Princípio Ativo + + + + label !== '' && value > 0, - { - message: 'Produto é obrigatório', - } - ), - activeIngredient: optionSchema.refine( - ({ label, value }) => label !== '' && value > 0, - { - message: 'Princípio ativo é obrigatório', - } - ), + product: createOptionSchemaWithExtraData({ + activeIngredient: z.string(), + }).refine(({ label, value }) => label !== '' && value > 0, { + message: 'Produto é obrigatório', + }), appliedDose: z.string().min(1, { message: 'Campo obrigatório' }), applicationMethod: z.enum(['IM', 'IV', 'SC', 'IntraMammary', 'PourOn'], { message: 'Campo obrigatório', diff --git a/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-get-input-use-product-use-case.ts b/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-get-input-use-product-use-case.ts index 3074128a..b246ab0c 100644 --- a/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-get-input-use-product-use-case.ts +++ b/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-get-input-use-product-use-case.ts @@ -1,15 +1,16 @@ import { type HttpClient, HttpStatusCode } from '@/core/data/protocols/http' import { - UnexpectedError, - NotFoundError, + BadRequestError, ForbiddenError, + NotFoundError, + UnexpectedError, } from '@/core/domain/errors' import type { InputUseProductDetailsApiResponse, InputUseProductDetailsModel, -} from '../../../domain/models/input-use-products-model' -import type { GetInputUseProductUseCase } from '../../../domain/use-cases/input-use-products-use-cases' +} from '@/app/modules/input-uses/domain/models/input-use-products-model' +import type { GetInputUseProductUseCase } from '@/app/modules/input-uses/domain/use-cases/input-use-products-use-cases' export class RemoteGetInputUseProductUseCase implements GetInputUseProductUseCase @@ -32,16 +33,20 @@ export class RemoteGetInputUseProductUseCase return { name: body.name, category: body.category, + activeIngredient: body.activeIngredient, } } - if (statusCode === HttpStatusCode.notFound) - throw new NotFoundError('Produto') - if (statusCode === HttpStatusCode.forbidden) { - throw new ForbiddenError('Você não tem permissão para buscar um produto') + throw new ForbiddenError() } + if (statusCode === HttpStatusCode.notFound) { + throw new NotFoundError('Produto') + } + + if (statusCode === HttpStatusCode.badRequest) throw new BadRequestError() + throw new UnexpectedError() } } diff --git a/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-get-input-use-products-use-case.ts b/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-get-input-use-products-use-case.ts index 781ba6ae..e8e9ab31 100644 --- a/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-get-input-use-products-use-case.ts +++ b/src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-get-input-use-products-use-case.ts @@ -1,15 +1,15 @@ import { type HttpClient, HttpStatusCode } from '@/core/data/protocols/http' import { - UnexpectedError, - NotFoundError, ForbiddenError, + NotFoundError, + UnexpectedError, } from '@/core/domain/errors' import type { InputUseProductApiResponse, InputUseProductModel, -} from '../../../domain/models/input-use-products-model' -import type { GetInputUseProductsUseCase } from '../../../domain/use-cases/input-use-products-use-cases' +} from '@/app/modules/input-uses/domain/models/input-use-products-model' +import type { GetInputUseProductsUseCase } from '@/app/modules/input-uses/domain/use-cases/input-use-products-use-cases' import type { ListApiResponse, MapApiProperties } from '@/core/domain/types' export class RemoteGetInputUseProductsUseCase @@ -36,6 +36,7 @@ export class RemoteGetInputUseProductsUseCase id: 'id', name: 'name', category: 'category', + activeIngredient: 'activeIngredient', } const { statusCode, body } = await this.httpClient.request({ @@ -49,14 +50,13 @@ export class RemoteGetInputUseProductsUseCase if (statusCode === HttpStatusCode.ok && !!body) { return { - resources: body.content.map((item) => { - return { - id: item.id, - name: item.name, - category: item.category, - } - }), - totalPages: body.totalPages, + resources: body.content.map((item) => ({ + id: item.id, + name: item.name, + category: item.category, + activeIngredient: item.activeIngredient, + })), + totalPages: Math.ceil(body.numberOfElements / body.pageable.pageSize), } } diff --git a/src/app/modules/input-uses/domain/models/input-use-products-model.ts b/src/app/modules/input-uses/domain/models/input-use-products-model.ts index 68e67e3c..3760fe60 100644 --- a/src/app/modules/input-uses/domain/models/input-use-products-model.ts +++ b/src/app/modules/input-uses/domain/models/input-use-products-model.ts @@ -15,11 +15,11 @@ export type InputUseProductDetailsApiResponse = { export type InputUseProductModel = WithId<{ name: string category: string - activeIngredient: Option + activeIngredient: string }> export type InputUseProductApiResponse = WithId<{ name: string category: string - activeIngredient: Option + activeIngredient: string }> diff --git a/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-product-handler.ts b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-product-handler.ts index a830e325..a60e351f 100644 --- a/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-product-handler.ts +++ b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-product-handler.ts @@ -1,17 +1,17 @@ -import { HttpResponse } from 'msw' +import { faker } from '@faker-js/faker/locale/pt_BR' +import { HttpResponse, type PathParams } from 'msw' import { HttpStatusCode } from '@/core/data/protocols/http' import { httpWithMiddleware } from '@/core/mocks/lib' import { withAuth, withDelay } from '@/core/mocks/middleware' -import inputUseProductCategoriesData from '@database/inputUseProductCategoriesData.json' import inputUseProductsData from '@database/inputUseProductsData.json' import type { InputUseProductDetailsApiResponse } from '@/app/modules/input-uses/domain/models/input-use-products-model' export const getInputUseProductHandler = httpWithMiddleware< - { id: string }, - undefined, + PathParams<'id'>, + never, InputUseProductDetailsApiResponse >({ routePath: '/api/input-uses/products/:id', @@ -28,17 +28,17 @@ export const getInputUseProductHandler = httpWithMiddleware< return HttpResponse.json(null, { status: HttpStatusCode.notFound }) } - const category = inputUseProductCategoriesData.find( - (cat) => cat.id === inputUseProduct.productCategoryId - ) - return HttpResponse.json( { name: inputUseProduct.name, - category: category - ? { value: category.id, label: category.name } - : { value: 0, label: '' }, - activeIngredient: inputUseProduct.activeIngredient, + category: { + label: faker.commerce.department(), + value: faker.number.int({ min: 1, max: 100 }), + }, + activeIngredient: { + label: faker.science.chemicalElement().name, + value: faker.number.int({ min: 1, max: 100 }), + }, }, { status: HttpStatusCode.ok } ) diff --git a/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-products-handler.ts b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-products-handler.ts index 7e4e0992..e3b752e4 100644 --- a/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-products-handler.ts +++ b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-products-handler.ts @@ -5,10 +5,9 @@ import { httpWithMiddleware } from '@/core/mocks/lib' import { withAuth, withDelay } from '@/core/mocks/middleware' import { filterData, paginateData, sortData } from '@/core/mocks/utils' -import inputUseProductCategoriesData from '@database/inputUseProductCategoriesData.json' import inputUseProductsData from '@database/inputUseProductsData.json' -import type { InputUseProductApiResponse } from '@/app/modules/input-uses/domain/models/input-use-products-model' +import type { InputUseProductApiResponse } from '../../../domain/models/input-use-products-model' import type { MockParams } from '@/core/mocks/types/mock-params-type' import type { MockResponse } from '@/core/mocks/types/mock-response-type' @@ -39,13 +38,10 @@ export const getInputUseProductsHandler = httpWithMiddleware< } let inputUseProducts = inputUseProductsData.map((product) => { - const category = inputUseProductCategoriesData.find( - (cat) => cat.id === product.productCategoryId - ) return { id: product.id, name: product.name, - category: category?.name ?? 'Não informada', + category: product.category, activeIngredient: product.activeIngredient, } }) diff --git a/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/update-input-use-product-handler.ts b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/update-input-use-product-handler.ts index 9b83658e..6482239d 100644 --- a/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/update-input-use-product-handler.ts +++ b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/update-input-use-product-handler.ts @@ -4,17 +4,19 @@ import { HttpStatusCode } from '@/core/data/protocols/http' import { httpWithMiddleware } from '@/core/mocks/lib' import { withAuth, withDelay } from '@/core/mocks/middleware' -import type { UpdateInputUseProductUseCase } from '../../../domain/use-cases/input-use-products-use-cases' +import type { InputUseProductApiResponse } from '../../../domain/models/input-use-products-model' +import type { PathParam } from 'react-router-dom' export const updateInputUseProductHandler = httpWithMiddleware< - { id: string }, - Parameters[0], - undefined + PathParam<'id'>, + Omit, + never >({ routePath: '/api/input-uses/products/:id', method: 'patch', middlewares: [withDelay(), withAuth], - resolver: async () => { - return HttpResponse.json(null, { status: HttpStatusCode.noContent }) - }, + resolver: async () => + HttpResponse.json(undefined, { + status: HttpStatusCode.noContent, + }), }) diff --git a/src/app/modules/input-uses/presentation/components/input-use-product-data-table/input-use-product-data-table.hook.tsx b/src/app/modules/input-uses/presentation/components/input-use-product-data-table/input-use-product-data-table.hook.tsx index 429c3a20..3593c19b 100644 --- a/src/app/modules/input-uses/presentation/components/input-use-product-data-table/input-use-product-data-table.hook.tsx +++ b/src/app/modules/input-uses/presentation/components/input-use-product-data-table/input-use-product-data-table.hook.tsx @@ -37,9 +37,13 @@ export function useInputUseProductDataTable() { header: 'Produto', }, { - accessorKey: 'productCategory', + accessorKey: 'category', header: 'Categoria', }, + { + accessorKey: 'activeIngredient', + header: 'Princípio Ativo', + }, { id: 'row-actions', header: '', diff --git a/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-form-inputs.tsx b/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-form-inputs.tsx index e2d94195..49dede13 100644 --- a/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-form-inputs.tsx +++ b/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-form-inputs.tsx @@ -4,6 +4,7 @@ import { useFormContext } from 'react-hook-form' import { Combobox, Form, Input } from '@/core/presentation/components/ui' import { useDebounce } from '@/core/presentation/hooks' +import { useAllActiveIngredientsQuery } from '@/core/presentation/hooks/queries/all-active-ingredients-query.hook' import { useAllInputUseProductCategoriesQuery } from '../../hooks/queries/all-input-use-product-categories-query.hook' @@ -13,10 +14,14 @@ export function InputUseProductFormInputs() { const form = useFormContext() const [searchCategory, setSearchCategory] = useState('') + const [searchActiveIngredient, setSearchActiveIngredient] = useState('') const debouncedCategory = useDebounce({ value: searchCategory }) + const debouncedActiveIngredient = useDebounce({ + value: searchActiveIngredient, + }) - const { allInputUseProductCategories, isLoading } = + const { allInputUseProductCategories, isLoading: isLoadingCategories } = useAllInputUseProductCategoriesQuery({ filters: { name: { @@ -26,6 +31,16 @@ export function InputUseProductFormInputs() { }, }) + const { allActiveIngredients, isLoading: isLoadingActiveIngredients } = + useAllActiveIngredientsQuery({ + filters: { + name: { + value: debouncedActiveIngredient, + type: 'LIKE', + }, + }, + }) + return ( <> + + { + const { error } = fieldState + + return ( + + Princípio Ativo* + + + + + + ) + }} + /> ) } diff --git a/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-initial-form-data.ts b/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-initial-form-data.ts index d8b740dc..6ec26bfd 100644 --- a/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-initial-form-data.ts +++ b/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-initial-form-data.ts @@ -6,4 +6,8 @@ export const INPUT_USE_PRODUCT_INITIAL_FORM_DATA: InputUseProductFormSchema = { label: '', value: 0, }, + activeIngredient: { + label: '', + value: 0, + }, } diff --git a/src/app/modules/input-uses/presentation/validations/input-use-product-form-schema.ts b/src/app/modules/input-uses/presentation/validations/input-use-product-form-schema.ts index 61b1450c..a3c64554 100644 --- a/src/app/modules/input-uses/presentation/validations/input-use-product-form-schema.ts +++ b/src/app/modules/input-uses/presentation/validations/input-use-product-form-schema.ts @@ -13,6 +13,12 @@ export const inputUseProductFormSchema = z.object({ message: 'Categoria é obrigatória', } ), + activeIngredient: optionSchema.refine( + ({ label, value }) => label !== '' && value !== 0, + { + message: 'Princípio ativo é obrigatório', + } + ), }) export type InputUseProductFormSchema = z.infer< diff --git a/src/core/validation/schemas/index.ts b/src/core/validation/schemas/index.ts index 547a6d4a..0c56e09c 100644 --- a/src/core/validation/schemas/index.ts +++ b/src/core/validation/schemas/index.ts @@ -1,2 +1,2 @@ export { fileTypeSchema } from './file-type-schema' -export { optionSchema } from './option-schema' +export { optionSchema, createOptionSchemaWithExtraData } from './option-schema' diff --git a/src/core/validation/schemas/option-schema.ts b/src/core/validation/schemas/option-schema.ts index af4dc83a..26d1a8b1 100644 --- a/src/core/validation/schemas/option-schema.ts +++ b/src/core/validation/schemas/option-schema.ts @@ -4,3 +4,10 @@ export const optionSchema = z.object({ label: z.string().min(1, { message: 'Campo obrigatório' }), value: z.number().min(1, { message: 'Campo obrigatório' }), }) + +export const createOptionSchemaWithExtraData = ( + extraDataShape: T +) => + optionSchema.extend({ + extraData: z.object(extraDataShape).optional(), + })