Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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 `<DataTable />` component from `@/core/presentation/components/ui`.
- Include an action column with a `DropdownMenu` for "Editar" and "Excluir".
- Implement the table (`.tsx`) using the generic `<DataTable />` component from `@/core/presentation/components/ui`.
- **Delete Dialog**: `components/{entity}-delete-dialog/`
- Implement `.tsx` with `<AlertDialog />` and a `useMutation` calling the delete factory.
- Needs 2 files:
- `index.ts` (exporting `{entity}-delete-dialog.tsx`)
- `{entity}-delete-dialog.tsx` (Implement with `<AlertDialog />`, 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 `<Sheet />`)
- `edit-{entity}-form.tsx` (fetches the item via Get One hook, displays `<Loading />` 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 `<Sheet />`, form className `flex flex-col gap-4`, button text "Criar")
- `edit-{entity}-form.tsx` (fetches the item via Get One hook, displays `<Loading />` 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 `<Input />`.
- 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 />`.
- `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 />`.

Expand Down
2 changes: 1 addition & 1 deletion scripts/seed/data/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
20 changes: 20 additions & 0 deletions scripts/seed/data/input-use-products.mjs
Original file line number Diff line number Diff line change
@@ -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,
})
}
})
23 changes: 0 additions & 23 deletions scripts/seed/data/products.mjs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@ export type AnimalMedicationApplicationMethod =
export type AnimalMedicationDetailsModel = {
date: Date
product: Option
activeIngredient: Option
appliedDose: string
applicationMethod: AnimalMedicationApplicationMethod
}

export type AnimalMedicationDetailsApiResponse = {
date: string
product: Option
activeIngredient: Option
appliedDose: string
applicationMethod: string
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -12,27 +13,27 @@ 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<AnimalMedicationFormSchema>()

const [searchProduct, setSearchProduct] = useState('')

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 (
<>
Expand Down Expand Up @@ -69,21 +70,13 @@ export function AnimalMedicationFormInputs() {
<Form.Item>
<Form.Label>Produto*</Form.Label>
<Form.Control>
<Combobox<{ activeIngredient: Option }>
<Combobox<{ activeIngredient: string }>
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"
Expand All @@ -96,32 +89,16 @@ export function AnimalMedicationFormInputs() {
}}
/>

<Form.Field
name="activeIngredient"
control={form.control}
render={({ field, fieldState }) => {
const { error } = fieldState

return (
<Form.Item>
<Form.Label>Princípio Ativo*</Form.Label>
<Form.Control>
<Combobox
disabled
search=""
items={[]}
selected={field.value}
handleSearch={() => null}
handleSelect={field.onChange}
isError={!!error}
placeholder="Princípio ativo do produto"
/>
</Form.Control>
<Form.Message />
</Form.Item>
)
}}
/>
<Form.Item>
<Form.Label>Princípio Ativo</Form.Label>
<Form.Control>
<Input
disabled
value={selectedProduct?.extraData?.activeIngredient ?? ''}
placeholder="Princípio ativo do produto"
/>
</Form.Control>
</Form.Item>
</Grouper>

<Form.Field
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ import type { AnimalMedicationFormSchema } from '../../validations/animal-medica

export const ANIMAL_MEDICATION_INITIAL_FORM_DATA: AnimalMedicationFormSchema = {
date: new Date(),
activeIngredient: {
label: '',
value: 0,
},
applicationMethod: 'IM',
appliedDose: '',
product: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ export function EditAnimalMedicationForm() {
values: {
...animalMedication,
appliedDose: floatMask(animalMedication.appliedDose, 'mg/ml'),
product: {
...animalMedication.product,
extraData: {
activeIngredient: String(
animalMedication.product.extraData?.activeIngredient ?? ''
),
},
},
},
}),
resolver: zodResolver(animalMedicationFormSchema),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import { z } from 'zod'

import { optionSchema } from '@/core/validation/schemas'
import { createOptionSchemaWithExtraData } from '@/core/validation/schemas'

export const animalMedicationFormSchema = z.object({
date: z.date().max(new Date(), { message: 'Data inválida' }),
product: optionSchema.refine(
({ label, value }) => 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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Loading
Loading