From bd8fb2333ca85cec2e413fc691b2cd788a5491c3 Mon Sep 17 00:00:00 2001 From: Mateus Martinez rosa Date: Tue, 26 Aug 2025 10:06:43 -0300 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20implementando=20CRUD=20de=20Elei?= =?UTF-8?q?=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/eleicao/dto/create-eleicao.dto.ts | 23 +++ backend/src/eleicao/dto/filter-eleicao.dto.ts | 33 ++++ backend/src/eleicao/dto/update-eleicao.dto.ts | 4 + backend/src/eleicao/eleicao.controller.ts | 60 ++++++- backend/src/eleicao/eleicao.service.ts | 154 +++++++++++++++++- 5 files changed, 267 insertions(+), 7 deletions(-) create mode 100644 backend/src/eleicao/dto/create-eleicao.dto.ts create mode 100644 backend/src/eleicao/dto/filter-eleicao.dto.ts create mode 100644 backend/src/eleicao/dto/update-eleicao.dto.ts diff --git a/backend/src/eleicao/dto/create-eleicao.dto.ts b/backend/src/eleicao/dto/create-eleicao.dto.ts new file mode 100644 index 0000000000..db888064be --- /dev/null +++ b/backend/src/eleicao/dto/create-eleicao.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { EleicaoTipo } from '@prisma/client'; +import { Transform } from 'class-transformer'; +import { IsBoolean, IsEnum, IsInt, Max, Min } from 'class-validator'; + +export class CreateEleicaoDto { + @ApiProperty({ enum: EleicaoTipo, enumName: 'EleicaoTipo' }) + @IsEnum(EleicaoTipo, { + message: '$property| Precisa ser um dos seguintes valores: ' + Object.values(EleicaoTipo).join(', '), + }) + tipo: EleicaoTipo; + + @ApiProperty({ description: 'Ano da eleição', example: 2024 }) + @IsInt() + @Min(1900) + @Max(2100) + ano: number; + + @ApiProperty({ description: 'Se esta eleição está sendo usada para mandatos atuais' }) + @IsBoolean() + @Transform(({ value }) => value === 'true' || value === true) + atual_para_mandatos: boolean; +} diff --git a/backend/src/eleicao/dto/filter-eleicao.dto.ts b/backend/src/eleicao/dto/filter-eleicao.dto.ts new file mode 100644 index 0000000000..da2e6e2195 --- /dev/null +++ b/backend/src/eleicao/dto/filter-eleicao.dto.ts @@ -0,0 +1,33 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { EleicaoTipo } from '@prisma/client'; +import { Transform } from 'class-transformer'; +import { IsBoolean, IsEnum, IsInt, IsOptional, Max, Min } from 'class-validator'; + +export class FilterEleicaoDto { + @ApiPropertyOptional({ enum: EleicaoTipo, enumName: 'EleicaoTipo' }) + @IsOptional() + @IsEnum(EleicaoTipo) + tipo?: EleicaoTipo; + + @ApiPropertyOptional({ description: 'Ano da eleição' }) + @IsOptional() + @IsInt() + @Min(1900) + @Max(2100) + @Transform(({ value }) => (value === '' || value === null || value === undefined ? undefined : +value)) + ano?: number; + + @ApiPropertyOptional({ description: 'Filtrar apenas eleições atuais para mandatos' }) + @IsOptional() + @IsBoolean() + @Transform(({ value }) => { + if (value === undefined || value === null || value === '') { + return undefined; + } + if (typeof value === 'string') { + return value.toLowerCase() === 'true'; + } + return value; + }) + atual_para_mandatos?: boolean; +} diff --git a/backend/src/eleicao/dto/update-eleicao.dto.ts b/backend/src/eleicao/dto/update-eleicao.dto.ts new file mode 100644 index 0000000000..b81e86e335 --- /dev/null +++ b/backend/src/eleicao/dto/update-eleicao.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateEleicaoDto } from './create-eleicao.dto'; + +export class UpdateEleicaoDto extends PartialType(CreateEleicaoDto) {} diff --git a/backend/src/eleicao/eleicao.controller.ts b/backend/src/eleicao/eleicao.controller.ts index 97a758eac0..462cb75c4b 100644 --- a/backend/src/eleicao/eleicao.controller.ts +++ b/backend/src/eleicao/eleicao.controller.ts @@ -1,8 +1,17 @@ -import { Controller, Get } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpCode, Param, Patch, Post, Query } from '@nestjs/common'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { CurrentUser } from '../auth/decorators/current-user.decorator'; import { PessoaFromJwt } from '../auth/models/PessoaFromJwt'; import { EleicaoService } from './eleicao.service'; +import { Roles } from 'src/auth/decorators/roles.decorator'; +import { CreateEleicaoDto } from './dto/create-eleicao.dto'; +import { RecordWithId } from 'src/common/dto/record-with-id.dto'; +import { FilterEleicaoDto } from './dto/filter-eleicao.dto'; +import { Eleicao } from '@prisma/client'; +import { FindOneParams } from 'src/common/decorators/find-params'; +import { UpdateEleicaoDto } from './dto/update-eleicao.dto'; +import { Logger } from '@nestjs/common'; +import { ListEleicaoDto } from './entity/eleicao.entity'; @ApiTags('Eleição') @Controller('eleicao') @@ -11,7 +20,52 @@ export class EleicaoController { @Get() @ApiBearerAuth('access-token') - async getList(@CurrentUser() user: PessoaFromJwt) { - return await this.eleicaoService.findAll(); + async getList(@Query() filters: FilterEleicaoDto, @CurrentUser() user: PessoaFromJwt) { + return await this.eleicaoService.findAll(filters); + } + + @Post() + @ApiBearerAuth('access-token') + @Roles(['SMAE.superadmin']) + async create( + @Body() createEleicaoDto: CreateEleicaoDto, + @CurrentUser() user: PessoaFromJwt + ): Promise { + const created = await this.eleicaoService.create(createEleicaoDto, user); + return { id: created.id }; + } + + @Get() + @ApiBearerAuth('access-token') + @Roles(['SMAE.superadmin']) + async findAll(@Query() filters: FilterEleicaoDto): Promise { + return this.eleicaoService.findAll(filters); + } + + @Get(':id') + @ApiBearerAuth('access-token') + @Roles(['SMAE.superadmin']) + async findOne(@Param() params: FindOneParams): Promise { + return this.eleicaoService.findOne(params.id); + } + + @Patch(':id') + @ApiBearerAuth('access-token') + @Roles(['SMAE.superadmin']) + async update( + @Param() params: FindOneParams, + @Body() updateEleicaoDto: UpdateEleicaoDto, + @CurrentUser() user: PessoaFromJwt + ): Promise { + const updated = await this.eleicaoService.update(params.id, updateEleicaoDto, user); + return { id: updated.id }; + } + + @Delete(':id') + @ApiBearerAuth('access-token') + @Roles(['SMAE.superadmin']) + @HttpCode(204) + async remove(@Param() params: FindOneParams, @CurrentUser() user: PessoaFromJwt): Promise { + await this.eleicaoService.remove(params.id, user); } } diff --git a/backend/src/eleicao/eleicao.service.ts b/backend/src/eleicao/eleicao.service.ts index 2814fcc2cc..fcc2058e89 100644 --- a/backend/src/eleicao/eleicao.service.ts +++ b/backend/src/eleicao/eleicao.service.ts @@ -1,12 +1,158 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, BadRequestException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; -import { EleicaoDto } from './entity/eleicao.entity'; +import { Prisma } from '@prisma/client'; +import { ListEleicaoDto } from './entity/eleicao.entity'; +import { CreateEleicaoDto } from './dto/create-eleicao.dto'; +import { Eleicao } from '@prisma/client'; +import { FilterEleicaoDto } from './dto/filter-eleicao.dto'; +import { UpdateEleicaoDto } from './dto/update-eleicao.dto'; +import { LoggerWithLog } from 'src/common/LoggerWithLog'; +import { PessoaFromJwt } from 'src/auth/models/PessoaFromJwt'; @Injectable() export class EleicaoService { constructor(private readonly prisma: PrismaService) {} - async findAll(): Promise { - return await this.prisma.eleicao.findMany(); + async create(createEleicaoDto: CreateEleicaoDto, user: PessoaFromJwt): Promise { + const logger = LoggerWithLog('Eleição: Criação'); + try { + const created = await this.prisma.$transaction(async (tx) => { + logger.log(`Criando eleição do tipo ${createEleicaoDto.tipo}, ano ${createEleicaoDto.ano}`); + const record = await tx.eleicao.create({ + data: createEleicaoDto, + select: { + id: true, + tipo: true, + ano: true, + atual_para_mandatos: true, + removido_em: true, + }, + }); + await logger.saveLogs(tx, user.getLogData()); + return record; + }); + return created; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === 'P2002') { + throw new BadRequestException('Já existe uma eleição deste tipo para este ano'); + } + } + throw error; + } + } + + async findAll(filters: FilterEleicaoDto): Promise { + const eleicoes = await this.prisma.eleicao.findMany({ + where: { + removido_em: null, + tipo: filters?.tipo, + ano: filters?.ano, + atual_para_mandatos: filters?.atual_para_mandatos, + }, + select: { + id: true, + tipo: true, + ano: true, + atual_para_mandatos: true, + removido_em: true, + }, + orderBy: [{ ano: 'desc' }, { tipo: 'asc' }], + }); + return [{ linhas: eleicoes }]; + } + + async findOne(id: number): Promise { + const eleicao = await this.prisma.eleicao.findFirst({ + where: { + id: id, + removido_em: null, + }, + select: { + id: true, + tipo: true, + ano: true, + atual_para_mandatos: true, + removido_em: true, + }, + }); + + if (!eleicao) { + throw new BadRequestException('Eleição não encontrada'); + } + + return eleicao; + } + + async update(id: number, dto: UpdateEleicaoDto, user: PessoaFromJwt): Promise { + const logger = LoggerWithLog('Eleição: Atualização'); + + if (dto.tipo !== undefined && dto.ano !== undefined) { + const conflito = await this.prisma.eleicao.count({ + where: { + tipo: dto.tipo, + ano: dto.ano, + removido_em: null, + NOT: { id }, + }, + }); + if (conflito > 0) { + throw new BadRequestException('Já existe uma eleição deste tipo para este ano'); + } + } + + const updated = await this.prisma.$transaction(async (tx) => { + logger.log( + `Atualizando eleição ${id}: novo tipo=${dto.tipo ?? 'sem alteração'}, novo ano=${dto.ano ?? 'sem alteração'}` + ); + + const record = await tx.eleicao.update({ + where: { id }, + data: dto, + select: { + id: true, + tipo: true, + ano: true, + atual_para_mandatos: true, + removido_em: true, + }, + }); + + await logger.saveLogs(tx, user.getLogData()); + + return record; + }); + + return updated; + } + + async remove(id: number, user: PessoaFromJwt): Promise { + const logger = LoggerWithLog('Eleição: Remoção'); + logger.log(`Removendo eleição ${id}`); + await this.findOne(id); // Verifica se existe + + // Verifica se tem relacionamentos antes de remover + const mandatos = await this.prisma.parlamentarMandato.count({ + where: { eleicao_id: id }, + }); + + const comparecimentos = await this.prisma.eleicaoComparecimento.count({ + where: { eleicao_id: id }, + }); + + if (mandatos > 0 || comparecimentos > 0) { + throw new BadRequestException( + 'Não é possível remover esta eleição pois ela possui mandatos ou comparecimentos associados' + ); + } + + await this.prisma.$transaction(async (prismaTx: Prisma.TransactionClient) => { + logger.log(`Eleição: ${id}`); + await prismaTx.eleicao.update({ + where: { id: id }, + data: { removido_em: new Date() }, + }); + await logger.saveLogs(prismaTx, user.getLogData()); + }); } } From 18f38b51587382c39a5aa60f86c9232066f8e340 Mon Sep 17 00:00:00 2001 From: Mateus Martinez rosa Date: Tue, 26 Aug 2025 11:06:07 -0300 Subject: [PATCH 2/2] feat: aplicando melhorias --- backend/src/eleicao/dto/create-eleicao.dto.ts | 3 +- backend/src/eleicao/dto/update-eleicao.dto.ts | 15 +++++- backend/src/eleicao/eleicao.controller.ts | 6 --- backend/src/eleicao/eleicao.service.ts | 47 ++++++++++--------- 4 files changed, 41 insertions(+), 30 deletions(-) diff --git a/backend/src/eleicao/dto/create-eleicao.dto.ts b/backend/src/eleicao/dto/create-eleicao.dto.ts index db888064be..2dee2f4e92 100644 --- a/backend/src/eleicao/dto/create-eleicao.dto.ts +++ b/backend/src/eleicao/dto/create-eleicao.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { EleicaoTipo } from '@prisma/client'; -import { Transform } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; import { IsBoolean, IsEnum, IsInt, Max, Min } from 'class-validator'; export class CreateEleicaoDto { @@ -11,6 +11,7 @@ export class CreateEleicaoDto { tipo: EleicaoTipo; @ApiProperty({ description: 'Ano da eleição', example: 2024 }) + @Type(() => Number) @IsInt() @Min(1900) @Max(2100) diff --git a/backend/src/eleicao/dto/update-eleicao.dto.ts b/backend/src/eleicao/dto/update-eleicao.dto.ts index b81e86e335..a8062584a5 100644 --- a/backend/src/eleicao/dto/update-eleicao.dto.ts +++ b/backend/src/eleicao/dto/update-eleicao.dto.ts @@ -1,4 +1,17 @@ import { PartialType } from '@nestjs/swagger'; import { CreateEleicaoDto } from './create-eleicao.dto'; +import { Transform } from 'class-transformer'; +import { IsBoolean, IsOptional } from 'class-validator'; -export class UpdateEleicaoDto extends PartialType(CreateEleicaoDto) {} +export class UpdateEleicaoDto extends PartialType(CreateEleicaoDto) { + @IsOptional() + @IsBoolean() + @Transform(({ value }) => + value === '' || value === null || value === undefined + ? undefined + : typeof value === 'string' + ? value.toLowerCase() === 'true' + : value + ) + atual_para_mandatos?: boolean; +} diff --git a/backend/src/eleicao/eleicao.controller.ts b/backend/src/eleicao/eleicao.controller.ts index 462cb75c4b..f562bff295 100644 --- a/backend/src/eleicao/eleicao.controller.ts +++ b/backend/src/eleicao/eleicao.controller.ts @@ -18,12 +18,6 @@ import { ListEleicaoDto } from './entity/eleicao.entity'; export class EleicaoController { constructor(private readonly eleicaoService: EleicaoService) {} - @Get() - @ApiBearerAuth('access-token') - async getList(@Query() filters: FilterEleicaoDto, @CurrentUser() user: PessoaFromJwt) { - return await this.eleicaoService.findAll(filters); - } - @Post() @ApiBearerAuth('access-token') @Roles(['SMAE.superadmin']) diff --git a/backend/src/eleicao/eleicao.service.ts b/backend/src/eleicao/eleicao.service.ts index fcc2058e89..7893f768ba 100644 --- a/backend/src/eleicao/eleicao.service.ts +++ b/backend/src/eleicao/eleicao.service.ts @@ -101,29 +101,32 @@ export class EleicaoService { } } - const updated = await this.prisma.$transaction(async (tx) => { - logger.log( - `Atualizando eleição ${id}: novo tipo=${dto.tipo ?? 'sem alteração'}, novo ano=${dto.ano ?? 'sem alteração'}` - ); - - const record = await tx.eleicao.update({ - where: { id }, - data: dto, - select: { - id: true, - tipo: true, - ano: true, - atual_para_mandatos: true, - removido_em: true, - }, + try { + const updated = await this.prisma.$transaction(async (tx) => { + logger.log( + `Atualizando eleição ${id}: novo tipo=${dto.tipo ?? 'sem alteração'}, novo ano=${dto.ano ?? 'sem alteração'}, atual_para_mandatos=${dto.atual_para_mandatos ?? 'sem alteração'}` + ); + const record = await tx.eleicao.update({ + where: { id }, + data: dto, + select: { + id: true, + tipo: true, + ano: true, + atual_para_mandatos: true, + removido_em: true, + }, + }); + await logger.saveLogs(tx, user.getLogData()); + return record; }); - - await logger.saveLogs(tx, user.getLogData()); - - return record; - }); - - return updated; + return updated; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') { + throw new BadRequestException('Já existe uma eleição deste tipo para este ano'); + } + throw error; + } } async remove(id: number, user: PessoaFromJwt): Promise {