From fd34e41807aa7e60f789494b9005da12b19c3f29 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Mon, 20 Oct 2025 21:24:23 -0300 Subject: [PATCH 01/82] feat(nutritional-balancings/domain): add model and use cases --- .../models/nutritional-balancings-model.ts | 77 +++++++++++++++++++ .../create-nutritional-balancing-use-case.ts | 10 +++ .../delete-nutritional-balancing-use-case.ts | 9 +++ .../get-nutritional-balancing-use-case.ts | 10 +++ .../get-nutritional-balancings-use-case.ts | 11 +++ .../domain/use-cases/index.ts | 5 ++ .../update-nutritional-balancing-use-case.ts | 10 +++ 7 files changed, 132 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/domain/models/nutritional-balancings-model.ts create mode 100644 src/app/modules/nutritional-balancings/domain/use-cases/create-nutritional-balancing-use-case.ts create mode 100644 src/app/modules/nutritional-balancings/domain/use-cases/delete-nutritional-balancing-use-case.ts create mode 100644 src/app/modules/nutritional-balancings/domain/use-cases/get-nutritional-balancing-use-case.ts create mode 100644 src/app/modules/nutritional-balancings/domain/use-cases/get-nutritional-balancings-use-case.ts create mode 100644 src/app/modules/nutritional-balancings/domain/use-cases/index.ts create mode 100644 src/app/modules/nutritional-balancings/domain/use-cases/update-nutritional-balancing-use-case.ts diff --git a/src/app/modules/nutritional-balancings/domain/models/nutritional-balancings-model.ts b/src/app/modules/nutritional-balancings/domain/models/nutritional-balancings-model.ts new file mode 100644 index 00000000..35731445 --- /dev/null +++ b/src/app/modules/nutritional-balancings/domain/models/nutritional-balancings-model.ts @@ -0,0 +1,77 @@ +import type { WithId } from '@/core/domain/types' + +type NutritionalBalancingEvaluationStatus = 'ABOVE' | 'BELOW' | 'NORMAL' + +type NutritionalBalancingIngredientCategory = + | 'FORAGE' + | 'CONCENTRATE' + | 'MINERAL' + +type NutritionalBalancingAnimal = { + name: string + breed: string + ecc: string + weight: string + milkProduction: string + estimatedMilkProduction: string +} + +type NutritionalBalancingSummary = { + totalDryMatter: string + etherExtractPercent: string + forageDryMatterPercent: string + concentrateDryMatterPercent: string + nonFibrousCarbohydratesPercent: string + rdpTdnRatio: string +} + +type NutritionalEvaluation = { + nutrientName: string + requiredValue: number + providedValue: number + evaluationStatus: NutritionalBalancingEvaluationStatus +} + +type IngredientItem = { + name: string + quantity: number +} + +type IngredientGroup = { + category: NutritionalBalancingIngredientCategory + ingredients: WithId[] +} + +export type NutritionalBalancingDetailsModel = { + date: Date + animal: WithId + summary: NutritionalBalancingSummary + evaluations: NutritionalEvaluation[] + ingredientGroups: IngredientGroup[] +} + +export type NutritionalBalancingDetailsApiResponse = { + date: string + animal: WithId + summary: NutritionalBalancingSummary + evaluations: NutritionalEvaluation[] + ingredientGroups: IngredientGroup[] +} + +export type NutritionalBalancingModel = WithId<{ + date: Date + animal: string + breed: string + weight: string + milkProduction: string + estimatedMilkProduction: string +}> + +export type NutritionalBalancingApiResponse = WithId<{ + date: string + animal: string + breed: string + weight: string + milkProduction: string + estimatedMilkProduction: string +}> diff --git a/src/app/modules/nutritional-balancings/domain/use-cases/create-nutritional-balancing-use-case.ts b/src/app/modules/nutritional-balancings/domain/use-cases/create-nutritional-balancing-use-case.ts new file mode 100644 index 00000000..006867b2 --- /dev/null +++ b/src/app/modules/nutritional-balancings/domain/use-cases/create-nutritional-balancing-use-case.ts @@ -0,0 +1,10 @@ +import type { NutritionalBalancingDetailsModel } from '../models/nutritional-balancings-model' +import type { RequestInterface } from '@/core/domain/types' + +export type CreateNutritionalBalancingUseCase = RequestInterface< + { + propertyId: number + nutritionalBalancings: NutritionalBalancingDetailsModel + }, + void +> diff --git a/src/app/modules/nutritional-balancings/domain/use-cases/delete-nutritional-balancing-use-case.ts b/src/app/modules/nutritional-balancings/domain/use-cases/delete-nutritional-balancing-use-case.ts new file mode 100644 index 00000000..e6c01882 --- /dev/null +++ b/src/app/modules/nutritional-balancings/domain/use-cases/delete-nutritional-balancing-use-case.ts @@ -0,0 +1,9 @@ +import type { RequestInterface } from '@/core/domain/types' + +export type DeleteNutritionalBalancingUseCase = RequestInterface< + { + propertyId: number + nutritionalBalancingId: number + }, + void +> diff --git a/src/app/modules/nutritional-balancings/domain/use-cases/get-nutritional-balancing-use-case.ts b/src/app/modules/nutritional-balancings/domain/use-cases/get-nutritional-balancing-use-case.ts new file mode 100644 index 00000000..f28686af --- /dev/null +++ b/src/app/modules/nutritional-balancings/domain/use-cases/get-nutritional-balancing-use-case.ts @@ -0,0 +1,10 @@ +import type { NutritionalBalancingDetailsModel } from '../models/nutritional-balancings-model' +import type { RequestInterface } from '@/core/domain/types' + +export type GetNutritionalBalancingUseCase = RequestInterface< + { + propertyId: number + nutritionalBalancingId: number + }, + NutritionalBalancingDetailsModel +> diff --git a/src/app/modules/nutritional-balancings/domain/use-cases/get-nutritional-balancings-use-case.ts b/src/app/modules/nutritional-balancings/domain/use-cases/get-nutritional-balancings-use-case.ts new file mode 100644 index 00000000..005d2363 --- /dev/null +++ b/src/app/modules/nutritional-balancings/domain/use-cases/get-nutritional-balancings-use-case.ts @@ -0,0 +1,11 @@ +import type { NutritionalBalancingModel } from '../models/nutritional-balancings-model' +import type { + RequestInterface, + ListParams, + ListResponse, +} from '@/core/domain/types' + +export type GetNutritionalBalancingsUseCase = RequestInterface< + ListParams & { propertyId: number }, + ListResponse +> diff --git a/src/app/modules/nutritional-balancings/domain/use-cases/index.ts b/src/app/modules/nutritional-balancings/domain/use-cases/index.ts new file mode 100644 index 00000000..e37d4100 --- /dev/null +++ b/src/app/modules/nutritional-balancings/domain/use-cases/index.ts @@ -0,0 +1,5 @@ +export * from './create-nutritional-balancing-use-case' +export * from './delete-nutritional-balancing-use-case' +export * from './get-nutritional-balancings-use-case' +export * from './get-nutritional-balancing-use-case' +export * from './update-nutritional-balancing-use-case' diff --git a/src/app/modules/nutritional-balancings/domain/use-cases/update-nutritional-balancing-use-case.ts b/src/app/modules/nutritional-balancings/domain/use-cases/update-nutritional-balancing-use-case.ts new file mode 100644 index 00000000..3139d0dc --- /dev/null +++ b/src/app/modules/nutritional-balancings/domain/use-cases/update-nutritional-balancing-use-case.ts @@ -0,0 +1,10 @@ +import type { NutritionalBalancingDetailsModel } from '../models/nutritional-balancings-model' +import type { RequestInterface, WithId } from '@/core/domain/types' + +export type UpdateNutritionalBalancingUseCase = RequestInterface< + { + propertyId: number + nutritionalBalancing: WithId + }, + void +> From 41392a725b3ad803b5d7558594a616c98dca813f Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 21 Oct 2025 21:40:17 -0300 Subject: [PATCH 02/82] feat(nutritional-balancings/data): add use cases --- .../data/use-cases/index.ts | 5 ++ ...e-create-nutritional-balancing-use-case.ts | 42 ++++++++++ ...e-delete-nutritional-balancing-use-case.ts | 43 ++++++++++ ...mote-get-nutritional-balancing-use-case.ts | 57 +++++++++++++ ...ote-get-nutritional-balancings-use-case.ts | 83 +++++++++++++++++++ ...e-update-nutritional-balancing-use-case.ts | 42 ++++++++++ 6 files changed, 272 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/data/use-cases/index.ts create mode 100644 src/app/modules/nutritional-balancings/data/use-cases/remote-create-nutritional-balancing-use-case.ts create mode 100644 src/app/modules/nutritional-balancings/data/use-cases/remote-delete-nutritional-balancing-use-case.ts create mode 100644 src/app/modules/nutritional-balancings/data/use-cases/remote-get-nutritional-balancing-use-case.ts create mode 100644 src/app/modules/nutritional-balancings/data/use-cases/remote-get-nutritional-balancings-use-case.ts create mode 100644 src/app/modules/nutritional-balancings/data/use-cases/remote-update-nutritional-balancing-use-case.ts diff --git a/src/app/modules/nutritional-balancings/data/use-cases/index.ts b/src/app/modules/nutritional-balancings/data/use-cases/index.ts new file mode 100644 index 00000000..e2bc239c --- /dev/null +++ b/src/app/modules/nutritional-balancings/data/use-cases/index.ts @@ -0,0 +1,5 @@ +export * from './remote-create-nutritional-balancing-use-case' +export * from './remote-delete-nutritional-balancing-use-case' +export * from './remote-get-nutritional-balancing-use-case' +export * from './remote-get-nutritional-balancings-use-case' +export * from './remote-update-nutritional-balancing-use-case' diff --git a/src/app/modules/nutritional-balancings/data/use-cases/remote-create-nutritional-balancing-use-case.ts b/src/app/modules/nutritional-balancings/data/use-cases/remote-create-nutritional-balancing-use-case.ts new file mode 100644 index 00000000..64a71e3a --- /dev/null +++ b/src/app/modules/nutritional-balancings/data/use-cases/remote-create-nutritional-balancing-use-case.ts @@ -0,0 +1,42 @@ +import { type HttpClient, HttpStatusCode } from '@/core/data/protocols/http' +import { + BadRequestError, + ForbiddenError, + UnexpectedError, +} from '@/core/domain/errors' + +import type { CreateNutritionalBalancingUseCase } from '../../domain/use-cases' + +export class RemoteCreateNutritionalBalancingUseCase + implements CreateNutritionalBalancingUseCase +{ + constructor( + private readonly url: string, + private readonly httpClient: HttpClient + ) {} + + execute: CreateNutritionalBalancingUseCase['execute'] = async ({ + propertyId, + nutritionalBalancings, + }) => { + const url = this.url.replace(':propertyId', String(propertyId)) + + const { statusCode } = await this.httpClient.request({ + url, + method: 'post', + body: nutritionalBalancings, + }) + + 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 novo balanceamento nutricional' + ) + } + + throw new UnexpectedError() + } +} diff --git a/src/app/modules/nutritional-balancings/data/use-cases/remote-delete-nutritional-balancing-use-case.ts b/src/app/modules/nutritional-balancings/data/use-cases/remote-delete-nutritional-balancing-use-case.ts new file mode 100644 index 00000000..7ffbcf91 --- /dev/null +++ b/src/app/modules/nutritional-balancings/data/use-cases/remote-delete-nutritional-balancing-use-case.ts @@ -0,0 +1,43 @@ +import { type HttpClient, HttpStatusCode } from '@/core/data/protocols/http' +import { + UnexpectedError, + NotFoundError, + ForbiddenError, +} from '@/core/domain/errors' + +import type { DeleteNutritionalBalancingUseCase } from '../../domain/use-cases' + +export class RemoteDeleteNutritionalBalancingUseCase + implements DeleteNutritionalBalancingUseCase +{ + constructor( + private readonly url: string, + private readonly httpClient: HttpClient + ) {} + + execute: DeleteNutritionalBalancingUseCase['execute'] = async ({ + propertyId, + nutritionalBalancingId, + }) => { + const url = this.url.replace(':propertyId', String(propertyId)) + + const { statusCode } = await this.httpClient.request({ + url: `${url}/${nutritionalBalancingId}`, + method: 'delete', + }) + + if (statusCode === HttpStatusCode.noContent) return + + if (statusCode === HttpStatusCode.notFound) { + throw new NotFoundError('Balanceamento Nutricional') + } + + if (statusCode === HttpStatusCode.forbidden) { + throw new ForbiddenError( + 'Você não tem permissão para excluir um balanceamento nutricional' + ) + } + + throw new UnexpectedError() + } +} diff --git a/src/app/modules/nutritional-balancings/data/use-cases/remote-get-nutritional-balancing-use-case.ts b/src/app/modules/nutritional-balancings/data/use-cases/remote-get-nutritional-balancing-use-case.ts new file mode 100644 index 00000000..dc40b2ab --- /dev/null +++ b/src/app/modules/nutritional-balancings/data/use-cases/remote-get-nutritional-balancing-use-case.ts @@ -0,0 +1,57 @@ +import { type HttpClient, HttpStatusCode } from '@/core/data/protocols/http' +import { + UnexpectedError, + NotFoundError, + ForbiddenError, +} from '@/core/domain/errors' + +import type { + NutritionalBalancingDetailsApiResponse, + NutritionalBalancingDetailsModel, +} from '../../domain/models/nutritional-balancings-model' +import type { GetNutritionalBalancingUseCase } from '../../domain/use-cases' + +export class RemoteGetNutritionalBalancingUseCase + implements GetNutritionalBalancingUseCase +{ + constructor( + private readonly url: string, + private readonly httpClient: HttpClient< + NutritionalBalancingDetailsModel, + NutritionalBalancingDetailsApiResponse + > + ) {} + + execute: GetNutritionalBalancingUseCase['execute'] = async ({ + nutritionalBalancingId, + propertyId, + }) => { + const url = this.url.replace(':propertyId', String(propertyId)) + + const { statusCode, body } = await this.httpClient.request({ + url: `${url}/${nutritionalBalancingId}`, + method: 'get', + }) + + if (statusCode === HttpStatusCode.ok && !!body) { + return { + date: new Date(body.date), + animal: body.animal, + summary: body.summary, + evaluations: body.evaluations, + ingredientGroups: body.ingredientGroups, + } + } + + if (statusCode === HttpStatusCode.notFound) + throw new NotFoundError('Balanceamento Nutricional') + + if (statusCode === HttpStatusCode.forbidden) { + throw new ForbiddenError( + 'Você não tem permissão para buscar um balanceamento nutricional' + ) + } + + throw new UnexpectedError() + } +} diff --git a/src/app/modules/nutritional-balancings/data/use-cases/remote-get-nutritional-balancings-use-case.ts b/src/app/modules/nutritional-balancings/data/use-cases/remote-get-nutritional-balancings-use-case.ts new file mode 100644 index 00000000..6fc52a7a --- /dev/null +++ b/src/app/modules/nutritional-balancings/data/use-cases/remote-get-nutritional-balancings-use-case.ts @@ -0,0 +1,83 @@ +import { type HttpClient, HttpStatusCode } from '@/core/data/protocols/http' +import { + UnexpectedError, + NotFoundError, + ForbiddenError, +} from '@/core/domain/errors' + +import type { + NutritionalBalancingApiResponse, + NutritionalBalancingModel, +} from '../../domain/models/nutritional-balancings-model' +import type { GetNutritionalBalancingsUseCase } from '../../domain/use-cases' +import type { ListApiResponse, MapApiProperties } from '@/core/domain/types' + +export class RemoteGetNutritionalBalancingsUseCase + implements GetNutritionalBalancingsUseCase +{ + constructor( + private readonly url: string, + private readonly httpClient: HttpClient< + NutritionalBalancingModel, + NutritionalBalancingApiResponse, + ListApiResponse + > + ) {} + + execute: GetNutritionalBalancingsUseCase['execute'] = async ({ + propertyId, + filters, + pagination, + sort, + }) => { + const mapApiProperties: MapApiProperties< + NutritionalBalancingModel, + NutritionalBalancingApiResponse + > = { + date: 'date', + animal: 'animal', + breed: 'breed', + weight: 'weight', + milkProduction: 'milkProduction', + estimatedMilkProduction: 'estimatedMilkProduction', + } + + const url = this.url.replace(':propertyId', String(propertyId)) + + const { statusCode, body } = await this.httpClient.request({ + url: `${url}/search`, + method: 'post', + filters, + pagination, + sort, + mapApiProperties, + }) + + if (statusCode === HttpStatusCode.ok && !!body) { + return { + resources: body.content.map((item) => ({ + id: item.id, + date: new Date(item.date), + animal: item.animal, + breed: item.breed, + weight: item.weight, + milkProduction: item.milkProduction, + estimatedMilkProduction: item.estimatedMilkProduction, + })), + totalPages: Math.ceil(body.numberOfElements / body.pageable.pageSize), + } + } + + if (statusCode === HttpStatusCode.notFound) { + throw new NotFoundError('Balanceamentos Nutricionais') + } + + if (statusCode === HttpStatusCode.forbidden) { + throw new ForbiddenError( + 'Você não tem permissão para buscar os balanceamentos nutricionais' + ) + } + + throw new UnexpectedError() + } +} diff --git a/src/app/modules/nutritional-balancings/data/use-cases/remote-update-nutritional-balancing-use-case.ts b/src/app/modules/nutritional-balancings/data/use-cases/remote-update-nutritional-balancing-use-case.ts new file mode 100644 index 00000000..d7cf6db6 --- /dev/null +++ b/src/app/modules/nutritional-balancings/data/use-cases/remote-update-nutritional-balancing-use-case.ts @@ -0,0 +1,42 @@ +import { type HttpClient, HttpStatusCode } from '@/core/data/protocols/http' +import { + BadRequestError, + ForbiddenError, + UnexpectedError, +} from '@/core/domain/errors' + +import type { UpdateNutritionalBalancingUseCase } from '../../domain/use-cases' + +export class RemoteUpdateNutritionalBalancingUseCase + implements UpdateNutritionalBalancingUseCase +{ + constructor( + private readonly url: string, + private readonly httpClient: HttpClient + ) {} + + execute: UpdateNutritionalBalancingUseCase['execute'] = async ({ + propertyId, + nutritionalBalancing: { id, ...nutritionalBalancing }, + }) => { + const url = this.url.replace(':propertyId', String(propertyId)) + + const { statusCode } = await this.httpClient.request({ + url: `${url}/${id}`, + method: 'patch', + body: nutritionalBalancing, + }) + + 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 editar um balanceamento nutricional' + ) + } + + throw new UnexpectedError() + } +} From 2269ce1ea8998b4e6118e03d04022bfc06d3fa2e Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Wed, 22 Oct 2025 22:03:35 -0300 Subject: [PATCH 03/82] feat(nutritional-balancings/main): add factories use cases --- .../main/factories/index.ts | 5 +++++ ...-nutritional-balancing-use-case-factory.ts | 12 +++++++++++ ...-nutritional-balancing-use-case-factory.ts | 12 +++++++++++ ...-nutritional-balancing-use-case-factory.ts | 19 +++++++++++++++++ ...nutritional-balancings-use-case-factory.ts | 21 +++++++++++++++++++ ...-nutritional-balancing-use-case-factory.ts | 12 +++++++++++ 6 files changed, 81 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/main/factories/index.ts create mode 100644 src/app/modules/nutritional-balancings/main/factories/remote-create-nutritional-balancing-use-case-factory.ts create mode 100644 src/app/modules/nutritional-balancings/main/factories/remote-delete-nutritional-balancing-use-case-factory.ts create mode 100644 src/app/modules/nutritional-balancings/main/factories/remote-get-nutritional-balancing-use-case-factory.ts create mode 100644 src/app/modules/nutritional-balancings/main/factories/remote-get-nutritional-balancings-use-case-factory.ts create mode 100644 src/app/modules/nutritional-balancings/main/factories/remote-update-nutritional-balancing-use-case-factory.ts diff --git a/src/app/modules/nutritional-balancings/main/factories/index.ts b/src/app/modules/nutritional-balancings/main/factories/index.ts new file mode 100644 index 00000000..9ce0619b --- /dev/null +++ b/src/app/modules/nutritional-balancings/main/factories/index.ts @@ -0,0 +1,5 @@ +export * from './remote-create-nutritional-balancing-use-case-factory' +export * from './remote-delete-nutritional-balancing-use-case-factory' +export * from './remote-get-nutritional-balancing-use-case-factory' +export * from './remote-get-nutritional-balancings-use-case-factory' +export * from './remote-update-nutritional-balancing-use-case-factory' diff --git a/src/app/modules/nutritional-balancings/main/factories/remote-create-nutritional-balancing-use-case-factory.ts b/src/app/modules/nutritional-balancings/main/factories/remote-create-nutritional-balancing-use-case-factory.ts new file mode 100644 index 00000000..e8d6502d --- /dev/null +++ b/src/app/modules/nutritional-balancings/main/factories/remote-create-nutritional-balancing-use-case-factory.ts @@ -0,0 +1,12 @@ +import { makeApiHttpClient } from '@/core/main/factories/http' + +import { RemoteCreateNutritionalBalancingUseCase } from '../../data/use-cases' + +import type { CreateNutritionalBalancingUseCase } from '../../domain/use-cases' + +export function makeRemoteCreateNutritionalBalancingUseCase(): CreateNutritionalBalancingUseCase { + return new RemoteCreateNutritionalBalancingUseCase( + 'properties/:propertyId/nutritional-balancings', + makeApiHttpClient() + ) +} diff --git a/src/app/modules/nutritional-balancings/main/factories/remote-delete-nutritional-balancing-use-case-factory.ts b/src/app/modules/nutritional-balancings/main/factories/remote-delete-nutritional-balancing-use-case-factory.ts new file mode 100644 index 00000000..5bb53535 --- /dev/null +++ b/src/app/modules/nutritional-balancings/main/factories/remote-delete-nutritional-balancing-use-case-factory.ts @@ -0,0 +1,12 @@ +import { makeApiHttpClient } from '@/core/main/factories/http' + +import { RemoteDeleteNutritionalBalancingUseCase } from '../../data/use-cases' + +import type { DeleteNutritionalBalancingUseCase } from '../../domain/use-cases' + +export function makeRemoteDeleteNutritionalBalancingUseCase(): DeleteNutritionalBalancingUseCase { + return new RemoteDeleteNutritionalBalancingUseCase( + 'properties/:propertyId/nutritional-balancings', + makeApiHttpClient() + ) +} diff --git a/src/app/modules/nutritional-balancings/main/factories/remote-get-nutritional-balancing-use-case-factory.ts b/src/app/modules/nutritional-balancings/main/factories/remote-get-nutritional-balancing-use-case-factory.ts new file mode 100644 index 00000000..5309e31f --- /dev/null +++ b/src/app/modules/nutritional-balancings/main/factories/remote-get-nutritional-balancing-use-case-factory.ts @@ -0,0 +1,19 @@ +import { makeApiHttpClient } from '@/core/main/factories/http' + +import { RemoteGetNutritionalBalancingUseCase } from '../../data/use-cases' + +import type { + NutritionalBalancingDetailsModel, + NutritionalBalancingDetailsApiResponse, +} from '../../domain/models/nutritional-balancings-model' +import type { GetNutritionalBalancingUseCase } from '../../domain/use-cases' + +export function makeRemoteGetNutritionalBalancingUseCase(): GetNutritionalBalancingUseCase { + return new RemoteGetNutritionalBalancingUseCase( + 'properties/:propertyId/nutritional-balancings', + makeApiHttpClient< + NutritionalBalancingDetailsModel, + NutritionalBalancingDetailsApiResponse + >() + ) +} diff --git a/src/app/modules/nutritional-balancings/main/factories/remote-get-nutritional-balancings-use-case-factory.ts b/src/app/modules/nutritional-balancings/main/factories/remote-get-nutritional-balancings-use-case-factory.ts new file mode 100644 index 00000000..2847c59f --- /dev/null +++ b/src/app/modules/nutritional-balancings/main/factories/remote-get-nutritional-balancings-use-case-factory.ts @@ -0,0 +1,21 @@ +import { makeApiHttpClient } from '@/core/main/factories/http' + +import { RemoteGetNutritionalBalancingsUseCase } from '../../data/use-cases' + +import type { + NutritionalBalancingModel, + NutritionalBalancingApiResponse, +} from '../../domain/models/nutritional-balancings-model' +import type { GetNutritionalBalancingsUseCase } from '../../domain/use-cases' +import type { ListApiResponse } from '@/core/domain/types' + +export function makeRemoteGetNutritionalBalancingsUseCase(): GetNutritionalBalancingsUseCase { + return new RemoteGetNutritionalBalancingsUseCase( + 'properties/:propertyId/nutritional-balancings', + makeApiHttpClient< + NutritionalBalancingModel, + NutritionalBalancingApiResponse, + ListApiResponse + >() + ) +} diff --git a/src/app/modules/nutritional-balancings/main/factories/remote-update-nutritional-balancing-use-case-factory.ts b/src/app/modules/nutritional-balancings/main/factories/remote-update-nutritional-balancing-use-case-factory.ts new file mode 100644 index 00000000..08b943a2 --- /dev/null +++ b/src/app/modules/nutritional-balancings/main/factories/remote-update-nutritional-balancing-use-case-factory.ts @@ -0,0 +1,12 @@ +import { makeApiHttpClient } from '@/core/main/factories/http' + +import { RemoteUpdateNutritionalBalancingUseCase } from '../../data/use-cases' + +import type { UpdateNutritionalBalancingUseCase } from '../../domain/use-cases' + +export function makeRemoteUpdateNutritionalBalancingUseCase(): UpdateNutritionalBalancingUseCase { + return new RemoteUpdateNutritionalBalancingUseCase( + 'properties/:propertyId/nutritional-balancings', + makeApiHttpClient() + ) +} From 4da137f304e3cb9d8a71b1a8cd9ede84d2a87d66 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Wed, 22 Oct 2025 10:25:39 -0300 Subject: [PATCH 04/82] feat(scripts): add scripts to generate fake data --- scripts/seed/data/index.mjs | 1 + scripts/seed/data/nutritional-balancings.mjs | 26 ++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 scripts/seed/data/nutritional-balancings.mjs diff --git a/scripts/seed/data/index.mjs b/scripts/seed/data/index.mjs index 0531a824..cde707e8 100644 --- a/scripts/seed/data/index.mjs +++ b/scripts/seed/data/index.mjs @@ -23,3 +23,4 @@ export * from './animal-mastitides.mjs' export * from './cultivation-diseases.mjs' export * from './general-cultivation-pests.mjs' export * from './cultivation-pests.mjs' +export * from './nutritional-balancings.mjs' diff --git a/scripts/seed/data/nutritional-balancings.mjs b/scripts/seed/data/nutritional-balancings.mjs new file mode 100644 index 00000000..74d66175 --- /dev/null +++ b/scripts/seed/data/nutritional-balancings.mjs @@ -0,0 +1,26 @@ +import { faker } from '@faker-js/faker/locale/pt_BR' + +export const nutritionalBalancingsData = Array.from( + { length: faker.number.int({ min: 1, max: 100 }) }, + (_, index) => ({ + id: index + 1, + date: faker.date.past().toISOString(), + animal: faker.lorem.word(), + breed: faker.animal.cow(), + weight: faker.number + .float({ min: 200, max: 700, precision: 0.1 }) + .toFixed(2), + milkProduction: faker.number + .float({ + min: 5, + max: 50, + }) + .toFixed(2), + estimatedMilkProduction: faker.number + .float({ + min: 5, + max: 50, + }) + .toFixed(2), + }) +) From f57c292e6e431a2f8c71d9c1191b5b4e0d43261e Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Thu, 23 Oct 2025 16:31:54 -0300 Subject: [PATCH 05/82] feat(nutritional-balancings/mocks): add fake endpoint handlers --- .../create-nutritional-balancing-handler.ts | 19 +++ .../delete-nutritional-balancing-handler.ts | 18 +++ .../get-nutritional-balancing-handler.ts | 148 ++++++++++++++++++ .../get-nutritional-balancings-handler.ts | 70 +++++++++ .../mocks/handlers/index.ts | 5 + .../update-nutritional-balancing-handler.ts | 17 ++ 6 files changed, 277 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/mocks/handlers/create-nutritional-balancing-handler.ts create mode 100644 src/app/modules/nutritional-balancings/mocks/handlers/delete-nutritional-balancing-handler.ts create mode 100644 src/app/modules/nutritional-balancings/mocks/handlers/get-nutritional-balancing-handler.ts create mode 100644 src/app/modules/nutritional-balancings/mocks/handlers/get-nutritional-balancings-handler.ts create mode 100644 src/app/modules/nutritional-balancings/mocks/handlers/index.ts create mode 100644 src/app/modules/nutritional-balancings/mocks/handlers/update-nutritional-balancing-handler.ts diff --git a/src/app/modules/nutritional-balancings/mocks/handlers/create-nutritional-balancing-handler.ts b/src/app/modules/nutritional-balancings/mocks/handlers/create-nutritional-balancing-handler.ts new file mode 100644 index 00000000..8a88cf58 --- /dev/null +++ b/src/app/modules/nutritional-balancings/mocks/handlers/create-nutritional-balancing-handler.ts @@ -0,0 +1,19 @@ +import { HttpResponse, 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 { NutritionalBalancingDetailsModel } from '../../domain/models/nutritional-balancings-model' + +export const createNutritionalBalancingHandler = httpWithMiddleware< + PathParams<'propertyId'>, + NutritionalBalancingDetailsModel, + never +>({ + routePath: '/api/properties/:propertyId/nutritional-balancings', + method: 'post', + middlewares: [withDelay(), withAuth], + resolver: async () => + HttpResponse.json({}, { status: HttpStatusCode.created }), +}) diff --git a/src/app/modules/nutritional-balancings/mocks/handlers/delete-nutritional-balancing-handler.ts b/src/app/modules/nutritional-balancings/mocks/handlers/delete-nutritional-balancing-handler.ts new file mode 100644 index 00000000..9aa2c8f4 --- /dev/null +++ b/src/app/modules/nutritional-balancings/mocks/handlers/delete-nutritional-balancing-handler.ts @@ -0,0 +1,18 @@ +import { HttpResponse, type PathParams } from 'msw' + +import { httpWithMiddleware } from '@/core/mocks/lib' +import { withDelay, withAuth } from '@/core/mocks/middleware' + +export const deleteNutritionalBalancingHandler = httpWithMiddleware< + PathParams<'propertyId' | 'id'>, + never, + never +>({ + routePath: '/api/properties/:propertyId/nutritional-balancings/:id', + method: 'delete', + middlewares: [withDelay(), withAuth], + resolver: async () => + HttpResponse.json(undefined, { + status: 204, + }), +}) diff --git a/src/app/modules/nutritional-balancings/mocks/handlers/get-nutritional-balancing-handler.ts b/src/app/modules/nutritional-balancings/mocks/handlers/get-nutritional-balancing-handler.ts new file mode 100644 index 00000000..45a6be1c --- /dev/null +++ b/src/app/modules/nutritional-balancings/mocks/handlers/get-nutritional-balancing-handler.ts @@ -0,0 +1,148 @@ +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 { withDelay, withAuth } from '@/core/mocks/middleware' + +import nutritionalBalancingsData from '@database/nutritionalBalancingsData.json' + +import type { NutritionalBalancingDetailsApiResponse } from '../../domain/models/nutritional-balancings-model' + +export const getNutritionalBalancingHandler = httpWithMiddleware< + PathParams<'propertyId' | 'id'>, + never, + NutritionalBalancingDetailsApiResponse +>({ + routePath: '/api/properties/:propertyId/nutritional-balancings/:id', + method: 'get', + middlewares: [withDelay(), withAuth], + resolver: async ({ params }) => { + if (!nutritionalBalancingsData.length) { + return HttpResponse.json({} as NutritionalBalancingDetailsApiResponse, { + status: 404, + }) + } + + const nutritionalBalancingFound = nutritionalBalancingsData.find( + (nutritionalBalancing) => nutritionalBalancing.id === Number(params.id) + ) + + if (!nutritionalBalancingFound) { + return HttpResponse.json({} as NutritionalBalancingDetailsApiResponse, { + status: 404, + }) + } + + return HttpResponse.json( + { + date: nutritionalBalancingFound.date, + animal: { + id: nutritionalBalancingFound.id, + name: nutritionalBalancingFound.animal, + breed: nutritionalBalancingFound.breed, + weight: nutritionalBalancingFound.weight, + milkProduction: nutritionalBalancingFound.milkProduction, + ecc: faker.number.float({ min: 1, max: 10 }).toFixed(2), + estimatedMilkProduction: + nutritionalBalancingFound.estimatedMilkProduction, + }, + summary: { + concentrateDryMatterPercent: faker.number + .float({ min: 1, max: 100 }) + .toFixed(2), + etherExtractPercent: faker.number + .float({ min: 1, max: 100 }) + .toFixed(2), + forageDryMatterPercent: faker.number + .float({ min: 1, max: 100 }) + .toFixed(2), + nonFibrousCarbohydratesPercent: faker.number + .float({ min: 1, max: 100 }) + .toFixed(2), + totalDryMatter: faker.number.float({ min: 1, max: 100 }).toFixed(2), + rdpTdnRatio: faker.number.float({ min: 0, max: 1 }).toFixed(3), + }, + evaluations: [ + { + nutrientName: 'IMS (Ingestão de Matéria Seca)', + evaluationStatus: faker.helpers.arrayElement([ + 'ABOVE', + 'BELOW', + 'NORMAL', + ]), + providedValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), + requiredValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), + }, + { + nutrientName: 'NDT (Nutrientes Digestíveis Totais)', + evaluationStatus: faker.helpers.arrayElement([ + 'ABOVE', + 'BELOW', + 'NORMAL', + ]), + providedValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), + requiredValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), + }, + { + nutrientName: 'PB (Proteína Bruta)', + evaluationStatus: faker.helpers.arrayElement([ + 'ABOVE', + 'BELOW', + 'NORMAL', + ]), + providedValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), + requiredValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), + }, + { + nutrientName: 'Ca (Cálcio)', + evaluationStatus: faker.helpers.arrayElement([ + 'ABOVE', + 'BELOW', + 'NORMAL', + ]), + providedValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), + requiredValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), + }, + { + nutrientName: 'P (Fósforo)', + evaluationStatus: faker.helpers.arrayElement([ + 'ABOVE', + 'BELOW', + 'NORMAL', + ]), + providedValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), + requiredValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), + }, + ], + ingredientGroups: [ + { + category: 'FORAGE', + ingredients: Array.from({ length: 3 }).map(() => ({ + id: faker.number.int({ min: 1, max: 10000 }), + name: faker.commerce.productName(), + quantity: faker.number.float({ min: 1, max: 100 }).toFixed(2), + })), + }, + { + category: 'CONCENTRATE', + ingredients: Array.from({ length: 3 }).map(() => ({ + id: faker.number.int({ min: 1, max: 10000 }), + name: faker.commerce.productName(), + quantity: faker.number.float({ min: 1, max: 100 }).toFixed(2), + })), + }, + { + category: 'MINERAL', + ingredients: Array.from({ length: 3 }).map(() => ({ + id: faker.number.int({ min: 1, max: 10000 }), + name: faker.commerce.productName(), + quantity: faker.number.float({ min: 1, max: 100 }).toFixed(2), + })), + }, + ], + }, + { status: HttpStatusCode.ok } + ) + }, +}) diff --git a/src/app/modules/nutritional-balancings/mocks/handlers/get-nutritional-balancings-handler.ts b/src/app/modules/nutritional-balancings/mocks/handlers/get-nutritional-balancings-handler.ts new file mode 100644 index 00000000..f1d4dad4 --- /dev/null +++ b/src/app/modules/nutritional-balancings/mocks/handlers/get-nutritional-balancings-handler.ts @@ -0,0 +1,70 @@ +import { HttpResponse, type PathParams } from 'msw' + +import { HttpStatusCode } from '@/core/data/protocols/http' +import { httpWithMiddleware } from '@/core/mocks/lib' +import { withDelay, withAuth } from '@/core/mocks/middleware' +import { filterData, sortData, paginateData } from '@/core/mocks/utils' + +import nutritionalBalancingsData from '@database/nutritionalBalancingsData.json' + +import type { NutritionalBalancingApiResponse } from '../../domain/models/nutritional-balancings-model' +import type { MockParams } from '@/core/mocks/types/mock-params-type' +import type { MockResponse } from '@/core/mocks/types/mock-response-type' + +export const getNutritionalBalancingsHandler = httpWithMiddleware< + PathParams<'propertyId'>, + MockParams, + MockResponse +>({ + routePath: '/api/properties/:propertyId/nutritional-balancings/search', + method: 'post', + middlewares: [withDelay(), withAuth], + resolver: async ({ request }) => { + const { filters, page, rows, sort } = await request.json() + + if (!nutritionalBalancingsData.length) { + return HttpResponse.json( + { + content: [], + numberOfElements: 0, + pageable: { + pageSize: 0, + }, + }, + { + status: 404, + } + ) + } + + let nutritionalBalancings = + nutritionalBalancingsData as NutritionalBalancingApiResponse[] + + if (filters) + nutritionalBalancings = filterData( + filters, + nutritionalBalancings + ) + if (sort) + nutritionalBalancings = sortData( + sort, + nutritionalBalancings + ) + const numberOfElements = nutritionalBalancings.length + nutritionalBalancings = paginateData( + { page, perPage: rows }, + nutritionalBalancings + ) + + return HttpResponse.json( + { + content: nutritionalBalancings, + numberOfElements, + pageable: { + pageSize: rows, + }, + }, + { status: HttpStatusCode.ok } + ) + }, +}) diff --git a/src/app/modules/nutritional-balancings/mocks/handlers/index.ts b/src/app/modules/nutritional-balancings/mocks/handlers/index.ts new file mode 100644 index 00000000..49cc75aa --- /dev/null +++ b/src/app/modules/nutritional-balancings/mocks/handlers/index.ts @@ -0,0 +1,5 @@ +export * from './create-nutritional-balancing-handler' +export * from './delete-nutritional-balancing-handler' +export * from './get-nutritional-balancing-handler' +export * from './get-nutritional-balancings-handler' +export * from './update-nutritional-balancing-handler' diff --git a/src/app/modules/nutritional-balancings/mocks/handlers/update-nutritional-balancing-handler.ts b/src/app/modules/nutritional-balancings/mocks/handlers/update-nutritional-balancing-handler.ts new file mode 100644 index 00000000..6fce879a --- /dev/null +++ b/src/app/modules/nutritional-balancings/mocks/handlers/update-nutritional-balancing-handler.ts @@ -0,0 +1,17 @@ +import { HttpResponse, PathParams } from 'msw' + +import { HttpStatusCode } from '@/core/data/protocols/http' +import { httpWithMiddleware } from '@/core/mocks/lib' +import { withDelay, withAuth } from '@/core/mocks/middleware' + +export const updateNutritionalBalancingHandler = httpWithMiddleware< + PathParams<'propertyId' | 'id'>, + never, + never +>({ + routePath: '/api/properties/:propertyId/nutritional-balancings/:id', + method: 'patch', + middlewares: [withDelay(), withAuth], + resolver: async () => + HttpResponse.json(undefined, { status: HttpStatusCode.noContent }), +}) From 58d2abb8d03fa28dcf3e104dbc000ee997899071 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Thu, 23 Oct 2025 16:33:04 -0300 Subject: [PATCH 06/82] feat(nutritional-balancings/domain): change types on nutrition evaluation type --- .../domain/models/nutritional-balancings-model.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/modules/nutritional-balancings/domain/models/nutritional-balancings-model.ts b/src/app/modules/nutritional-balancings/domain/models/nutritional-balancings-model.ts index 35731445..dce51a04 100644 --- a/src/app/modules/nutritional-balancings/domain/models/nutritional-balancings-model.ts +++ b/src/app/modules/nutritional-balancings/domain/models/nutritional-balancings-model.ts @@ -27,14 +27,14 @@ type NutritionalBalancingSummary = { type NutritionalEvaluation = { nutrientName: string - requiredValue: number - providedValue: number + requiredValue: string + providedValue: string evaluationStatus: NutritionalBalancingEvaluationStatus } type IngredientItem = { name: string - quantity: number + quantity: string } type IngredientGroup = { From be46731f666eff86226465ae2f22d18f087082e2 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Thu, 23 Oct 2025 16:35:23 -0300 Subject: [PATCH 07/82] feat(core/mocks): register nutritional balancing mocks endpoints --- src/core/mocks/browser.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/core/mocks/browser.ts b/src/core/mocks/browser.ts index 280e5776..2b405a56 100644 --- a/src/core/mocks/browser.ts +++ b/src/core/mocks/browser.ts @@ -136,6 +136,13 @@ import { getMachinesHandler, updateMachineHandler, } from '@/app/modules/machines/mocks/handlers' +import { + createNutritionalBalancingHandler, + deleteNutritionalBalancingHandler, + getNutritionalBalancingHandler, + getNutritionalBalancingsHandler, + updateNutritionalBalancingHandler, +} from '@/app/modules/nutritional-balancings/mocks/handlers' import { createPropertyHandler, deletePropertyHandler, @@ -284,6 +291,12 @@ const handlers: HttpHandler[] = [ getCultivationPestHandler, getCultivationPestsHandler, updateCultivationPestHandler, + + createNutritionalBalancingHandler, + deleteNutritionalBalancingHandler, + getNutritionalBalancingHandler, + getNutritionalBalancingsHandler, + updateNutritionalBalancingHandler, ] export const worker = setupWorker(...handlers) From 0034e35a508fd790e2466d416c7d708982c0d6fa Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Fri, 24 Oct 2025 16:39:01 -0300 Subject: [PATCH 08/82] feat(nutritional-balancings/presentation): add types --- .../presentation/types/nutritional-balancing-types.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/types/nutritional-balancing-types.ts diff --git a/src/app/modules/nutritional-balancings/presentation/types/nutritional-balancing-types.ts b/src/app/modules/nutritional-balancings/presentation/types/nutritional-balancing-types.ts new file mode 100644 index 00000000..f72bb94b --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/types/nutritional-balancing-types.ts @@ -0,0 +1,5 @@ +import type { NutritionalBalancingModel } from '../../domain/models/nutritional-balancings-model' +import type { Filters, Sort } from '@/core/domain/types' + +export type NutritionalBalancingFilters = Filters +export type NutritionalBalancingSort = Sort From 6cfc12c5616238c6a1011cdda44c17367a8c4372 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Mon, 27 Oct 2025 17:03:03 -0300 Subject: [PATCH 09/82] feat(nutritional-balancings/presentation): add form schema and validations --- .../nutritional-balancing-form-schema.ts | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/validations/nutritional-balancing-form-schema.ts diff --git a/src/app/modules/nutritional-balancings/presentation/validations/nutritional-balancing-form-schema.ts b/src/app/modules/nutritional-balancings/presentation/validations/nutritional-balancing-form-schema.ts new file mode 100644 index 00000000..a9567385 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/validations/nutritional-balancing-form-schema.ts @@ -0,0 +1,87 @@ +import { array, z } from 'zod' + +import { optionSchema } from '@/core/validation/schemas' + +const nutritionalBalancingAnimalSchema = z.object({ + id: z.number().min(1, { message: 'Animal é obrigatório' }), + name: z.string().min(1, { message: 'Nome do animal é obrigatório' }), + breed: optionSchema.refine(({ label, value }) => label !== '' && value > 0, { + message: 'Raça é obrigatória', + }), + ecc: z.string().min(1, { message: 'ECC é obrigatório' }), + weight: z.string().min(1, { message: 'Peso é obrigatório' }), + milkProduction: z + .string() + .min(1, { message: 'Produção de leite é obrigatória' }), + estimatedMilkProduction: z + .string() + .min(1, { message: 'Produção de leite estimada é obrigatória' }), +}) + +const nutritionalBalancingSummarySchema = z.object({ + totalDryMatter: z + .string() + .min(1, { message: 'Matéria seca total é obrigatória' }), + etherExtractPercent: z + .string() + .min(1, { message: 'Extrato etéreo é obrigatório' }), + forageDryMatterPercent: z + .string() + .min(1, { message: 'Matéria seca de forragem é obrigatória' }), + concentrateDryMatterPercent: z + .string() + .min(1, { message: 'Matéria seca de concentrado é obrigatória' }), + nonFibrousCarbohydratesPercent: z + .string() + .min(1, { message: 'Carboidratos não fibrosos são obrigatórios' }), + rdpTdnRatio: z.string().min(1, { message: 'RDP/TDN é obrigatório' }), +}) + +const nutritionalEvaluationSchema = z.object({ + nutrientName: z + .string() + .min(1, { message: 'Nome do nutriente é obrigatório' }), + requiredValue: z + .string() + .min(1, { message: 'Valor requerido é obrigatório' }), + providedValue: z + .string() + .min(1, { message: 'Valor fornecido é obrigatório' }), + evaluationStatus: z.enum(['ABOVE', 'BELOW', 'NORMAL'], { + errorMap: () => ({ message: 'Status inválido' }), + }), +}) + +const ingredientItemSchema = z.object({ + name: z.string().min(1, { message: 'Nome do ingrediente é obrigatório' }), + quantity: z + .string() + .min(1, { message: 'Quantidade do ingrediente é obrigatória' }), +}) + +const ingredientGroupSchema = z.object({ + category: z.enum(['FORAGE', 'CONCENTRATE', 'MINERAL'], { + errorMap: () => ({ message: 'Categoria inválida' }), + }), + ingredients: array(ingredientItemSchema).min(1, { + message: 'Adicione ao menos um ingrediente', + }), +}) + +export const nutritionalBalancingFormSchema = z.object({ + date: z + .date() + .max(new Date(), { message: 'A data deve ser menor que a data atual' }), + animal: nutritionalBalancingAnimalSchema, + summary: nutritionalBalancingSummarySchema, + evaluations: z + .array(nutritionalEvaluationSchema) + .min(1, { message: 'Adicione ao menos uma avaliação' }), + ingredientGroups: z + .array(ingredientGroupSchema) + .min(1, { message: 'Adicione ao menos um grupo de ingredientes' }), +}) + +export type NutritionalBalancingFormSchema = z.infer< + typeof nutritionalBalancingFormSchema +> From beedd9d39d920622eb780a578083498142ac473c Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Mon, 27 Oct 2025 19:20:48 -0300 Subject: [PATCH 10/82] feat(nutritional-balancings/presentation): add context and use context hook --- .../nutritional-balancing-context.tsx | 152 ++++++++++++++++++ .../nutritional-balancing-context.hook.ts | 15 ++ 2 files changed, 167 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/contexts/nutritional-balancing-context.tsx create mode 100644 src/app/modules/nutritional-balancings/presentation/hooks/nutritional-balancing-context.hook.ts diff --git a/src/app/modules/nutritional-balancings/presentation/contexts/nutritional-balancing-context.tsx b/src/app/modules/nutritional-balancings/presentation/contexts/nutritional-balancing-context.tsx new file mode 100644 index 00000000..baaa87d2 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/contexts/nutritional-balancing-context.tsx @@ -0,0 +1,152 @@ +import { + createContext, + useCallback, + useMemo, + useState, + type PropsWithChildren, +} from 'react' + +import { useParams } from 'react-router-dom' + +import type { NutritionalBalancingModel } from '../../domain/models/nutritional-balancings-model' +import type { NutritionalBalancingFilters } from '../types/nutritional-balancing-types' + +type NutritionalBalancingContextValue = { + propertyId: number + filters: NutritionalBalancingFilters + handleChangeFilters: (newFilters: NutritionalBalancingFilters) => void + selectedNutritionalBalancing?: NutritionalBalancingModel + isOpenNewNutritionalBalancingScreen: boolean + isOpenEditNutritionalBalancingScreen: boolean + isOpenDeleteNutritionalBalancingContainer: boolean + openNewNutritionalBalancingScreen: () => void + closeNewNutritionalBalancingScreen: () => void + openEditNutritionalBalancingScreen: ( + nutritionalBalancing: NutritionalBalancingModel + ) => void + closeEditNutritionalBalancingScreen: () => void + openDeleteNutritionalBalancingContainer: ( + nutritionalBalancing: NutritionalBalancingModel + ) => void + closeDeleteNutritionalBalancingContainer: () => void +} + +export const NutritionalBalancingContext = + createContext( + {} as NutritionalBalancingContextValue + ) + +export function NutritionalBalancingProvider({ + children, +}: Readonly) { + const params = useParams<{ propertyId: string }>() + + const [filters, setFilters] = useState({}) + + const handleChangeFilters = useCallback( + (newFilters: NutritionalBalancingFilters) => { + setFilters((prevState) => ({ + ...prevState, + ...newFilters, + })) + }, + [] + ) + + const [ + isOpenNewNutritionalBalancingScreen, + setIsOpenNewNutritionalBalancingScreen, + ] = useState(false) + + const [ + isOpenEditNutritionalBalancingScreen, + setIsOpenEditNutritionalBalancingScreen, + ] = useState(false) + + const [ + isOpenDeleteNutritionalBalancingContainer, + setIsOpenDeleteNutritionalBalancingContainer, + ] = useState(false) + + const [selectedNutritionalBalancing, setSelectedNutritionalBalancing] = + useState() + + const openNewNutritionalBalancingScreen = useCallback(() => { + setIsOpenNewNutritionalBalancingScreen(true) + }, []) + + const closeNewNutritionalBalancingScreen = useCallback(() => { + setIsOpenNewNutritionalBalancingScreen(false) + }, []) + + const openEditNutritionalBalancingScreen = useCallback( + (nutritionalBalancing: NutritionalBalancingModel) => { + setSelectedNutritionalBalancing(nutritionalBalancing) + setIsOpenEditNutritionalBalancingScreen(true) + }, + [] + ) + + const closeEditNutritionalBalancingScreen = useCallback(() => { + setSelectedNutritionalBalancing(undefined) + setIsOpenEditNutritionalBalancingScreen(false) + }, []) + + const openDeleteNutritionalBalancingContainer = useCallback( + (nutritionalBalancing: NutritionalBalancingModel) => { + setSelectedNutritionalBalancing(nutritionalBalancing) + setIsOpenDeleteNutritionalBalancingContainer(true) + }, + [] + ) + + const closeDeleteNutritionalBalancingContainer = useCallback(() => { + setSelectedNutritionalBalancing(undefined) + setIsOpenDeleteNutritionalBalancingContainer(false) + }, []) + + const providerValues = useMemo( + () => ({ + propertyId: Number(params.propertyId), + filters, + handleChangeFilters, + selectedNutritionalBalancing, + isOpenNewNutritionalBalancingScreen, + isOpenEditNutritionalBalancingScreen, + isOpenDeleteNutritionalBalancingContainer, + openNewNutritionalBalancingScreen, + closeNewNutritionalBalancingScreen, + openEditNutritionalBalancingScreen, + closeEditNutritionalBalancingScreen, + openDeleteNutritionalBalancingContainer, + closeDeleteNutritionalBalancingContainer, + }), + [ + params.propertyId, + filters, + handleChangeFilters, + selectedNutritionalBalancing, + isOpenNewNutritionalBalancingScreen, + isOpenEditNutritionalBalancingScreen, + isOpenDeleteNutritionalBalancingContainer, + openNewNutritionalBalancingScreen, + closeNewNutritionalBalancingScreen, + openEditNutritionalBalancingScreen, + closeEditNutritionalBalancingScreen, + openDeleteNutritionalBalancingContainer, + closeDeleteNutritionalBalancingContainer, + ] + ) + + if (!params.propertyId) { + return null + } + + return ( + + {children} + + ) +} + +NutritionalBalancingProvider.displayName = 'NutritionalBalancingProvider' diff --git a/src/app/modules/nutritional-balancings/presentation/hooks/nutritional-balancing-context.hook.ts b/src/app/modules/nutritional-balancings/presentation/hooks/nutritional-balancing-context.hook.ts new file mode 100644 index 00000000..1210edb3 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/hooks/nutritional-balancing-context.hook.ts @@ -0,0 +1,15 @@ +import { useContext } from 'react' + +import { NutritionalBalancingContext } from '../contexts/nutritional-balancing-context' + +export function useNutritionalBalancingContext() { + const context = useContext(NutritionalBalancingContext) + + if (!context) { + throw new Error( + 'useNutritionalBalancingContext should be used within ' + ) + } + + return context +} From 558bd73450ebb75708341f14421c1a62193fce98 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 28 Oct 2025 19:25:47 -0300 Subject: [PATCH 11/82] feat(nutritional-balancings/presentation): add hook queries --- .../nutritional-balancing-query.hook.ts | 42 +++++++++++++ .../nutritional-balancings-query.hook.ts | 61 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/hooks/queries/nutritional-balancing-query.hook.ts create mode 100644 src/app/modules/nutritional-balancings/presentation/hooks/queries/nutritional-balancings-query.hook.ts diff --git a/src/app/modules/nutritional-balancings/presentation/hooks/queries/nutritional-balancing-query.hook.ts b/src/app/modules/nutritional-balancings/presentation/hooks/queries/nutritional-balancing-query.hook.ts new file mode 100644 index 00000000..8abf984e --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/hooks/queries/nutritional-balancing-query.hook.ts @@ -0,0 +1,42 @@ +import { useEffect } from 'react' + +import { useQuery } from '@tanstack/react-query' +import toast from 'react-hot-toast' + +import { makeRemoteGetNutritionalBalancingUseCase } from '../../../main/factories' + +type Props = { + id: number + propertyId: number +} + +export function useNutritionalBalancingQuery({ id, propertyId }: Props) { + const getNutritionalBalancingUseCase = + makeRemoteGetNutritionalBalancingUseCase() + + const { + data: nutritionalBalancing, + isError, + error, + isLoading, + refetch: refetchNutritionalBalancing, + } = useQuery({ + queryKey: ['nutritional-balancing', id], + queryFn: () => + getNutritionalBalancingUseCase.execute({ + propertyId, + nutritionalBalancingId: id, + }), + }) + + useEffect(() => { + if (isError) + toast.error(error?.message ?? 'Erro ao buscar balanceamento nutricional') + }, [error, isError]) + + return { + nutritionalBalancing, + isLoading, + refetchNutritionalBalancing, + } +} diff --git a/src/app/modules/nutritional-balancings/presentation/hooks/queries/nutritional-balancings-query.hook.ts b/src/app/modules/nutritional-balancings/presentation/hooks/queries/nutritional-balancings-query.hook.ts new file mode 100644 index 00000000..e407093d --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/hooks/queries/nutritional-balancings-query.hook.ts @@ -0,0 +1,61 @@ +import { useEffect } from 'react' + +import { useQuery } from '@tanstack/react-query' +import toast from 'react-hot-toast' + +import { makeRemoteGetNutritionalBalancingsUseCase } from '../../../main/factories' + +import type { + NutritionalBalancingFilters, + NutritionalBalancingSort, +} from '../../types/nutritional-balancing-types' + +type Props = { + propertyId: number + filters: NutritionalBalancingFilters + page: number + sort?: NutritionalBalancingSort +} + +export function useNutritionalBalancingsQuery({ + propertyId, + filters, + page, + sort, +}: Props) { + const getNutritionalBalancingsUseCase = + makeRemoteGetNutritionalBalancingsUseCase() + + const { + data, + isError, + error, + isLoading, + refetch: refetchNutritionalBalancings, + } = useQuery({ + queryKey: ['nutritional-balancings', { page, sort, filters }], + queryFn: () => + getNutritionalBalancingsUseCase.execute({ + propertyId, + pagination: { page }, + sort, + filters, + }), + }) + + useEffect(() => { + if (isError) + toast.error( + error?.message ?? 'Erro ao buscar balanceamentos nutricionais' + ) + }, [error, isError]) + + return { + nutritionalBalancings: data ?? { + resources: [], + totalPages: 1, + }, + isLoading, + refetchNutritionalBalancings, + } +} From 934b6d1c6ef588174bdd8504cc41ba1c6883d56e Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 28 Oct 2025 19:35:26 -0300 Subject: [PATCH 12/82] feat(nutritional-balancings/presentation): add data table component --- .../nutritional-balancing-data-table/index.ts | 1 + .../nutritional-balancing-data-table.hook.tsx | 142 ++++++++++++++++++ .../nutritional-balancing-data-table.tsx | 36 +++++ 3 files changed, 179 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-data-table/index.ts create mode 100644 src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-data-table/nutritional-balancing-data-table.hook.tsx create mode 100644 src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-data-table/nutritional-balancing-data-table.tsx diff --git a/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-data-table/index.ts b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-data-table/index.ts new file mode 100644 index 00000000..a8a0adc1 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-data-table/index.ts @@ -0,0 +1 @@ +export * from './nutritional-balancing-data-table' diff --git a/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-data-table/nutritional-balancing-data-table.hook.tsx b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-data-table/nutritional-balancing-data-table.hook.tsx new file mode 100644 index 00000000..b1d0e44e --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-data-table/nutritional-balancing-data-table.hook.tsx @@ -0,0 +1,142 @@ +import { useMemo, useState } from 'react' + +import { format } from 'date-fns' +import { MoreHorizontalIcon, PencilIcon, Trash2Icon } from 'lucide-react' + +import { floatMask } from '@/core/masker' +import { DropdownMenu } from '@/core/presentation/components/ui' +import { useDebounce } from '@/core/presentation/hooks' + +import { useNutritionalBalancingContext } from '../../hooks/nutritional-balancing-context.hook' +import { useNutritionalBalancingsQuery } from '../../hooks/queries/nutritional-balancings-query.hook' + +import type { NutritionalBalancingModel } from '../../../domain/models/nutritional-balancings-model' +import type { NutritionalBalancingSort } from '../../types/nutritional-balancing-types' +import type { ColumnDef } from '@tanstack/react-table' + +export function useNutritionalBalancingDataTable() { + const { + propertyId, + filters, + openEditNutritionalBalancingScreen, + openDeleteNutritionalBalancingContainer, + } = useNutritionalBalancingContext() + + const [page, setPage] = useState(1) + const [sort, setSort] = useState() + + const debouncedFilters = useDebounce({ value: filters }) + + const { isLoading, nutritionalBalancings } = useNutritionalBalancingsQuery({ + propertyId, + filters: debouncedFilters, + page, + sort, + }) + + const columns = useMemo[]>( + () => [ + { + accessorKey: 'date', + header: 'Data', + cell: ({ row }) => { + const { original: nutritionalBalancing } = row + + return nutritionalBalancing.date + ? format(nutritionalBalancing.date, 'dd/MM/yyyy') + : '-' + }, + }, + { + accessorKey: 'animal', + header: 'Animal', + }, + { + accessorKey: 'breed', + header: 'Tipo de Raça', + }, + { + accessorKey: 'weight', + header: 'Peso', + cell: ({ row }) => { + const { original: nutritionalBalancing } = row + + return floatMask(nutritionalBalancing.weight, 'kg') + }, + }, + { + accessorKey: 'milkProduction', + header: 'Produção de Leite', + cell: ({ row }) => { + const { original: nutritionalBalancing } = row + + return floatMask(nutritionalBalancing.milkProduction, 'kg/dia') + }, + }, + { + accessorKey: 'estimatedMilkProduction', + header: 'Produção de Leite Estimada', + cell: ({ row }) => { + const { original: nutritionalBalancing } = row + + return floatMask( + nutritionalBalancing.estimatedMilkProduction, + 'kg/dia' + ) + }, + }, + { + id: 'row-actions', + header: '', + cell: ({ row }) => { + const { original: nutritionalBalancing } = row + + return ( + + + + + + { + event.stopPropagation() + openEditNutritionalBalancingScreen(nutritionalBalancing) + }} + > + Editar + + + { + event.stopPropagation() + openDeleteNutritionalBalancingContainer( + nutritionalBalancing + ) + }} + > + Excluir + + + + ) + }, + }, + ], + [ + openDeleteNutritionalBalancingContainer, + openEditNutritionalBalancingScreen, + ] + ) + + return { + columns, + nutritionalBalancings, + isLoading, + page, + sort, + setSort, + setPage, + } +} diff --git a/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-data-table/nutritional-balancing-data-table.tsx b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-data-table/nutritional-balancing-data-table.tsx new file mode 100644 index 00000000..b5ae1732 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-data-table/nutritional-balancing-data-table.tsx @@ -0,0 +1,36 @@ +import { DataTable } from '@/core/presentation/components/ui' + +import { useNutritionalBalancingDataTable } from './nutritional-balancing-data-table.hook' + +import type { NutritionalBalancingModel } from '../../../domain/models/nutritional-balancings-model' + +export function NutritionalBalancingDataTable() { + const { + columns, + nutritionalBalancings, + isLoading, + page, + sort, + setSort, + setPage, + } = useNutritionalBalancingDataTable() + + return ( + + columns={columns} + data={nutritionalBalancings.resources} + totalPages={nutritionalBalancings.totalPages} + pagination={{ + currentPage: page, + onPageChange: setPage, + }} + sorting={{ + currentSorting: sort, + onSorting: setSort, + }} + loading={isLoading} + /> + ) +} + +NutritionalBalancingDataTable.displayName = 'NutritionalBalancingDataTable' From 31bf9c1396fbedfe959715abdb23297af169bac7 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 28 Oct 2025 19:40:40 -0300 Subject: [PATCH 13/82] feat(nutritional-balancings/presentation): add delete dialog component --- .../index.ts | 1 + .../nutritional-balancing-delete-dialog.tsx | 84 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-delete-dialog/index.ts create mode 100644 src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-delete-dialog/nutritional-balancing-delete-dialog.tsx diff --git a/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-delete-dialog/index.ts b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-delete-dialog/index.ts new file mode 100644 index 00000000..ba9cf2fc --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-delete-dialog/index.ts @@ -0,0 +1 @@ +export * from './nutritional-balancing-delete-dialog' diff --git a/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-delete-dialog/nutritional-balancing-delete-dialog.tsx b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-delete-dialog/nutritional-balancing-delete-dialog.tsx new file mode 100644 index 00000000..0253cf23 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-delete-dialog/nutritional-balancing-delete-dialog.tsx @@ -0,0 +1,84 @@ +import { useCallback } from 'react' + +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { format } from 'date-fns' +import toast from 'react-hot-toast' + +import { AlertDialog } from '@/core/presentation/components/ui' + +import { makeRemoteDeleteNutritionalBalancingUseCase } from '../../../main/factories' +import { useNutritionalBalancingContext } from '../../hooks/nutritional-balancing-context.hook' + +export function NutritionalBalancingDeleteDialog() { + const deleteNutritionalBalancingUseCase = + makeRemoteDeleteNutritionalBalancingUseCase() + + const { + propertyId, + selectedNutritionalBalancing, + isOpenDeleteNutritionalBalancingContainer, + closeDeleteNutritionalBalancingContainer, + } = useNutritionalBalancingContext() + + const queryClient = useQueryClient() + + const { mutateAsync: mutateHandleDeleteNutritionalBalancing } = useMutation({ + mutationFn: deleteNutritionalBalancingUseCase.execute, + }) + + const handleDeleteNutritionalBalancing = useCallback(async () => { + if (!selectedNutritionalBalancing) { + toast.error('Erro ao remover balanceamento nutricional') + return + } + + try { + await mutateHandleDeleteNutritionalBalancing({ + propertyId, + nutritionalBalancingId: selectedNutritionalBalancing.id, + }) + + queryClient.invalidateQueries({ + queryKey: ['nutritional-balancings'], + exact: false, + }) + + toast.success('Balanceamento nutricional removido com sucesso') + } catch { + toast.error('Erro ao remover balanceamento nutricional') + } finally { + closeDeleteNutritionalBalancingContainer() + } + }, [ + closeDeleteNutritionalBalancingContainer, + mutateHandleDeleteNutritionalBalancing, + propertyId, + queryClient, + selectedNutritionalBalancing, + ]) + + return ( + + + + {`Deseja remover o balanceamento nutricional do animal ${selectedNutritionalBalancing?.animal} no dia ${selectedNutritionalBalancing?.date ? format(selectedNutritionalBalancing?.date, 'dd/MM/yyyy') : '-'}?`} + + Não será possível desfazer essa ação! + + + + Cancelar + + Remover + + + + + ) +} + +NutritionalBalancingDeleteDialog.displayName = + 'NutritionalBalancingDeleteDialog' From fe65a41e8c49de0610a9f9ee0c4bcfda51768a4b Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Wed, 29 Oct 2025 10:27:59 -0300 Subject: [PATCH 14/82] feat(core/mocks/utils): add not in filter --- src/core/mocks/utils/filter-data.ts | 66 +++++++++++++++++------------ 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/src/core/mocks/utils/filter-data.ts b/src/core/mocks/utils/filter-data.ts index 26c9a9d5..b37a1404 100644 --- a/src/core/mocks/utils/filter-data.ts +++ b/src/core/mocks/utils/filter-data.ts @@ -1,55 +1,67 @@ -import { isSameDay } from 'date-fns' - import { isValidDate } from './date' import { getNestedValue } from './get-nested-value' +import { valueEquals } from './value-equals' +import { valueIncludes } from './value-includes' -import type { Filters } from '@/core/domain/types' +import type { FilterType } from '@/core/domain/types' +import type { MockFilter } from '@/core/mocks/types/mock-params-type' -type FilterValue = { +type ActiveFilter = { field: keyof TData value: unknown + type: FilterType } -// Implemented only the LIKE filter for simplicity +// Supports LIKE (default) and NOT_IN. Extend as needed for other operators. export function filterData( - filters: Filters, + filters: Array>, data: TData[] ) { - const activeFilters = Object.values(filters).filter( - (filterValue): filterValue is FilterValue => { - return ( - !!filterValue && - typeof filterValue === 'object' && - 'value' in filterValue && - filterValue.value !== undefined && - filterValue.value !== null && - filterValue.value !== '' - ) - } - ) + const activeFilters: ActiveFilter[] = filters.reduce((acc, filter) => { + if (!filter || typeof filter !== 'object') return acc + + const { field, value, type } = filter + if (value === undefined || value === null || value === '') return acc + + acc.push({ + field: field as keyof TData, + value, + type: (type ?? 'LIKE') as FilterType, + }) + + return acc + }, [] as ActiveFilter[]) if (activeFilters.length === 0) { return data } return data.filter((item) => - activeFilters.every((filterValue) => { - const { field, value } = filterValue + activeFilters.every(({ field, value, type }) => { const itemValue = getNestedValue(item, String(field)) if (itemValue === null || itemValue === undefined) { return false } + if (type === 'NOT_IN') { + const values = Array.isArray(value) ? value : [value] + + if (Array.isArray(itemValue)) { + return itemValue.every( + (item) => !values.some((value) => valueEquals(item, value)) + ) + } + + return !values.some((value) => valueEquals(itemValue, value)) + } + if (isValidDate(itemValue)) { - const valueDate = new Date(String(value)) - return isSameDay(itemValue, valueDate) + return valueEquals(itemValue, value) } if (Array.isArray(itemValue)) { - return itemValue.some((element) => - String(element).toLowerCase().includes(String(value).toLowerCase()) - ) + return valueIncludes(itemValue, value) } if ( @@ -57,9 +69,7 @@ export function filterData( typeof itemValue === 'number' || typeof itemValue === 'boolean' ) { - return String(itemValue) - .toLowerCase() - .includes(String(value).toLowerCase()) + return valueIncludes(itemValue, value) } return false From 4553033bf029a61b90395e70dd79b4bcef68c07c Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Wed, 29 Oct 2025 10:28:40 -0300 Subject: [PATCH 15/82] feat(core/mocks/utils): add value equals and value includes --- src/core/mocks/utils/value-equals.ts | 14 ++++++++++++++ src/core/mocks/utils/value-includes.ts | 15 +++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/core/mocks/utils/value-equals.ts create mode 100644 src/core/mocks/utils/value-includes.ts diff --git a/src/core/mocks/utils/value-equals.ts b/src/core/mocks/utils/value-equals.ts new file mode 100644 index 00000000..3c13dc2e --- /dev/null +++ b/src/core/mocks/utils/value-equals.ts @@ -0,0 +1,14 @@ +import { isValidDate, isSameDay } from './date' + +export function valueEquals( + firstValue: unknown, + secondValue: unknown +): boolean { + if (isValidDate(firstValue) || isValidDate(secondValue)) { + const firstValueDate = new Date(firstValue as string | Date) + const secondValueDate = new Date(secondValue as string | Date) + return isSameDay(firstValueDate, secondValueDate) + } + + return String(firstValue).toLowerCase() === String(secondValue).toLowerCase() +} diff --git a/src/core/mocks/utils/value-includes.ts b/src/core/mocks/utils/value-includes.ts new file mode 100644 index 00000000..31b95b2d --- /dev/null +++ b/src/core/mocks/utils/value-includes.ts @@ -0,0 +1,15 @@ +export function valueIncludes(haystack: unknown, needle: unknown): boolean { + if (Array.isArray(haystack)) { + return haystack.some((value) => valueIncludes(value, needle)) + } + + if ( + typeof haystack === 'string' || + typeof haystack === 'number' || + typeof haystack === 'boolean' + ) { + return String(haystack).toLowerCase().includes(String(needle).toLowerCase()) + } + + return false +} From c3f037484bb8b726125964b3072270bfbb12b21a Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Wed, 29 Oct 2025 10:33:49 -0300 Subject: [PATCH 16/82] feat(core): filters could be an array --- src/core/domain/types/filter-type.ts | 2 +- .../http/api-http-client/api-http-client.ts | 62 ++++++++++++------- src/core/mocks/types/mock-params-type.ts | 10 ++- 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/src/core/domain/types/filter-type.ts b/src/core/domain/types/filter-type.ts index f9a19b07..ab3fbcd0 100644 --- a/src/core/domain/types/filter-type.ts +++ b/src/core/domain/types/filter-type.ts @@ -14,7 +14,7 @@ export type FilterType = | 'BETWEEN' export type FilterValue = { - value: T + value: T | T[] type: FilterType } diff --git a/src/core/infra/http/api-http-client/api-http-client.ts b/src/core/infra/http/api-http-client/api-http-client.ts index 9d6fb656..4d7a8c07 100644 --- a/src/core/infra/http/api-http-client/api-http-client.ts +++ b/src/core/infra/http/api-http-client/api-http-client.ts @@ -9,7 +9,13 @@ import { env } from '@/core/env' import { authInterceptorRequest } from './interceptors/auth-interceptor' -import type { ApiSort } from '@/core/domain/types' +import type { ApiSort, NestedKeyOf } from '@/core/domain/types' + +type ApiFilterTriplet = { + field: NestedKeyOf + value: unknown + type: string +} export const ITEMS_PER_PAGE = 10 @@ -45,38 +51,48 @@ export class ApiHttpClient< const { value } = filter return value !== undefined && value !== null && value !== '' }) - .reduce>( - (acc, field) => { - const filter = filters[field] - if (!filter) return acc + .reduce>>((acc, field) => { + const filter = filters[field] + if (!filter) return acc - const { value, type = 'EQUALS' } = filter + const { value, type = 'LIKE' } = filter - if (!mapApiProperties || !(field in mapApiProperties)) { - return acc - } - - const mappedField = mapApiProperties[field] as string + if (!mapApiProperties || !(field in mapApiProperties)) { + return acc + } - if (value instanceof Date) { - acc.push({ - field: mappedField, - value: value.toISOString(), - type, - }) - return acc - } + const mappedField = mapApiProperties[ + field + ] as ApiFilterTriplet['field'] + if (value instanceof Date) { acc.push({ field: mappedField, - value: String(value), + value: value.toISOString(), type, }) + return acc + } + if (Array.isArray(value)) { + acc.push({ + field: mappedField, + value: value.map((item) => + item instanceof Date ? item.toISOString() : item + ), + type, + }) return acc - }, - [] - ) + } + + acc.push({ + field: mappedField, + value, + type, + }) + + return acc + }, []) : undefined const sortInfo: ApiSort | undefined = diff --git a/src/core/mocks/types/mock-params-type.ts b/src/core/mocks/types/mock-params-type.ts index 341fce92..eb72e8d6 100644 --- a/src/core/mocks/types/mock-params-type.ts +++ b/src/core/mocks/types/mock-params-type.ts @@ -1,7 +1,13 @@ -import type { Filters, ApiSort } from '@/core/domain/types' +import type { ApiSort } from '@/core/domain/types' + +export type MockFilter = { + field: K + value: TData[K] | Array + type: string +} export type MockParams = { - filters: Filters + filters: Array> sort: ApiSort page: number rows: number From d2bdab52f13d5f9f7bd0af779be8b90a2eba975b Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Wed, 29 Oct 2025 10:34:28 -0300 Subject: [PATCH 17/82] feat: add extra data when get all animals and map on domain --- scripts/seed/data/animals.mjs | 18 ++++++++++++++++++ .../use-cases/remote-get-animals-use-case.ts | 7 +++++++ .../animals/domain/models/animals-model.ts | 9 +++++++++ .../hooks/queries/all-animals-query.hook.ts | 9 ++++++++- 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/scripts/seed/data/animals.mjs b/scripts/seed/data/animals.mjs index 4ec7680f..48d4865c 100644 --- a/scripts/seed/data/animals.mjs +++ b/scripts/seed/data/animals.mjs @@ -11,5 +11,23 @@ export const animalsData = Array.from( id: index + 1, name: faker.lorem.word(), breed: faker.animal.cow(), + weight: faker.number + .float({ + min: 1, + max: 1000, + }) + .toFixed(2), + ecc: faker.number + .float({ + min: 1, + max: 10, + }) + .toFixed(2), + milkProduction: faker.number + .float({ + min: 1, + max: 50, + }) + .toFixed(2), }) ) diff --git a/src/app/modules/animals/data/use-cases/remote-get-animals-use-case.ts b/src/app/modules/animals/data/use-cases/remote-get-animals-use-case.ts index 96ef4b4f..9c011436 100644 --- a/src/app/modules/animals/data/use-cases/remote-get-animals-use-case.ts +++ b/src/app/modules/animals/data/use-cases/remote-get-animals-use-case.ts @@ -29,8 +29,12 @@ export class RemoteGetAnimalsUseCase implements GetAnimalsUseCase { sort, }) => { const mapApiProperties: MapApiProperties = { + id: 'id', name: 'name', breed: 'breed', + ecc: 'ecc', + weight: 'weight', + milkProduction: 'milkProduction', } const url = this.url.replace(':propertyId', String(propertyId)) @@ -50,6 +54,9 @@ export class RemoteGetAnimalsUseCase implements GetAnimalsUseCase { id: item.id, name: item.name, breed: item.breed, + weight: item.weight, + ecc: item.ecc, + milkProduction: item.milkProduction, })), totalPages: Math.ceil(body.numberOfElements / body.pageable.pageSize), } diff --git a/src/app/modules/animals/domain/models/animals-model.ts b/src/app/modules/animals/domain/models/animals-model.ts index e390f0bc..3991acb5 100644 --- a/src/app/modules/animals/domain/models/animals-model.ts +++ b/src/app/modules/animals/domain/models/animals-model.ts @@ -3,16 +3,25 @@ import type { Option, WithId } from '@/core/domain/types' export type AnimalDetailsModel = { name: string breed: Option + weight: string + ecc: string + milkProduction: string } export type AnimalDetailsApiResponse = { name: string breed: Option + weight: string + ecc: string + milkProduction: string } export type AnimalModel = WithId<{ name: string breed: string + weight: string + ecc: string + milkProduction: string }> // todo: refactor to be consistent with Api response diff --git a/src/app/modules/animals/presentation/hooks/queries/all-animals-query.hook.ts b/src/app/modules/animals/presentation/hooks/queries/all-animals-query.hook.ts index 1cb4ea6c..d2497e10 100644 --- a/src/app/modules/animals/presentation/hooks/queries/all-animals-query.hook.ts +++ b/src/app/modules/animals/presentation/hooks/queries/all-animals-query.hook.ts @@ -38,7 +38,14 @@ export function useAllAnimalsQuery({ propertyId, filters }: Props) { return { allAnimals: - data?.resources.map((resource) => toOption(resource, 'name')) ?? [], + data?.resources.map((resource) => + toOption(resource, 'name', { + breed: resource.breed, + weight: resource.weight, + ecc: resource.ecc, + milkProduction: resource.milkProduction, + }) + ) ?? [], isLoading, refetchAllAnimals, } From 87deb6843ab8412aa9170a054bb3e602b0bfb479 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Wed, 29 Oct 2025 11:30:47 -0300 Subject: [PATCH 18/82] feat: add badge component --- .../components/ui/badge/badge.tsx | 49 ++++++++++ .../components/ui/badge/index.stories.tsx | 95 +++++++++++++++++++ .../presentation/components/ui/badge/index.ts | 1 + 3 files changed, 145 insertions(+) create mode 100644 src/core/presentation/components/ui/badge/badge.tsx create mode 100644 src/core/presentation/components/ui/badge/index.stories.tsx create mode 100644 src/core/presentation/components/ui/badge/index.ts diff --git a/src/core/presentation/components/ui/badge/badge.tsx b/src/core/presentation/components/ui/badge/badge.tsx new file mode 100644 index 00000000..3b98ed95 --- /dev/null +++ b/src/core/presentation/components/ui/badge/badge.tsx @@ -0,0 +1,49 @@ +import * as React from 'react' + +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/core/utils' + +const badgeVariants = cva( + 'inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', + secondary: + 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', + destructive: + 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +) + +export type BadgeProps = React.ComponentProps<'span'> & + VariantProps & { + asChild?: boolean + } + +export const Badge = React.forwardRef( + ({ className, variant, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'span' + + return ( + + ) + } +) + +Badge.displayName = 'Badge' diff --git a/src/core/presentation/components/ui/badge/index.stories.tsx b/src/core/presentation/components/ui/badge/index.stories.tsx new file mode 100644 index 00000000..868381b8 --- /dev/null +++ b/src/core/presentation/components/ui/badge/index.stories.tsx @@ -0,0 +1,95 @@ +import { Check, Info, ShieldAlert } from 'lucide-react' + +import { Badge, type BadgeProps } from './badge' + +import type { Meta, StoryObj } from '@storybook/react' + +export default { + title: 'Components/UI/Badge', + component: Badge, + tags: ['autodocs'], + args: { + children: 'Badge', + variant: 'default', + }, + argTypes: { + variant: { + control: { type: 'select' }, + options: ['default', 'secondary', 'destructive', 'outline'], + }, + asChild: { + control: { type: 'boolean' }, + }, + className: { + control: { type: 'text' }, + }, + children: { + control: { type: 'text' }, + }, + }, +} as Meta + +type Story = StoryObj + +export const Default: Story = {} + +export const Secondary: Story = { + args: { + variant: 'secondary', + children: 'Secondary', + }, +} + +export const Destructive: Story = { + args: { + variant: 'destructive', + children: 'Destructive', + }, +} + +export const Outline: Story = { + args: { + variant: 'outline', + children: 'Outline', + }, +} + +export const WithIcon: Story = { + render: (args: BadgeProps) => ( +
+ + + Sucesso + + + + Informação + + + + Erro + +
+ ), +} + +export const AsLink: Story = { + args: { + asChild: true, + }, + render: (args: BadgeProps) => ( + + {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} + e.preventDefault()}> + Link como badge + + + ), +} + +export const LongText: Story = { + args: { + children: + 'Texto bem longo para verificar truncamento e nowrap do badge em diferentes larguras de container', + }, +} diff --git a/src/core/presentation/components/ui/badge/index.ts b/src/core/presentation/components/ui/badge/index.ts new file mode 100644 index 00000000..883be942 --- /dev/null +++ b/src/core/presentation/components/ui/badge/index.ts @@ -0,0 +1 @@ +export * from './badge' From 2e74cae8ce5a6bd2739e35035ea77c68637b45ee Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Wed, 29 Oct 2025 11:31:17 -0300 Subject: [PATCH 19/82] fix: storybook styles config --- .storybook/preview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.storybook/preview.js b/.storybook/preview.js index 33b0946e..5a0eb262 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,6 +1,6 @@ import { withThemeByClassName } from '@storybook/addon-themes' -import '../src/styles/globals.css' +import '../src/core/styles/globals.css' import { withRouter } from './decorators' export const parameters = { From 2fed4e446144581e158075146379859362832839 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Wed, 29 Oct 2025 20:08:30 -0300 Subject: [PATCH 20/82] fix: badge secondary variant --- src/core/presentation/components/ui/badge/badge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/presentation/components/ui/badge/badge.tsx b/src/core/presentation/components/ui/badge/badge.tsx index 3b98ed95..cf1d2c5d 100644 --- a/src/core/presentation/components/ui/badge/badge.tsx +++ b/src/core/presentation/components/ui/badge/badge.tsx @@ -13,7 +13,7 @@ const badgeVariants = cva( default: 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', secondary: - 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', + 'border-transparent bg-slate-200 text-secondary-foreground [a&]:hover:bg-slate-200/90', destructive: 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', outline: From c916e2fd46eb29921ea9948312bbdd3542c4b593 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Wed, 29 Oct 2025 21:12:35 -0300 Subject: [PATCH 21/82] feat(nutritional-balancings/presentation): add without nutritional balancing component --- .../without-nutritional-balancing/index.ts | 1 + .../without-nutritional-balancing.tsx | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/components/without-nutritional-balancing/index.ts create mode 100644 src/app/modules/nutritional-balancings/presentation/components/without-nutritional-balancing/without-nutritional-balancing.tsx diff --git a/src/app/modules/nutritional-balancings/presentation/components/without-nutritional-balancing/index.ts b/src/app/modules/nutritional-balancings/presentation/components/without-nutritional-balancing/index.ts new file mode 100644 index 00000000..28136d67 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/without-nutritional-balancing/index.ts @@ -0,0 +1 @@ +export * from './without-nutritional-balancing' diff --git a/src/app/modules/nutritional-balancings/presentation/components/without-nutritional-balancing/without-nutritional-balancing.tsx b/src/app/modules/nutritional-balancings/presentation/components/without-nutritional-balancing/without-nutritional-balancing.tsx new file mode 100644 index 00000000..ed1af6ef --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/without-nutritional-balancing/without-nutritional-balancing.tsx @@ -0,0 +1,15 @@ +import { Card } from '@/core/presentation/components/ui' + +export function WithoutNutritionalBalancing() { + return ( + + +

+ Selecione um animal para iniciar o balanceamento nutricional. +

+
+
+ ) +} + +WithoutNutritionalBalancing.displayName = 'WithoutNutritionalBalancing' From b6bf1e3b81809e223ffd32f2ed15368dbfc7d150 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Wed, 29 Oct 2025 21:12:55 -0300 Subject: [PATCH 22/82] feat(nutritional-balancings/presentation): add nutritional balancing summary component --- .../nutritional-balancing-summary/index.ts | 1 + .../nutritional-balancing-summary.tsx | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-summary/index.ts create mode 100644 src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-summary/nutritional-balancing-summary.tsx diff --git a/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-summary/index.ts b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-summary/index.ts new file mode 100644 index 00000000..ab91a511 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-summary/index.ts @@ -0,0 +1 @@ +export * from './nutritional-balancing-summary' diff --git a/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-summary/nutritional-balancing-summary.tsx b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-summary/nutritional-balancing-summary.tsx new file mode 100644 index 00000000..1e8b693c --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-summary/nutritional-balancing-summary.tsx @@ -0,0 +1,33 @@ +import { Card } from '@/core/presentation/components/ui' + +import type { Option } from '@/core/domain/types' + +type NutritionalBalancingSummaryProps = { + items: Option[] +} + +export function NutritionalBalancingSummary({ + items, +}: Readonly) { + return ( + + + Resumo Nutricional + + + {items.map((item) => ( + + + {item.label} + + + {item.value} + + + ))} + + + ) +} + +NutritionalBalancingSummary.displayName = 'NutritionalBalancingSummary' From 309d0c5da589f67f6c2349e5412f23d483866a2c Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Wed, 29 Oct 2025 21:13:07 -0300 Subject: [PATCH 23/82] feat(nutritional-balancings/presentation): add nutritional balancing animal navigation component --- .../index.ts | 1 + ...utritional-balancing-animal-navigation.tsx | 72 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-animal-navigation/index.ts create mode 100644 src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-animal-navigation/nutritional-balancing-animal-navigation.tsx diff --git a/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-animal-navigation/index.ts b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-animal-navigation/index.ts new file mode 100644 index 00000000..dc4d0d11 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-animal-navigation/index.ts @@ -0,0 +1 @@ +export * from './nutritional-balancing-animal-navigation' diff --git a/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-animal-navigation/nutritional-balancing-animal-navigation.tsx b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-animal-navigation/nutritional-balancing-animal-navigation.tsx new file mode 100644 index 00000000..84233d14 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-animal-navigation/nutritional-balancing-animal-navigation.tsx @@ -0,0 +1,72 @@ +import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react' + +import { Button, Card } from '@/core/presentation/components/ui' +import { Badge } from '@/core/presentation/components/ui/badge' + +type NutritionalBalancingAnimalNavigationProps = { + currentNutritionalBalancingIndex: number + totalNutritionalBalancings: number + animalName: string + handleSelectNutritionalBalancing: (index: number) => void +} + +export function NutritionalBalancingAnimalNavigation({ + currentNutritionalBalancingIndex, + totalNutritionalBalancings, + animalName, + handleSelectNutritionalBalancing, +}: Readonly) { + const hasPrevious = + currentNutritionalBalancingIndex !== null && + currentNutritionalBalancingIndex > 0 + const hasNext = + currentNutritionalBalancingIndex !== null && + currentNutritionalBalancingIndex < totalNutritionalBalancings - 1 + + return ( + + + + +
+

+ {animalName} +

+ + + {`Animal ${currentNutritionalBalancingIndex + 1} de ${totalNutritionalBalancings}`} + +
+ + +
+
+ ) +} + +NutritionalBalancingAnimalNavigation.displayName = + 'NutritionalBalancingAnimalNavigation' From c63f28c627ca5fa9b871746aff4d36e19b079082 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Wed, 29 Oct 2025 21:15:45 -0300 Subject: [PATCH 24/82] feat(nutritional-balancings/presentation): add nutritional balancing animal information component --- .../index.ts | 1 + ...tritional-balancing-animal-information.tsx | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-animal-information/index.ts create mode 100644 src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-animal-information/nutritional-balancing-animal-information.tsx diff --git a/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-animal-information/index.ts b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-animal-information/index.ts new file mode 100644 index 00000000..5de67d28 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-animal-information/index.ts @@ -0,0 +1 @@ +export * from './nutritional-balancing-animal-information' diff --git a/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-animal-information/nutritional-balancing-animal-information.tsx b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-animal-information/nutritional-balancing-animal-information.tsx new file mode 100644 index 00000000..8357dff0 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-animal-information/nutritional-balancing-animal-information.tsx @@ -0,0 +1,32 @@ +import { Card } from '@/core/presentation/components/ui' + +import type { Option } from '@/core/domain/types' + +type NutritionalBalancingAnimalInformationProps = { + items: Option[] +} + +export function NutritionalBalancingAnimalInformation({ + items, +}: Readonly) { + return ( + + + Informações do Animal + + + {items.map((item) => ( +
+ + {item.label} + + {item.value} +
+ ))} +
+
+ ) +} + +NutritionalBalancingAnimalInformation.displayName = + 'NutritionalBalancingAnimalInformation' From f56ddd5b6b110024caff5d5b89111bf2861781d8 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Thu, 30 Oct 2025 19:24:20 -0300 Subject: [PATCH 25/82] feat(nutritional-balancings/presentation): add new nutritional balancing header --- .../new-nutritional-balancing-header.tsx | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/components/new-nutritional-balancing-header/new-nutritional-balancing-header.tsx diff --git a/src/app/modules/nutritional-balancings/presentation/components/new-nutritional-balancing-header/new-nutritional-balancing-header.tsx b/src/app/modules/nutritional-balancings/presentation/components/new-nutritional-balancing-header/new-nutritional-balancing-header.tsx new file mode 100644 index 00000000..c3dd94cb --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/new-nutritional-balancing-header/new-nutritional-balancing-header.tsx @@ -0,0 +1,131 @@ +import { useState, useMemo } from 'react' + +import { useFormContext } from 'react-hook-form' + +import { useAllAnimalsQuery } from '@/app/modules/animals/presentation/hooks/queries/all-animals-query.hook' +import { + Breadcrumb, + Button, + Combobox, + Form, +} from '@/core/presentation/components/ui' +import { useDebounce } from '@/core/presentation/hooks' + +import { useNutritionalBalancingContext } from '../../hooks/nutritional-balancing-context.hook' +import { createEmptyNutritionalBalancingEntry } from '../../utils/create-empty-nutritional-balancing-entry' +import { type NutritionalBalancingFormSchema } from '../../validations/nutritional-balancing-form-schema' + +type NewNutritionalBalancingHeaderProps = { + buttonDisabled: boolean + nutritionalBalancings: NutritionalBalancingFormSchema['nutritionalBalancings'] + handleAppendNutritionalBalancing: ( + data: NutritionalBalancingFormSchema['nutritionalBalancings'][number] + ) => void +} + +export function NewNutritionalBalancingHeader({ + buttonDisabled, + nutritionalBalancings, + handleAppendNutritionalBalancing, +}: Readonly) { + const form = useFormContext() + const { propertyId, closeNewNutritionalBalancingScreen } = + useNutritionalBalancingContext() + + const [searchAnimal, setSearchAnimal] = useState('') + const debouncedAnimal = useDebounce({ value: searchAnimal }) + const selectedIds = useMemo( + () => + nutritionalBalancings + .map((field) => field?.animal?.id) + .filter((id): id is number => typeof id === 'number' && id > 0), + [nutritionalBalancings] + ) + + const { allAnimals, isLoading } = useAllAnimalsQuery({ + propertyId, + filters: { + name: { + value: debouncedAnimal, + type: 'LIKE', + }, + ...(selectedIds.length + ? { + id: { + value: selectedIds, + type: 'NOT_IN', + }, + } + : {}), + }, + }) + + return ( +
+ + + + Balanceamentos Nutricionais + + + + Novo + + + + +
+ + + { + const { error } = fieldState + + return ( + + + { + const nextEntry = createEmptyNutritionalBalancingEntry({ + animal: { + id: selectedAnimal.value, + name: selectedAnimal.label, + breed: selectedAnimal.extraData?.breed ?? '', + ecc: selectedAnimal.extraData?.ecc ?? '', + weight: selectedAnimal.extraData?.weight ?? '', + milkProduction: + selectedAnimal.extraData?.milkProduction ?? '', + estimatedMilkProduction: '', + }, + }) + handleAppendNutritionalBalancing(nextEntry) + }} + isError={!!error} + placeholder="Selecione o animal" + emptyMessage="Nenhum animal encontrado" + searchPlaceholder="Buscar animal" + /> + + + + ) + }} + /> +
+
+ ) +} + +NewNutritionalBalancingHeader.displayName = 'NewNutritionalBalancingHeader' From ba99cd7c716ae992138f4864325ee0ed9f244baa Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Thu, 30 Oct 2025 19:24:58 -0300 Subject: [PATCH 26/82] feat(nutritional-balancings/presentation): add summary tab --- .../presentation/tabs/summary-tab.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/tabs/summary-tab.tsx diff --git a/src/app/modules/nutritional-balancings/presentation/tabs/summary-tab.tsx b/src/app/modules/nutritional-balancings/presentation/tabs/summary-tab.tsx new file mode 100644 index 00000000..a3f0c283 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/tabs/summary-tab.tsx @@ -0,0 +1,22 @@ +import { NutritionalBalancingAnimalInformation } from '../components/nutritional-balancing-animal-information' +import { NutritionalBalancingSummary } from '../components/nutritional-balancing-summary' + +import type { Option } from '@/core/domain/types' + +type SummaryTabProps = { + animalInformationItems: Option[] + nutritionalSummaryItems: Option[] +} + +export function SummaryTab({ + animalInformationItems, + nutritionalSummaryItems, +}: Readonly) { + return ( +
+ + + +
+ ) +} From 5312d06c4e6854ae7b91226272353c20a05d9723 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Thu, 30 Oct 2025 19:25:31 -0300 Subject: [PATCH 27/82] feat(nutritional-balancings/presentation): add util function to create empty nutritional balancing --- ...reate-empty-nutritional-balancing-entry.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/utils/create-empty-nutritional-balancing-entry.ts diff --git a/src/app/modules/nutritional-balancings/presentation/utils/create-empty-nutritional-balancing-entry.ts b/src/app/modules/nutritional-balancings/presentation/utils/create-empty-nutritional-balancing-entry.ts new file mode 100644 index 00000000..cb2d3ebb --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/utils/create-empty-nutritional-balancing-entry.ts @@ -0,0 +1,60 @@ +import type { NutritionalBalancingSchema } from '../validations/nutritional-balancing-form-schema' + +export function createEmptyNutritionalBalancingEntry( + nutritionalBalancing: Partial +) { + return { + ...nutritionalBalancing, + animal: { + id: 0, + name: '', + breed: '', + ecc: '', + weight: '', + milkProduction: '', + estimatedMilkProduction: '', + ...nutritionalBalancing.animal, + }, + summary: { + totalDryMatter: '10', + etherExtractPercent: '20', + forageDryMatterPercent: '30', + concentrateDryMatterPercent: '40', + nonFibrousCarbohydratesPercent: '50', + rdpTdnRatio: '60', + }, + evaluations: [ + { + nutrientName: 'IMS (Ingestão de Matéria Seca)', + providedValue: '10', + requiredValue: '15', + evaluationStatus: 'BELOW' as const, + }, + { + nutrientName: 'NDT (Nutrientes Digestíveis Totais)', + providedValue: '15', + requiredValue: '15', + evaluationStatus: 'NORMAL' as const, + }, + { + nutrientName: 'PB (Proteína Bruta)', + providedValue: '15', + requiredValue: '10', + evaluationStatus: 'ABOVE' as const, + }, + { + nutrientName: 'Ca (Cálcio)', + providedValue: '15', + requiredValue: '10', + evaluationStatus: 'ABOVE' as const, + }, + { + nutrientName: 'P (Fósforo)', + providedValue: '15', + requiredValue: '17', + evaluationStatus: 'NORMAL' as const, + }, + ], + ingredientGroups: [], + } +} From f9a31b11e31c9a89dcf4e8b53815748ae44a866d Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Fri, 31 Oct 2025 19:25:57 -0300 Subject: [PATCH 28/82] feat(nutritional-balancings/presentation): add util function to mount animal information items --- .../utils/make-animal-information-items.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/utils/make-animal-information-items.ts diff --git a/src/app/modules/nutritional-balancings/presentation/utils/make-animal-information-items.ts b/src/app/modules/nutritional-balancings/presentation/utils/make-animal-information-items.ts new file mode 100644 index 00000000..a7b3056c --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/utils/make-animal-information-items.ts @@ -0,0 +1,59 @@ +import { floatMask } from '@/core/masker' + +import type { NutritionalBalancingFormSchema } from '../validations/nutritional-balancing-form-schema' +import type { UseFormReturn } from 'react-hook-form' + +export function makeAnimalInformationItems( + form: UseFormReturn, + currentNutritionalBalancingIndex: number +) { + return [ + { + label: 'Nome do Animal', + value: form.getValues( + `nutritionalBalancings.${currentNutritionalBalancingIndex}.animal.name` + ), + }, + { + label: 'Tipo de Raça', + value: form.getValues( + `nutritionalBalancings.${currentNutritionalBalancingIndex}.animal.breed` + ), + }, + { + label: 'ECC', + value: floatMask( + form.getValues( + `nutritionalBalancings.${currentNutritionalBalancingIndex}.animal.ecc` + ) + ), + }, + { + label: 'Peso Vivo', + value: floatMask( + form.getValues( + `nutritionalBalancings.${currentNutritionalBalancingIndex}.animal.weight` + ), + 'kg' + ), + }, + { + label: 'Produção de Leite', + value: floatMask( + form.getValues( + `nutritionalBalancings.${currentNutritionalBalancingIndex}.animal.milkProduction` + ), + 'kg' + ), + }, + { + label: 'Produção de Leite Projetada', + value: floatMask( + form.getValues( + `nutritionalBalancings.${currentNutritionalBalancingIndex}.animal.estimatedMilkProduction` + ), + 'kg/dia' + ), + }, + ] +} From 9dfc137451e8182f30137c08421bb2ec21aa5f17 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Fri, 31 Oct 2025 19:27:39 -0300 Subject: [PATCH 29/82] feat(nutritional-balancings/presentation): add function to mount nutritional balancing summary items --- .../utils/make-nutritional-summary-items.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/utils/make-nutritional-summary-items.ts diff --git a/src/app/modules/nutritional-balancings/presentation/utils/make-nutritional-summary-items.ts b/src/app/modules/nutritional-balancings/presentation/utils/make-nutritional-summary-items.ts new file mode 100644 index 00000000..658bc8b8 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/utils/make-nutritional-summary-items.ts @@ -0,0 +1,62 @@ +import { percentMask, floatMask } from '@/core/masker' + +import type { NutritionalBalancingFormSchema } from '../validations/nutritional-balancing-form-schema' +import type { UseFormReturn } from 'react-hook-form' + +export function makeNutritionalSummaryItems( + form: UseFormReturn, + currentNutritionalBalancingIndex: number +) { + return [ + { + label: 'EE da ração (%)', + value: percentMask( + form.getValues( + `nutritionalBalancings.${currentNutritionalBalancingIndex}.summary.etherExtractPercent` + ) + ), + }, + { + label: 'Total de Matéria Seca', + value: floatMask( + form.getValues( + `nutritionalBalancings.${currentNutritionalBalancingIndex}.summary.totalDryMatter` + ), + 'kg' + ), + }, + { + label: 'Relação MS: Volumoso', + value: percentMask( + form.getValues( + `nutritionalBalancings.${currentNutritionalBalancingIndex}.summary.forageDryMatterPercent` + ) + ), + }, + { + label: 'Relação MS: Concentrado', + value: percentMask( + form.getValues( + `nutritionalBalancings.${currentNutritionalBalancingIndex}.summary.concentrateDryMatterPercent` + ) + ), + }, + { + label: 'Carboidratos Não Fibrosos (CNF em % MS)', + value: percentMask( + form.getValues( + `nutritionalBalancings.${currentNutritionalBalancingIndex}.summary.nonFibrousCarbohydratesPercent` + ) + ), + }, + { + label: 'Relação PDR/NDT (g/kg)', + value: floatMask( + form.getValues( + `nutritionalBalancings.${currentNutritionalBalancingIndex}.summary.rdpTdnRatio` + ), + 'g/kg' + ), + }, + ] +} From cb27c0b460aede74d2fce43100f8197c2bb2b18d Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Fri, 31 Oct 2025 19:28:50 -0300 Subject: [PATCH 30/82] feat(nutritional-balancings/presentation): improvement nutritional balancing form validations --- .../nutritional-balancing-form-schema.ts | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/app/modules/nutritional-balancings/presentation/validations/nutritional-balancing-form-schema.ts b/src/app/modules/nutritional-balancings/presentation/validations/nutritional-balancing-form-schema.ts index a9567385..d9935961 100644 --- a/src/app/modules/nutritional-balancings/presentation/validations/nutritional-balancing-form-schema.ts +++ b/src/app/modules/nutritional-balancings/presentation/validations/nutritional-balancing-form-schema.ts @@ -1,13 +1,9 @@ import { array, z } from 'zod' -import { optionSchema } from '@/core/validation/schemas' - const nutritionalBalancingAnimalSchema = z.object({ id: z.number().min(1, { message: 'Animal é obrigatório' }), name: z.string().min(1, { message: 'Nome do animal é obrigatório' }), - breed: optionSchema.refine(({ label, value }) => label !== '' && value > 0, { - message: 'Raça é obrigatória', - }), + breed: z.string().min(1, { message: 'Raça é obrigatória' }), ecc: z.string().min(1, { message: 'ECC é obrigatório' }), weight: z.string().min(1, { message: 'Peso é obrigatório' }), milkProduction: z @@ -68,20 +64,42 @@ const ingredientGroupSchema = z.object({ }), }) +const nutritionalBalancingSchema = z + .object({ + animal: nutritionalBalancingAnimalSchema, + summary: nutritionalBalancingSummarySchema, + evaluations: z + .array(nutritionalEvaluationSchema) + .min(1, { message: 'Adicione ao menos uma avaliação' }), + ingredientGroups: z + .array(ingredientGroupSchema) + .min(1, { message: 'Adicione ao menos um grupo de ingredientes' }), + }) + .superRefine((value, context) => { + if (!value?.animal || !value.animal.id || value.animal.id < 1) { + context.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Selecione o animal', + path: ['animal'], + }) + } + }) + +const nutritionalBalancingsSchema = z.array(nutritionalBalancingSchema).min(1, { + message: 'Adicione ao menos um balanceamento nutricional para um animal', +}) + export const nutritionalBalancingFormSchema = z.object({ date: z .date() .max(new Date(), { message: 'A data deve ser menor que a data atual' }), - animal: nutritionalBalancingAnimalSchema, - summary: nutritionalBalancingSummarySchema, - evaluations: z - .array(nutritionalEvaluationSchema) - .min(1, { message: 'Adicione ao menos uma avaliação' }), - ingredientGroups: z - .array(ingredientGroupSchema) - .min(1, { message: 'Adicione ao menos um grupo de ingredientes' }), + nutritionalBalancings: nutritionalBalancingsSchema, }) export type NutritionalBalancingFormSchema = z.infer< typeof nutritionalBalancingFormSchema > + +export type NutritionalBalancingSchema = z.infer< + typeof nutritionalBalancingSchema +> From 589509dbdb9b8462b054030114fdca405d38969e Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Mon, 3 Nov 2025 21:37:42 -0300 Subject: [PATCH 31/82] refactor: use number for values instead of string on nutritional balancing --- scripts/seed/data/nutritional-balancings.mjs | 27 ++-- .../models/nutritional-balancings-model.ts | 38 +++--- .../get-nutritional-balancing-handler.ts | 124 ++++++++++++++---- 3 files changed, 127 insertions(+), 62 deletions(-) diff --git a/scripts/seed/data/nutritional-balancings.mjs b/scripts/seed/data/nutritional-balancings.mjs index 74d66175..94e7bad5 100644 --- a/scripts/seed/data/nutritional-balancings.mjs +++ b/scripts/seed/data/nutritional-balancings.mjs @@ -7,20 +7,17 @@ export const nutritionalBalancingsData = Array.from( date: faker.date.past().toISOString(), animal: faker.lorem.word(), breed: faker.animal.cow(), - weight: faker.number - .float({ min: 200, max: 700, precision: 0.1 }) - .toFixed(2), - milkProduction: faker.number - .float({ - min: 5, - max: 50, - }) - .toFixed(2), - estimatedMilkProduction: faker.number - .float({ - min: 5, - max: 50, - }) - .toFixed(2), + weight: faker.number.float({ min: 200, max: 700, fractionDigits: 2 }), + + milkProduction: faker.number.float({ + min: 5, + max: 50, + fractionDigits: 2, + }), + estimatedMilkProduction: faker.number.float({ + min: 5, + max: 50, + fractionDigits: 2, + }), }) ) diff --git a/src/app/modules/nutritional-balancings/domain/models/nutritional-balancings-model.ts b/src/app/modules/nutritional-balancings/domain/models/nutritional-balancings-model.ts index dce51a04..1ea5f86e 100644 --- a/src/app/modules/nutritional-balancings/domain/models/nutritional-balancings-model.ts +++ b/src/app/modules/nutritional-balancings/domain/models/nutritional-balancings-model.ts @@ -10,31 +10,31 @@ type NutritionalBalancingIngredientCategory = type NutritionalBalancingAnimal = { name: string breed: string - ecc: string - weight: string - milkProduction: string - estimatedMilkProduction: string + ecc: number + weight: number + milkProduction: number + estimatedMilkProduction: number } type NutritionalBalancingSummary = { - totalDryMatter: string - etherExtractPercent: string - forageDryMatterPercent: string - concentrateDryMatterPercent: string - nonFibrousCarbohydratesPercent: string - rdpTdnRatio: string + totalDryMatter: number + etherExtractPercent: number + forageDryMatterPercent: number + concentrateDryMatterPercent: number + nonFibrousCarbohydratesPercent: number + rdpTdnRatio: number } type NutritionalEvaluation = { nutrientName: string - requiredValue: string - providedValue: string + requiredValue: number + providedValue: number evaluationStatus: NutritionalBalancingEvaluationStatus } type IngredientItem = { name: string - quantity: string + quantity: number } type IngredientGroup = { @@ -62,16 +62,16 @@ export type NutritionalBalancingModel = WithId<{ date: Date animal: string breed: string - weight: string - milkProduction: string - estimatedMilkProduction: string + weight: number + milkProduction: number + estimatedMilkProduction: number }> export type NutritionalBalancingApiResponse = WithId<{ date: string animal: string breed: string - weight: string - milkProduction: string - estimatedMilkProduction: string + weight: number + milkProduction: number + estimatedMilkProduction: number }> diff --git a/src/app/modules/nutritional-balancings/mocks/handlers/get-nutritional-balancing-handler.ts b/src/app/modules/nutritional-balancings/mocks/handlers/get-nutritional-balancing-handler.ts index 45a6be1c..9683854f 100644 --- a/src/app/modules/nutritional-balancings/mocks/handlers/get-nutritional-balancing-handler.ts +++ b/src/app/modules/nutritional-balancings/mocks/handlers/get-nutritional-balancing-handler.ts @@ -43,25 +43,41 @@ export const getNutritionalBalancingHandler = httpWithMiddleware< breed: nutritionalBalancingFound.breed, weight: nutritionalBalancingFound.weight, milkProduction: nutritionalBalancingFound.milkProduction, - ecc: faker.number.float({ min: 1, max: 10 }).toFixed(2), + ecc: faker.number.float({ min: 1, max: 10, fractionDigits: 2 }), estimatedMilkProduction: nutritionalBalancingFound.estimatedMilkProduction, }, summary: { - concentrateDryMatterPercent: faker.number - .float({ min: 1, max: 100 }) - .toFixed(2), - etherExtractPercent: faker.number - .float({ min: 1, max: 100 }) - .toFixed(2), - forageDryMatterPercent: faker.number - .float({ min: 1, max: 100 }) - .toFixed(2), - nonFibrousCarbohydratesPercent: faker.number - .float({ min: 1, max: 100 }) - .toFixed(2), - totalDryMatter: faker.number.float({ min: 1, max: 100 }).toFixed(2), - rdpTdnRatio: faker.number.float({ min: 0, max: 1 }).toFixed(3), + concentrateDryMatterPercent: faker.number.float({ + min: 1, + max: 100, + fractionDigits: 2, + }), + etherExtractPercent: faker.number.float({ + min: 1, + max: 100, + fractionDigits: 2, + }), + forageDryMatterPercent: faker.number.float({ + min: 1, + max: 100, + fractionDigits: 2, + }), + nonFibrousCarbohydratesPercent: faker.number.float({ + min: 1, + max: 100, + fractionDigits: 2, + }), + totalDryMatter: faker.number.float({ + min: 1, + max: 100, + fractionDigits: 2, + }), + rdpTdnRatio: faker.number.float({ + min: 0, + max: 1, + fractionDigits: 3, + }), }, evaluations: [ { @@ -71,8 +87,16 @@ export const getNutritionalBalancingHandler = httpWithMiddleware< 'BELOW', 'NORMAL', ]), - providedValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), - requiredValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), + providedValue: faker.number.float({ + min: 1, + max: 10, + fractionDigits: 2, + }), + requiredValue: faker.number.float({ + min: 1, + max: 10, + fractionDigits: 2, + }), }, { nutrientName: 'NDT (Nutrientes Digestíveis Totais)', @@ -81,8 +105,16 @@ export const getNutritionalBalancingHandler = httpWithMiddleware< 'BELOW', 'NORMAL', ]), - providedValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), - requiredValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), + providedValue: faker.number.float({ + min: 1, + max: 10, + fractionDigits: 2, + }), + requiredValue: faker.number.float({ + min: 1, + max: 10, + fractionDigits: 2, + }), }, { nutrientName: 'PB (Proteína Bruta)', @@ -91,8 +123,16 @@ export const getNutritionalBalancingHandler = httpWithMiddleware< 'BELOW', 'NORMAL', ]), - providedValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), - requiredValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), + providedValue: faker.number.float({ + min: 1, + max: 10, + fractionDigits: 2, + }), + requiredValue: faker.number.float({ + min: 1, + max: 10, + fractionDigits: 2, + }), }, { nutrientName: 'Ca (Cálcio)', @@ -101,8 +141,16 @@ export const getNutritionalBalancingHandler = httpWithMiddleware< 'BELOW', 'NORMAL', ]), - providedValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), - requiredValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), + providedValue: faker.number.float({ + min: 1, + max: 10, + fractionDigits: 2, + }), + requiredValue: faker.number.float({ + min: 1, + max: 10, + fractionDigits: 2, + }), }, { nutrientName: 'P (Fósforo)', @@ -111,8 +159,16 @@ export const getNutritionalBalancingHandler = httpWithMiddleware< 'BELOW', 'NORMAL', ]), - providedValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), - requiredValue: faker.number.float({ min: 1, max: 10 }).toFixed(2), + providedValue: faker.number.float({ + min: 1, + max: 10, + fractionDigits: 2, + }), + requiredValue: faker.number.float({ + min: 1, + max: 10, + fractionDigits: 2, + }), }, ], ingredientGroups: [ @@ -121,7 +177,11 @@ export const getNutritionalBalancingHandler = httpWithMiddleware< ingredients: Array.from({ length: 3 }).map(() => ({ id: faker.number.int({ min: 1, max: 10000 }), name: faker.commerce.productName(), - quantity: faker.number.float({ min: 1, max: 100 }).toFixed(2), + quantity: faker.number.float({ + min: 1, + max: 100, + fractionDigits: 2, + }), })), }, { @@ -129,7 +189,11 @@ export const getNutritionalBalancingHandler = httpWithMiddleware< ingredients: Array.from({ length: 3 }).map(() => ({ id: faker.number.int({ min: 1, max: 10000 }), name: faker.commerce.productName(), - quantity: faker.number.float({ min: 1, max: 100 }).toFixed(2), + quantity: faker.number.float({ + min: 1, + max: 100, + fractionDigits: 2, + }), })), }, { @@ -137,7 +201,11 @@ export const getNutritionalBalancingHandler = httpWithMiddleware< ingredients: Array.from({ length: 3 }).map(() => ({ id: faker.number.int({ min: 1, max: 10000 }), name: faker.commerce.productName(), - quantity: faker.number.float({ min: 1, max: 100 }).toFixed(2), + quantity: faker.number.float({ + min: 1, + max: 100, + fractionDigits: 2, + }), })), }, ], From 4bd490096c418b89b874046e61360fefdf5201f5 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Mon, 3 Nov 2025 21:38:17 -0300 Subject: [PATCH 32/82] feat: add formatNumber mask --- .../nutritional-balancing-data-table.hook.tsx | 15 +++---- .../utils/make-animal-information-items.ts | 22 ++++++---- .../utils/make-nutritional-summary-items.ts | 42 +++++++++++++------ src/core/masker/masks/format-number.ts | 33 +++++++++++++++ src/core/masker/masks/index.ts | 1 + 5 files changed, 85 insertions(+), 28 deletions(-) create mode 100644 src/core/masker/masks/format-number.ts diff --git a/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-data-table/nutritional-balancing-data-table.hook.tsx b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-data-table/nutritional-balancing-data-table.hook.tsx index b1d0e44e..32a41c44 100644 --- a/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-data-table/nutritional-balancing-data-table.hook.tsx +++ b/src/app/modules/nutritional-balancings/presentation/components/nutritional-balancing-data-table/nutritional-balancing-data-table.hook.tsx @@ -3,7 +3,7 @@ import { useMemo, useState } from 'react' import { format } from 'date-fns' import { MoreHorizontalIcon, PencilIcon, Trash2Icon } from 'lucide-react' -import { floatMask } from '@/core/masker' +import { formatNumber } from '@/core/masker' import { DropdownMenu } from '@/core/presentation/components/ui' import { useDebounce } from '@/core/presentation/hooks' @@ -61,7 +61,7 @@ export function useNutritionalBalancingDataTable() { cell: ({ row }) => { const { original: nutritionalBalancing } = row - return floatMask(nutritionalBalancing.weight, 'kg') + return formatNumber(nutritionalBalancing.weight, { suffix: 'kg' }) }, }, { @@ -70,7 +70,9 @@ export function useNutritionalBalancingDataTable() { cell: ({ row }) => { const { original: nutritionalBalancing } = row - return floatMask(nutritionalBalancing.milkProduction, 'kg/dia') + return formatNumber(nutritionalBalancing.milkProduction, { + suffix: 'kg/dia', + }) }, }, { @@ -79,10 +81,9 @@ export function useNutritionalBalancingDataTable() { cell: ({ row }) => { const { original: nutritionalBalancing } = row - return floatMask( - nutritionalBalancing.estimatedMilkProduction, - 'kg/dia' - ) + return formatNumber(nutritionalBalancing.estimatedMilkProduction, { + suffix: 'kg/dia', + }) }, }, { diff --git a/src/app/modules/nutritional-balancings/presentation/utils/make-animal-information-items.ts b/src/app/modules/nutritional-balancings/presentation/utils/make-animal-information-items.ts index a7b3056c..d85f10ee 100644 --- a/src/app/modules/nutritional-balancings/presentation/utils/make-animal-information-items.ts +++ b/src/app/modules/nutritional-balancings/presentation/utils/make-animal-information-items.ts @@ -1,4 +1,4 @@ -import { floatMask } from '@/core/masker' +import { formatNumber } from '@/core/masker' import type { NutritionalBalancingFormSchema } from '../validations/nutritional-balancing-form-schema' import type { UseFormReturn } from 'react-hook-form' @@ -22,7 +22,7 @@ export function makeAnimalInformationItems( }, { label: 'ECC', - value: floatMask( + value: formatNumber( form.getValues( `nutritionalBalancings.${currentNutritionalBalancingIndex}.animal.ecc` ) @@ -30,29 +30,35 @@ export function makeAnimalInformationItems( }, { label: 'Peso Vivo', - value: floatMask( + value: formatNumber( form.getValues( `nutritionalBalancings.${currentNutritionalBalancingIndex}.animal.weight` ), - 'kg' + { + suffix: 'kg', + } ), }, { label: 'Produção de Leite', - value: floatMask( + value: formatNumber( form.getValues( `nutritionalBalancings.${currentNutritionalBalancingIndex}.animal.milkProduction` ), - 'kg' + { + suffix: 'kg', + } ), }, { label: 'Produção de Leite Projetada', - value: floatMask( + value: formatNumber( form.getValues( `nutritionalBalancings.${currentNutritionalBalancingIndex}.animal.estimatedMilkProduction` ), - 'kg/dia' + { + suffix: 'kg/dia', + } ), }, ] diff --git a/src/app/modules/nutritional-balancings/presentation/utils/make-nutritional-summary-items.ts b/src/app/modules/nutritional-balancings/presentation/utils/make-nutritional-summary-items.ts index 658bc8b8..d65286a2 100644 --- a/src/app/modules/nutritional-balancings/presentation/utils/make-nutritional-summary-items.ts +++ b/src/app/modules/nutritional-balancings/presentation/utils/make-nutritional-summary-items.ts @@ -1,4 +1,4 @@ -import { percentMask, floatMask } from '@/core/masker' +import { formatNumber } from '@/core/masker' import type { NutritionalBalancingFormSchema } from '../validations/nutritional-balancing-form-schema' import type { UseFormReturn } from 'react-hook-form' @@ -10,52 +10,68 @@ export function makeNutritionalSummaryItems( return [ { label: 'EE da ração (%)', - value: percentMask( + value: formatNumber( form.getValues( `nutritionalBalancings.${currentNutritionalBalancingIndex}.summary.etherExtractPercent` - ) + ), + { + suffix: '%', + } ), }, { label: 'Total de Matéria Seca', - value: floatMask( + value: formatNumber( form.getValues( `nutritionalBalancings.${currentNutritionalBalancingIndex}.summary.totalDryMatter` ), - 'kg' + { + suffix: 'kg', + } ), }, { label: 'Relação MS: Volumoso', - value: percentMask( + value: formatNumber( form.getValues( `nutritionalBalancings.${currentNutritionalBalancingIndex}.summary.forageDryMatterPercent` - ) + ), + { + suffix: '%', + } ), }, { label: 'Relação MS: Concentrado', - value: percentMask( + value: formatNumber( form.getValues( `nutritionalBalancings.${currentNutritionalBalancingIndex}.summary.concentrateDryMatterPercent` - ) + ), + { + suffix: '%', + } ), }, { label: 'Carboidratos Não Fibrosos (CNF em % MS)', - value: percentMask( + value: formatNumber( form.getValues( `nutritionalBalancings.${currentNutritionalBalancingIndex}.summary.nonFibrousCarbohydratesPercent` - ) + ), + { + suffix: '%', + } ), }, { label: 'Relação PDR/NDT (g/kg)', - value: floatMask( + value: formatNumber( form.getValues( `nutritionalBalancings.${currentNutritionalBalancingIndex}.summary.rdpTdnRatio` ), - 'g/kg' + { + suffix: 'g/kg', + } ), }, ] diff --git a/src/core/masker/masks/format-number.ts b/src/core/masker/masks/format-number.ts new file mode 100644 index 00000000..f63d34e9 --- /dev/null +++ b/src/core/masker/masks/format-number.ts @@ -0,0 +1,33 @@ +type Options = { + prefix?: string + suffix?: string + decimals?: number +} + +export function formatNumber( + value: string | number, + options?: Options +): string { + const { prefix, suffix, decimals = 2 } = options || {} + + if (value === '' || value === null || value === undefined) { + return '' + } + + const numericValue = + typeof value === 'string' ? Number.parseFloat(value) : value + + if (Number.isNaN(numericValue)) { + return '' + } + + const formatted = numericValue.toLocaleString('pt-BR', { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }) + + const prefixPart = prefix ? `${prefix} ` : '' + const suffixPart = suffix ? ` ${suffix.trim()}` : '' + + return `${prefixPart}${formatted}${suffixPart}` +} diff --git a/src/core/masker/masks/index.ts b/src/core/masker/masks/index.ts index ee3fcbf1..1a892a10 100644 --- a/src/core/masker/masks/index.ts +++ b/src/core/masker/masks/index.ts @@ -1,6 +1,7 @@ export * from './cep-mask' export * from './cpf-mask' export * from './float-mask' +export * from './format-number' export * from './money-mask' export * from './only-numbers-mask' export * from './phone-mask' From 8485d5f0458cd86aab9c37b454e685b594d8187a Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Mon, 3 Nov 2025 21:38:39 -0300 Subject: [PATCH 33/82] feat: make pagination and sort on datatable optional --- .../components/ui/data-table/data-table.tsx | 219 ++++++++++-------- 1 file changed, 124 insertions(+), 95 deletions(-) diff --git a/src/core/presentation/components/ui/data-table/data-table.tsx b/src/core/presentation/components/ui/data-table/data-table.tsx index ba65e20e..9637d2b0 100644 --- a/src/core/presentation/components/ui/data-table/data-table.tsx +++ b/src/core/presentation/components/ui/data-table/data-table.tsx @@ -28,12 +28,12 @@ import type { Sort } from '@/core/domain/types' export type DataTableProps = { data: TData[] columns: ColumnDef[] - totalPages: number - pagination: { + totalPages?: number + pagination?: { currentPage: number onPageChange: (page: number) => void } - sorting: { + sorting?: { currentSorting?: Sort onSorting: (sort?: Sort) => void } @@ -82,7 +82,7 @@ export function TableBody({ onClick={(event) => handleOnClickRow(event, row.original)} > {row.getVisibleCells().map((cell) => ( - + {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} @@ -95,15 +95,21 @@ export function DataTable({ data, sorting, pagination, - totalPages, + totalPages = 1, loading = false, onClickRow, }: Readonly>) { - const { currentSorting, onSorting } = sorting - const { currentPage, onPageChange } = pagination + const currentSorting = sorting?.currentSorting + const onSorting = sorting?.onSorting + const currentPage = pagination?.currentPage ?? 1 + const onPageChange = pagination?.onPageChange + const hasPagination = !!pagination && totalPages > 1 + const hasSorting = !!sorting const onSortingChange: OnChangeFn = useCallback( (updaterOrValue: Updater) => { + if (!onSorting) return + const [sort] = typeof updaterOrValue === 'function' ? updaterOrValue([ @@ -129,6 +135,8 @@ export function DataTable({ const onPaginationChange: OnChangeFn = useCallback( (updaterOrValue: Updater) => { + if (!onPageChange) return + const page = typeof updaterOrValue === 'function' ? updaterOrValue({ @@ -155,24 +163,28 @@ export function DataTable({ columns, data, state: { - sorting: [ - { - id: String(currentSorting?.field), - desc: currentSorting?.direction === 'desc', - }, - ], - pagination: { - pageIndex: currentPage - 1, - pageSize: ITEMS_PER_PAGE, - }, + sorting: hasSorting + ? [ + { + id: String(currentSorting?.field), + desc: currentSorting?.direction === 'desc', + }, + ] + : [], + pagination: hasPagination + ? { + pageIndex: currentPage - 1, + pageSize: ITEMS_PER_PAGE, + } + : undefined, }, - manualSorting: true, - manualPagination: true, + manualSorting: hasSorting, + manualPagination: hasPagination, pageCount: totalPages, - onSortingChange, - onPaginationChange, + onSortingChange: hasSorting ? onSortingChange : undefined, + onPaginationChange: hasPagination ? onPaginationChange : undefined, getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), + getPaginationRowModel: hasPagination ? getPaginationRowModel() : undefined, }) const tooltipText = (value: 'asc' | 'desc' | false) => { @@ -182,7 +194,7 @@ export function DataTable({ return 'Limpar ordenação' } - const page = getState().pagination.pageIndex + 1 + const page = hasPagination ? getState().pagination.pageIndex + 1 : 1 const showFinalEllipsis = useMemo(() => page + 2 > 3, [page]) const isAfterFirstPage = useMemo(() => page > 1, [page]) const isBeforeLastPage = useMemo( @@ -202,39 +214,54 @@ export function DataTable({ - {header.isPlaceholder ? null : ( - - - -
- {flexRender( - header.column.columnDef.header, - header.getContext() - )} - {{ - asc: , - desc: , - }[header.column.getIsSorted() as string] ?? - null} -
-
- -

- {tooltipText( - header.column.getNextSortingOrder() - )} -

-
-
-
- )} + {header.isPlaceholder + ? null + : hasSorting && ( + + + + + + +

+ {tooltipText( + header.column.getNextSortingOrder() + )} +

+
+
+
+ )} + {header.isPlaceholder + ? null + : !hasSorting && ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ )}
))} @@ -252,59 +279,61 @@ export function DataTable({ - - - - - + {hasPagination && ( + + + + + - {isAfterFirstPage && ( - <> - setPageIndex(0)}> - 1 - + {isAfterFirstPage && ( + <> + setPageIndex(0)}> + 1 + + + + + + + )} + + + {page} + + {page === 1 && isBeforeLastPage && ( - - )} - - - {page} - + )} - {page === 1 && isBeforeLastPage && ( - - - - )} + {isBeforeLastPage && ( + <> + {showFinalEllipsis && ( + + + + )} - {isBeforeLastPage && ( - <> - {showFinalEllipsis && ( - - + setPageIndex(totalPages - 1)} + isDisabled={!getCanNextPage()} + > + {totalPages} - )} + + )} - setPageIndex(totalPages - 1)} - isDisabled={!getCanNextPage()} - > - {totalPages} - - - )} - - - - - - + + + + + + )} ) } From ea5ee497e22f47606620740d3113df16f85df576 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Mon, 3 Nov 2025 21:39:21 -0300 Subject: [PATCH 34/82] feat: add SummaryTab display name --- .../nutritional-balancings/presentation/tabs/summary-tab.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/modules/nutritional-balancings/presentation/tabs/summary-tab.tsx b/src/app/modules/nutritional-balancings/presentation/tabs/summary-tab.tsx index a3f0c283..af01cfb9 100644 --- a/src/app/modules/nutritional-balancings/presentation/tabs/summary-tab.tsx +++ b/src/app/modules/nutritional-balancings/presentation/tabs/summary-tab.tsx @@ -20,3 +20,5 @@ export function SummaryTab({ ) } + +SummaryTab.displayName = 'SummaryTab' From 671461ecca71ef3be664b5dcef0d8ae43c3ef0ba Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Mon, 3 Nov 2025 21:39:38 -0300 Subject: [PATCH 35/82] feat: add NutritionalRequirementsTable component --- .../nutritional-requirements-table/index.ts | 1 + .../nutritional-requirements-table.hook.tsx | 65 +++++++++++++++++++ .../nutritional-requirements-table.tsx | 24 +++++++ 3 files changed, 90 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/components/nutritional-requirements-table/index.ts create mode 100644 src/app/modules/nutritional-balancings/presentation/components/nutritional-requirements-table/nutritional-requirements-table.hook.tsx create mode 100644 src/app/modules/nutritional-balancings/presentation/components/nutritional-requirements-table/nutritional-requirements-table.tsx diff --git a/src/app/modules/nutritional-balancings/presentation/components/nutritional-requirements-table/index.ts b/src/app/modules/nutritional-balancings/presentation/components/nutritional-requirements-table/index.ts new file mode 100644 index 00000000..f469348f --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/nutritional-requirements-table/index.ts @@ -0,0 +1 @@ +export * from './nutritional-requirements-table' diff --git a/src/app/modules/nutritional-balancings/presentation/components/nutritional-requirements-table/nutritional-requirements-table.hook.tsx b/src/app/modules/nutritional-balancings/presentation/components/nutritional-requirements-table/nutritional-requirements-table.hook.tsx new file mode 100644 index 00000000..74da3e51 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/nutritional-requirements-table/nutritional-requirements-table.hook.tsx @@ -0,0 +1,65 @@ +import { useMemo } from 'react' + +import { type ColumnDef } from '@tanstack/react-table' + +import { formatNumber } from '@/core/masker' +import { Badge } from '@/core/presentation/components/ui/badge' + +import type { NutritionalBalancingEvaluationSchema } from '../../validations/nutritional-balancing-form-schema' + +const evaluationStatusMap = { + ABOVE: 'Acima', + BELOW: 'Abaixo', + NORMAL: 'Normal', +} as const + +const evaluationStatusVariant = { + ABOVE: 'destructive', + BELOW: 'destructive', + NORMAL: 'secondary', +} as const + +export function useNutritionalRequirementsTable() { + const columns = useMemo[]>( + () => [ + { + accessorKey: 'nutrientName', + header: 'Nutriente', + }, + { + accessorKey: 'requiredValue', + header: 'Exigência', + cell: ({ getValue }) => formatNumber(getValue()), + }, + { + accessorKey: 'providedValue', + header: 'Oferecido', + cell: ({ getValue }) => formatNumber(getValue()), + }, + { + accessorKey: 'evaluationStatus', + header: 'Status', + cell: ({ getValue }) => { + const status = getValue() + const label = evaluationStatusMap[status] || status + const variant = evaluationStatusVariant[status] || 'default' + + return ( + + {label} + + ) + }, + }, + ], + [] + ) + + return { + columns, + } +} diff --git a/src/app/modules/nutritional-balancings/presentation/components/nutritional-requirements-table/nutritional-requirements-table.tsx b/src/app/modules/nutritional-balancings/presentation/components/nutritional-requirements-table/nutritional-requirements-table.tsx new file mode 100644 index 00000000..4cd81f54 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/nutritional-requirements-table/nutritional-requirements-table.tsx @@ -0,0 +1,24 @@ +import { DataTable } from '@/core/presentation/components/ui' + +import { useNutritionalRequirementsTable } from './nutritional-requirements-table.hook' + +import type { NutritionalBalancingEvaluationSchema } from '../../validations/nutritional-balancing-form-schema' + +type NutritionalRequirementsTableProps = { + rows: NutritionalBalancingEvaluationSchema[] +} + +export function NutritionalRequirementsTable({ + rows, +}: Readonly) { + const { columns } = useNutritionalRequirementsTable() + + return ( + + columns={columns} + data={rows} + /> + ) +} + +NutritionalRequirementsTable.displayName = 'NutritionalRequirementsTable' From 355cbb7dc95d981102628d5e62062b0579a0f775 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Mon, 3 Nov 2025 21:40:10 -0300 Subject: [PATCH 36/82] feat: add NutritionalEvaluationTab --- .../tabs/nutritional-evaluation-tab.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/tabs/nutritional-evaluation-tab.tsx diff --git a/src/app/modules/nutritional-balancings/presentation/tabs/nutritional-evaluation-tab.tsx b/src/app/modules/nutritional-balancings/presentation/tabs/nutritional-evaluation-tab.tsx new file mode 100644 index 00000000..511424ec --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/tabs/nutritional-evaluation-tab.tsx @@ -0,0 +1,29 @@ +import { Card } from '@/core/presentation/components/ui' + +import { NutritionalRequirementsTable } from '../components/nutritional-requirements-table' + +import type { NutritionalBalancingEvaluationSchema } from '../validations/nutritional-balancing-form-schema' + +type NutritionalEvaluationTabProps = { + rows: NutritionalBalancingEvaluationSchema[] +} + +export function NutritionalEvaluationTab({ + rows, +}: Readonly) { + return ( + + + Informações do Animal + + Comparação entre exigências e valores oferecidos + + + + + + + ) +} + +NutritionalEvaluationTab.displayName = 'NutritionalEvaluationTab' From 3b35973c4948d3832087af9937c566fd7ce43d5b Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 4 Nov 2025 09:05:16 -0300 Subject: [PATCH 37/82] feat: add type on cultivations model --- scripts/seed/data/general-cultivations.mjs | 1 + .../remote-get-general-cultivation-use-case.ts | 1 + .../remote-get-general-cultivations-use-case.ts | 2 ++ .../domain/models/general-cultivations-model.ts | 4 ++++ 4 files changed, 8 insertions(+) diff --git a/scripts/seed/data/general-cultivations.mjs b/scripts/seed/data/general-cultivations.mjs index 38593ae7..d0d25cd0 100644 --- a/scripts/seed/data/general-cultivations.mjs +++ b/scripts/seed/data/general-cultivations.mjs @@ -10,5 +10,6 @@ export const generalCultivationsData = Array.from( (_, index) => ({ id: index + 1, name: faker.food.vegetable(), + type: faker.helpers.arrayElement(['FORAGE', 'CONCENTRATE', 'MINERAL']), }) ) diff --git a/src/app/modules/general-cultivations/data/use-cases/general-cultivations-use-cases/remote-get-general-cultivation-use-case.ts b/src/app/modules/general-cultivations/data/use-cases/general-cultivations-use-cases/remote-get-general-cultivation-use-case.ts index 09930c44..36799c57 100644 --- a/src/app/modules/general-cultivations/data/use-cases/general-cultivations-use-cases/remote-get-general-cultivation-use-case.ts +++ b/src/app/modules/general-cultivations/data/use-cases/general-cultivations-use-cases/remote-get-general-cultivation-use-case.ts @@ -31,6 +31,7 @@ export class RemoteGetGeneralCultivationUseCase if (statusCode === HttpStatusCode.ok && !!body) { return { name: body.name, + type: body.type, } } diff --git a/src/app/modules/general-cultivations/data/use-cases/general-cultivations-use-cases/remote-get-general-cultivations-use-case.ts b/src/app/modules/general-cultivations/data/use-cases/general-cultivations-use-cases/remote-get-general-cultivations-use-case.ts index d508ff48..59ef4a84 100644 --- a/src/app/modules/general-cultivations/data/use-cases/general-cultivations-use-cases/remote-get-general-cultivations-use-case.ts +++ b/src/app/modules/general-cultivations/data/use-cases/general-cultivations-use-cases/remote-get-general-cultivations-use-case.ts @@ -35,6 +35,7 @@ export class RemoteGetGeneralCultivationsUseCase > = { id: 'id', name: 'name', + type: 'type', } const { statusCode, body } = await this.httpClient.request({ @@ -52,6 +53,7 @@ export class RemoteGetGeneralCultivationsUseCase return { id: item.id, name: item.name, + type: item.type, } }), totalPages: Math.ceil(body.numberOfElements / body.pageable.pageSize), diff --git a/src/app/modules/general-cultivations/domain/models/general-cultivations-model.ts b/src/app/modules/general-cultivations/domain/models/general-cultivations-model.ts index 318449b1..69cdb026 100644 --- a/src/app/modules/general-cultivations/domain/models/general-cultivations-model.ts +++ b/src/app/modules/general-cultivations/domain/models/general-cultivations-model.ts @@ -2,16 +2,20 @@ import type { WithId } from '../../../../../core/domain/types' export type GeneralCultivationDetailsModel = { name: string + type: 'FORAGE' | 'CONCENTRATE' | 'MINERAL' } export type GeneralCultivationDetailsApiResponse = { name: string + type: 'FORAGE' | 'CONCENTRATE' | 'MINERAL' } export type GeneralCultivationModel = WithId<{ name: string + type: 'FORAGE' | 'CONCENTRATE' | 'MINERAL' }> export type GeneralCultivationApiResponse = WithId<{ name: string + type: 'FORAGE' | 'CONCENTRATE' | 'MINERAL' }> From 1701f2f6b92383a3a1f2a8418a7b2e717a6dfc94 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 4 Nov 2025 09:05:41 -0300 Subject: [PATCH 38/82] feat: add className on combobox --- src/core/presentation/components/ui/combobox/combobox.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/presentation/components/ui/combobox/combobox.tsx b/src/core/presentation/components/ui/combobox/combobox.tsx index ab76cf42..d2dabca1 100644 --- a/src/core/presentation/components/ui/combobox/combobox.tsx +++ b/src/core/presentation/components/ui/combobox/combobox.tsx @@ -28,6 +28,7 @@ export type ComboboxProps< loading?: boolean isError?: boolean disabled?: boolean + className?: string } export function Combobox< @@ -47,6 +48,7 @@ export function Combobox< loading = false, isError = false, disabled = false, + className, }: Readonly>) { const [open, setOpen] = useState(false) @@ -60,7 +62,8 @@ export function Combobox< className={cn( 'w-full justify-between', isError && 'border border-red-500', - !selected && 'text-muted-foreground' + !selected && 'text-muted-foreground', + className )} > {selected?.label From e4be4919e6e901e0384a6fb0341eb95a01d9e808 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 4 Nov 2025 09:06:05 -0300 Subject: [PATCH 39/82] feat: add noBorder and cell alignment props on datatable --- .../components/ui/data-table/data-table.tsx | 161 +++++++++++------- .../presentation/components/ui/table/body.tsx | 26 +-- 2 files changed, 116 insertions(+), 71 deletions(-) diff --git a/src/core/presentation/components/ui/data-table/data-table.tsx b/src/core/presentation/components/ui/data-table/data-table.tsx index 9637d2b0..bcafa78a 100644 --- a/src/core/presentation/components/ui/data-table/data-table.tsx +++ b/src/core/presentation/components/ui/data-table/data-table.tsx @@ -16,6 +16,7 @@ import { import { ArrowDownNarrowWide, ArrowUpNarrowWide } from 'lucide-react' import { ITEMS_PER_PAGE } from '@/core/infra/http' +import { cn } from '@/core/utils' import { Loading } from '../loading' import { Pagination } from '../pagination' @@ -25,6 +26,8 @@ import { Tooltip } from '../tooltip' import type { Sort } from '@/core/domain/types' +export type CellAlignment = 'left' | 'center' | 'right' + export type DataTableProps = { data: TData[] columns: ColumnDef[] @@ -39,6 +42,7 @@ export type DataTableProps = { } loading?: boolean onClickRow?: (row: TData) => void + noBorder?: boolean } export function TableBody({ @@ -81,11 +85,27 @@ export function TableBody({ className={onClickRow ? 'cursor-pointer' : ''} onClick={(event) => handleOnClickRow(event, row.original)} > - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} + {row.getVisibleCells().map((cell) => { + const alignment = + (cell.column.columnDef.meta as { align?: CellAlignment })?.align || + 'left' + + const alignmentMap = { + left: 'text-left', + center: 'text-center', + right: 'text-right', + } + const alignmentClass = alignmentMap[alignment] + + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ) + })} )) } @@ -98,6 +118,7 @@ export function DataTable({ totalPages = 1, loading = false, onClickRow, + noBorder = false, }: Readonly>) { const currentSorting = sorting?.currentSorting const onSorting = sorting?.onSorting @@ -205,69 +226,87 @@ export function DataTable({ return (
-
+
{getHeaderGroups().map((headerGroup) => ( - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : hasSorting && ( - - - - - - -

- {tooltipText( - header.column.getNextSortingOrder() - )} -

-
-
-
- )} - {header.isPlaceholder - ? null - : !hasSorting && ( -
- {flexRender( - header.column.columnDef.header, - header.getContext() - )} -
- )} -
- ))} + {headerGroup.headers.map((header) => { + const alignment = + ( + header.column.columnDef.meta as { + align?: CellAlignment + } + )?.align || 'left' + + const alignmentMap = { + left: 'text-left', + center: 'text-center', + right: 'text-right', + } + const alignmentClass = alignmentMap[alignment] + + const baseClassName = + hasSorting && header.column.getCanSort() + ? 'cursor-pointer select-none whitespace-nowrap min-w-fit' + : 'whitespace-nowrap min-w-fit' + + return ( + + {header.isPlaceholder + ? null + : hasSorting && ( + + + + + + +

+ {tooltipText( + header.column.getNextSortingOrder() + )} +

+
+
+
+ )} + {header.isPlaceholder + ? null + : !hasSorting && ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ )} +
+ ) + })}
))}
- + ->(({ className, ...props }, ref) => ( - -)) +type BodyProps = HTMLAttributes & { + showLastRowBorder?: boolean +} + +export const Body = forwardRef( + ({ className, showLastRowBorder = false, ...props }, ref) => ( + + ) +) Body.displayName = 'TableBody' From a192cc08d08aa4c14887113a93214c8c5ead75de Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 4 Nov 2025 09:06:46 -0300 Subject: [PATCH 40/82] feat: add ingredients tab with table --- .../tabs/ingredients-tab/index.ts | 1 + .../ingredients-tab/ingredients-tab.hook.ts | 102 ++++++++++++++++++ .../tabs/ingredients-tab/ingredients-tab.tsx | 99 +++++++++++++++++ ...reate-empty-nutritional-balancing-entry.ts | 30 +++++- 4 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 src/app/modules/nutritional-balancings/presentation/tabs/ingredients-tab/index.ts create mode 100644 src/app/modules/nutritional-balancings/presentation/tabs/ingredients-tab/ingredients-tab.hook.ts create mode 100644 src/app/modules/nutritional-balancings/presentation/tabs/ingredients-tab/ingredients-tab.tsx diff --git a/src/app/modules/nutritional-balancings/presentation/tabs/ingredients-tab/index.ts b/src/app/modules/nutritional-balancings/presentation/tabs/ingredients-tab/index.ts new file mode 100644 index 00000000..cee55832 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/tabs/ingredients-tab/index.ts @@ -0,0 +1 @@ +export * from './ingredients-tab' diff --git a/src/app/modules/nutritional-balancings/presentation/tabs/ingredients-tab/ingredients-tab.hook.ts b/src/app/modules/nutritional-balancings/presentation/tabs/ingredients-tab/ingredients-tab.hook.ts new file mode 100644 index 00000000..eff96e18 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/tabs/ingredients-tab/ingredients-tab.hook.ts @@ -0,0 +1,102 @@ +import { useMemo, useState } from 'react' + +import { useFormContext } from 'react-hook-form' + +import { onlyNumbersMask } from '@/core/masker' + +import type { NutritionalBalancingFormSchema } from '../../validations/nutritional-balancing-form-schema' +import type { Option } from '@/core/domain/types' + +type UseIngredientsTabProps = { + currentAnimalIndex: number +} + +export function useIngredientsTab({ + currentAnimalIndex, +}: Readonly) { + const form = useFormContext() + + const [searchAnimal, setSearchAnimal] = useState('') + const [selectedAnimalToCopy, setSelectedAnimalToCopy] = + useState | null>(null) + + const animalsAddedWithIngredients = useMemo(() => { + const animalsAdded = form.getValues('nutritionalBalancings') + return animalsAdded + .map((added, index) => ({ ...added, index })) + .filter( + (added) => + added.ingredientGroups.length > 0 && + added.index !== currentAnimalIndex + ) + }, [form, currentAnimalIndex]) + + const animalOptions = useMemo[]>(() => { + return animalsAddedWithIngredients.map((animal) => ({ + value: animal.index, + label: animal.animal.name, + })) + }, [animalsAddedWithIngredients]) + + const ingredients = useMemo(() => { + const ingredientGroups = + form.getValues( + `nutritionalBalancings.${currentAnimalIndex}.ingredientGroups` + ) ?? [] + + const forageGroup = ingredientGroups?.find( + (group) => group.category === 'FORAGE' + ) + + const concentrateGroup = ingredientGroups?.find( + (group) => group.category === 'CONCENTRATE' + ) + + const mineralGroup = ingredientGroups?.find( + (group) => group.category === 'MINERAL' + ) + + const total = ingredientGroups.reduce((acc, group) => { + return ( + acc + + group.ingredients.reduce((groupAcc, ingredient) => { + return groupAcc + Number(onlyNumbersMask(ingredient.quantity)) + }, 0) + ) + }, 0) + + return { + forage: forageGroup?.ingredients ?? [], + concentrate: concentrateGroup?.ingredients ?? [], + mineral: mineralGroup?.ingredients ?? [], + total, + } + }, [form, currentAnimalIndex]) + + const handleCopyIngredients = () => { + if (!selectedAnimalToCopy) return + + const sourceAnimalIndex = selectedAnimalToCopy.value + const sourceAnimal = form.getValues( + `nutritionalBalancings.${sourceAnimalIndex}` + ) + + if (sourceAnimal?.ingredientGroups) { + form.setValue( + `nutritionalBalancings.${currentAnimalIndex}.ingredientGroups`, + sourceAnimal.ingredientGroups + ) + setSelectedAnimalToCopy(null) + } + } + + return { + ingredients, + searchAnimal, + setSearchAnimal, + selectedAnimalToCopy, + setSelectedAnimalToCopy, + animalOptions, + handleCopyIngredients, + } +} diff --git a/src/app/modules/nutritional-balancings/presentation/tabs/ingredients-tab/ingredients-tab.tsx b/src/app/modules/nutritional-balancings/presentation/tabs/ingredients-tab/ingredients-tab.tsx new file mode 100644 index 00000000..4d6dd21d --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/tabs/ingredients-tab/ingredients-tab.tsx @@ -0,0 +1,99 @@ +import { CopyIcon, PlusIcon } from 'lucide-react' + +import { formatNumber } from '@/core/masker' +import { Button, Card, Combobox } from '@/core/presentation/components/ui' + +import { IngredientsTable } from '../../components/ingredients-table' + +import { useIngredientsTab } from './ingredients-tab.hook' + +type IngredientsTabProps = { + currentAnimalIndex: number +} + +export function IngredientsTab({ + currentAnimalIndex, +}: Readonly) { + const { + ingredients, + searchAnimal, + setSearchAnimal, + selectedAnimalToCopy, + setSelectedAnimalToCopy, + animalOptions, + handleCopyIngredients, + } = useIngredientsTab({ currentAnimalIndex }) + + return ( + +
+ + Informações do Animal + + Comparação entre exigências e valores oferecidos + + + + +
+ +
+ Copiar Ingredientes de: + setSelectedAnimalToCopy(item)} + placeholder="Selecione um animal" + emptyMessage="Nenhum animal com ingredientes encontrado" + className="w-auto" + /> + + +
+ + + + + + + +
+ TOTAL + + {formatNumber(ingredients.total, { + suffix: 'kg', + })} + +
+
+
+ ) +} + +IngredientsTab.displayName = 'IngredientsTab' diff --git a/src/app/modules/nutritional-balancings/presentation/utils/create-empty-nutritional-balancing-entry.ts b/src/app/modules/nutritional-balancings/presentation/utils/create-empty-nutritional-balancing-entry.ts index cb2d3ebb..c072e213 100644 --- a/src/app/modules/nutritional-balancings/presentation/utils/create-empty-nutritional-balancing-entry.ts +++ b/src/app/modules/nutritional-balancings/presentation/utils/create-empty-nutritional-balancing-entry.ts @@ -55,6 +55,34 @@ export function createEmptyNutritionalBalancingEntry( evaluationStatus: 'NORMAL' as const, }, ], - ingredientGroups: [], + ingredientGroups: [ + { + category: 'FORAGE' as const, + ingredients: [ + { + name: 'Silagem de milho', + quantity: '1000', + }, + ], + }, + { + category: 'CONCENTRATE' as const, + ingredients: [ + { + name: 'Silagem de milho', + quantity: '1000', + }, + ], + }, + { + category: 'MINERAL' as const, + ingredients: [ + { + name: 'Silagem de milho', + quantity: '1000', + }, + ], + }, + ], } } From 1b36b9ccee449d9633d0c601e74e69d2fa968e2b Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 4 Nov 2025 09:07:01 -0300 Subject: [PATCH 41/82] feat: add ingredients table --- .../components/ingredients-table/index.ts | 1 + .../ingredients-table.hook.tsx | 37 ++++++++++++++++++ .../ingredients-table/ingredients-table.tsx | 38 +++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 src/app/modules/nutritional-balancings/presentation/components/ingredients-table/index.ts create mode 100644 src/app/modules/nutritional-balancings/presentation/components/ingredients-table/ingredients-table.hook.tsx create mode 100644 src/app/modules/nutritional-balancings/presentation/components/ingredients-table/ingredients-table.tsx diff --git a/src/app/modules/nutritional-balancings/presentation/components/ingredients-table/index.ts b/src/app/modules/nutritional-balancings/presentation/components/ingredients-table/index.ts new file mode 100644 index 00000000..6f79fea0 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/ingredients-table/index.ts @@ -0,0 +1 @@ +export * from './ingredients-table' diff --git a/src/app/modules/nutritional-balancings/presentation/components/ingredients-table/ingredients-table.hook.tsx b/src/app/modules/nutritional-balancings/presentation/components/ingredients-table/ingredients-table.hook.tsx new file mode 100644 index 00000000..4a871e66 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/ingredients-table/ingredients-table.hook.tsx @@ -0,0 +1,37 @@ +import { useMemo } from 'react' + +import { type ColumnDef } from '@tanstack/react-table' + +import { formatNumber } from '@/core/masker' + +import type { IngredientItemSchema } from '../../validations/nutritional-balancing-form-schema' + +export function useIngredientsTable() { + const columns = useMemo[]>( + () => [ + { + accessorKey: 'name', + header: 'Ingrediente', + meta: { + align: 'left', + }, + }, + { + accessorKey: 'quantity', + header: 'Quantidade', + meta: { + align: 'right', + }, + cell: ({ getValue }) => + formatNumber(getValue(), { + suffix: ' kg', + }), + }, + ], + [] + ) + + return { + columns, + } +} diff --git a/src/app/modules/nutritional-balancings/presentation/components/ingredients-table/ingredients-table.tsx b/src/app/modules/nutritional-balancings/presentation/components/ingredients-table/ingredients-table.tsx new file mode 100644 index 00000000..f0df944a --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/ingredients-table/ingredients-table.tsx @@ -0,0 +1,38 @@ +import { DotIcon } from 'lucide-react' + +import { DataTable } from '@/core/presentation/components/ui' +import { cn } from '@/core/utils' + +import { useIngredientsTable } from './ingredients-table.hook' + +import type { IngredientItemSchema } from '../../validations/nutritional-balancing-form-schema' + +type IngredientsTableProps = { + category: string + rows: IngredientItemSchema[] + categoryClassName?: string + pointerClassName?: string +} + +export function IngredientsTable({ + category, + rows, + categoryClassName, + pointerClassName, +}: Readonly) { + const { columns } = useIngredientsTable() + + return ( +
+
+ + + {category} + +
+ columns={columns} data={rows} noBorder /> +
+ ) +} + +IngredientsTable.displayName = 'IngredientsTable' From c8414a92665c5821ebe4e3ca63dad1a161ea1254 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 4 Nov 2025 11:49:16 -0300 Subject: [PATCH 42/82] feat: add general cultivation type --- .../remote-get-general-cultivation-use-case.ts | 3 ++- .../remote-get-general-cultivations-use-case.ts | 3 ++- .../domain/models/general-cultivations-model.ts | 10 ++++++---- .../get-general-cultivations-handler.ts | 5 +---- .../queries/all-general-cultivations-query.hook.ts | 6 +++++- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/app/modules/general-cultivations/data/use-cases/general-cultivations-use-cases/remote-get-general-cultivation-use-case.ts b/src/app/modules/general-cultivations/data/use-cases/general-cultivations-use-cases/remote-get-general-cultivation-use-case.ts index 36799c57..f00db5ee 100644 --- a/src/app/modules/general-cultivations/data/use-cases/general-cultivations-use-cases/remote-get-general-cultivation-use-case.ts +++ b/src/app/modules/general-cultivations/data/use-cases/general-cultivations-use-cases/remote-get-general-cultivation-use-case.ts @@ -8,6 +8,7 @@ import { import type { GeneralCultivationDetailsModel, GeneralCultivationDetailsApiResponse, + GeneralCultivationType, } from '../../../domain/models/general-cultivations-model' import type { GetGeneralCultivationUseCase } from '../../../domain/use-cases/general-cultivations-use-cases' @@ -31,7 +32,7 @@ export class RemoteGetGeneralCultivationUseCase if (statusCode === HttpStatusCode.ok && !!body) { return { name: body.name, - type: body.type, + type: body.type as GeneralCultivationType, } } diff --git a/src/app/modules/general-cultivations/data/use-cases/general-cultivations-use-cases/remote-get-general-cultivations-use-case.ts b/src/app/modules/general-cultivations/data/use-cases/general-cultivations-use-cases/remote-get-general-cultivations-use-case.ts index 59ef4a84..7e8d2d71 100644 --- a/src/app/modules/general-cultivations/data/use-cases/general-cultivations-use-cases/remote-get-general-cultivations-use-case.ts +++ b/src/app/modules/general-cultivations/data/use-cases/general-cultivations-use-cases/remote-get-general-cultivations-use-case.ts @@ -8,6 +8,7 @@ import { import type { GeneralCultivationApiResponse, GeneralCultivationModel, + GeneralCultivationType, } from '../../../domain/models/general-cultivations-model' import type { GetGeneralCultivationsUseCase } from '../../../domain/use-cases/general-cultivations-use-cases' import type { ListApiResponse, MapApiProperties } from '@/core/domain/types' @@ -53,7 +54,7 @@ export class RemoteGetGeneralCultivationsUseCase return { id: item.id, name: item.name, - type: item.type, + type: item.type as GeneralCultivationType, } }), totalPages: Math.ceil(body.numberOfElements / body.pageable.pageSize), diff --git a/src/app/modules/general-cultivations/domain/models/general-cultivations-model.ts b/src/app/modules/general-cultivations/domain/models/general-cultivations-model.ts index 69cdb026..cc40055d 100644 --- a/src/app/modules/general-cultivations/domain/models/general-cultivations-model.ts +++ b/src/app/modules/general-cultivations/domain/models/general-cultivations-model.ts @@ -1,21 +1,23 @@ import type { WithId } from '../../../../../core/domain/types' +export type GeneralCultivationType = 'FORAGE' | 'CONCENTRATE' | 'MINERAL' + export type GeneralCultivationDetailsModel = { name: string - type: 'FORAGE' | 'CONCENTRATE' | 'MINERAL' + type: GeneralCultivationType } export type GeneralCultivationDetailsApiResponse = { name: string - type: 'FORAGE' | 'CONCENTRATE' | 'MINERAL' + type: string } export type GeneralCultivationModel = WithId<{ name: string - type: 'FORAGE' | 'CONCENTRATE' | 'MINERAL' + type: GeneralCultivationType }> export type GeneralCultivationApiResponse = WithId<{ name: string - type: 'FORAGE' | 'CONCENTRATE' | 'MINERAL' + type: string }> diff --git a/src/app/modules/general-cultivations/mocks/handlers/general-cultivations-handlers/get-general-cultivations-handler.ts b/src/app/modules/general-cultivations/mocks/handlers/general-cultivations-handlers/get-general-cultivations-handler.ts index d23442ab..7a5bb9fe 100644 --- a/src/app/modules/general-cultivations/mocks/handlers/general-cultivations-handlers/get-general-cultivations-handler.ts +++ b/src/app/modules/general-cultivations/mocks/handlers/general-cultivations-handlers/get-general-cultivations-handler.ts @@ -37,10 +37,7 @@ export const getGeneralCultivationsHandler = httpWithMiddleware< ) } - let generalCultivations = generalCultivationsData.map((disease) => ({ - id: disease.id, - name: disease.name, - })) + let generalCultivations = generalCultivationsData if (filters) generalCultivations = filterData( diff --git a/src/app/modules/general-cultivations/presentation/hooks/queries/all-general-cultivations-query.hook.ts b/src/app/modules/general-cultivations/presentation/hooks/queries/all-general-cultivations-query.hook.ts index c23594de..f3fd57b6 100644 --- a/src/app/modules/general-cultivations/presentation/hooks/queries/all-general-cultivations-query.hook.ts +++ b/src/app/modules/general-cultivations/presentation/hooks/queries/all-general-cultivations-query.hook.ts @@ -38,7 +38,11 @@ export function useAllGeneralCultivationsQuery({ filters }: Props) { return { allGeneralCultivations: - data?.resources.map((resource) => toOption(resource, 'name')) ?? [], + data?.resources.map((resource) => + toOption(resource, 'name', { + type: resource.type, + }) + ) ?? [], isLoading, refetchAllGeneralCultivations, } From c238ac041660a429918660e9f2b47ba11c370a11 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 4 Nov 2025 11:49:35 -0300 Subject: [PATCH 43/82] feat: add onlyNumbersAndDecimalMask mask --- src/core/masker/masks/only-numbers-mask.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/masker/masks/only-numbers-mask.ts b/src/core/masker/masks/only-numbers-mask.ts index 55dacee7..08aa6879 100644 --- a/src/core/masker/masks/only-numbers-mask.ts +++ b/src/core/masker/masks/only-numbers-mask.ts @@ -1,3 +1,11 @@ export function onlyNumbersMask(value: string) { return value.replaceAll(/[^\d]/g, '') } + +export function onlyNumbersAndDecimalMask(value: string) { + let cleaned = value.replace(/[^\d,.]/g, '') + + cleaned = cleaned.replace(',', '.') + + return cleaned +} From 033a05a1fb6d4311e74d8c17a49537c97ddfb0c3 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 4 Nov 2025 11:49:50 -0300 Subject: [PATCH 44/82] feat: improve data table not found results message --- src/core/presentation/components/ui/data-table/data-table.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/presentation/components/ui/data-table/data-table.tsx b/src/core/presentation/components/ui/data-table/data-table.tsx index bcafa78a..d2bd3c58 100644 --- a/src/core/presentation/components/ui/data-table/data-table.tsx +++ b/src/core/presentation/components/ui/data-table/data-table.tsx @@ -67,7 +67,7 @@ export function TableBody({ return ( - No results. + Sem dados para exibir ) From fcc62a960ce6722d8eaf6c6f7fa32cd9cb72c3e3 Mon Sep 17 00:00:00 2001 From: Guilherme Minozzi Date: Tue, 4 Nov 2025 11:50:59 -0300 Subject: [PATCH 45/82] feat: add addIngredientDialog --- .../add-ingredient-dialog.hook.ts | 117 ++++++++++++++ .../add-ingredient-dialog.tsx | 144 ++++++++++++++++++ .../components/add-ingredient-dialog/index.ts | 1 + .../ingredients-table.hook.tsx | 15 +- .../ingredients-tab/ingredients-tab.hook.ts | 93 +++++++++-- .../tabs/ingredients-tab/ingredients-tab.tsx | 132 +++++++++------- ...reate-empty-nutritional-balancing-entry.ts | 30 +--- .../nutritional-balancing-form-schema.ts | 23 ++- 8 files changed, 445 insertions(+), 110 deletions(-) create mode 100644 src/app/modules/nutritional-balancings/presentation/components/add-ingredient-dialog/add-ingredient-dialog.hook.ts create mode 100644 src/app/modules/nutritional-balancings/presentation/components/add-ingredient-dialog/add-ingredient-dialog.tsx create mode 100644 src/app/modules/nutritional-balancings/presentation/components/add-ingredient-dialog/index.ts diff --git a/src/app/modules/nutritional-balancings/presentation/components/add-ingredient-dialog/add-ingredient-dialog.hook.ts b/src/app/modules/nutritional-balancings/presentation/components/add-ingredient-dialog/add-ingredient-dialog.hook.ts new file mode 100644 index 00000000..d450baf8 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/add-ingredient-dialog/add-ingredient-dialog.hook.ts @@ -0,0 +1,117 @@ +import { useMemo, useState } from 'react' + +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm, useFormContext } from 'react-hook-form' + +import { useAllGeneralCultivationsQuery } from '@/app/modules/general-cultivations/presentation/hooks/queries/all-general-cultivations-query.hook' +import { useDebounce } from '@/core/presentation/hooks' + +import { + ingredientItemSchema, + type IngredientItemSchema, + type NutritionalBalancingFormSchema, +} from '../../validations/nutritional-balancing-form-schema' + +import type { Option } from '@/core/domain/types' + +export const CATEGORY_LABELS: Record< + 'FORAGE' | 'CONCENTRATE' | 'MINERAL', + string +> = { + FORAGE: 'Volumoso', + CONCENTRATE: 'Concentrado', + MINERAL: 'Mineral', +} + +export type IngredientExtraData = { + type: 'FORAGE' | 'CONCENTRATE' | 'MINERAL' +} + +type UseAddIngredientDialogProps = { + currentAnimalIndex: number + onOpenChange: (open: boolean) => void + onSubmit: (data: IngredientItemSchema) => void +} + +export function useAddIngredientDialog({ + currentAnimalIndex, + onOpenChange, + onSubmit, +}: UseAddIngredientDialogProps) { + const parentForm = useFormContext() + const [searchIngredient, setSearchIngredient] = useState('') + const debouncedIngredient = useDebounce({ value: searchIngredient }) + + const ingredientGroups = parentForm.watch( + `nutritionalBalancings.${currentAnimalIndex}.ingredientGroups` + ) + + const selectedIds = useMemo( + () => + ingredientGroups + ?.flatMap((group) => group.ingredients) + .map((ingredient) => ingredient.ingredient.value) + .filter((id): id is number => typeof id === 'number' && id > 0) ?? [], + [ingredientGroups] + ) + + const { allGeneralCultivations, isLoading } = useAllGeneralCultivationsQuery({ + filters: { + name: { + value: debouncedIngredient, + type: 'LIKE', + }, + ...(selectedIds.length + ? { + id: { + value: selectedIds, + type: 'NOT_IN', + }, + } + : {}), + }, + }) + + const form = useForm({ + resolver: zodResolver(ingredientItemSchema), + defaultValues: { + ingredient: { label: '', value: 0 }, + quantity: '', + type: 'FORAGE', + }, + }) + + const handleSubmit = (data: IngredientItemSchema) => { + onSubmit(data) + form.reset() + setSearchIngredient('') + onOpenChange(false) + } + + const handleClose = () => { + form.reset() + setSearchIngredient('') + onOpenChange(false) + } + + const handleSelectIngredient = ( + item: Option, + onChange: (value: Option) => void + ) => { + onChange(item) + if (item.extraData?.type) { + form.setValue('type', item.extraData.type) + } + } + + return { + form, + searchIngredient, + setSearchIngredient, + allGeneralCultivations, + isLoading, + handleSubmit, + handleClose, + handleSelectIngredient, + } +} diff --git a/src/app/modules/nutritional-balancings/presentation/components/add-ingredient-dialog/add-ingredient-dialog.tsx b/src/app/modules/nutritional-balancings/presentation/components/add-ingredient-dialog/add-ingredient-dialog.tsx new file mode 100644 index 00000000..d23a0eaa --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/add-ingredient-dialog/add-ingredient-dialog.tsx @@ -0,0 +1,144 @@ +import { floatMask } from '@/core/masker' +import { + Button, + Combobox, + Dialog, + Form, + Input, +} from '@/core/presentation/components/ui/' + +import { + CATEGORY_LABELS, + useAddIngredientDialog, + type IngredientExtraData, +} from './add-ingredient-dialog.hook' + +import type { IngredientItemSchema } from '../../validations/nutritional-balancing-form-schema' + +type AddIngredientDialogProps = { + currentAnimalIndex: number + open: boolean + onOpenChange: (open: boolean) => void + onSubmit: (data: IngredientItemSchema) => void +} + +export function AddIngredientDialog({ + currentAnimalIndex, + open, + onOpenChange, + onSubmit, +}: Readonly) { + const { + form, + searchIngredient, + setSearchIngredient, + allGeneralCultivations, + isLoading, + handleSubmit, + handleClose, + handleSelectIngredient, + } = useAddIngredientDialog({ currentAnimalIndex, onOpenChange, onSubmit }) + + return ( + + + + Adicionar Ingrediente + + Selecione o ingrediente e informe a quantidade em kg. + + + + +
+ { + const { error } = fieldState + + return ( + + Ingrediente* + + + search={searchIngredient} + items={allGeneralCultivations} + loading={isLoading} + selected={field.value} + handleSearch={setSearchIngredient} + handleSelect={(item) => + handleSelectIngredient(item, field.onChange) + } + isError={!!error} + placeholder="Selecione um ingrediente" + emptyMessage="Nenhum ingrediente encontrado" + searchPlaceholder="Buscar ingrediente" + /> + + + + ) + }} + /> + + { + return ( + + Categoria + + + + + ) + }} + /> + + { + const { error } = fieldState + + return ( + + Quantidade (kg)* + + floatMask(value, 'kg')} + placeholder="0.00" + isError={!!error} + /> + + + + ) + }} + /> + +
+ + + + + +
+
+ ) +} diff --git a/src/app/modules/nutritional-balancings/presentation/components/add-ingredient-dialog/index.ts b/src/app/modules/nutritional-balancings/presentation/components/add-ingredient-dialog/index.ts new file mode 100644 index 00000000..4fbef354 --- /dev/null +++ b/src/app/modules/nutritional-balancings/presentation/components/add-ingredient-dialog/index.ts @@ -0,0 +1 @@ +export * from './add-ingredient-dialog' diff --git a/src/app/modules/nutritional-balancings/presentation/components/ingredients-table/ingredients-table.hook.tsx b/src/app/modules/nutritional-balancings/presentation/components/ingredients-table/ingredients-table.hook.tsx index 4a871e66..8f2cf3ba 100644 --- a/src/app/modules/nutritional-balancings/presentation/components/ingredients-table/ingredients-table.hook.tsx +++ b/src/app/modules/nutritional-balancings/presentation/components/ingredients-table/ingredients-table.hook.tsx @@ -2,19 +2,23 @@ import { useMemo } from 'react' import { type ColumnDef } from '@tanstack/react-table' -import { formatNumber } from '@/core/masker' - import type { IngredientItemSchema } from '../../validations/nutritional-balancing-form-schema' +import type { Option } from '@/core/domain/types' export function useIngredientsTable() { const columns = useMemo[]>( () => [ { - accessorKey: 'name', + accessorKey: 'ingredient', header: 'Ingrediente', meta: { align: 'left', }, + cell: ({ getValue }) => { + const { label: ingredientName } = getValue