import { Response, NextFunction } from 'express'; import { z } from 'zod'; import { employeesService, CreateEmployeeDto, UpdateEmployeeDto, EmployeeFilters } from './employees.service.js'; import { departmentsService, CreateDepartmentDto, UpdateDepartmentDto, DepartmentFilters, CreateJobPositionDto, UpdateJobPositionDto } from './departments.service.js'; import { contractsService, CreateContractDto, UpdateContractDto, ContractFilters } from './contracts.service.js'; import { leavesService, CreateLeaveDto, UpdateLeaveDto, LeaveFilters, CreateLeaveTypeDto, UpdateLeaveTypeDto } from './leaves.service.js'; import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js'; import { ValidationError } from '../../shared/errors/index.js'; // Employee schemas const createEmployeeSchema = z.object({ company_id: z.string().uuid(), employee_number: z.string().min(1).max(50), first_name: z.string().min(1).max(100), last_name: z.string().min(1).max(100), middle_name: z.string().max(100).optional(), user_id: z.string().uuid().optional(), birth_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), gender: z.string().max(20).optional(), marital_status: z.string().max(20).optional(), nationality: z.string().max(100).optional(), identification_id: z.string().max(50).optional(), identification_type: z.string().max(50).optional(), social_security_number: z.string().max(50).optional(), tax_id: z.string().max(50).optional(), email: z.string().email().max(255).optional(), work_email: z.string().email().max(255).optional(), phone: z.string().max(50).optional(), work_phone: z.string().max(50).optional(), mobile: z.string().max(50).optional(), emergency_contact: z.string().max(255).optional(), emergency_phone: z.string().max(50).optional(), street: z.string().max(255).optional(), city: z.string().max(100).optional(), state: z.string().max(100).optional(), zip: z.string().max(20).optional(), country: z.string().max(100).optional(), department_id: z.string().uuid().optional(), job_position_id: z.string().uuid().optional(), manager_id: z.string().uuid().optional(), hire_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), bank_name: z.string().max(100).optional(), bank_account: z.string().max(50).optional(), bank_clabe: z.string().max(20).optional(), photo_url: z.string().url().max(500).optional(), notes: z.string().optional(), }); const updateEmployeeSchema = createEmployeeSchema.partial().omit({ company_id: true, employee_number: true, hire_date: true }); const employeeQuerySchema = z.object({ company_id: z.string().uuid().optional(), department_id: z.string().uuid().optional(), status: z.enum(['active', 'inactive', 'on_leave', 'terminated']).optional(), manager_id: z.string().uuid().optional(), search: z.string().optional(), page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(20), }); // Department schemas const createDepartmentSchema = z.object({ company_id: z.string().uuid(), name: z.string().min(1).max(100), code: z.string().max(20).optional(), parent_id: z.string().uuid().optional(), manager_id: z.string().uuid().optional(), description: z.string().optional(), color: z.string().max(20).optional(), }); const updateDepartmentSchema = z.object({ name: z.string().min(1).max(100).optional(), code: z.string().max(20).optional().nullable(), parent_id: z.string().uuid().optional().nullable(), manager_id: z.string().uuid().optional().nullable(), description: z.string().optional().nullable(), color: z.string().max(20).optional().nullable(), active: z.boolean().optional(), }); const departmentQuerySchema = z.object({ company_id: z.string().uuid().optional(), active: z.coerce.boolean().optional(), search: z.string().optional(), page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(50), }); // Job Position schemas const createJobPositionSchema = z.object({ name: z.string().min(1).max(100), department_id: z.string().uuid().optional(), description: z.string().optional(), requirements: z.string().optional(), responsibilities: z.string().optional(), min_salary: z.number().min(0).optional(), max_salary: z.number().min(0).optional(), }); const updateJobPositionSchema = z.object({ name: z.string().min(1).max(100).optional(), department_id: z.string().uuid().optional().nullable(), description: z.string().optional().nullable(), requirements: z.string().optional().nullable(), responsibilities: z.string().optional().nullable(), min_salary: z.number().min(0).optional().nullable(), max_salary: z.number().min(0).optional().nullable(), active: z.boolean().optional(), }); // Contract schemas const createContractSchema = z.object({ company_id: z.string().uuid(), employee_id: z.string().uuid(), name: z.string().min(1).max(100), reference: z.string().max(100).optional(), contract_type: z.enum(['permanent', 'temporary', 'contractor', 'internship', 'part_time']), job_position_id: z.string().uuid().optional(), department_id: z.string().uuid().optional(), date_start: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), date_end: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), trial_date_end: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), wage: z.number().min(0), wage_type: z.string().max(20).optional(), currency_id: z.string().uuid().optional(), hours_per_week: z.number().min(0).max(168).optional(), vacation_days: z.number().int().min(0).optional(), christmas_bonus_days: z.number().int().min(0).optional(), document_url: z.string().url().max(500).optional(), notes: z.string().optional(), }); const updateContractSchema = z.object({ reference: z.string().max(100).optional().nullable(), job_position_id: z.string().uuid().optional().nullable(), department_id: z.string().uuid().optional().nullable(), date_end: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(), trial_date_end: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(), wage: z.number().min(0).optional(), wage_type: z.string().max(20).optional(), currency_id: z.string().uuid().optional().nullable(), hours_per_week: z.number().min(0).max(168).optional(), vacation_days: z.number().int().min(0).optional(), christmas_bonus_days: z.number().int().min(0).optional(), document_url: z.string().url().max(500).optional().nullable(), notes: z.string().optional().nullable(), }); const contractQuerySchema = z.object({ company_id: z.string().uuid().optional(), employee_id: z.string().uuid().optional(), status: z.enum(['draft', 'active', 'expired', 'terminated', 'cancelled']).optional(), contract_type: z.enum(['permanent', 'temporary', 'contractor', 'internship', 'part_time']).optional(), search: z.string().optional(), page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(20), }); // Leave Type schemas const createLeaveTypeSchema = z.object({ name: z.string().min(1).max(100), code: z.string().max(20).optional(), leave_type: z.enum(['vacation', 'sick', 'personal', 'maternity', 'paternity', 'bereavement', 'unpaid', 'other']), requires_approval: z.boolean().optional(), max_days: z.number().int().min(1).optional(), is_paid: z.boolean().optional(), color: z.string().max(20).optional(), }); const updateLeaveTypeSchema = z.object({ name: z.string().min(1).max(100).optional(), code: z.string().max(20).optional().nullable(), requires_approval: z.boolean().optional(), max_days: z.number().int().min(1).optional().nullable(), is_paid: z.boolean().optional(), color: z.string().max(20).optional().nullable(), active: z.boolean().optional(), }); // Leave schemas const createLeaveSchema = z.object({ company_id: z.string().uuid(), employee_id: z.string().uuid(), leave_type_id: z.string().uuid(), name: z.string().max(255).optional(), date_from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), date_to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), description: z.string().optional(), }); const updateLeaveSchema = z.object({ leave_type_id: z.string().uuid().optional(), name: z.string().max(255).optional().nullable(), date_from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), date_to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), description: z.string().optional().nullable(), }); const leaveQuerySchema = z.object({ company_id: z.string().uuid().optional(), employee_id: z.string().uuid().optional(), leave_type_id: z.string().uuid().optional(), status: z.enum(['draft', 'submitted', 'approved', 'rejected', 'cancelled']).optional(), date_from: z.string().optional(), date_to: z.string().optional(), search: z.string().optional(), page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(20), }); const terminateSchema = z.object({ termination_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), }); const rejectSchema = z.object({ reason: z.string().min(1), }); class HrController { // ========== EMPLOYEES ========== async getEmployees(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const queryResult = employeeQuerySchema.safeParse(req.query); if (!queryResult.success) { throw new ValidationError('Parametros de consulta invalidos', queryResult.error.errors); } const filters: EmployeeFilters = queryResult.data; const result = await employeesService.findAll(req.tenantId!, filters); res.json({ success: true, data: result.data, meta: { total: result.total, page: filters.page, limit: filters.limit, totalPages: Math.ceil(result.total / (filters.limit || 20)) }, }); } catch (error) { next(error); } } async getEmployee(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const employee = await employeesService.findById(req.params.id, req.tenantId!); res.json({ success: true, data: employee }); } catch (error) { next(error); } } async createEmployee(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = createEmployeeSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de empleado invalidos', parseResult.error.errors); } const dto: CreateEmployeeDto = parseResult.data; const employee = await employeesService.create(dto, req.tenantId!, req.user!.userId); res.status(201).json({ success: true, data: employee, message: 'Empleado creado exitosamente' }); } catch (error) { next(error); } } async updateEmployee(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = updateEmployeeSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de empleado invalidos', parseResult.error.errors); } const dto: UpdateEmployeeDto = parseResult.data; const employee = await employeesService.update(req.params.id, dto, req.tenantId!, req.user!.userId); res.json({ success: true, data: employee, message: 'Empleado actualizado exitosamente' }); } catch (error) { next(error); } } async terminateEmployee(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = terminateSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos invalidos', parseResult.error.errors); } const employee = await employeesService.terminate(req.params.id, parseResult.data.termination_date, req.tenantId!, req.user!.userId); res.json({ success: true, data: employee, message: 'Empleado dado de baja exitosamente' }); } catch (error) { next(error); } } async reactivateEmployee(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const employee = await employeesService.reactivate(req.params.id, req.tenantId!, req.user!.userId); res.json({ success: true, data: employee, message: 'Empleado reactivado exitosamente' }); } catch (error) { next(error); } } async deleteEmployee(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { await employeesService.delete(req.params.id, req.tenantId!); res.json({ success: true, message: 'Empleado eliminado exitosamente' }); } catch (error) { next(error); } } async getSubordinates(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const subordinates = await employeesService.getSubordinates(req.params.id, req.tenantId!); res.json({ success: true, data: subordinates }); } catch (error) { next(error); } } // ========== DEPARTMENTS ========== async getDepartments(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const queryResult = departmentQuerySchema.safeParse(req.query); if (!queryResult.success) { throw new ValidationError('Parametros de consulta invalidos', queryResult.error.errors); } const filters: DepartmentFilters = queryResult.data; const result = await departmentsService.findAll(req.tenantId!, filters); res.json({ success: true, data: result.data, meta: { total: result.total, page: filters.page, limit: filters.limit, totalPages: Math.ceil(result.total / (filters.limit || 50)) }, }); } catch (error) { next(error); } } async getDepartment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const department = await departmentsService.findById(req.params.id, req.tenantId!); res.json({ success: true, data: department }); } catch (error) { next(error); } } async createDepartment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = createDepartmentSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de departamento invalidos', parseResult.error.errors); } const dto: CreateDepartmentDto = parseResult.data; const department = await departmentsService.create(dto, req.tenantId!, req.user!.userId); res.status(201).json({ success: true, data: department, message: 'Departamento creado exitosamente' }); } catch (error) { next(error); } } async updateDepartment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = updateDepartmentSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de departamento invalidos', parseResult.error.errors); } const dto: UpdateDepartmentDto = parseResult.data; const department = await departmentsService.update(req.params.id, dto, req.tenantId!); res.json({ success: true, data: department, message: 'Departamento actualizado exitosamente' }); } catch (error) { next(error); } } async deleteDepartment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { await departmentsService.delete(req.params.id, req.tenantId!); res.json({ success: true, message: 'Departamento eliminado exitosamente' }); } catch (error) { next(error); } } // ========== JOB POSITIONS ========== async getJobPositions(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const includeInactive = req.query.include_inactive === 'true'; const positions = await departmentsService.getJobPositions(req.tenantId!, includeInactive); res.json({ success: true, data: positions }); } catch (error) { next(error); } } async createJobPosition(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = createJobPositionSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de puesto invalidos', parseResult.error.errors); } const dto: CreateJobPositionDto = parseResult.data; const position = await departmentsService.createJobPosition(dto, req.tenantId!); res.status(201).json({ success: true, data: position, message: 'Puesto creado exitosamente' }); } catch (error) { next(error); } } async updateJobPosition(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = updateJobPositionSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de puesto invalidos', parseResult.error.errors); } const dto: UpdateJobPositionDto = parseResult.data; const position = await departmentsService.updateJobPosition(req.params.id, dto, req.tenantId!); res.json({ success: true, data: position, message: 'Puesto actualizado exitosamente' }); } catch (error) { next(error); } } async deleteJobPosition(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { await departmentsService.deleteJobPosition(req.params.id, req.tenantId!); res.json({ success: true, message: 'Puesto eliminado exitosamente' }); } catch (error) { next(error); } } // ========== CONTRACTS ========== async getContracts(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const queryResult = contractQuerySchema.safeParse(req.query); if (!queryResult.success) { throw new ValidationError('Parametros de consulta invalidos', queryResult.error.errors); } const filters: ContractFilters = queryResult.data; const result = await contractsService.findAll(req.tenantId!, filters); res.json({ success: true, data: result.data, meta: { total: result.total, page: filters.page, limit: filters.limit, totalPages: Math.ceil(result.total / (filters.limit || 20)) }, }); } catch (error) { next(error); } } async getContract(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const contract = await contractsService.findById(req.params.id, req.tenantId!); res.json({ success: true, data: contract }); } catch (error) { next(error); } } async createContract(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = createContractSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de contrato invalidos', parseResult.error.errors); } const dto: CreateContractDto = parseResult.data; const contract = await contractsService.create(dto, req.tenantId!, req.user!.userId); res.status(201).json({ success: true, data: contract, message: 'Contrato creado exitosamente' }); } catch (error) { next(error); } } async updateContract(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = updateContractSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de contrato invalidos', parseResult.error.errors); } const dto: UpdateContractDto = parseResult.data; const contract = await contractsService.update(req.params.id, dto, req.tenantId!, req.user!.userId); res.json({ success: true, data: contract, message: 'Contrato actualizado exitosamente' }); } catch (error) { next(error); } } async activateContract(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const contract = await contractsService.activate(req.params.id, req.tenantId!, req.user!.userId); res.json({ success: true, data: contract, message: 'Contrato activado exitosamente' }); } catch (error) { next(error); } } async terminateContract(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = terminateSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos invalidos', parseResult.error.errors); } const contract = await contractsService.terminate(req.params.id, parseResult.data.termination_date, req.tenantId!, req.user!.userId); res.json({ success: true, data: contract, message: 'Contrato terminado exitosamente' }); } catch (error) { next(error); } } async cancelContract(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const contract = await contractsService.cancel(req.params.id, req.tenantId!, req.user!.userId); res.json({ success: true, data: contract, message: 'Contrato cancelado exitosamente' }); } catch (error) { next(error); } } async deleteContract(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { await contractsService.delete(req.params.id, req.tenantId!); res.json({ success: true, message: 'Contrato eliminado exitosamente' }); } catch (error) { next(error); } } // ========== LEAVE TYPES ========== async getLeaveTypes(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const includeInactive = req.query.include_inactive === 'true'; const leaveTypes = await leavesService.getLeaveTypes(req.tenantId!, includeInactive); res.json({ success: true, data: leaveTypes }); } catch (error) { next(error); } } async createLeaveType(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = createLeaveTypeSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de tipo de ausencia invalidos', parseResult.error.errors); } const dto: CreateLeaveTypeDto = parseResult.data; const leaveType = await leavesService.createLeaveType(dto, req.tenantId!); res.status(201).json({ success: true, data: leaveType, message: 'Tipo de ausencia creado exitosamente' }); } catch (error) { next(error); } } async updateLeaveType(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = updateLeaveTypeSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de tipo de ausencia invalidos', parseResult.error.errors); } const dto: UpdateLeaveTypeDto = parseResult.data; const leaveType = await leavesService.updateLeaveType(req.params.id, dto, req.tenantId!); res.json({ success: true, data: leaveType, message: 'Tipo de ausencia actualizado exitosamente' }); } catch (error) { next(error); } } async deleteLeaveType(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { await leavesService.deleteLeaveType(req.params.id, req.tenantId!); res.json({ success: true, message: 'Tipo de ausencia eliminado exitosamente' }); } catch (error) { next(error); } } // ========== LEAVES ========== async getLeaves(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const queryResult = leaveQuerySchema.safeParse(req.query); if (!queryResult.success) { throw new ValidationError('Parametros de consulta invalidos', queryResult.error.errors); } const filters: LeaveFilters = queryResult.data; const result = await leavesService.findAll(req.tenantId!, filters); res.json({ success: true, data: result.data, meta: { total: result.total, page: filters.page, limit: filters.limit, totalPages: Math.ceil(result.total / (filters.limit || 20)) }, }); } catch (error) { next(error); } } async getLeave(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const leave = await leavesService.findById(req.params.id, req.tenantId!); res.json({ success: true, data: leave }); } catch (error) { next(error); } } async createLeave(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = createLeaveSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de ausencia invalidos', parseResult.error.errors); } const dto: CreateLeaveDto = parseResult.data; const leave = await leavesService.create(dto, req.tenantId!, req.user!.userId); res.status(201).json({ success: true, data: leave, message: 'Solicitud de ausencia creada exitosamente' }); } catch (error) { next(error); } } async updateLeave(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = updateLeaveSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de ausencia invalidos', parseResult.error.errors); } const dto: UpdateLeaveDto = parseResult.data; const leave = await leavesService.update(req.params.id, dto, req.tenantId!, req.user!.userId); res.json({ success: true, data: leave, message: 'Solicitud de ausencia actualizada exitosamente' }); } catch (error) { next(error); } } async submitLeave(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const leave = await leavesService.submit(req.params.id, req.tenantId!, req.user!.userId); res.json({ success: true, data: leave, message: 'Solicitud enviada exitosamente' }); } catch (error) { next(error); } } async approveLeave(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const leave = await leavesService.approve(req.params.id, req.tenantId!, req.user!.userId); res.json({ success: true, data: leave, message: 'Solicitud aprobada exitosamente' }); } catch (error) { next(error); } } async rejectLeave(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = rejectSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos invalidos', parseResult.error.errors); } const leave = await leavesService.reject(req.params.id, parseResult.data.reason, req.tenantId!, req.user!.userId); res.json({ success: true, data: leave, message: 'Solicitud rechazada' }); } catch (error) { next(error); } } async cancelLeave(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const leave = await leavesService.cancel(req.params.id, req.tenantId!, req.user!.userId); res.json({ success: true, data: leave, message: 'Solicitud cancelada exitosamente' }); } catch (error) { next(error); } } async deleteLeave(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { await leavesService.delete(req.params.id, req.tenantId!); res.json({ success: true, message: 'Solicitud eliminada exitosamente' }); } catch (error) { next(error); } } } export const hrController = new HrController();