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 />`. 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..3f019559 --- /dev/null +++ b/scripts/seed/data/input-use-products.mjs @@ -0,0 +1,20 @@ +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: category.name, + activeIngredient: 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/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 5e7bfeff..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 @@ -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,12 +13,9 @@ 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' -import type { Option } from '@/core/domain/types' - export function AnimalMedicationFormInputs() { const form = useFormContext() @@ -25,14 +23,17 @@ 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', + }, }, - }, - }) + }) + + const selectedProduct = form.watch('product') return ( <> @@ -69,21 +70,13 @@ export function AnimalMedicationFormInputs() { Produto* - + search={searchProduct} - items={allProducts} + 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" @@ -96,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/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..b246ab0c --- /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,52 @@ +import { type HttpClient, HttpStatusCode } from '@/core/data/protocols/http' +import { + BadRequestError, + ForbiddenError, + NotFoundError, + UnexpectedError, +} from '@/core/domain/errors' + +import type { + InputUseProductDetailsApiResponse, + InputUseProductDetailsModel, +} 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 +{ + 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, + activeIngredient: body.activeIngredient, + } + } + + if (statusCode === HttpStatusCode.forbidden) { + 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/core/data/use-cases/products-use-cases/remote-get-all-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 similarity index 54% rename from src/core/data/use-cases/products-use-cases/remote-get-all-products-use-case.ts rename to src/app/modules/input-uses/data/use-cases/input-use-products-use-cases/remote-get-input-use-products-use-case.ts index 2ef9d360..e8e9ab31 100644 --- a/src/core/data/use-cases/products-use-cases/remote-get-all-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,42 +1,50 @@ 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' + InputUseProductApiResponse, + InputUseProductModel, +} 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' -import type { GetAllProductsUseCase } from '@/core/domain/use-cases/products-use-cases' -export class RemoteGetAllProductsUseCase implements GetAllProductsUseCase { +export class RemoteGetInputUseProductsUseCase + implements GetInputUseProductsUseCase +{ constructor( private readonly url: string, private readonly httpClient: HttpClient< - ProductModel, - ProductApiResponse, - ListApiResponse + InputUseProductModel, + InputUseProductApiResponse, + ListApiResponse > ) {} - execute: GetAllProductsUseCase['execute'] = async ({ filters }) => { - const mapApiProperties: MapApiProperties = - { - id: 'id', - name: 'name', - description: 'description', - activeIngredient: 'activeIngredient', - category: 'category', - } + execute: GetInputUseProductsUseCase['execute'] = async ({ + filters, + pagination, + sort, + }) => { + const mapApiProperties: MapApiProperties< + InputUseProductModel, + InputUseProductApiResponse + > = { + id: 'id', + name: 'name', + category: 'category', + activeIngredient: 'activeIngredient', + } const { statusCode, body } = await this.httpClient.request({ url: `${this.url}/search`, method: 'post', filters, + pagination, + sort, mapApiProperties, }) @@ -45,23 +53,22 @@ export class RemoteGetAllProductsUseCase implements GetAllProductsUseCase { resources: body.content.map((item) => ({ id: item.id, name: item.name, - description: item.description, - activeIngredient: item.activeIngredient, category: item.category, + activeIngredient: item.activeIngredient, })), 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() + 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() + } +} 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..3760fe60 --- /dev/null +++ b/src/app/modules/input-uses/domain/models/input-use-products-model.ts @@ -0,0 +1,25 @@ +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: string +}> + +export type InputUseProductApiResponse = WithId<{ + name: string + category: string + activeIngredient: 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 +> 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() + ) +} 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..a60e351f --- /dev/null +++ b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-product-handler.ts @@ -0,0 +1,46 @@ +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 inputUseProductsData from '@database/inputUseProductsData.json' + +import type { InputUseProductDetailsApiResponse } from '@/app/modules/input-uses/domain/models/input-use-products-model' + +export const getInputUseProductHandler = httpWithMiddleware< + PathParams<'id'>, + never, + 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 }) + } + + return HttpResponse.json( + { + name: inputUseProduct.name, + 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 new file mode 100644 index 00000000..e3b752e4 --- /dev/null +++ b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/get-input-use-products-handler.ts @@ -0,0 +1,79 @@ +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 inputUseProductsData from '@database/inputUseProductsData.json' + +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' + +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) => { + return { + id: product.id, + name: product.name, + category: product.category, + activeIngredient: product.activeIngredient, + } + }) + + 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..6482239d --- /dev/null +++ b/src/app/modules/input-uses/mocks/handlers/input-use-products-handlers/update-input-use-product-handler.ts @@ -0,0 +1,22 @@ +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 { InputUseProductApiResponse } from '../../../domain/models/input-use-products-model' +import type { PathParam } from 'react-router-dom' + +export const updateInputUseProductHandler = httpWithMiddleware< + PathParam<'id'>, + Omit, + never +>({ + routePath: '/api/input-uses/products/:id', + method: 'patch', + middlewares: [withDelay(), withAuth], + resolver: async () => + HttpResponse.json(undefined, { + status: HttpStatusCode.noContent, + }), +}) 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..3593c19b --- /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,96 @@ +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: 'category', + header: 'Categoria', + }, + { + accessorKey: 'activeIngredient', + header: 'Princípio Ativo', + }, + { + 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 + + + + + + + + + + + + + Criar + + + + + ) +} + +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 ? ( + + + + ) : ( + + )} + + + + + + + Salvar + + + + + ) +} + +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..49dede13 --- /dev/null +++ b/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-form-inputs.tsx @@ -0,0 +1,129 @@ +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 { useAllActiveIngredientsQuery } from '@/core/presentation/hooks/queries/all-active-ingredients-query.hook' + +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 [searchActiveIngredient, setSearchActiveIngredient] = useState('') + + const debouncedCategory = useDebounce({ value: searchCategory }) + const debouncedActiveIngredient = useDebounce({ + value: searchActiveIngredient, + }) + + const { allInputUseProductCategories, isLoading: isLoadingCategories } = + useAllInputUseProductCategoriesQuery({ + filters: { + name: { + value: debouncedCategory, + type: 'LIKE', + }, + }, + }) + + const { allActiveIngredients, isLoading: isLoadingActiveIngredients } = + useAllActiveIngredientsQuery({ + filters: { + name: { + value: debouncedActiveIngredient, + type: 'LIKE', + }, + }, + }) + + return ( + <> + { + const { error } = fieldState + + return ( + + Nome do Produto* + + + + + + ) + }} + /> + + { + const { error } = fieldState + + return ( + + Categoria* + + + + + + ) + }} + /> + + { + const { error } = fieldState + + return ( + + Princípio Ativo* + + + + + + ) + }} + /> + > + ) +} + +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..6ec26bfd --- /dev/null +++ b/src/app/modules/input-uses/presentation/forms/input-use-product-form/input-use-product-initial-form-data.ts @@ -0,0 +1,13 @@ +import type { InputUseProductFormSchema } from '../../validations/input-use-product-form-schema' + +export const INPUT_USE_PRODUCT_INITIAL_FORM_DATA: InputUseProductFormSchema = { + name: '', + category: { + label: '', + value: 0, + }, + activeIngredient: { + 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/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/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, + }) => ( + + + + Adicionar Produto + + + { + 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..a3c64554 --- /dev/null +++ b/src/app/modules/input-uses/presentation/validations/input-use-product-form-schema.ts @@ -0,0 +1,26 @@ +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', + } + ), + activeIngredient: optionSchema.refine( + ({ label, value }) => label !== '' && value !== 0, + { + message: 'Princípio ativo é obrigatório', + } + ), +}) + +export type InputUseProductFormSchema = z.infer< + typeof inputUseProductFormSchema +> 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/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/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 a63c0c27..c44aa1a2 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, @@ -169,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[] = [ @@ -179,8 +185,6 @@ const handlers: HttpHandler[] = [ getAllActiveIngredientsHandler, - getAllProductsHandler, - getAllUsersHandler, getMeHandler, @@ -318,6 +322,12 @@ const handlers: HttpHandler[] = [ getInputUseProductCategoriesHandler, updateInputUseProductCategoryHandler, + createInputUseProductHandler, + deleteInputUseProductHandler, + getInputUseProductHandler, + getInputUseProductsHandler, + updateInputUseProductHandler, + createNutritionalBalancingHandler, deleteNutritionalBalancingHandler, getLastVisitNutritionalBalancingsHandler, 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, - } -} 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(), + })