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..2dee2f4e92 --- /dev/null +++ b/backend/src/eleicao/dto/create-eleicao.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { EleicaoTipo } from '@prisma/client'; +import { Transform, Type } 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 }) + @Type(() => Number) + @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..a8062584a5 --- /dev/null +++ b/backend/src/eleicao/dto/update-eleicao.dto.ts @@ -0,0 +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) { + @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 97a758eac0..f562bff295 100644 --- a/backend/src/eleicao/eleicao.controller.ts +++ b/backend/src/eleicao/eleicao.controller.ts @@ -1,17 +1,65 @@ -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') export class EleicaoController { constructor(private readonly eleicaoService: EleicaoService) {} + @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') - async getList(@CurrentUser() user: PessoaFromJwt) { - return await this.eleicaoService.findAll(); + @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..7893f768ba 100644 --- a/backend/src/eleicao/eleicao.service.ts +++ b/backend/src/eleicao/eleicao.service.ts @@ -1,12 +1,161 @@ -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'); + } + } + + 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; + }); + 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 { + 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()); + }); } }