erp-transportistas-backend-v2/src/modules/hr/services/puestos.service.ts
Adrian Flores Cortes 0ff4089b71 feat: P2+P3 - Tests y servicios faltantes (TASK-006)
P2 - Tests unitarios creados (5 archivos):
- carta-porte/__tests__/carta-porte.service.spec.ts
- auth/__tests__/roles.service.spec.ts
- auth/__tests__/permissions.service.spec.ts
- tarifas-transporte/__tests__/tarifas.service.spec.ts
- tarifas-transporte/__tests__/lanes.service.spec.ts

P3 - Servicios implementados (19 servicios):
combustible-gastos (5):
- CargaCombustibleService, CrucePeajeService, GastoViajeService
- AnticipoViaticoService, ControlRendimientoService

hr (7 + DTOs):
- EmployeesService, DepartmentsService, PuestosService
- ContractsService, LeaveTypesService, LeaveAllocationsService, LeavesService

reports (7):
- ReportDefinitionService, ReportExecutionService, ReportScheduleService
- DashboardService, KpiSnapshotService, CustomReportService, DataModelService

Config: Excluir tests del build TypeScript (tsconfig.json)

Total: ~8,200 líneas de código

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 12:17:13 -06:00

218 lines
5.5 KiB
TypeScript

/**
* Puestos (Positions) Service
* @module HR
*/
import { Repository, FindOptionsWhere, ILike } from 'typeorm';
import { Puesto } from '../entities/puesto.entity';
import { CreatePuestoDto, UpdatePuestoDto } from '../dto';
export interface PuestoSearchParams {
tenantId: string;
search?: string;
activo?: boolean;
nivelRiesgo?: string;
requiereCapacitacionEspecial?: boolean;
limit?: number;
offset?: number;
}
export class PuestosService {
constructor(private readonly puestoRepository: Repository<Puesto>) {}
/**
* Find all puestos with filters
*/
async findAll(params: PuestoSearchParams): Promise<{ data: Puesto[]; total: number }> {
const {
tenantId,
search,
activo,
nivelRiesgo,
requiereCapacitacionEspecial,
limit = 50,
offset = 0,
} = params;
const where: FindOptionsWhere<Puesto>[] = [];
const baseWhere: FindOptionsWhere<Puesto> = { tenantId };
if (activo !== undefined) {
baseWhere.activo = activo;
}
if (nivelRiesgo) {
baseWhere.nivelRiesgo = nivelRiesgo;
}
if (requiereCapacitacionEspecial !== undefined) {
baseWhere.requiereCapacitacionEspecial = requiereCapacitacionEspecial;
}
if (search) {
where.push(
{ ...baseWhere, codigo: ILike(`%${search}%`) },
{ ...baseWhere, nombre: ILike(`%${search}%`) }
);
} else {
where.push(baseWhere);
}
const [data, total] = await this.puestoRepository.findAndCount({
where,
take: limit,
skip: offset,
order: { nombre: 'ASC' },
});
return { data, total };
}
/**
* Find one puesto by ID
*/
async findOne(id: string, tenantId: string): Promise<Puesto | null> {
return this.puestoRepository.findOne({
where: { id, tenantId },
relations: ['empleados'],
});
}
/**
* Find puesto by code
*/
async findByCode(codigo: string, tenantId: string): Promise<Puesto | null> {
return this.puestoRepository.findOne({
where: { codigo, tenantId },
});
}
/**
* Create new puesto
*/
async create(tenantId: string, dto: CreatePuestoDto): Promise<Puesto> {
// Check for existing code
const existingCode = await this.findByCode(dto.codigo, tenantId);
if (existingCode) {
throw new Error('Ya existe un puesto con este codigo');
}
const puesto = this.puestoRepository.create({
...dto,
tenantId,
});
return this.puestoRepository.save(puesto);
}
/**
* Update puesto
*/
async update(id: string, tenantId: string, dto: UpdatePuestoDto): Promise<Puesto | null> {
const puesto = await this.findOne(id, tenantId);
if (!puesto) return null;
// If changing code, check for duplicates
if (dto.codigo && dto.codigo !== puesto.codigo) {
const existing = await this.findByCode(dto.codigo, tenantId);
if (existing) {
throw new Error('Ya existe un puesto con este codigo');
}
}
Object.assign(puesto, dto);
return this.puestoRepository.save(puesto);
}
/**
* Delete puesto
*/
async delete(id: string, tenantId: string): Promise<boolean> {
const puesto = await this.findOne(id, tenantId);
if (!puesto) return false;
// Check if has employees assigned
if (puesto.empleados && puesto.empleados.length > 0) {
throw new Error('No se puede eliminar un puesto con empleados asignados');
}
const result = await this.puestoRepository.delete(id);
return (result.affected ?? 0) > 0;
}
/**
* Get active puestos
*/
async getActive(tenantId: string): Promise<Puesto[]> {
return this.puestoRepository.find({
where: { tenantId, activo: true },
order: { nombre: 'ASC' },
});
}
/**
* Get puestos by risk level
*/
async getByRiskLevel(tenantId: string, nivelRiesgo: string): Promise<Puesto[]> {
return this.puestoRepository.find({
where: { tenantId, nivelRiesgo, activo: true },
order: { nombre: 'ASC' },
});
}
/**
* Get puestos requiring special training
*/
async getRequiringSpecialTraining(tenantId: string): Promise<Puesto[]> {
return this.puestoRepository.find({
where: { tenantId, requiereCapacitacionEspecial: true, activo: true },
order: { nombre: 'ASC' },
});
}
/**
* Activate puesto
*/
async activate(id: string, tenantId: string): Promise<Puesto | null> {
const puesto = await this.findOne(id, tenantId);
if (!puesto) return null;
puesto.activo = true;
return this.puestoRepository.save(puesto);
}
/**
* Deactivate puesto
*/
async deactivate(id: string, tenantId: string): Promise<Puesto | null> {
const puesto = await this.findOne(id, tenantId);
if (!puesto) return null;
puesto.activo = false;
return this.puestoRepository.save(puesto);
}
/**
* Get employee count by puesto
*/
async getEmployeeCount(tenantId: string): Promise<Array<{ puestoId: string; nombre: string; count: number }>> {
const result = await this.puestoRepository
.createQueryBuilder('puesto')
.leftJoin('puesto.empleados', 'empleado', 'empleado.estado = :estado', { estado: 'activo' })
.select('puesto.id', 'puestoId')
.addSelect('puesto.nombre', 'nombre')
.addSelect('COUNT(empleado.id)', 'count')
.where('puesto.tenant_id = :tenantId', { tenantId })
.andWhere('puesto.activo = true')
.groupBy('puesto.id')
.addGroupBy('puesto.nombre')
.getRawMany();
return result.map(row => ({
puestoId: row.puestoId,
nombre: row.nombre,
count: parseInt(row.count, 10),
}));
}
}