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>
218 lines
5.5 KiB
TypeScript
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),
|
|
}));
|
|
}
|
|
}
|