791 lines
26 KiB
Markdown
791 lines
26 KiB
Markdown
# RF-HR-001: Gestión de Empleados y Cuadrillas
|
||
|
||
**Epic:** MAI-007 - RRHH, Asistencias y Nómina
|
||
**Tipo:** Requerimiento Funcional
|
||
**Prioridad:** Alta
|
||
**Estado:** 🚧 En Definición
|
||
**Última actualización:** 2025-11-17
|
||
|
||
---
|
||
|
||
## 📋 Descripción
|
||
|
||
El sistema debe permitir la gestión completa del catálogo de empleados de obra, organizados en cuadrillas y clasificados por oficios, con toda la información necesaria para cumplimiento legal (IMSS, INFONAVIT, nómina) y operativo (asistencias, costeo).
|
||
|
||
---
|
||
|
||
## 🎯 Objetivo de Negocio
|
||
|
||
1. **Organización Operativa:**
|
||
- Tener visibilidad clara de la estructura de personal en cada obra
|
||
- Facilitar asignación de cuadrillas a diferentes frentes de trabajo
|
||
- Conocer capacidades y especialidades disponibles
|
||
|
||
2. **Cumplimiento Legal:**
|
||
- Mantener datos actualizados para IMSS e INFONAVIT
|
||
- Facilitar alta/baja de trabajadores ante instituciones
|
||
- Cumplir con requisitos de documentación laboral
|
||
|
||
3. **Control de Costos:**
|
||
- Base para cálculo de costeo de mano de obra
|
||
- Asociar empleados a partidas específicas
|
||
- Proyección de nómina por obra
|
||
|
||
---
|
||
|
||
## 👥 Actores
|
||
|
||
- **Director:** Aprueba contrataciones, define sueldos
|
||
- **HR (Recursos Humanos):** Gestiona empleados, cuadrillas, documentación
|
||
- **Residente de Obra:** Asigna cuadrillas a frentes de trabajo
|
||
- **Finance:** Consulta para cálculo de nómina y costos
|
||
|
||
---
|
||
|
||
## ✅ Casos de Uso
|
||
|
||
### UC-HR-001: Registrar Nuevo Empleado
|
||
|
||
**Actor:** HR
|
||
**Flujo principal:**
|
||
|
||
1. HR accede a "Empleados" → "Nuevo Empleado"
|
||
2. Ingresa **datos personales:**
|
||
- Nombre completo
|
||
- Fecha de nacimiento
|
||
- CURP (validación de 18 caracteres)
|
||
- RFC (validación de 13 caracteres)
|
||
- NSS (Número de Seguro Social, validación de 11 dígitos)
|
||
- Género
|
||
- Estado civil
|
||
3. Ingresa **datos de contacto:**
|
||
- Teléfono celular
|
||
- Email (opcional)
|
||
- Dirección completa
|
||
4. Ingresa **datos laborales:**
|
||
- Oficio principal (de catálogo)
|
||
- Fecha de ingreso
|
||
- Tipo de contrato (por obra, planta, eventual)
|
||
- Salario diario integrado (SDI)
|
||
- Jornada (diurna, mixta, nocturna)
|
||
5. Asigna **constructora** (multi-tenant)
|
||
6. Carga **documentos:**
|
||
- INE/IFE (PDF)
|
||
- Comprobante de domicilio (PDF)
|
||
- Acta de nacimiento (PDF)
|
||
- CURP (PDF)
|
||
- Carta de antecedentes no penales (PDF, opcional)
|
||
- Constancia de estudios (PDF, opcional)
|
||
7. Opcionalmente asigna a **cuadrilla**
|
||
8. Sistema valida datos (duplicados, formato)
|
||
9. Sistema genera **código de empleado** (autoincrementable)
|
||
10. Sistema crea **QR code** para asistencia
|
||
11. Sistema guarda empleado con estado "activo"
|
||
|
||
**Resultado:** Empleado registrado y listo para asignación a obra
|
||
|
||
**Excepciones:**
|
||
- E1: NSS duplicado → Mostrar error, empleado ya existe
|
||
- E2: CURP inválido → Mostrar error, validar formato
|
||
- E3: RFC no coincide con CURP → Advertencia, permitir continuar
|
||
- E4: Empleado menor de 18 años → Bloquear, no permitir registro
|
||
|
||
---
|
||
|
||
### UC-HR-002: Crear Cuadrilla
|
||
|
||
**Actor:** HR o Residente
|
||
**Flujo principal:**
|
||
|
||
1. Usuario accede a "Cuadrillas" → "Nueva Cuadrilla"
|
||
2. Ingresa **datos de cuadrilla:**
|
||
- Nombre (ej: "Cuadrilla A - Albañilería")
|
||
- Tipo de trabajo (albañilería, electricidad, plomería, acabados, etc.)
|
||
- Obra asignada (obligatorio)
|
||
- Responsable/Jefe de cuadrilla (seleccionar de empleados)
|
||
3. Asigna **empleados** a la cuadrilla:
|
||
- Búsqueda por nombre, código o oficio
|
||
- Selección múltiple
|
||
- Definir rol en cuadrilla (jefe, oficial, ayudante)
|
||
4. Opcionalmente asigna **herramientas** o **equipos**
|
||
5. Sistema valida:
|
||
- Mínimo 1 empleado
|
||
- Todos los empleados deben estar activos
|
||
- Empleados no pueden estar en 2 cuadrillas simultáneamente
|
||
6. Sistema guarda cuadrilla
|
||
|
||
**Resultado:** Cuadrilla creada y lista para asignación a frente de trabajo
|
||
|
||
**Excepciones:**
|
||
- E1: Empleado ya asignado a otra cuadrilla → Preguntar si desea reasignar
|
||
- E2: Obra no tiene empleados disponibles → Advertencia, sugerir asignar empleados a obra primero
|
||
|
||
---
|
||
|
||
### UC-HR-003: Asignar Empleado a Obra
|
||
|
||
**Actor:** HR o Director
|
||
**Flujo principal:**
|
||
|
||
1. Usuario accede a ficha del empleado
|
||
2. Selecciona "Asignar a Obra"
|
||
3. Selecciona **obra destino** (de obras activas de la constructora)
|
||
4. Define **fecha de inicio** en la obra
|
||
5. Define **fecha de fin estimada** (opcional)
|
||
6. Define **salario específico para esta obra** (puede variar del SDI base)
|
||
7. Opcionalmente asigna a **cuadrilla** de la obra
|
||
8. Sistema valida:
|
||
- Empleado debe estar activo
|
||
- Obra debe estar activa
|
||
- No puede haber overlapping de fechas en diferentes obras
|
||
9. Sistema crea registro en `employee_work_assignments`
|
||
10. Sistema genera **QR code específico de obra** (opcional)
|
||
|
||
**Resultado:** Empleado asignado a obra y disponible para registro de asistencia
|
||
|
||
**Excepciones:**
|
||
- E1: Empleado ya asignado a otra obra en mismo período → Bloquear, resolver primero
|
||
- E2: Salario específico < 50% del SDI base → Advertencia, validar con Director
|
||
|
||
---
|
||
|
||
### UC-HR-004: Dar de Baja a Empleado
|
||
|
||
**Actor:** HR o Director
|
||
**Flujo principal:**
|
||
|
||
1. Usuario accede a ficha del empleado
|
||
2. Selecciona "Dar de Baja"
|
||
3. Ingresa **motivo de baja:**
|
||
- Renuncia voluntaria
|
||
- Término de contrato
|
||
- Despido justificado
|
||
- Despido injustificado
|
||
- Abandono de trabajo
|
||
- Defunción
|
||
4. Ingresa **fecha de baja**
|
||
5. Opcionalmente ingresa **comentarios** (justificación detallada)
|
||
6. Sistema valida:
|
||
- No puede haber asistencias posteriores a la fecha de baja
|
||
- No puede haber obras activas asignadas
|
||
7. Sistema cambia estado a "terminated"
|
||
8. Sistema marca `deleted_at` (soft delete)
|
||
9. Sistema genera **alerta** para Finance (liquidación pendiente)
|
||
10. Sistema genera **alerta** para IMSS/INFONAVIT (baja ante instituciones)
|
||
|
||
**Resultado:** Empleado dado de baja, no aparece en listados activos
|
||
|
||
**Excepciones:**
|
||
- E1: Empleado tiene asistencias pendientes de aprobar → Advertencia, resolver primero
|
||
- E2: Empleado tiene adeudos → Advertencia, liquidar antes de dar de baja
|
||
- E3: Empleado con crédito INFONAVIT activo → Advertencia especial, notificar a Finance
|
||
|
||
---
|
||
|
||
### UC-HR-005: Modificar Salario de Empleado
|
||
|
||
**Actor:** Director o HR (con permiso)
|
||
**Flujo principal:**
|
||
|
||
1. Usuario accede a ficha del empleado → "Modificar Salario"
|
||
2. Visualiza **historial salarial:**
|
||
- Fecha de cada cambio
|
||
- Salario anterior
|
||
- Salario nuevo
|
||
- Autorizado por
|
||
- Motivo
|
||
3. Ingresa **nuevo salario diario integrado**
|
||
4. Ingresa **fecha efectiva** del cambio
|
||
5. Ingresa **motivo:**
|
||
- Aumento por antigüedad
|
||
- Aumento por mérito
|
||
- Ajuste por inflación
|
||
- Cambio de oficio/puesto
|
||
- Corrección de error
|
||
- Incremento de salario mínimo
|
||
6. Sistema valida:
|
||
- Nuevo salario >= Salario Mínimo vigente (consultar API SAT o tabla)
|
||
- Fecha efectiva >= hoy
|
||
- Usuario tiene permisos (solo Director o HR autorizado)
|
||
7. Sistema guarda en tabla `salary_history`
|
||
8. Sistema actualiza `current_salary` en tabla `employees`
|
||
9. Sistema genera **alerta** para IMSS (modificación salarial)
|
||
10. Sistema recalcula **costeo de mano de obra** en obras afectadas
|
||
|
||
**Resultado:** Salario actualizado, cambio registrado en historial
|
||
|
||
**Excepciones:**
|
||
- E1: Nuevo salario < Salario Mínimo → Bloquear, mostrar error
|
||
- E2: Reducción salarial > 20% → Advertencia, requiere justificación adicional
|
||
- E3: Empleado con crédito INFONAVIT → Advertencia, informar cambio a INFONAVIT
|
||
|
||
---
|
||
|
||
### UC-HR-006: Suspender Empleado Temporalmente
|
||
|
||
**Actor:** Director o Residente
|
||
**Flujo principal:**
|
||
|
||
1. Usuario accede a ficha del empleado → "Suspender"
|
||
2. Ingresa **motivo de suspensión:**
|
||
- Falta injustificada
|
||
- Incapacidad médica
|
||
- Licencia sin goce de sueldo
|
||
- Sanción disciplinaria
|
||
- Suspensión preventiva (investigación)
|
||
3. Ingresa **fecha de inicio** de suspensión
|
||
4. Ingresa **fecha estimada de reactivación** (puede ser "indefinido")
|
||
5. Opcionalmente adjunta **documentos** (incapacidad médica, acta administrativa)
|
||
6. Sistema cambia estado a "suspended"
|
||
7. Sistema **bloquea** registro de asistencia
|
||
8. Sistema genera **alerta** en dashboard del Residente
|
||
9. Si suspensión > 7 días, genera **alerta** para IMSS (incapacidad)
|
||
|
||
**Resultado:** Empleado suspendido, no puede registrar asistencia
|
||
|
||
**Excepciones:**
|
||
- E1: Empleado ya suspendido → Mostrar suspensión activa, preguntar si desea modificar
|
||
|
||
---
|
||
|
||
### UC-HR-007: Reactivar Empleado Suspendido
|
||
|
||
**Actor:** HR o Director
|
||
**Flujo principal:**
|
||
|
||
1. Usuario accede a ficha del empleado suspendido
|
||
2. Selecciona "Reactivar"
|
||
3. Visualiza **motivo de suspensión** original
|
||
4. Confirma reactivación
|
||
5. Opcionalmente ingresa **comentarios** (ej: "Presentó justificante médico")
|
||
6. Sistema cambia estado a "active"
|
||
7. Sistema **habilita** registro de asistencia
|
||
8. Sistema actualiza `suspended_until = NULL`
|
||
9. Sistema notifica al Residente de la obra
|
||
|
||
**Resultado:** Empleado reactivado, puede registrar asistencia normalmente
|
||
|
||
---
|
||
|
||
## 📊 Modelo de Datos
|
||
|
||
### Empleados (employees)
|
||
|
||
```sql
|
||
CREATE TABLE hr.employees (
|
||
-- Identificación
|
||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
employee_code VARCHAR(20) UNIQUE NOT NULL, -- Auto-generado (ej: EMP-00001)
|
||
constructora_id UUID NOT NULL REFERENCES auth_management.constructoras(id),
|
||
|
||
-- Datos personales
|
||
first_name VARCHAR(100) NOT NULL,
|
||
last_name VARCHAR(100) NOT NULL,
|
||
full_name VARCHAR(255) GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED,
|
||
date_of_birth DATE NOT NULL,
|
||
gender VARCHAR(20) CHECK (gender IN ('male', 'female', 'other')),
|
||
marital_status VARCHAR(20) CHECK (marital_status IN ('single', 'married', 'divorced', 'widowed')),
|
||
|
||
-- Datos fiscales y legales (CRÍTICOS)
|
||
curp VARCHAR(18) UNIQUE NOT NULL, -- Clave Única de Registro de Población
|
||
rfc VARCHAR(13) NOT NULL, -- Registro Federal de Contribuyentes
|
||
nss VARCHAR(11) UNIQUE NOT NULL, -- Número de Seguro Social (IMSS)
|
||
infonavit_number VARCHAR(11), -- Número de crédito INFONAVIT (si aplica)
|
||
|
||
-- Contacto
|
||
phone VARCHAR(20) NOT NULL,
|
||
email VARCHAR(255),
|
||
address TEXT,
|
||
city VARCHAR(100),
|
||
state VARCHAR(100),
|
||
postal_code VARCHAR(10),
|
||
|
||
-- Datos laborales
|
||
primary_trade_id UUID REFERENCES hr.trades(id), -- Oficio principal
|
||
hire_date DATE NOT NULL, -- Fecha de ingreso a la constructora
|
||
contract_type VARCHAR(50) CHECK (contract_type IN ('permanent', 'temporary', 'per_project')),
|
||
base_daily_salary DECIMAL(10, 2) NOT NULL, -- Salario Diario Integrado (SDI) base
|
||
current_salary DECIMAL(10, 2) NOT NULL, -- Salario actual (puede variar del base)
|
||
work_shift VARCHAR(20) CHECK (work_shift IN ('day', 'night', 'mixed')),
|
||
|
||
-- Estado
|
||
status hr.employee_status DEFAULT 'active', -- ENUM: active, suspended, terminated
|
||
termination_date DATE,
|
||
termination_reason TEXT,
|
||
suspended_until DATE,
|
||
suspension_reason TEXT,
|
||
|
||
-- QR para asistencia
|
||
qr_code TEXT UNIQUE, -- QR code único para registro de asistencia
|
||
|
||
-- Documentos (URLs a S3/storage)
|
||
documents JSONB DEFAULT '[]'::jsonb,
|
||
-- Ejemplo: [{"type": "ine", "url": "s3://...", "uploaded_at": "2025-01-15"}]
|
||
|
||
-- Metadata
|
||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ, -- Soft delete
|
||
created_by UUID REFERENCES auth_management.profiles(id),
|
||
|
||
-- Constraints
|
||
CONSTRAINT valid_curp_format CHECK (curp ~ '^[A-Z]{4}[0-9]{6}[HM][A-Z]{5}[0-9]{2}$'),
|
||
CONSTRAINT valid_rfc_format CHECK (rfc ~ '^[A-ZÑ&]{3,4}[0-9]{6}[A-Z0-9]{3}$'),
|
||
CONSTRAINT valid_nss_format CHECK (nss ~ '^[0-9]{11}$'),
|
||
CONSTRAINT valid_salary CHECK (current_salary >= 0),
|
||
CONSTRAINT valid_hire_date CHECK (hire_date <= CURRENT_DATE),
|
||
CONSTRAINT valid_termination CHECK (
|
||
(status = 'terminated' AND termination_date IS NOT NULL) OR
|
||
(status != 'terminated' AND termination_date IS NULL)
|
||
)
|
||
);
|
||
|
||
-- Índices
|
||
CREATE INDEX idx_employees_constructora ON hr.employees(constructora_id) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_employees_status ON hr.employees(status) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_employees_nss ON hr.employees(nss);
|
||
CREATE INDEX idx_employees_curp ON hr.employees(curp);
|
||
CREATE INDEX idx_employees_code ON hr.employees(employee_code);
|
||
CREATE INDEX idx_employees_qr ON hr.employees(qr_code);
|
||
|
||
-- Trigger para actualizar updated_at
|
||
CREATE TRIGGER set_employees_updated_at
|
||
BEFORE UPDATE ON hr.employees
|
||
FOR EACH ROW
|
||
EXECUTE FUNCTION hr.update_updated_at_column();
|
||
|
||
-- Trigger para generar employee_code automáticamente
|
||
CREATE OR REPLACE FUNCTION hr.generate_employee_code()
|
||
RETURNS TRIGGER AS $$
|
||
DECLARE
|
||
next_number INTEGER;
|
||
new_code VARCHAR(20);
|
||
BEGIN
|
||
-- Obtener el siguiente número
|
||
SELECT COALESCE(MAX(CAST(SUBSTRING(employee_code FROM 5) AS INTEGER)), 0) + 1
|
||
INTO next_number
|
||
FROM hr.employees
|
||
WHERE constructora_id = NEW.constructora_id;
|
||
|
||
-- Generar código (ej: EMP-00001)
|
||
new_code := 'EMP-' || LPAD(next_number::TEXT, 5, '0');
|
||
|
||
NEW.employee_code := new_code;
|
||
RETURN NEW;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
|
||
CREATE TRIGGER trigger_generate_employee_code
|
||
BEFORE INSERT ON hr.employees
|
||
FOR EACH ROW
|
||
WHEN (NEW.employee_code IS NULL)
|
||
EXECUTE FUNCTION hr.generate_employee_code();
|
||
|
||
-- Trigger para generar QR code automáticamente
|
||
CREATE OR REPLACE FUNCTION hr.generate_employee_qr()
|
||
RETURNS TRIGGER AS $$
|
||
BEGIN
|
||
-- QR code = BASE64(employee_id + timestamp)
|
||
NEW.qr_code := encode(
|
||
(NEW.id::TEXT || '-' || EXTRACT(EPOCH FROM NOW())::TEXT)::bytea,
|
||
'base64'
|
||
);
|
||
RETURN NEW;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
|
||
CREATE TRIGGER trigger_generate_employee_qr
|
||
BEFORE INSERT ON hr.employees
|
||
FOR EACH ROW
|
||
WHEN (NEW.qr_code IS NULL)
|
||
EXECUTE FUNCTION hr.generate_employee_qr();
|
||
```
|
||
|
||
---
|
||
|
||
### Oficios (trades)
|
||
|
||
```sql
|
||
CREATE TABLE hr.trades (
|
||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
name VARCHAR(100) UNIQUE NOT NULL, -- ej: "Albañil", "Electricista", "Plomero"
|
||
description TEXT,
|
||
category VARCHAR(50), -- ej: "Estructura", "Instalaciones", "Acabados"
|
||
requires_certification BOOLEAN DEFAULT false, -- Si requiere certificación oficial
|
||
|
||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||
);
|
||
|
||
-- Datos iniciales
|
||
INSERT INTO hr.trades (name, description, category, requires_certification) VALUES
|
||
('Albañil', 'Construcción de muros, losas, cimentaciones', 'Estructura', false),
|
||
('Fierrero', 'Armado de acero de refuerzo', 'Estructura', false),
|
||
('Carpintero', 'Construcción de cimbra y acabados de madera', 'Estructura', false),
|
||
('Electricista', 'Instalaciones eléctricas', 'Instalaciones', true),
|
||
('Plomero', 'Instalaciones hidráulicas y sanitarias', 'Instalaciones', true),
|
||
('Yesero', 'Acabados de yeso', 'Acabados', false),
|
||
('Pintor', 'Acabados de pintura', 'Acabados', false),
|
||
('Herrero', 'Estructuras metálicas y herrería', 'Estructura', false),
|
||
('Impermeabilizador', 'Impermeabilización de losas y azoteas', 'Acabados', true),
|
||
('Vidriero', 'Instalación de cancelería y vidrios', 'Acabados', false),
|
||
('Ayudante General', 'Apoyo general en obra', 'General', false);
|
||
```
|
||
|
||
---
|
||
|
||
### Cuadrillas (crews)
|
||
|
||
```sql
|
||
CREATE TABLE hr.crews (
|
||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
constructora_id UUID NOT NULL REFERENCES auth_management.constructoras(id),
|
||
work_id UUID REFERENCES projects.projects(id), -- Obra asignada (puede cambiar)
|
||
|
||
name VARCHAR(255) NOT NULL, -- ej: "Cuadrilla A - Albañilería"
|
||
trade_type_id UUID REFERENCES hr.trades(id), -- Tipo de trabajo principal
|
||
foreman_employee_id UUID REFERENCES hr.employees(id), -- Jefe de cuadrilla
|
||
|
||
status VARCHAR(20) CHECK (status IN ('active', 'inactive', 'disbanded')),
|
||
|
||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ,
|
||
|
||
UNIQUE(constructora_id, name)
|
||
);
|
||
|
||
CREATE INDEX idx_crews_work ON hr.crews(work_id) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_crews_status ON hr.crews(status) WHERE deleted_at IS NULL;
|
||
```
|
||
|
||
---
|
||
|
||
### Miembros de Cuadrilla (crew_members)
|
||
|
||
```sql
|
||
CREATE TABLE hr.crew_members (
|
||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
crew_id UUID NOT NULL REFERENCES hr.crews(id) ON DELETE CASCADE,
|
||
employee_id UUID NOT NULL REFERENCES hr.employees(id),
|
||
|
||
role_in_crew VARCHAR(50) CHECK (role_in_crew IN ('foreman', 'skilled', 'helper')),
|
||
-- foreman: jefe de cuadrilla
|
||
-- skilled: oficial / trabajador calificado
|
||
-- helper: ayudante
|
||
|
||
joined_at DATE DEFAULT CURRENT_DATE,
|
||
left_at DATE,
|
||
|
||
UNIQUE(crew_id, employee_id, left_at), -- Un empleado no puede estar 2 veces activo en la misma cuadrilla
|
||
CONSTRAINT no_overlapping_crew_membership CHECK (
|
||
(left_at IS NULL) OR (left_at >= joined_at)
|
||
)
|
||
);
|
||
|
||
CREATE INDEX idx_crew_members_employee ON hr.crew_members(employee_id) WHERE left_at IS NULL;
|
||
CREATE INDEX idx_crew_members_crew ON hr.crew_members(crew_id) WHERE left_at IS NULL;
|
||
|
||
-- Constraint: Empleado no puede estar en 2 cuadrillas simultáneamente
|
||
CREATE OR REPLACE FUNCTION hr.check_employee_single_crew()
|
||
RETURNS TRIGGER AS $$
|
||
DECLARE
|
||
active_crews INTEGER;
|
||
BEGIN
|
||
SELECT COUNT(*)
|
||
INTO active_crews
|
||
FROM hr.crew_members
|
||
WHERE employee_id = NEW.employee_id
|
||
AND left_at IS NULL
|
||
AND id != COALESCE(NEW.id, '00000000-0000-0000-0000-000000000000'::UUID);
|
||
|
||
IF active_crews > 0 THEN
|
||
RAISE EXCEPTION 'El empleado ya está asignado a otra cuadrilla activa';
|
||
END IF;
|
||
|
||
RETURN NEW;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
|
||
CREATE TRIGGER trigger_check_employee_single_crew
|
||
BEFORE INSERT OR UPDATE ON hr.crew_members
|
||
FOR EACH ROW
|
||
WHEN (NEW.left_at IS NULL)
|
||
EXECUTE FUNCTION hr.check_employee_single_crew();
|
||
```
|
||
|
||
---
|
||
|
||
### Asignaciones de Empleado a Obra (employee_work_assignments)
|
||
|
||
```sql
|
||
CREATE TABLE hr.employee_work_assignments (
|
||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
employee_id UUID NOT NULL REFERENCES hr.employees(id),
|
||
work_id UUID NOT NULL REFERENCES projects.projects(id),
|
||
|
||
start_date DATE NOT NULL,
|
||
end_date DATE, -- NULL = asignación activa
|
||
|
||
work_specific_salary DECIMAL(10, 2), -- Salario específico para esta obra (puede diferir del base)
|
||
notes TEXT,
|
||
|
||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||
|
||
CONSTRAINT no_overlapping_assignments CHECK (
|
||
(end_date IS NULL) OR (end_date >= start_date)
|
||
)
|
||
);
|
||
|
||
CREATE INDEX idx_work_assignments_employee ON hr.employee_work_assignments(employee_id);
|
||
CREATE INDEX idx_work_assignments_work ON hr.employee_work_assignments(work_id);
|
||
CREATE INDEX idx_work_assignments_active ON hr.employee_work_assignments(employee_id, work_id)
|
||
WHERE end_date IS NULL;
|
||
|
||
-- Constraint: Empleado no puede estar en 2 obras simultáneamente
|
||
CREATE OR REPLACE FUNCTION hr.check_employee_single_work()
|
||
RETURNS TRIGGER AS $$
|
||
DECLARE
|
||
overlapping_assignments INTEGER;
|
||
BEGIN
|
||
SELECT COUNT(*)
|
||
INTO overlapping_assignments
|
||
FROM hr.employee_work_assignments
|
||
WHERE employee_id = NEW.employee_id
|
||
AND id != COALESCE(NEW.id, '00000000-0000-0000-0000-000000000000'::UUID)
|
||
AND (
|
||
(end_date IS NULL AND NEW.end_date IS NULL) OR
|
||
(end_date IS NULL AND NEW.start_date <= end_date) OR
|
||
(NEW.end_date IS NULL AND start_date <= NEW.end_date) OR
|
||
(start_date <= NEW.end_date AND end_date >= NEW.start_date)
|
||
);
|
||
|
||
IF overlapping_assignments > 0 THEN
|
||
RAISE EXCEPTION 'El empleado ya está asignado a otra obra en el mismo período';
|
||
END IF;
|
||
|
||
RETURN NEW;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
|
||
CREATE TRIGGER trigger_check_employee_single_work
|
||
BEFORE INSERT OR UPDATE ON hr.employee_work_assignments
|
||
FOR EACH ROW
|
||
EXECUTE FUNCTION hr.check_employee_single_work();
|
||
```
|
||
|
||
---
|
||
|
||
### Historial Salarial (salary_history)
|
||
|
||
```sql
|
||
CREATE TABLE hr.salary_history (
|
||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
employee_id UUID NOT NULL REFERENCES hr.employees(id),
|
||
|
||
previous_salary DECIMAL(10, 2) NOT NULL,
|
||
new_salary DECIMAL(10, 2) NOT NULL,
|
||
effective_date DATE NOT NULL,
|
||
|
||
reason VARCHAR(255) NOT NULL, -- Motivo del cambio
|
||
notes TEXT,
|
||
|
||
authorized_by UUID REFERENCES auth_management.profiles(id), -- Quién autorizó
|
||
|
||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_salary_history_employee ON hr.salary_history(employee_id);
|
||
CREATE INDEX idx_salary_history_date ON hr.salary_history(effective_date DESC);
|
||
|
||
-- Trigger: Guardar en historial al cambiar salario en employees
|
||
CREATE OR REPLACE FUNCTION hr.track_salary_changes()
|
||
RETURNS TRIGGER AS $$
|
||
BEGIN
|
||
IF OLD.current_salary IS DISTINCT FROM NEW.current_salary THEN
|
||
INSERT INTO hr.salary_history (
|
||
employee_id,
|
||
previous_salary,
|
||
new_salary,
|
||
effective_date,
|
||
reason,
|
||
authorized_by
|
||
) VALUES (
|
||
NEW.id,
|
||
OLD.current_salary,
|
||
NEW.current_salary,
|
||
CURRENT_DATE,
|
||
'Modificación manual', -- Puede mejorarse pasando el motivo desde la app
|
||
auth_management.get_current_user_id()
|
||
);
|
||
END IF;
|
||
|
||
RETURN NEW;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
|
||
CREATE TRIGGER trigger_track_salary_changes
|
||
AFTER UPDATE ON hr.employees
|
||
FOR EACH ROW
|
||
EXECUTE FUNCTION hr.track_salary_changes();
|
||
```
|
||
|
||
---
|
||
|
||
## 🔐 Reglas de Negocio
|
||
|
||
### RN-HR-001: Validación de CURP
|
||
- CURP debe tener exactamente 18 caracteres
|
||
- Formato: 4 letras + 6 dígitos (fecha) + H/M (sexo) + 5 letras (lugar) + 2 dígitos (verificación)
|
||
- Debe ser único en el sistema por constructora
|
||
- Fecha en CURP debe coincidir con fecha de nacimiento del empleado
|
||
|
||
### RN-HR-002: Validación de RFC
|
||
- RFC debe tener 13 caracteres (personas físicas)
|
||
- Formato: 4 letras + 6 dígitos (fecha) + 3 caracteres (homoclave)
|
||
- Debe coincidir con CURP (primeros 10 caracteres)
|
||
|
||
### RN-HR-003: Validación de NSS
|
||
- NSS debe tener exactamente 11 dígitos
|
||
- Debe ser único a nivel nacional (no puede haber duplicados)
|
||
- Validar contra algoritmo de dígito verificador del IMSS
|
||
|
||
### RN-HR-004: Salario Mínimo
|
||
- Salario diario integrado debe ser >= Salario Mínimo vigente en México
|
||
- Salario mínimo varía por zona geográfica (consultar tabla)
|
||
- Al 2025: ~$248.93 MXN/día (zona general)
|
||
|
||
### RN-HR-005: Edad Mínima
|
||
- Empleado debe tener mínimo 18 años (mayoría de edad)
|
||
- Bloquear registro de menores de edad
|
||
|
||
### RN-HR-006: Fecha de Ingreso
|
||
- Fecha de ingreso no puede ser futura
|
||
- Fecha de ingreso debe ser >= fecha de creación de la constructora
|
||
|
||
### RN-HR-007: Baja de Empleado
|
||
- No pueden existir asistencias posteriores a la fecha de baja
|
||
- No pueden existir asignaciones a obras activas
|
||
- Fecha de baja debe ser >= fecha de ingreso
|
||
|
||
### RN-HR-008: Cuadrillas
|
||
- Una cuadrilla debe tener mínimo 1 empleado (el jefe)
|
||
- Un empleado solo puede estar en 1 cuadrilla activa a la vez
|
||
- Jefe de cuadrilla debe ser miembro de la misma cuadrilla
|
||
|
||
### RN-HR-009: Asignación a Obra
|
||
- Empleado solo puede estar asignado a 1 obra a la vez
|
||
- Empleado debe estar en estado "active" para ser asignado
|
||
- Obra debe estar en estado "active" o "planning"
|
||
|
||
### RN-HR-010: Suspensión
|
||
- Empleado suspendido no puede registrar asistencia
|
||
- Suspensión no afecta asignación a obra (sigue asignado)
|
||
- Suspensión > 7 días debe generar alerta para IMSS
|
||
|
||
---
|
||
|
||
## 🧪 Criterios de Aceptación
|
||
|
||
### CA-HR-001: Registro de Empleado
|
||
- ✅ Permite registrar empleado con todos los campos requeridos
|
||
- ✅ Valida formato de CURP (18 caracteres, patrón correcto)
|
||
- ✅ Valida formato de RFC (13 caracteres, coincide con CURP)
|
||
- ✅ Valida formato de NSS (11 dígitos, único)
|
||
- ✅ Bloquea registro de empleados < 18 años
|
||
- ✅ Genera código de empleado automáticamente (EMP-00001, EMP-00002, etc.)
|
||
- ✅ Genera QR code único para asistencia
|
||
- ✅ Permite cargar documentos (PDF)
|
||
- ✅ Guarda empleado en estado "active"
|
||
|
||
### CA-HR-002: Cuadrillas
|
||
- ✅ Permite crear cuadrilla con nombre, tipo de trabajo, jefe
|
||
- ✅ Permite asignar empleados a cuadrilla
|
||
- ✅ Bloquea asignación de empleado a 2 cuadrillas simultáneas
|
||
- ✅ Permite modificar miembros de cuadrilla
|
||
- ✅ Permite disolver cuadrilla (cambiar estado a "disbanded")
|
||
- ✅ Al disolver, libera a todos los empleados
|
||
|
||
### CA-HR-003: Asignación a Obra
|
||
- ✅ Permite asignar empleado a obra con fecha de inicio
|
||
- ✅ Bloquea asignación a 2 obras simultáneas
|
||
- ✅ Permite definir salario específico de obra
|
||
- ✅ Permite desasignar empleado (definir fecha de fin)
|
||
- ✅ Listado de empleados de una obra muestra solo asignados activos
|
||
|
||
### CA-HR-004: Cambio de Salario
|
||
- ✅ Permite modificar salario de empleado
|
||
- ✅ Requiere motivo obligatorio
|
||
- ✅ Guarda cambio en historial salarial
|
||
- ✅ Bloquea salario < Salario Mínimo vigente
|
||
- ✅ Advertencia si reducción > 20%
|
||
- ✅ Solo usuarios autorizados (Director, HR) pueden cambiar salario
|
||
|
||
### CA-HR-005: Baja de Empleado
|
||
- ✅ Permite dar de baja con motivo y fecha
|
||
- ✅ Valida que no haya asistencias posteriores a la fecha de baja
|
||
- ✅ Valida que no haya asignaciones activas a obras
|
||
- ✅ Cambia estado a "terminated"
|
||
- ✅ Soft delete (marca deleted_at)
|
||
- ✅ Genera alerta para Finance y para IMSS/INFONAVIT
|
||
|
||
### CA-HR-006: Suspensión
|
||
- ✅ Permite suspender empleado con motivo
|
||
- ✅ Cambia estado a "suspended"
|
||
- ✅ Bloquea registro de asistencia mientras esté suspendido
|
||
- ✅ Permite reactivar empleado
|
||
- ✅ Si suspensión > 7 días, genera alerta para IMSS
|
||
|
||
---
|
||
|
||
## 📐 Dependencias
|
||
|
||
### Upstream (depende de):
|
||
- ✅ RF-AUTH-003: Multi-tenancy (empleados por constructora)
|
||
- ✅ RF-AUTH-001: RBAC (permisos de HR, Director)
|
||
|
||
### Downstream (otros dependen de esto):
|
||
- 🔜 RF-HR-002: Asistencia Biométrica (requiere catálogo de empleados)
|
||
- 🔜 RF-HR-003: Costeo de Mano de Obra (requiere salarios)
|
||
- 🔜 RF-HR-004: Integración IMSS (requiere NSS, CURP, RFC)
|
||
- 🔜 RF-HR-005: Integración INFONAVIT (requiere NSS, RFC)
|
||
|
||
---
|
||
|
||
## 🎯 KPIs
|
||
|
||
- **Tiempo promedio de registro de empleado:** < 5 minutos
|
||
- **Tasa de error en validación de CURP/RFC/NSS:** < 2%
|
||
- **Empleados activos por constructora:** Métrica visible en dashboard
|
||
- **Empleados por obra:** Métrica visible por proyecto
|
||
- **Rotación de personal:** (Bajas del mes / Empleados promedio) × 100
|
||
|
||
---
|
||
|
||
## 📝 Notas Adicionales
|
||
|
||
### Datos Sensibles (GDPR / LFPDPPP)
|
||
- CURP, RFC, NSS son datos personales sensibles
|
||
- Requieren consentimiento explícito del empleado
|
||
- Acceso restringido (solo HR, Director, Finance)
|
||
- Logs de auditoría en accesos a datos sensibles
|
||
- Encriptación en base de datos (columnas sensibles)
|
||
|
||
### Cumplimiento Legal
|
||
- Mantener expediente digital por empleado (documentos PDF)
|
||
- Retención de datos: Mínimo 5 años después de baja
|
||
- Derecho al olvido: Eliminación de datos a solicitud (después del período legal)
|
||
|
||
### Escalabilidad
|
||
- Constructora pequeña: ~20-50 empleados
|
||
- Constructora mediana: ~100-300 empleados
|
||
- Constructora grande: ~500-2000 empleados
|
||
- Sistema debe soportar hasta 10,000 empleados por instancia
|
||
|
||
---
|
||
|
||
**Fecha de creación:** 2025-11-17
|
||
**Última actualización:** 2025-11-17
|
||
**Versión:** 1.0
|