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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions backend/src/eleicao/dto/create-eleicao.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
33 changes: 33 additions & 0 deletions backend/src/eleicao/dto/filter-eleicao.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
17 changes: 17 additions & 0 deletions backend/src/eleicao/dto/update-eleicao.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
54 changes: 51 additions & 3 deletions backend/src/eleicao/eleicao.controller.ts
Original file line number Diff line number Diff line change
@@ -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<RecordWithId> {
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<ListEleicaoDto[]> {
return this.eleicaoService.findAll(filters);
}

@Get(':id')
@ApiBearerAuth('access-token')
@Roles(['SMAE.superadmin'])
async findOne(@Param() params: FindOneParams): Promise<Eleicao> {
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<RecordWithId> {
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<void> {
await this.eleicaoService.remove(params.id, user);
}
}
157 changes: 153 additions & 4 deletions backend/src/eleicao/eleicao.service.ts
Original file line number Diff line number Diff line change
@@ -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<EleicaoDto[]> {
return await this.prisma.eleicao.findMany();
async create(createEleicaoDto: CreateEleicaoDto, user: PessoaFromJwt): Promise<Eleicao> {
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<ListEleicaoDto[]> {
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<Eleicao> {
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<Eleicao> {
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<void> {
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());
});
}
}