// Fundación de datos · Capacitaciones & Competencias (Fase A)
// Fuente única para Proveedores, Instructores, Cargos y Matriz Cargo↔Competencia.
// Consumido por: training.jsx, competencias.jsx, automation.jsx, portal.jsx.
//
// Convención: todos los mocks se cuelgan de window al final del archivo
// para que los demás .jsx (que se cargan después) puedan leerlos sin imports.

// Catálogo de tipos de contrato — fuente única para el filtro de Empleados
// (employee-list.jsx) y el asistente Crear empleado (direct-hire.jsx), para
// que un empleado recién creado siempre sea encontrable por ese filtro.
// Nombrado EMP_CONTRACT_TYPES (no CONTRACT_TYPES) porque settings.jsx ya
// declara su propio CONTRACT_TYPES (forma distinta, catálogo de nómina) en
// el mismo scope global — un nombre igual rompería la carga de scripts.
const EMP_CONTRACT_TYPES = ['Indefinido', 'Fijo', 'Aprendizaje', 'Obra labor'];

// Catálogo de países del piloto — fuente única para Empleados (tabs) y el
// asistente Crear empleado (filtro y resumen de posición).
const COUNTRY_META = {
  CO: { label: 'Colombia', flag: '🇨🇴' },
  DO: { label: 'República Dominicana', flag: '🇩🇴' },
  GT: { label: 'Guatemala', flag: '🇬🇹' },
};

// ─── Proveedores ──────────────────────────────────────────
// Tipo: ARL | SENA | consultor | interno | elearning | universidad
// Estructura de soporte: cada `soporte` tiene tipo (licenciaSST, NIT, contrato,
// resoluciónFuncionamiento) + número + vencimiento + URL del archivo.

const PROVEEDORES = [
  {
    id: 'PRV-001', tipo: 'ARL', razonSocial: 'ARL Sura',
    nit: '800.256.353-1',
    contacto: { nombre: 'Carlos Fernando Vélez', email: 'cvelez@sura.com.co', tel: '+57 314 555 4123' },
    cobertura: 'Nacional · planta Cali y Barranquilla',
    soportes: [
      { tipo: 'Resolución habilitación ARL', numero: 'Res. 1832/2003', vence: null, url: '#mock-res-arl-sura.pdf' },
      { tipo: 'Contrato vigencia 2026', numero: 'CTR-AGR-2026-001', vence: '31/12/2026', url: '#mock-ctr-sura.pdf' },
    ],
    estado: 'activo',
  },
  {
    id: 'PRV-002', tipo: 'SENA', razonSocial: 'SENA Regional Valle del Cauca',
    nit: '899.999.034-1',
    contacto: { nombre: 'Andrea Mejía Rojas', email: 'amejia@sena.edu.co', tel: '+57 2 333 0500' },
    cobertura: 'Valle del Cauca · centro de formación Cali',
    soportes: [
      { tipo: 'Entidad pública SENA', numero: 'Decreto 249/2004', vence: null, url: '#mock-sena-decreto.pdf' },
    ],
    estado: 'activo',
  },
  {
    id: 'PRV-003', tipo: 'consultor', razonSocial: 'Consejo Colombiano de Seguridad (CCS)',
    nit: '860.025.792-1',
    contacto: { nombre: 'Equipo de formación CCS', email: 'formacion@ccs.org.co', tel: '+57 1 288 6355' },
    cobertura: 'Nacional · centro de entrenamiento alturas',
    soportes: [
      { tipo: 'Licencia SST entidad', numero: 'LIC-SST-2018-0042', vence: '15/08/2027', url: '#mock-ccs-lic.pdf' },
      { tipo: 'Acreditación trabajo en alturas', numero: 'ONAC-CCS-2024', vence: '20/03/2027', url: '#mock-ccs-acredit.pdf' },
    ],
    estado: 'activo',
  },
  {
    id: 'PRV-004', tipo: 'universidad', razonSocial: 'Universidad EAFIT · Educación continua',
    nit: '890.901.389-5',
    contacto: { nombre: 'Coordinación Diplomados', email: 'diplomados@eafit.edu.co', tel: '+57 4 261 9500' },
    cobertura: 'Nacional · diplomado virtual',
    soportes: [
      { tipo: 'Resolución MEN', numero: 'Res. 23439/2022', vence: null, url: '#mock-eafit-men.pdf' },
    ],
    estado: 'activo',
  },
  {
    id: 'PRV-005', tipo: 'consultor', razonSocial: 'INVIMA · Capacitación BPM acreditada',
    nit: '800.142.965-1',
    contacto: { nombre: 'Mauricio Ortiz Bernal', email: 'mortiz@invima-cap.co', tel: '+57 313 222 8901' },
    cobertura: 'Valle del Cauca',
    soportes: [
      { tipo: 'Aval INVIMA', numero: 'AVAL-BPM-2024-118', vence: '30/11/2026', url: '#mock-invima-aval.pdf' },
    ],
    estado: 'activo',
  },
  {
    id: 'PRV-006', tipo: 'consultor', razonSocial: 'Secretaría de Salud Valle del Cauca',
    nit: '891.480.085-9',
    contacto: { nombre: 'Adriana Patricia Henao', email: 'capacitacion@valledelcauca.gov.co', tel: '+57 2 311 6511' },
    cobertura: 'Valle del Cauca',
    soportes: [
      { tipo: 'Entidad gubernamental', numero: 'Decreto 1011/2006', vence: null, url: '#mock-secsalud.pdf' },
    ],
    estado: 'activo',
  },
  {
    id: 'PRV-007', tipo: 'elearning', razonSocial: 'Plataforma e-learning interna · Atica',
    nit: 'N/A · interna',
    contacto: { nombre: 'Carolina Restrepo · RRHH', email: 'capacitacion@atica.co', tel: '+57 2 333 1100' },
    cobertura: 'Interna · todos los colaboradores',
    soportes: [
      { tipo: 'Plataforma propia', numero: 'INT-LMS-2025', vence: null, url: '#mock-lms-interno.pdf' },
    ],
    estado: 'activo',
  },
  {
    id: 'PRV-008', tipo: 'interno', razonSocial: 'Instructores internos · Atica',
    nit: 'N/A · interno',
    contacto: { nombre: 'Carolina Restrepo · Jefe RRHH', email: 'crestrepo@atica.co', tel: '+57 2 333 1100' },
    cobertura: 'Planta Cali',
    soportes: [
      { tipo: 'Designación interna', numero: 'INT-RRHH-2026', vence: null, url: '#mock-int.pdf' },
    ],
    estado: 'activo',
  },
];

// ─── Instructores ────────────────────────────────────────
// Cada instructor pertenece a un proveedor. La licencia SST tiene vencimiento
// individual — bloqueante al programar sesión si vencida (Fase B).

const INSTRUCTORES = [
  {
    id: 'INS-001', proveedorId: 'PRV-001',
    nombre: 'Carlos Fernando Vélez',
    profesion: 'Ingeniero industrial · especialista SST',
    licenciaSST: { numero: 'LIC-SST-RIS-08421', vence: '15/09/2027', expedidaPor: 'SecSalud Valle del Cauca' },
    tarjetaProfesional: 'COPNIA 2008-203-115443',
    hojaDeVidaUrl: '#mock-cv-cvelez.pdf',
    competencias: ['CMP-01', 'CMP-05', 'CMP-11', 'CMP-12'],
  },
  {
    id: 'INS-002', proveedorId: 'PRV-002',
    nombre: 'Andrea Mejía Rojas',
    profesion: 'Tecnóloga en seguridad ocupacional',
    licenciaSST: { numero: 'LIC-SST-RIS-09102', vence: '02/06/2028', expedidaPor: 'SecSalud Valle del Cauca' },
    tarjetaProfesional: 'SENA-INS-RIS-2019-882',
    hojaDeVidaUrl: '#mock-cv-amejia.pdf',
    competencias: ['CMP-04', 'CMP-11'],
  },
  {
    id: 'INS-003', proveedorId: 'PRV-003',
    nombre: 'Equipo CCS · Centro de entrenamiento alturas',
    profesion: 'Equipo de instructores certificados',
    licenciaSST: { numero: 'LIC-CCS-2018-0042', vence: '15/08/2027', expedidaPor: 'MinTrabajo' },
    tarjetaProfesional: 'CCS-EQUIPO-2024',
    hojaDeVidaUrl: '#mock-ccs-equipo.pdf',
    competencias: ['CMP-04'],
  },
  {
    id: 'INS-004', proveedorId: 'PRV-004',
    nombre: 'Universidad EAFIT · Tutores diplomado',
    profesion: 'Docentes universitarios',
    licenciaSST: null, // no aplica · ámbito académico
    tarjetaProfesional: 'EAFIT-DOC-2025',
    hojaDeVidaUrl: '#mock-eafit-docentes.pdf',
    competencias: ['CMP-08'],
  },
  {
    id: 'INS-005', proveedorId: 'PRV-005',
    nombre: 'Mauricio Ortiz Bernal',
    profesion: 'Bacteriólogo · auditor BPM INVIMA',
    licenciaSST: { numero: 'LIC-BPM-INV-2421', vence: '30/11/2026', expedidaPor: 'INVIMA' },
    tarjetaProfesional: 'TP-BAC-2010-9982',
    hojaDeVidaUrl: '#mock-cv-mortiz.pdf',
    competencias: ['CMP-02', 'CMP-03'],
  },
  {
    id: 'INS-006', proveedorId: 'PRV-006',
    nombre: 'Adriana Patricia Henao',
    profesion: 'Nutricionista · capacitación municipal',
    licenciaSST: null,
    tarjetaProfesional: 'TP-NUT-2015-4421',
    hojaDeVidaUrl: '#mock-cv-aphenao.pdf',
    competencias: ['CMP-03'],
  },
  {
    id: 'INS-007', proveedorId: 'PRV-007',
    nombre: 'Plataforma e-learning · Sistema autoinstructivo',
    profesion: 'Plataforma LMS',
    licenciaSST: null,
    tarjetaProfesional: null,
    hojaDeVidaUrl: null,
    competencias: ['CMP-02', 'CMP-06', 'CMP-10'],
  },
  {
    id: 'INS-008', proveedorId: 'PRV-008',
    nombre: 'Carolina Restrepo Vargas',
    profesion: 'Profesional en RRHH · Jefe de Talento Humano',
    licenciaSST: null,
    tarjetaProfesional: 'COPNIA-RH-2009-5512',
    hojaDeVidaUrl: '#mock-cv-crestrepo.pdf',
    competencias: ['CMP-06', 'CMP-09', 'CMP-10'],
  },
  {
    id: 'INS-009', proveedorId: 'PRV-003',
    nombre: 'Gabriel Alejandro Marín',
    profesion: 'Ingeniero ambiental · gestor de residuos certificado',
    licenciaSST: null,
    tarjetaProfesional: 'GA-2021-CO-318',
    hojaDeVidaUrl: '#mock-cv-gmarin.pdf',
    competencias: ['CMP-07'],
  },
  {
    // Caso de prueba: licencia VENCIDA (para Fase B validar bloqueo)
    id: 'INS-010', proveedorId: 'PRV-001',
    nombre: 'Pedro Antonio Gallego',
    profesion: 'Técnico SST',
    licenciaSST: { numero: 'LIC-SST-RIS-07120', vence: '12/01/2026', expedidaPor: 'SecSalud Valle del Cauca' },
    tarjetaProfesional: 'COPNIA-TEC-2014-7711',
    hojaDeVidaUrl: '#mock-cv-pgallego.pdf',
    competencias: ['CMP-05', 'CMP-11'],
  },
];

// ─── Cargos ──────────────────────────────────────────────
// Catálogo de cargos vigentes en Atica con su perfil de riesgos IPVR.
// Los nombres coinciden con los strings que ya usan training.jsx (forRoles)
// y employee-list.jsx — no inventar nuevos.

const CARGOS = [
  {
    id: 'CGO-001', nombre: 'Operario de Clasificación', area: 'Operaciones',
    nivel: 'operativo',
    riesgosIPVR: ['térmico-alto', 'mecánico', 'ruido', 'gas-GLP', 'polvo-orgánico'],
    headcount: 38,
  },
  {
    id: 'CGO-002', nombre: 'Operaria de Planta', area: 'Operaciones',
    nivel: 'operativo',
    riesgosIPVR: ['ergonómico', 'mecánico', 'ruido', 'biológico'],
    headcount: 142,
  },
  {
    id: 'CGO-003', nombre: 'Op. Aprovechamiento', area: 'Aprovechamiento',
    nivel: 'operativo',
    riesgosIPVR: ['ergonómico-repetitivo', 'mecánico', 'biológico'],
    headcount: 86,
  },
  {
    id: 'CGO-004', nombre: 'Líder de Aprovechamiento', area: 'Aprovechamiento',
    nivel: 'liderazgo',
    riesgosIPVR: ['ergonómico-repetitivo', 'biológico', 'liderazgo-equipos'],
    headcount: 6,
  },
  {
    id: 'CGO-005', nombre: 'Aux. Calidad', area: 'Calidad',
    nivel: 'operativo',
    riesgosIPVR: ['biológico', 'químico', 'ergonómico'],
    headcount: 12,
  },
  {
    id: 'CGO-006', nombre: 'Téc. Mantenimiento', area: 'Mantenimiento',
    nivel: 'técnico',
    riesgosIPVR: ['alturas', 'eléctrico', 'mecánico', 'espacios-confinados', 'LOTO'],
    headcount: 8,
  },
  {
    id: 'CGO-007', nombre: 'Coord. Compras', area: 'Compras',
    nivel: 'liderazgo',
    riesgosIPVR: ['administrativo', 'datos-personales'],
    headcount: 3,
  },
  {
    id: 'CGO-008', nombre: 'Jefe de RRHH', area: 'RRHH',
    nivel: 'directivo',
    riesgosIPVR: ['administrativo', 'datos-personales', 'liderazgo-equipos'],
    headcount: 1,
  },
  {
    id: 'CGO-009', nombre: 'Conductor', area: 'Logística',
    nivel: 'operativo',
    riesgosIPVR: ['vial', 'fatiga', 'manipulación-cargas'],
    headcount: 4,
  },
  {
    id: 'CGO-010', nombre: 'Brigadistas voluntarios', area: 'Transversal',
    nivel: 'voluntario',
    riesgosIPVR: ['emergencias', 'rescate', 'fuego-incipiente'],
    headcount: 14,
  },
];

// ─── Matriz Cargo ↔ Competencia ──────────────────────────
// Tupla: { cargoId, competenciaId, obligatoria, plazoDiasParaCompletar }
// Origen: análisis de riesgos IPVR + normativa colombiana aplicable.
// Esta tabla es la fuente de verdad que consume el motor de automatización
// (Fase C) para asignar capacitaciones al ingreso o cambio de cargo.
//
// Reglas de modelado:
//   - CMP-09 (Inducción) y CMP-06 (RIT) aplican a todos los cargos.
//   - CMP-11 (SG-SST general) aplica a todos los cargos operativos y superiores.
//   - CMP-04 (Alturas) solo a Téc. Mantenimiento (riesgo IPVR alturas).
//   - CMP-12 (PESV) solo a Conductor.
//   - CMP-07 (Clasificación avanzada) electiva — no obligatoria.

const MATRIZ_CARGO_COMP = [
  // ── CGO-001 Operario de Clasificación ──
  { cargoId: 'CGO-001', competenciaId: 'CMP-01', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-001', competenciaId: 'CMP-02', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-001', competenciaId: 'CMP-03', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-001', competenciaId: 'CMP-06', obligatoria: true,  plazoDias: 15 },
  { cargoId: 'CGO-001', competenciaId: 'CMP-09', obligatoria: true,  plazoDias: 7  },
  { cargoId: 'CGO-001', competenciaId: 'CMP-11', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-001', competenciaId: 'CMP-07', obligatoria: false, plazoDias: 180 },

  // ── CGO-002 Operaria de Planta ──
  { cargoId: 'CGO-002', competenciaId: 'CMP-02', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-002', competenciaId: 'CMP-03', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-002', competenciaId: 'CMP-06', obligatoria: true,  plazoDias: 15 },
  { cargoId: 'CGO-002', competenciaId: 'CMP-09', obligatoria: true,  plazoDias: 7  },
  { cargoId: 'CGO-002', competenciaId: 'CMP-11', obligatoria: true,  plazoDias: 30 },

  // ── CGO-003 Op. Aprovechamiento ──
  { cargoId: 'CGO-003', competenciaId: 'CMP-02', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-003', competenciaId: 'CMP-03', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-003', competenciaId: 'CMP-06', obligatoria: true,  plazoDias: 15 },
  { cargoId: 'CGO-003', competenciaId: 'CMP-09', obligatoria: true,  plazoDias: 7  },
  { cargoId: 'CGO-003', competenciaId: 'CMP-11', obligatoria: true,  plazoDias: 30 },

  // ── CGO-004 Líder de Aprovechamiento ──
  { cargoId: 'CGO-004', competenciaId: 'CMP-02', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-004', competenciaId: 'CMP-03', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-004', competenciaId: 'CMP-06', obligatoria: true,  plazoDias: 15 },
  { cargoId: 'CGO-004', competenciaId: 'CMP-09', obligatoria: true,  plazoDias: 7  },
  { cargoId: 'CGO-004', competenciaId: 'CMP-11', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-004', competenciaId: 'CMP-08', obligatoria: false, plazoDias: 365 },

  // ── CGO-005 Aux. Calidad ──
  { cargoId: 'CGO-005', competenciaId: 'CMP-02', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-005', competenciaId: 'CMP-03', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-005', competenciaId: 'CMP-06', obligatoria: true,  plazoDias: 15 },
  { cargoId: 'CGO-005', competenciaId: 'CMP-09', obligatoria: true,  plazoDias: 7  },
  { cargoId: 'CGO-005', competenciaId: 'CMP-11', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-005', competenciaId: 'CMP-10', obligatoria: true,  plazoDias: 60 },

  // ── CGO-006 Téc. Mantenimiento ──
  { cargoId: 'CGO-006', competenciaId: 'CMP-04', obligatoria: true,  plazoDias: 60 },
  { cargoId: 'CGO-006', competenciaId: 'CMP-06', obligatoria: true,  plazoDias: 15 },
  { cargoId: 'CGO-006', competenciaId: 'CMP-09', obligatoria: true,  plazoDias: 7  },
  { cargoId: 'CGO-006', competenciaId: 'CMP-11', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-006', competenciaId: 'CMP-01', obligatoria: true,  plazoDias: 60 },

  // ── CGO-007 Coord. Compras ──
  { cargoId: 'CGO-007', competenciaId: 'CMP-06', obligatoria: true,  plazoDias: 15 },
  { cargoId: 'CGO-007', competenciaId: 'CMP-09', obligatoria: true,  plazoDias: 7  },
  { cargoId: 'CGO-007', competenciaId: 'CMP-10', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-007', competenciaId: 'CMP-11', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-007', competenciaId: 'CMP-08', obligatoria: false, plazoDias: 365 },

  // ── CGO-008 Jefe de RRHH ──
  { cargoId: 'CGO-008', competenciaId: 'CMP-06', obligatoria: true,  plazoDias: 15 },
  { cargoId: 'CGO-008', competenciaId: 'CMP-09', obligatoria: true,  plazoDias: 7  },
  { cargoId: 'CGO-008', competenciaId: 'CMP-10', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-008', competenciaId: 'CMP-11', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-008', competenciaId: 'CMP-08', obligatoria: false, plazoDias: 365 },

  // ── CGO-009 Conductor ──
  { cargoId: 'CGO-009', competenciaId: 'CMP-06', obligatoria: true,  plazoDias: 15 },
  { cargoId: 'CGO-009', competenciaId: 'CMP-09', obligatoria: true,  plazoDias: 7  },
  { cargoId: 'CGO-009', competenciaId: 'CMP-11', obligatoria: true,  plazoDias: 30 },
  { cargoId: 'CGO-009', competenciaId: 'CMP-12', obligatoria: true,  plazoDias: 30 },

  // ── CGO-010 Brigadistas voluntarios ──
  // Aplica encima de las del cargo principal (es transversal)
  { cargoId: 'CGO-010', competenciaId: 'CMP-05', obligatoria: true,  plazoDias: 90 },
  { cargoId: 'CGO-010', competenciaId: 'CMP-11', obligatoria: true,  plazoDias: 30 },
];

// ─── Helpers de consulta ─────────────────────────────────

const getProveedor = (id) => PROVEEDORES.find(p => p.id === id);
const getInstructor = (id) => INSTRUCTORES.find(i => i.id === id);
const getCargo = (id) => CARGOS.find(c => c.id === id);
const getCargoByNombre = (nombre) => CARGOS.find(c => c.nombre === nombre);

// Devuelve las competencias requeridas para un cargo (por id o por nombre).
// Resultado: array de { competenciaId, obligatoria, plazoDias }.
const competenciasPorCargo = (cargoIdOrNombre) => {
  const cargo = getCargo(cargoIdOrNombre) || getCargoByNombre(cargoIdOrNombre);
  if (!cargo) return [];
  return MATRIZ_CARGO_COMP.filter(m => m.cargoId === cargo.id);
};

// Inversa: ¿qué cargos exigen esta competencia?
const cargosPorCompetencia = (competenciaId) => {
  return MATRIZ_CARGO_COMP
    .filter(m => m.competenciaId === competenciaId)
    .map(m => ({ ...getCargo(m.cargoId), obligatoria: m.obligatoria, plazoDias: m.plazoDias }))
    .filter(Boolean);
};

// ¿La licencia SST del instructor está vigente respecto a `hoy`?
// Útil para Fase B (bloquear creación de sesión con instructor sin licencia).
const licenciaInstructorVigente = (instructorId, hoy = new Date()) => {
  const ins = getInstructor(instructorId);
  if (!ins) return false;
  if (!ins.licenciaSST) return true; // no requiere licencia (ej. tutor universitario)
  const [d, m, y] = ins.licenciaSST.vence.split('/').map(Number);
  return new Date(y, m - 1, d) >= hoy;
};

// ─── Motor (Fase C) — funciones puras ────────────────────
// Toman estado y devuelven asignaciones / vencimientos. No mutan globals.

// Parsea "DD/MM/YYYY" a Date. Devuelve null si inválido.
const parseDateCO = (s) => {
  if (!s || typeof s !== 'string') return null;
  const [d, m, y] = s.split('/').map(Number);
  if (!d || !m || !y) return null;
  return new Date(y, m - 1, d);
};

// Suma `nDays` a una Date y devuelve "DD/MM/YYYY".
const addDaysCO = (date, nDays) => {
  const d = new Date(date.getTime() + nDays * 86400000);
  return `${String(d.getDate()).padStart(2,'0')}/${String(d.getMonth()+1).padStart(2,'0')}/${d.getFullYear()}`;
};

// Dado un empleado { id, cargo } y `hoy`, devuelve las asignaciones que la matriz
// dice que debe tener: array de { competenciaId, obligatoria, plazoDias, dueDate }.
// El dueDate se calcula desde `hoy` + plazoDias (representa la fecha límite para completar).
const recalcularAsignaciones = (empleado, hoy = new Date()) => {
  if (!empleado || !empleado.cargo) return [];
  const items = competenciasPorCargo(empleado.cargo);
  return items.map(m => ({
    empleadoId: empleado.id,
    competenciaId: m.competenciaId,
    obligatoria: m.obligatoria,
    plazoDias: m.plazoDias,
    dueDate: addDaysCO(hoy, m.plazoDias),
    originada: empleado.originada || 'cambio-cargo',
  }));
};

// Evalúa los certificados de un empleado contra la matriz de su cargo.
// Devuelve { vigentes, porVencer60, porVencer30, vencidas, pendientes }.
//   - certificados: [{ competenciaId, vigenciaTo: 'DD/MM/YYYY' }]
//   - vencidas: certificados cuya vigenciaTo < hoy
//   - porVencer30 / 60: certificados que vencen entre [hoy, hoy+30] y (30, 60]
//   - pendientes: competencias requeridas SIN certificado
const evaluarVencimientos = (empleado, certificados = [], hoy = new Date()) => {
  const requeridas = competenciasPorCargo(empleado.cargo).map(m => m.competenciaId);
  const certByCmp = {};
  certificados.forEach(c => { certByCmp[c.competenciaId] = c; });

  const vigentes = [];
  const porVencer30 = [];
  const porVencer60 = [];
  const vencidas = [];
  const pendientes = [];

  requeridas.forEach(cmpId => {
    const cert = certByCmp[cmpId];
    if (!cert) { pendientes.push({ competenciaId: cmpId }); return; }
    if (!cert.vigenciaTo) { vigentes.push(cert); return; } // sin vencimiento (ej. inducción única)
    const fin = parseDateCO(cert.vigenciaTo);
    const dias = Math.round((fin - hoy) / 86400000);
    if      (dias < 0)  vencidas.push({ ...cert, diasVencida: -dias });
    else if (dias <= 30) porVencer30.push({ ...cert, diasParaVencer: dias });
    else if (dias <= 60) porVencer60.push({ ...cert, diasParaVencer: dias });
    else                 vigentes.push(cert);
  });

  return { vigentes, porVencer60, porVencer30, vencidas, pendientes };
};

window.parseDateCO          = parseDateCO;
window.addDaysCO            = addDaysCO;
window.recalcularAsignaciones = recalcularAsignaciones;
window.evaluarVencimientos    = evaluarVencimientos;

// ─── Certificados (Fase D) ───────────────────────────────
// Store global de certificados emitidos. cert-verify.jsx lo consume para resolver
// códigos públicos. SessionCapture al cerrar empuja certs nuevos aquí.
// Estructura: cada cert refleja CERT_DEMO de cert-verify.jsx (mismos campos).

const CERTIFICATES = [
  {
    code:        'CERT-2026-c-01-04821',
    status:      'vigente',
    origen:      'interno',
    emp:         { initials: 'AM', name: 'Andrés Felipe Mejía', doc: 'CC 1.087.456.231 *** *** 31', cargo: 'Operario de Clasificación', area: 'Operaciones', site: 'Cali' },
    empresa:     { name: 'Atica S.A.S.', nit: '900.123.456-7', logo: 'a' },
    plataforma:     { name: 'Atica SOMOS', logo: 'a' },
    course:      { name: 'Manejo seguro de maquinaria de clasificación', code: 'c-01', duration: '8 horas', mode: 'Presencial' },
    competencia: { id: 'CMP-01', name: 'Manejo seguro de maquinaria de clasificación', norma: 'Resolución 0312 de 2019 · SG-SST · Decreto 1072' },
    session:     { id: 'SES-2026-0412', date: '12/04/2026', site: 'Cali · Aula 2', instructor: 'Carlos Fernando Vélez', instructorOrg: 'ARL Sura', contentVersion: 'v3.2' },
    result:      { score: 92, minScore: 80, passed: true, issuedAt: '12/04/2026 16:42' },
    vigencia:    { from: '12/04/2026', to: '12/04/2027', daysLeft: 343 },
    evidence:    {
      attendanceSigned: true,
      attendanceMethod: 'OTP · email + SMS',
      evaluationApplied: true,
      evaluationQuestions: 5,
      evidencesCount: 4,
      evidencesList: ['Acta del instructor firmada', 'Foto del aula', 'Lista de asistencia escaneada', 'Material entregado'],
      contentVersionLocked: 'v3.2',
    },
    hash: 'a4f2c1e89b7d3f5e26c8d4b1a9f3e7c5d8b2a4f6e9c1d3b5a7f8e0c2d4b6a9f1',
    bitacoraEvents: [
      { when: '12/04/2026 16:42', text: 'Sesión cerrada · hash criptográfico generado',         tone: 'green' },
      { when: '12/04/2026 16:42', text: 'Certificado emitido automáticamente al colaborador',    tone: 'green' },
      { when: '12/04/2026 16:38', text: 'Evaluación aprobada con 92/100 (mínimo 80)',            tone: 'green' },
      { when: '12/04/2026 09:14', text: 'Asistencia firmada electrónicamente vía OTP email',     tone: 'green' },
      { when: '12/04/2026 08:00', text: 'Sesión iniciada · 18 inscritos',                        tone: 'navy' },
    ],
  },
  {
    code:        'CERT-2025-c-11-09921',
    status:      'vigente',
    origen:      'interno',
    emp:         { initials: 'CV', name: 'Carlos Andrés Vélez', doc: 'CC 1.087.554.892 *** *** 92', cargo: 'Operario de Clasificación', area: 'Operaciones', site: 'Cali' },
    empresa:     { name: 'Atica S.A.S.', nit: '900.123.456-7', logo: 'a' },
    plataforma:     { name: 'Atica SOMOS', logo: 'a' },
    course:      { name: 'SG-SST · Sistema de Gestión SST', code: 'c-11', duration: '4 horas', mode: 'E-learning' },
    competencia: { id: 'CMP-11', name: 'SG-SST · Sistema de Gestión SST', norma: 'Decreto 1072 · Resolución 0312' },
    session:     { id: 'SES-2025-0820', date: '20/08/2025', site: 'Plataforma e-learning interna', instructor: 'Plataforma autoinstructiva', instructorOrg: 'Atica LMS', contentVersion: 'v2.1' },
    result:      { score: 84, minScore: 80, passed: true, issuedAt: '20/08/2025 14:18' },
    vigencia:    { from: '20/08/2025', to: '20/08/2026', daysLeft: 106 },
    evidence:    {
      attendanceSigned: true,
      attendanceMethod: 'OTP · email',
      evaluationApplied: true,
      evaluationQuestions: 12,
      evidencesCount: 3,
      evidencesList: ['Reporte plataforma LMS', 'Material entregado', 'Lista asistencia digital'],
      contentVersionLocked: 'v2.1',
    },
    hash: 'b7e3d8f1c2a945e0762d8b3a1f4c5e9d8a2c6f0b3e7d4a8c1f5b9e2d7a0c4f6b8',
    bitacoraEvents: [
      { when: '20/08/2025 14:18', text: 'Sesión cerrada · certificado emitido',                  tone: 'green' },
      { when: '20/08/2025 14:00', text: 'Evaluación aprobada con 84/100 (mínimo 80)',            tone: 'green' },
      { when: '20/08/2025 09:30', text: 'Acceso a plataforma + firma electrónica de inicio',     tone: 'navy' },
    ],
  },
  {
    code:        'CERT-EXT-SENA-2026-08841',
    status:      'vigente',
    origen:      'externo',
    emp:         { initials: 'PV', name: 'Paula Valencia Ríos', doc: 'CC 1.087.221.443 *** *** 43', cargo: 'Téc. Mantenimiento', area: 'Mantenimiento', site: 'Cali' },
    empresa:     { name: 'Atica S.A.S.', nit: '900.123.456-7', logo: 'a' },
    plataforma:     { name: 'Atica SOMOS', logo: 'a' },
    course:      { name: 'Trabajo seguro en alturas · Nivel avanzado', code: 'c-08', duration: '40 horas', mode: 'Presencial' },
    competencia: { id: 'CMP-04', name: 'Trabajo seguro en alturas', norma: 'Resolución 1409 de 2012 · CCS' },
    session:     { id: 'SES-2026-0301', date: '01/03/2026', site: 'CCS · Centro de entrenamiento Cali', instructor: 'Equipo CCS', instructorOrg: 'Consejo Colombiano de Seguridad', contentVersion: 'externo' },
    result:      { score: 88, minScore: 85, passed: true, issuedAt: '05/03/2026 17:30' },
    vigencia:    { from: '05/03/2026', to: '05/03/2027', daysLeft: 305 },
    evidence:    {
      attendanceSigned: false,
      attendanceMethod: 'Capturada por el proveedor (CCS)',
      evaluationApplied: true,
      evaluationQuestions: null,
      evidencesCount: 1,
      evidencesList: ['Certificado emitido por CCS · cert-ccs-2026-08841.pdf'],
      contentVersionLocked: 'externo',
    },
    hash: null, // certificado externo · no hay hash interno
    bitacoraEvents: [
      { when: '05/03/2026 17:30', text: 'Certificado externo archivado · vinculado a hoja de vida', tone: 'green' },
      { when: '05/03/2026 17:25', text: 'PDF cargado · cert-ccs-2026-08841.pdf',                    tone: 'navy' },
      { when: '01/03/2026 08:00', text: 'Sesión presencial CCS · 5 días de entrenamiento',          tone: 'navy' },
    ],
  },
];

window.CERTIFICATES = CERTIFICATES;
window.findCertificate = (code) => (window.CERTIFICATES || []).find(c => c.code === code);

// ─── Banco de preguntas + Evaluaciones (Fase E.2) ────────
// BancoPregunta: asociado a competencia, reutilizable entre cursos de esa competencia.
// Evaluacion: asociado a un curso · puede ser fija o pool aleatorio por dificultad.

const BANCO_PREGUNTAS = [
  // CMP-01 · Manejo seguro de maquinaria de clasificación
  { id: 'BP-001', competenciaId: 'CMP-01', tipo: 'multiple', dificultad: 'media', enunciado: '¿Cuál es el primer paso del procedimiento LOTO antes de intervenir una trituradora?',
    opciones: ['Avisar al supervisor', 'Bloquear la fuente de energía y colocar etiqueta', 'Cerrar la válvula de gas', 'Apagar el extractor'], respuestaCorrecta: 1, etiquetas: ['LOTO', 'seguridad'] },
  { id: 'BP-002', competenciaId: 'CMP-01', tipo: 'multiple', dificultad: 'facil', enunciado: 'Temperatura mínima a la que un EPP térmico clase 2 protege:',
    opciones: ['100°C', '200°C', '350°C', '500°C'], respuestaCorrecta: 2, etiquetas: ['EPP'] },
  { id: 'BP-003', competenciaId: 'CMP-01', tipo: 'multiple', dificultad: 'dificil', enunciado: 'Si detectas humo en la zona de clasificación durante el ciclo, la primera acción es:',
    opciones: ['Activar parada de emergencia', 'Llamar a bomberos', 'Salir del área inmediatamente', 'Verificar la temperatura del lote'], respuestaCorrecta: 0, etiquetas: ['emergencia'] },
  { id: 'BP-004', competenciaId: 'CMP-01', tipo: 'multiple', dificultad: 'media', enunciado: '¿Cada cuánto debe revisarse el sistema de venteo de la trituradora?',
    opciones: ['Cada turno', 'Semanal', 'Quincenal', 'Mensual'], respuestaCorrecta: 0, etiquetas: ['mantenimiento'] },
  { id: 'BP-005', competenciaId: 'CMP-01', tipo: 'verdadero-falso', dificultad: 'facil', enunciado: 'El operario puede retirar la guarda mecánica si el lote está atascado, siempre que avise al supervisor.',
    opciones: ['Verdadero', 'Falso'], respuestaCorrecta: 1, etiquetas: ['guardas', 'LOTO'] },
  { id: 'BP-006', competenciaId: 'CMP-01', tipo: 'multiple', dificultad: 'dificil', enunciado: 'En caso de fuga de GLP detectada por olor, ¿cuál es la secuencia correcta?',
    opciones: ['Llamar bomberos · evacuar · cerrar válvula', 'Cerrar válvula · ventilar · evacuar · reportar', 'Evacuar · llamar bomberos · esperar', 'Continuar trabajo · reportar al supervisor'], respuestaCorrecta: 1, etiquetas: ['GLP', 'emergencia'] },

  // CMP-02 · BPM
  { id: 'BP-010', competenciaId: 'CMP-02', tipo: 'multiple', dificultad: 'facil', enunciado: '¿Qué resolución regula las BPM en alimentos en Colombia?',
    opciones: ['Resolución 0312 de 2019', 'Resolución 2674 de 2013', 'Resolución 1409 de 2012', 'Decreto 1072 de 2015'], respuestaCorrecta: 1, etiquetas: ['normativa'] },
  { id: 'BP-011', competenciaId: 'CMP-02', tipo: 'verdadero-falso', dificultad: 'facil', enunciado: 'El uso de joyería está permitido si se cubre con guantes.',
    opciones: ['Verdadero', 'Falso'], respuestaCorrecta: 1, etiquetas: ['higiene'] },
  { id: 'BP-012', competenciaId: 'CMP-02', tipo: 'multiple', dificultad: 'media', enunciado: 'Frecuencia mínima recomendada para lavado de manos en zona de proceso:',
    opciones: ['Cada hora', 'Al iniciar y al cambiar de tarea', 'Cada cambio de turno', 'Solo al iniciar el día'], respuestaCorrecta: 1, etiquetas: ['higiene'] },
  { id: 'BP-013', competenciaId: 'CMP-02', tipo: 'multiple', dificultad: 'media', enunciado: '¿Quién audita el cumplimiento BPM?',
    opciones: ['MinTrabajo', 'INVIMA', 'Secretaría de Salud', 'B y C son correctas'], respuestaCorrecta: 3, etiquetas: ['auditoría'] },
  { id: 'BP-014', competenciaId: 'CMP-02', tipo: 'multiple', dificultad: 'dificil', enunciado: 'Una temperatura de almacenamiento de 6°C para producto refrigerado es:',
    opciones: ['Aceptable', 'No aceptable · debe estar ≤ 4°C', 'Aceptable solo en lácteos', 'Aceptable si hay rotación rápida'], respuestaCorrecta: 1, etiquetas: ['cadena-frío'] },
];

const EVALUACIONES = [
  // Evaluación del curso c-01 · usa pool aleatorio CMP-01
  { id: 'EV-001', courseId: 'c-01', competenciaId: 'CMP-01', modo: 'pool-aleatorio',
    poolPorDificultad: { facil: 1, media: 2, dificil: 1 },
    preguntasFijas: [], notaMinima: 80, tiempoMinutos: 30, intentos: 2, mostrarRespuestas: false },
  // Evaluación del curso c-02 · BPM · preguntas fijas
  { id: 'EV-002', courseId: 'c-02', competenciaId: 'CMP-02', modo: 'fija',
    preguntasFijas: ['BP-010', 'BP-011', 'BP-012', 'BP-013', 'BP-014'],
    poolPorDificultad: null, notaMinima: 75, tiempoMinutos: 20, intentos: 3, mostrarRespuestas: true },
];

window.BANCO_PREGUNTAS = BANCO_PREGUNTAS;
window.EVALUACIONES    = EVALUACIONES;

// ─── Planes formativos (refactor · plan ≠ sesión) ────────
// Un plan agrupa N sesiones bajo un objetivo común con vigencia y eventual recurrencia.
// Los ciclos representan instancias del plan (uno nuevo por cada periodo si frecuencia ≠ una-vez).

const TIPO_PLAN = {
  puntual:    { label: 'Puntual',          desc: 'Un único ciclo · no se repite' },
  ciclico:    { label: 'Cíclico',          desc: 'Se renueva automáticamente al cumplirse el período' },
  'por-evento': { label: 'Por evento',     desc: 'Se dispara por ingreso, cambio de cargo o vencimiento' },
};

const FREQ_PLAN = {
  'una-vez':   { label: 'Una sola vez', meses: null },
  mensual:     { label: 'Mensual',      meses: 1   },
  trimestral:  { label: 'Trimestral',   meses: 3   },
  semestral:   { label: 'Semestral',    meses: 6   },
  anual:       { label: 'Anual',        meses: 12  },
  bienal:      { label: 'Bienal',       meses: 24  },
  trianual:    { label: 'Trianual',     meses: 36  },
};

const PLANES_FORMATIVOS = [
  {
    id: 'PLAN-SST-2026',
    nombre: 'Plan Anual SG-SST 2026',
    descripcion: 'Plan obligatorio Decreto 1072 art. 2.2.4.6.11 · mínimo 4 sesiones SST/año a todo el personal sobre los riesgos del cargo.',
    tipo: 'ciclico',
    frecuencia: 'anual',
    vigenciaFrom: '01/01/2026',
    vigenciaTo:   '31/12/2026',
    proximaRecurrencia: '01/01/2027',
    cursosIds: ['c-01', 'c-02', 'c-11', 'c-08'],
    audiencia: { tipo: 'cargo', cargosNombres: ['Operario de Clasificación', 'Operaria de Planta', 'Op. Aprovechamiento', 'Líder de Aprovechamiento', 'Téc. Mantenimiento'] },
    estado: 'activo',
    creadoPor: 'Carolina Restrepo · Jefe RRHH',
    creadoEn: '15/12/2025',
  },
  {
    id: 'PLAN-IND-E0247',
    nombre: 'Inducción Operario · Andrés Felipe Mejía',
    descripcion: 'Inducción al ingreso · 4 capacitaciones obligatorias en los primeros 30 días.',
    tipo: 'puntual',
    frecuencia: 'una-vez',
    vigenciaFrom: '02/05/2026',
    vigenciaTo:   '01/06/2026',
    proximaRecurrencia: null,
    cursosIds: ['c-09', 'c-01', 'c-02', 'c-05'],
    audiencia: { tipo: 'individual', empleadosIds: ['E0247'] },
    estado: 'activo',
    creadoPor: 'Sistema · evento de ingreso',
    creadoEn: '02/05/2026',
  },
  {
    id: 'PLAN-BPM-MAYO',
    nombre: 'Refresh BPM · Mayo 2026',
    descripcion: 'Repaso trimestral de BPM (Resolución 2674/2013) para todo el personal con contacto con producto.',
    tipo: 'ciclico',
    frecuencia: 'trimestral',
    vigenciaFrom: '01/05/2026',
    vigenciaTo:   '31/05/2026',
    proximaRecurrencia: '01/08/2026',
    cursosIds: ['c-02'],
    audiencia: { tipo: 'area', areas: ['Operaciones', 'Aprovechamiento', 'Calidad'] },
    estado: 'activo',
    creadoPor: 'Carolina Restrepo · Jefe RRHH',
    creadoEn: '20/04/2026',
  },
];

window.PLANES_FORMATIVOS = PLANES_FORMATIVOS;
window.TIPO_PLAN = TIPO_PLAN;
window.FREQ_PLAN = FREQ_PLAN;
window.findPlan = (id) => (window.PLANES_FORMATIVOS || []).find(p => p.id === id);

// ─── Conversión de fechas CO ↔ ISO + componentes reusables ─
// Toda la app guarda fechas en formato colombiano "DD/MM/AAAA". Los inputs nativos
// HTML5 (type=date) usan ISO "YYYY-MM-DD". Estos helpers cierran la brecha sin
// migrar los mocks ni romper compatibilidad.

const dateCO_to_ISO = (s) => {
  if (!s || typeof s !== 'string') return '';
  if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s; // ya está en ISO
  const [d, m, y] = s.split('/').map(x => x?.trim());
  if (!d || !m || !y) return '';
  return `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`;
};

const dateISO_to_CO = (s) => {
  if (!s || typeof s !== 'string') return '';
  if (/^\d{2}\/\d{2}\/\d{4}$/.test(s)) return s; // ya está en CO
  const [y, m, d] = s.split('-');
  if (!y || !m || !d) return '';
  return `${d}/${m}/${y}`;
};

window.dateCO_to_ISO = dateCO_to_ISO;
window.dateISO_to_CO = dateISO_to_CO;

// ─── DatePickerCO · popover con calendario estilo comms ──
// Reusa exactamente las clases CSS del módulo Comunicaciones (`co-cal*`,
// `co-time*`, `co-sched-*`) para mantener consistencia visual en toda la app.
// Mantiene el valor externo en formato CO `DD/MM/AAAA` (compatibilidad con mocks).

const _DP_TODAY = new Date(2026, 4, 6); // referencia del proyecto · 6 may 2026
const _DP_DOW    = ['dom', 'lun', 'mar', 'mié', 'jue', 'vie', 'sáb'];
const _DP_MONTHS = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
const _DP_MONTHS_S = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'];
const _dpPad = (n) => String(n).padStart(2, '0');
const _dpSameDay = (a, b) => a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
const _dpIsBefore = (a, b) => {
  if (!a || !b) return false;
  const aa = new Date(a.getFullYear(), a.getMonth(), a.getDate());
  const bb = new Date(b.getFullYear(), b.getMonth(), b.getDate());
  return aa < bb;
};
const _dpParse = (s) => {
  if (!s) return null;
  if (s instanceof Date) return s;
  const [d, m, y] = s.split('/').map(x => parseInt(x, 10));
  if (!d || !m || !y) return null;
  return new Date(y, m - 1, d);
};
const _dpFormat = (d) => d ? `${_dpPad(d.getDate())}/${_dpPad(d.getMonth()+1)}/${d.getFullYear()}` : '';

const DatePickerCO = ({
  value, onChange,
  min, max,
  withTime = false,
  presets = null,
  placeholder = 'Seleccionar fecha',
  className = '',
  disabled = false,
}) => {
  const wrapRef = React.useRef(null);
  const [open, setOpen] = React.useState(false);

  // Parseo del valor — soporta "DD/MM/AAAA" o "DD/MM/AAAA HH:MM"
  const parseFull = (s) => {
    if (!s) return { date: null, h: 8, m: 0 };
    const parts = String(s).split(' ');
    const date = _dpParse(parts[0]);
    if (parts[1]) {
      const [h, mi] = parts[1].split(':').map(x => parseInt(x, 10));
      return { date, h: h || 8, m: mi || 0 };
    }
    return { date, h: 8, m: 0 };
  };
  const initial = parseFull(value);

  const [calMonth, setCalMonth] = React.useState(() => initial.date
    ? { y: initial.date.getFullYear(), m: initial.date.getMonth() }
    : { y: _DP_TODAY.getFullYear(), m: _DP_TODAY.getMonth() });
  const [hour, setHour] = React.useState(initial.h);
  const [minute, setMinute] = React.useState(initial.m);

  const minDate = _dpParse(min);
  const maxDate = _dpParse(max);

  // Cerrar al hacer click fuera
  React.useEffect(() => {
    if (!open) return;
    const handler = (e) => {
      if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
    };
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [open]);

  const buildCells = () => {
    const first = new Date(calMonth.y, calMonth.m, 1);
    const startDow = first.getDay();
    const days = new Date(calMonth.y, calMonth.m + 1, 0).getDate();
    const cells = [];
    for (let i = 0; i < startDow; i++) cells.push(null);
    for (let d = 1; d <= days; d++) cells.push(new Date(calMonth.y, calMonth.m, d));
    while (cells.length % 7 !== 0) cells.push(null);
    return cells;
  };

  const prevMonth = () => setCalMonth(p => p.m === 0 ? { y: p.y - 1, m: 11 } : { y: p.y, m: p.m - 1 });
  const nextMonth = () => setCalMonth(p => p.m === 11 ? { y: p.y + 1, m: 0 } : { y: p.y, m: p.m + 1 });

  const emit = (date, h, m) => {
    if (!date) { onChange && onChange(''); return; }
    const formatted = withTime
      ? `${_dpFormat(date)} ${_dpPad(h)}:${_dpPad(m)}`
      : _dpFormat(date);
    onChange && onChange(formatted);
  };

  const pickDay = (d) => {
    if (minDate && _dpIsBefore(d, minDate)) return;
    if (maxDate && _dpIsBefore(maxDate, d)) return;
    emit(d, hour, minute);
    if (!withTime) setOpen(false);
  };
  const setH = (h) => { setHour(h); emit(initial.date, h, minute); };
  const setM = (m) => { setMinute(m); emit(initial.date, hour, m); };

  const display = initial.date
    ? (withTime
        ? `${_DP_DOW[initial.date.getDay()]} ${initial.date.getDate()} ${_DP_MONTHS_S[initial.date.getMonth()]} ${initial.date.getFullYear()} · ${_dpPad(initial.h)}:${_dpPad(initial.m)}`
        : `${_dpPad(initial.date.getDate())}/${_dpPad(initial.date.getMonth()+1)}/${initial.date.getFullYear()}`)
    : placeholder;

  return (
    <div ref={wrapRef} className="dp-wrap">
      <button
        type="button"
        className={`dp-trigger ${className} ${initial.date ? 'has-value' : ''}`}
        onClick={() => !disabled && setOpen(!open)}
        disabled={disabled}
      >
        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><rect x="3" y="4" width="18" height="18" rx="2" /><path d="M16 2v4M8 2v4M3 10h18" /></svg>
        <span className={initial.date ? '' : 'muted'}>{display}</span>
        <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" style={{ marginLeft: 'auto', opacity: 0.5 }}><path d={open ? "m18 15-6-6-6 6" : "m6 9 6 6 6-6"} /></svg>
      </button>

      {open && (
        <div className="dp-popover">
          {presets && presets.length > 0 && (
            <div className="co-sched-presets">
              {presets.map((p, i) => (
                <button key={i} type="button" className="co-sched-preset" onClick={() => {
                  const d = typeof p.compute === 'function' ? p.compute(_DP_TODAY) : p.date;
                  if (!d) return;
                  setCalMonth({ y: d.getFullYear(), m: d.getMonth() });
                  if (withTime && p.h != null) { setHour(p.h); setMinute(p.m || 0); emit(d, p.h, p.m || 0); }
                  else emit(d, hour, minute);
                  if (!withTime) setOpen(false);
                }}>
                  {p.label}
                </button>
              ))}
            </div>
          )}

          {/* Calendario */}
          <div className="co-cal">
            <div className="co-cal-head">
              <button type="button" className="co-cal-nav" onClick={prevMonth} title="Mes anterior">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="m15 6-6 6 6 6" /></svg>
              </button>
              <div className="co-cal-month">{_DP_MONTHS[calMonth.m]} {calMonth.y}</div>
              <button type="button" className="co-cal-nav" onClick={nextMonth} title="Mes siguiente">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="m9 6 6 6-6 6" /></svg>
              </button>
            </div>
            <div className="co-cal-weekdays">
              {['D', 'L', 'M', 'M', 'J', 'V', 'S'].map((w, i) => <span key={i}>{w}</span>)}
            </div>
            <div className="co-cal-grid">
              {buildCells().map((d, i) => {
                if (!d) return <span key={i} className="co-cal-day empty" />;
                const isPast     = minDate && _dpIsBefore(d, minDate);
                const isFuture   = maxDate && _dpIsBefore(maxDate, d);
                const isToday    = _dpSameDay(d, _DP_TODAY);
                const isSelected = _dpSameDay(d, initial.date);
                const cls = ['co-cal-day'];
                if (isPast || isFuture) cls.push('past');
                if (isToday) cls.push('today');
                if (isSelected) cls.push('selected');
                return (
                  <button key={i} type="button" className={cls.join(' ')} disabled={isPast || isFuture} onClick={() => pickDay(d)}>
                    {d.getDate()}
                  </button>
                );
              })}
            </div>
          </div>

          {/* Time picker */}
          {withTime && (
            <div className="co-sched-time">
              <div className="co-time-label">
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" /></svg>
                Hora
              </div>
              <div className="co-time-input">
                <input type="number" min="0" max="23" value={hour}
                  onChange={(e) => setH(Math.max(0, Math.min(23, parseInt(e.target.value, 10) || 0)))}
                  aria-label="Hora" />
                <span className="co-time-sep">:</span>
                <input type="number" min="0" max="59" step="5" value={minute}
                  onChange={(e) => setM(Math.max(0, Math.min(59, parseInt(e.target.value, 10) || 0)))}
                  aria-label="Minutos" />
              </div>
              <div className="co-time-quick">
                {[[8,0,'8:00'],[9,0,'9:00'],[12,0,'12:00'],[15,0,'15:00'],[17,0,'17:00']].map(([h,m,l]) => (
                  <button key={l} type="button"
                    className={`co-time-chip ${hour===h && minute===m ? 'active' : ''}`}
                    onClick={() => { setHour(h); setMinute(m); emit(initial.date, h, m); }}
                  >{l}</button>
                ))}
              </div>
            </div>
          )}

          {/* Resumen */}
          {initial.date && (
            <div className="co-sched-summary">
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><rect x="3" y="4" width="18" height="18" rx="2" /><path d="M16 2v4M8 2v4M3 10h18" /></svg>
              <span>{display}</span>
            </div>
          )}
        </div>
      )}
    </div>
  );
};

window.DatePickerCO = DatePickerCO;
// Backward-compat: el código viejo invoca window.DateInputCO
window.DateInputCO = DatePickerCO;

// ─── TimeRangeChipsCO · selector de rango horario con chips estilo comms ─
// Produce/consume strings "HH:MM - HH:MM". Reusa clases co-time-input + co-time-quick.
const TimeRangeChipsCO = ({ value, onChange, className = '' }) => {
  const parse = (s) => {
    if (!s) return [8, 0, 12, 0];
    const m = String(s).match(/(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})/);
    if (!m) return [8, 0, 12, 0];
    return [parseInt(m[1],10), parseInt(m[2],10), parseInt(m[3],10), parseInt(m[4],10)];
  };
  const [hi, mi, hf, mf] = parse(value);

  const emit = (a, b, c, d) => onChange && onChange(`${_dpPad(a)}:${_dpPad(b)} - ${_dpPad(c)}:${_dpPad(d)}`);

  const presets = [
    [8, 0, 12, 0, '8:00 – 12:00'],
    [13, 0, 17, 0, '1:00 – 5:00'],
    [8, 0, 17, 0, '8:00 – 5:00'],
    [18, 0, 22, 0, '6:00 – 10:00'],
  ];

  return (
    <div className={`tr-time-range ${className}`}>
      <div className="tr-time-range-row">
        <div className="tr-time-range-block">
          <div className="muted-xs">Inicio</div>
          <div className="co-time-input">
            <input type="number" min="0" max="23" value={hi}
              onChange={e => emit(Math.max(0, Math.min(23, parseInt(e.target.value,10) || 0)), mi, hf, mf)} />
            <span className="co-time-sep">:</span>
            <input type="number" min="0" max="59" step="5" value={mi}
              onChange={e => emit(hi, Math.max(0, Math.min(59, parseInt(e.target.value,10) || 0)), hf, mf)} />
          </div>
        </div>
        <span className="tr-time-range-arrow">→</span>
        <div className="tr-time-range-block">
          <div className="muted-xs">Fin</div>
          <div className="co-time-input">
            <input type="number" min="0" max="23" value={hf}
              onChange={e => emit(hi, mi, Math.max(0, Math.min(23, parseInt(e.target.value,10) || 0)), mf)} />
            <span className="co-time-sep">:</span>
            <input type="number" min="0" max="59" step="5" value={mf}
              onChange={e => emit(hi, mi, hf, Math.max(0, Math.min(59, parseInt(e.target.value,10) || 0)))} />
          </div>
        </div>
      </div>
      <div className="co-time-quick" style={{ marginTop: 6 }}>
        {presets.map(([a, b, c, d, label]) => {
          const active = hi === a && mi === b && hf === c && mf === d;
          return (
            <button key={label} type="button"
              className={`co-time-chip ${active ? 'active' : ''}`}
              onClick={() => emit(a, b, c, d)}
            >{label}</button>
          );
        })}
      </div>
    </div>
  );
};

window.TimeRangeChipsCO = TimeRangeChipsCO;
window.TimeRangeInput = TimeRangeChipsCO; // backward compat

// Resuelve la audiencia de un plan a una lista de empId (mock · usa PLANS de training.jsx para cruzar)
window.resolverAudienciaPlan = (plan, empleadosPool) => {
  if (!plan || !empleadosPool) return [];
  const aud = plan.audiencia || {};
  if (aud.tipo === 'individual') return empleadosPool.filter(e => aud.empleadosIds.includes(e.id));
  if (aud.tipo === 'cargo')      return empleadosPool.filter(e => aud.cargosNombres.includes(e.role));
  if (aud.tipo === 'area')       return empleadosPool.filter(e => aud.areas.includes(e.area));
  return [];
};
window.findEvaluacionByCourse = (courseId) => (window.EVALUACIONES || []).find(e => e.courseId === courseId);
window.findPreguntasByCompetencia = (cmpId) => (window.BANCO_PREGUNTAS || []).filter(p => p.competenciaId === cmpId);

// Resuelve la lista de preguntas concreta para una sesión: si modo=fija, devuelve las fijas;
// si modo=pool, toma N de cada dificultad aleatoriamente.
const resolverPreguntas = (evaluacion) => {
  if (!evaluacion) return [];
  const banco = (window.BANCO_PREGUNTAS || []).filter(p => p.competenciaId === evaluacion.competenciaId);
  if (evaluacion.modo === 'fija') {
    return evaluacion.preguntasFijas.map(id => banco.find(b => b.id === id)).filter(Boolean);
  }
  if (evaluacion.modo === 'pool-aleatorio') {
    const out = [];
    Object.entries(evaluacion.poolPorDificultad || {}).forEach(([dif, n]) => {
      const cands = banco.filter(b => b.dificultad === dif).sort(() => Math.random() - 0.5);
      out.push(...cands.slice(0, n));
    });
    return out;
  }
  return [];
};
window.resolverPreguntas = resolverPreguntas;

// ─── Export al window ────────────────────────────────────

window.PROVEEDORES = PROVEEDORES;
window.INSTRUCTORES = INSTRUCTORES;
window.CARGOS = CARGOS;
window.MATRIZ_CARGO_COMP = MATRIZ_CARGO_COMP;
window.getProveedor = getProveedor;
window.getInstructor = getInstructor;
window.getCargo = getCargo;
window.getCargoByNombre = getCargoByNombre;
window.competenciasPorCargo = competenciasPorCargo;
window.cargosPorCompetencia = cargosPorCompetencia;
window.licenciaInstructorVigente = licenciaInstructorVigente;

// ════════════════════════════════════════════════════════════════════
// ─── BENEFICIOS CORPORATIVOS · datasets + helpers ───────────────────
// Consumido por: components/benefits.jsx (vista admin + colaborador).
// Convención: nombres realistas para Atica, sede Cali.
// El wallet de puntos vive como mock interno y no toca remuneración real.
// ════════════════════════════════════════════════════════════════════

// Valores corporativos de Atica — taxonomía única de reconocimientos.
const VALORES_AGUILA = [
  { id: 'val-calidad',     nombre: 'Calidad',                  desc: 'Hacemos las cosas bien desde la primera vez.',         emoji: '🏆', tone: 'amber'  },
  { id: 'val-compromiso',  nombre: 'Compromiso',               desc: 'Cumplimos lo que prometemos al cliente y al equipo.',  emoji: '🤝', tone: 'navy'   },
  { id: 'val-equipo',      nombre: 'Trabajo en equipo',        desc: 'Sumamos talentos para entregar más juntos.',           emoji: '👥', tone: 'blue'   },
  { id: 'val-responsable', nombre: 'Responsabilidad',          desc: 'Cuidamos los recursos, los procesos y a las personas.', emoji: '🛡️', tone: 'green'  },
  { id: 'val-pasion',      nombre: 'Ingenio que transforma',   desc: 'Transformamos los residuos en valor con creatividad y orgullo.', emoji: '♻️', tone: 'rose'   },
];

// Categorías del catálogo de beneficios.
const BENEFICIOS_CATEGORIAS = [
  { id: 'cat-salud',      nombre: 'Salud',           tone: 'rose',   emoji: '🩺', desc: 'Planes médicos complementarios, exámenes y telemedicina.' },
  { id: 'cat-alimento',   nombre: 'Alimentación',    tone: 'amber',  emoji: '🥗', desc: 'Bonos, mercados, casino subsidiado y café y snacks ilimitados.' },
  { id: 'cat-financiero', nombre: 'Financiero',      tone: 'green',  emoji: '💰', desc: 'Auxilios, créditos blandos, fondos de empleados.' },
  { id: 'cat-bienestar',  nombre: 'Bienestar',       tone: 'blue',   emoji: '🧘', desc: 'Gimnasio, salud mental, yoga, jornadas de pausa activa.' },
  { id: 'cat-familia',    nombre: 'Familia',         tone: 'navy',   emoji: '👨‍👩‍👧', desc: 'Bonos por nacimiento, escolares, día de la familia.' },
  { id: 'cat-educacion',  nombre: 'Educación',       tone: 'blue',   emoji: '🎓', desc: 'Auxilios SENA / EAFIT, idiomas, gestión ambiental avanzada.' },
  { id: 'cat-tiempo',     nombre: 'Tiempo libre',    tone: 'green',  emoji: '🌴', desc: 'Día libre cumpleaños, viernes corto, días simbólicos.' },
  { id: 'cat-reconoce',   nombre: 'Reconocimiento',  tone: 'amber',  emoji: '🎉', desc: 'Premios, badges, kit Atica, viaje aniversario.' },
  { id: 'cat-convenios',  nombre: 'Convenios',       tone: 'rose',   emoji: '🤝', desc: 'Descuentos con cajas de compensación y comercios aliados.' },
];

// Catálogo curado de beneficios (24 ítems · cubre las 9 categorías).
// modalidad: 'mensual' | 'anual' | 'unico' | 'evento'
// trigger:   'solicitud' | 'auto-ingreso' | 'auto-cumple' | 'auto-aniv' | 'auto-ascenso' | 'canje-puntos'
// elegMin:   { antiguedadMeses, niveles, areas, sedes, modalidades }  (todos opcionales · AND lógico)
const BENEFICIOS_CATALOG = [
  // ── Salud ─────────────────────────────────
  { id: 'BEN-001', nombre: 'Plan complementario salud Sura',           categoriaId: 'cat-salud',     emoji: '🩺',
    descripcion: 'Cobertura médica adicional al POS para ti y tu núcleo familiar (cónyuge + 2 hijos).',
    proveedor: 'ARL Sura', costoEmpresa: 180000, costoEmpleado: 0, puntos: 0, modalidad: 'mensual',
    trigger: 'auto-ingreso', sla: 24, vigencia: 'permanente',
    elegMin: { antiguedadMeses: 0 }, esAutomatico: true, destacado: true, faq: 'La afiliación es inmediata al cierre del contrato. El plan incluye hospitalización, especialistas y medicamentos.' },
  { id: 'BEN-002', nombre: 'Telemedicina 24/7',                         categoriaId: 'cat-salud',     emoji: '📞',
    descripcion: 'Consulta médica virtual sin costo, las 24 horas, con cobertura para todo el grupo familiar.',
    proveedor: 'Doctoralia', costoEmpresa: 18000, costoEmpleado: 0, puntos: 0, modalidad: 'mensual',
    trigger: 'auto-ingreso', sla: 24, vigencia: 'permanente',
    elegMin: { antiguedadMeses: 0 }, esAutomatico: true, faq: 'Descarga la app, autentícate con tu cédula y agenda en menos de 2 minutos.' },
  { id: 'BEN-003', nombre: 'Examen visual y monturas',                  categoriaId: 'cat-salud',     emoji: '👓',
    descripcion: 'Examen optométrico anual + auxilio de $250.000 para monturas o lentes de contacto.',
    proveedor: 'Óptica Colombiana', costoEmpresa: 250000, costoEmpleado: 0, puntos: 0, modalidad: 'anual',
    trigger: 'solicitud', sla: 72, vigencia: 'anual',
    elegMin: { antiguedadMeses: 6 }, faq: 'Solicita el bono al cumplir 6 meses. Lo puedes reclamar en cualquier sede de la red.' },

  // ── Alimentación ──────────────────────────
  { id: 'BEN-004', nombre: 'Mercado mensual Atica',                    categoriaId: 'cat-alimento',  emoji: '🛒',
    descripcion: 'Bono de mercado de $180.000 redimible en Olímpica, Éxito o D1 cada mes.',
    proveedor: 'Sodexo', costoEmpresa: 180000, costoEmpleado: 0, puntos: 0, modalidad: 'mensual',
    trigger: 'auto-ingreso', sla: 0, vigencia: 'mensual',
    elegMin: { antiguedadMeses: 3, niveles: ['operativo','técnico','liderazgo','directivo'] }, esAutomatico: true, destacado: true,
    faq: 'Llega al correo el primer día hábil del mes. Vence al cierre del mes siguiente.' },
  { id: 'BEN-005', nombre: 'Casino subsidiado',                         categoriaId: 'cat-alimento',  emoji: '🍽️',
    descripcion: 'Almuerzo balanceado en casino interno · empleado paga $3.000 · empresa subsidia el 70%.',
    proveedor: 'Casino Atica', costoEmpresa: 7000, costoEmpleado: 3000, puntos: 0, modalidad: 'mensual',
    trigger: 'auto-ingreso', sla: 0, vigencia: 'permanente',
    elegMin: { antiguedadMeses: 0, sedes: ['Cali','Barranquilla'] }, esAutomatico: true,
    faq: 'Disponible en el casino de planta de lunes a sábado · turno almuerzo.' },
  { id: 'BEN-006', nombre: 'Material Atica ilimitado',                     categoriaId: 'cat-alimento',  emoji: '♻️',
    descripcion: 'Acceso libre a la zona de material procesado fresco en cualquier momento del turno.',
    proveedor: 'Atica', costoEmpresa: 25000, costoEmpleado: 0, puntos: 0, modalidad: 'mensual',
    trigger: 'auto-ingreso', sla: 0, vigencia: 'permanente',
    elegMin: { antiguedadMeses: 0 }, esAutomatico: true, faq: 'Es una tradición — toma el material que quieras siempre que lo disfrutes responsablemente.' },

  // ── Financiero ────────────────────────────
  { id: 'BEN-007', nombre: 'Auxilio de transporte adicional',           categoriaId: 'cat-financiero', emoji: '🚌',
    descripcion: 'Complemento de $80.000 al subsidio legal para sedes con difícil acceso.',
    proveedor: 'Atica', costoEmpresa: 80000, costoEmpleado: 0, puntos: 0, modalidad: 'mensual',
    trigger: 'auto-ingreso', sla: 0, vigencia: 'permanente',
    elegMin: { antiguedadMeses: 0, sedes: ['Cali'] }, esAutomatico: true,
    faq: 'Aplica a empleados cuyo salario ≤ 2 SMLV. Se carga en la nómina del mes.' },
  { id: 'BEN-008', nombre: 'Crédito blando vivienda',                   categoriaId: 'cat-financiero', emoji: '🏠',
    descripcion: 'Préstamo hasta $30M con tasa 0,8% mensual y descuento por nómina.',
    proveedor: 'Fondo Empleados Atica', costoEmpresa: 0, costoEmpleado: 0, puntos: 0, modalidad: 'unico',
    trigger: 'solicitud', sla: 168, vigencia: 'evento',
    elegMin: { antiguedadMeses: 24 }, faq: 'Requiere mínimo 2 años de antigüedad y aval del jefe directo. Comité revisa cada quincena.' },
  { id: 'BEN-009', nombre: 'Auxilio funerario',                         categoriaId: 'cat-financiero', emoji: '🕯️',
    descripcion: 'Cubrimiento de $4M para gastos funerarios de cónyuge, hijos o padres.',
    proveedor: 'Mapfre', costoEmpresa: 12000, costoEmpleado: 0, puntos: 0, modalidad: 'mensual',
    trigger: 'auto-ingreso', sla: 24, vigencia: 'permanente',
    elegMin: { antiguedadMeses: 0 }, esAutomatico: true,
    faq: 'Activación inmediata · contactar a RRHH para gestionar el pago directo a la funeraria.' },

  // ── Bienestar ─────────────────────────────
  { id: 'BEN-010', nombre: 'Auxilio gimnasio Smartfit',                 categoriaId: 'cat-bienestar',  emoji: '💪',
    descripcion: 'Reembolso del 60% de la mensualidad de gimnasio o estudio de yoga.',
    proveedor: 'Smartfit Cali', costoEmpresa: 60000, costoEmpleado: 40000, puntos: 0, modalidad: 'mensual',
    trigger: 'solicitud', sla: 48, vigencia: 'mensual',
    elegMin: { antiguedadMeses: 3 }, faq: 'Adjunta recibo del mes anterior · pago llega con la nómina del mes siguiente.' },
  { id: 'BEN-011', nombre: 'Salud mental Compensar',                    categoriaId: 'cat-bienestar',  emoji: '🧠',
    descripcion: '6 sesiones de psicoterapia anuales sin costo · presencial o virtual.',
    proveedor: 'Compensar', costoEmpresa: 480000, costoEmpleado: 0, puntos: 0, modalidad: 'anual',
    trigger: 'solicitud', sla: 24, vigencia: 'anual',
    elegMin: { antiguedadMeses: 0 }, destacado: true,
    faq: 'Confidencial · ni RRHH ni el jefe ven el contenido de las sesiones. Solo se ven asistencias.' },
  { id: 'BEN-012', nombre: 'Pausa activa guiada',                       categoriaId: 'cat-bienestar',  emoji: '🤸',
    descripcion: 'Sesión grupal de 10 min, dos veces al día, en zona de planta. Reduce 32% lesiones ergonómicas.',
    proveedor: 'ARL Sura', costoEmpresa: 0, costoEmpleado: 0, puntos: 0, modalidad: 'mensual',
    trigger: 'auto-ingreso', sla: 0, vigencia: 'permanente',
    elegMin: { antiguedadMeses: 0, areas: ['Operaciones','Aprovechamiento','Mantenimiento','Calidad'] }, esAutomatico: true,
    faq: 'Se realiza a las 9:30 a.m. y 3:30 p.m. en cada línea.' },

  // ── Familia ───────────────────────────────
  { id: 'BEN-013', nombre: 'Bono nacimiento hijo',                      categoriaId: 'cat-familia',    emoji: '👶',
    descripcion: 'Bono único de $800.000 + kit Atica para el bebé.',
    proveedor: 'Atica', costoEmpresa: 800000, costoEmpleado: 0, puntos: 200, modalidad: 'unico',
    trigger: 'solicitud', sla: 72, vigencia: 'evento',
    elegMin: { antiguedadMeses: 6 }, faq: 'Adjunta el registro civil. Pago dentro de la siguiente nómina.' },
  { id: 'BEN-014', nombre: 'Bono escolar hijos',                        categoriaId: 'cat-familia',    emoji: '🎒',
    descripcion: 'Auxilio de $300.000 por hijo para útiles escolares · una vez al año en febrero.',
    proveedor: 'Atica', costoEmpresa: 300000, costoEmpleado: 0, puntos: 0, modalidad: 'anual',
    trigger: 'solicitud', sla: 120, vigencia: 'anual',
    elegMin: { antiguedadMeses: 12 }, faq: 'Hijos entre 5 y 18 años · adjunta certificado de matrícula.' },
  { id: 'BEN-015', nombre: 'Día de la familia',                         categoriaId: 'cat-familia',    emoji: '🎪',
    descripcion: 'Evento anual con juegos, comida y actividades para los hijos en parque temático.',
    proveedor: 'Atica · Comfamiliar', costoEmpresa: 95000, costoEmpleado: 0, puntos: 50, modalidad: 'anual',
    trigger: 'auto-aniv', sla: 0, vigencia: 'anual',
    elegMin: { antiguedadMeses: 3 }, esAutomatico: true,
    faq: 'Una vez al año en septiembre · invitación digital con QR para entrada.' },

  // ── Educación ─────────────────────────────
  { id: 'BEN-016', nombre: 'Auxilio educativo SENA',                    categoriaId: 'cat-educacion',  emoji: '🎓',
    descripcion: 'Cubrimiento del 100% del costo del programa técnico o tecnólogo en SENA.',
    proveedor: 'SENA Regional Valle del Cauca', costoEmpresa: 0, costoEmpleado: 0, puntos: 0, modalidad: 'unico',
    trigger: 'solicitud', sla: 168, vigencia: 'evento',
    elegMin: { antiguedadMeses: 12 }, faq: 'Compromiso de permanencia mínima de 1 año al terminar la formación.' },
  { id: 'BEN-017', nombre: 'Diplomado liderazgo EAFIT',                 categoriaId: 'cat-educacion',  emoji: '📚',
    descripcion: 'Diplomado virtual de 120h en habilidades directivas · auxilio del 80% ($3M).',
    proveedor: 'Universidad EAFIT', costoEmpresa: 3000000, costoEmpleado: 750000, puntos: 0, modalidad: 'unico',
    trigger: 'solicitud', sla: 240, vigencia: 'evento',
    elegMin: { antiguedadMeses: 24, niveles: ['liderazgo','directivo'] },
    faq: 'Pre-aprobación del comité de talento · cohorte semestral.' },
  { id: 'BEN-018', nombre: 'Curso Gestor ambiental avanzado',               categoriaId: 'cat-educacion',  emoji: '♻️',
    descripcion: 'Certificación internacional Specialty Coffee Association · incluye examen.',
    proveedor: 'Gabriel Marín · Consultor ambiental', costoEmpresa: 2400000, costoEmpleado: 0, puntos: 300, modalidad: 'unico',
    trigger: 'solicitud', sla: 240, vigencia: 'evento',
    elegMin: { antiguedadMeses: 24, areas: ['Operaciones','Calidad'] }, destacado: true,
    faq: 'Beneficio insignia para el equipo de clasificación y calidad. 2 cupos por año.' },

  // ── Tiempo libre ──────────────────────────
  { id: 'BEN-019', nombre: 'Día libre cumpleaños',                      categoriaId: 'cat-tiempo',     emoji: '🎂',
    descripcion: 'Día libre remunerado en la semana de tu cumpleaños · no se acumula.',
    proveedor: 'Atica', costoEmpresa: 95000, costoEmpleado: 0, puntos: 100, modalidad: 'anual',
    trigger: 'auto-cumple', sla: 0, vigencia: 'anual',
    elegMin: { antiguedadMeses: 0 }, esAutomatico: true, destacado: true,
    faq: 'Se programa con tu jefe directo dentro de los 7 días alrededor de tu cumple.' },
  { id: 'BEN-020', nombre: 'Viernes corto',                             categoriaId: 'cat-tiempo',     emoji: '🌅',
    descripcion: 'Salida a las 3:00 p.m. los viernes · primer y tercer viernes del mes (admin).',
    proveedor: 'Atica', costoEmpresa: 0, costoEmpleado: 0, puntos: 0, modalidad: 'mensual',
    trigger: 'auto-ingreso', sla: 0, vigencia: 'permanente',
    elegMin: { antiguedadMeses: 0, niveles: ['liderazgo','directivo'], areas: ['RRHH','Compras','Calidad'] }, esAutomatico: true,
    faq: 'Aplica si la operación está al día · sujeto a confirmación del jefe directo el jueves.' },
  { id: 'BEN-021', nombre: 'Día simbólico anual',                       categoriaId: 'cat-tiempo',     emoji: '🌟',
    descripcion: 'Un día libre adicional para mudanza, grado, matrimonio o evento personal.',
    proveedor: 'Atica', costoEmpresa: 95000, costoEmpleado: 0, puntos: 50, modalidad: 'anual',
    trigger: 'solicitud', sla: 48, vigencia: 'anual',
    elegMin: { antiguedadMeses: 12 }, faq: 'Se solicita con 5 días hábiles de anticipación · cualquier motivo válido.' },

  // ── Reconocimiento ────────────────────────
  { id: 'BEN-022', nombre: 'Bono aniversario laboral',                  categoriaId: 'cat-reconoce',   emoji: '🏅',
    descripcion: 'Bono escalonado: $200K (1y) · $500K (3y) · $1M (5y) · $2.5M (10y) · $5M (15y).',
    proveedor: 'Atica', costoEmpresa: 500000, costoEmpleado: 0, puntos: 250, modalidad: 'anual',
    trigger: 'auto-aniv', sla: 0, vigencia: 'evento',
    elegMin: { antiguedadMeses: 12 }, esAutomatico: true, destacado: true,
    faq: 'Se entrega en el mes del aniversario con un comunicado del CEO.' },
  { id: 'BEN-023', nombre: 'Kit Atica ingreso',                        categoriaId: 'cat-reconoce',   emoji: '🎁',
    descripcion: 'Kit de bienvenida: termo, libreta, paquete de material procesado fresco y camiseta.',
    proveedor: 'Atica', costoEmpresa: 95000, costoEmpleado: 0, puntos: 50, modalidad: 'unico',
    trigger: 'auto-ingreso', sla: 0, vigencia: 'evento',
    elegMin: { antiguedadMeses: 0 }, esAutomatico: true,
    faq: 'Se entrega el primer día en la inducción.' },

  // ── Convenios ─────────────────────────────
  { id: 'BEN-024', nombre: 'Convenio Comfamiliar Valle del Cauca',            categoriaId: 'cat-convenios',  emoji: '🤝',
    descripcion: 'Acceso completo a parques, piscinas, biblioteca y descuentos en cursos.',
    proveedor: 'Comfamiliar Valle del Cauca', costoEmpresa: 0, costoEmpleado: 0, puntos: 0, modalidad: 'mensual',
    trigger: 'auto-ingreso', sla: 0, vigencia: 'permanente',
    elegMin: { antiguedadMeses: 0, sedes: ['Cali','Barranquilla'] }, esAutomatico: true,
    faq: 'Activa tu carnet en cualquier sede de Comfamiliar con tu cédula.' },
];

// Matriz cargo × beneficio · regla ∈ {obligatorio, opcional, no_aplica}
// "obligatorio" → se asigna automáticamente al ingreso del cargo.
// "opcional"    → aparece en el catálogo del colaborador y puede solicitarlo.
// "no_aplica"   → no se muestra al colaborador.
// Default global: si no hay entrada, los beneficios universales (ver BEN-006, BEN-019, BEN-009) se consideran "opcional".
const MATRIZ_BENEFICIOS_CARGO = (() => {
  const rows = [];
  const todosLosCargos = CARGOS.map(c => c.id);
  // Beneficios universales obligatorios (todos los cargos)
  ['BEN-001','BEN-002','BEN-006','BEN-009','BEN-019','BEN-023','BEN-024'].forEach(b => {
    todosLosCargos.forEach(c => rows.push({ cargoId: c, beneficioId: b, regla: 'obligatorio' }));
  });
  // Mercado mensual · todos menos voluntarios
  todosLosCargos.filter(c => c !== 'CGO-010').forEach(c => rows.push({ cargoId: c, beneficioId: 'BEN-004', regla: 'obligatorio' }));
  // Casino · solo Cali y Barranquilla (todos cargos operativos/liderazgo)
  todosLosCargos.forEach(c => rows.push({ cargoId: c, beneficioId: 'BEN-005', regla: 'obligatorio' }));
  // Auxilio transporte · cargos operativos en Cali
  ['CGO-001','CGO-002','CGO-003','CGO-004','CGO-005','CGO-006'].forEach(c => rows.push({ cargoId: c, beneficioId: 'BEN-007', regla: 'obligatorio' }));
  // Pausas activas · solo planta
  ['CGO-001','CGO-002','CGO-003','CGO-004','CGO-005','CGO-006'].forEach(c => rows.push({ cargoId: c, beneficioId: 'BEN-012', regla: 'obligatorio' }));
  // Aniversario · todos
  todosLosCargos.forEach(c => rows.push({ cargoId: c, beneficioId: 'BEN-022', regla: 'obligatorio' }));
  // Viernes corto · solo administrativos/líderes
  ['CGO-007','CGO-008'].forEach(c => rows.push({ cargoId: c, beneficioId: 'BEN-020', regla: 'obligatorio' }));

  // Opcionales (aparecen en el catálogo del colaborador)
  todosLosCargos.forEach(c => rows.push({ cargoId: c, beneficioId: 'BEN-003', regla: 'opcional' })); // visual
  todosLosCargos.forEach(c => rows.push({ cargoId: c, beneficioId: 'BEN-008', regla: 'opcional' })); // crédito vivienda
  todosLosCargos.forEach(c => rows.push({ cargoId: c, beneficioId: 'BEN-010', regla: 'opcional' })); // gimnasio
  todosLosCargos.forEach(c => rows.push({ cargoId: c, beneficioId: 'BEN-011', regla: 'opcional' })); // salud mental
  todosLosCargos.forEach(c => rows.push({ cargoId: c, beneficioId: 'BEN-013', regla: 'opcional' })); // bono nacimiento
  todosLosCargos.forEach(c => rows.push({ cargoId: c, beneficioId: 'BEN-014', regla: 'opcional' })); // bono escolar
  todosLosCargos.forEach(c => rows.push({ cargoId: c, beneficioId: 'BEN-015', regla: 'opcional' })); // día familia
  todosLosCargos.forEach(c => rows.push({ cargoId: c, beneficioId: 'BEN-016', regla: 'opcional' })); // SENA
  todosLosCargos.forEach(c => rows.push({ cargoId: c, beneficioId: 'BEN-021', regla: 'opcional' })); // día simbólico
  // Liderazgo y directivo · diplomado EAFIT
  ['CGO-004','CGO-007','CGO-008'].forEach(c => rows.push({ cargoId: c, beneficioId: 'BEN-017', regla: 'opcional' }));
  // Operaciones + Calidad · Gestor ambiental
  ['CGO-001','CGO-005'].forEach(c => rows.push({ cargoId: c, beneficioId: 'BEN-018', regla: 'opcional' }));

  return rows;
})();

// Empleados de referencia para mocks de wallet, solicitudes y reconocimientos.
// Reusa los IDs y nombres que ya viven en training.jsx (PLANS) para coherencia.
const BENEFICIOS_EMPLEADOS_DEMO = [
  { id: 'E0247', nombre: 'Andrés Felipe Mejía',  iniciales: 'AM', cargoId: 'CGO-001', area: 'Operaciones',     sede: 'Cali',    fechaIngreso: '02/05/2026', fechaNacimiento: '14/05/1993' },
  { id: 'E0246', nombre: 'Laura Gómez Restrepo', iniciales: 'LG', cargoId: 'CGO-002', area: 'Operaciones',     sede: 'Cali',    fechaIngreso: '15/03/2024', fechaNacimiento: '08/05/1989' },
  { id: 'E0205', nombre: 'Carlos Andrés Vélez',  iniciales: 'CV', cargoId: 'CGO-001', area: 'Operaciones',     sede: 'Cali',    fechaIngreso: '12/08/2022', fechaNacimiento: '21/02/1991' },
  { id: 'E0198', nombre: 'María Fernanda López', iniciales: 'MF', cargoId: 'CGO-003', area: 'Aprovechamiento',        sede: 'Cali',    fechaIngreso: '04/06/2023', fechaNacimiento: '18/05/1995' },
  { id: 'E0177', nombre: 'Jorge Iván Patiño',    iniciales: 'JP', cargoId: 'CGO-009', area: 'Logística',      sede: 'Barranquilla',  fechaIngreso: '20/01/2020', fechaNacimiento: '03/11/1985' },
  { id: 'E0156', nombre: 'Paula Valencia Ríos',  iniciales: 'PV', cargoId: 'CGO-006', area: 'Mantenimiento',  sede: 'Cali',    fechaIngreso: '08/09/2021', fechaNacimiento: '12/07/1988' },
  { id: 'E0142', nombre: 'Diego Andrés Toro',    iniciales: 'DT', cargoId: 'CGO-003', area: 'Aprovechamiento',        sede: 'Cali',    fechaIngreso: '14/02/2024', fechaNacimiento: '06/05/1996' },
  { id: 'E0128', nombre: 'Nicolás Cárdenas',     iniciales: 'NC', cargoId: 'CGO-009', area: 'Logística',      sede: 'Barranquilla',  fechaIngreso: '22/10/2019', fechaNacimiento: '15/12/1990' },
  { id: 'E0094', nombre: 'María Trujillo Henao', iniciales: 'MT', cargoId: 'CGO-008', area: 'RRHH',           sede: 'Cali',    fechaIngreso: '10/03/2018', fechaNacimiento: '28/06/1986' },
  { id: 'E0067', nombre: 'Luis Miguel Marín',    iniciales: 'LM', cargoId: 'CGO-004', area: 'Aprovechamiento',        sede: 'Cali',    fechaIngreso: '14/04/2017', fechaNacimiento: '02/05/1984' },
  { id: 'E0023', nombre: 'Carolina Restrepo S.', iniciales: 'CR', cargoId: 'CGO-008', area: 'RRHH',           sede: 'Cali',    fechaIngreso: '01/02/2015', fechaNacimiento: '19/09/1980' },
  { id: 'E0312', nombre: 'Lina Tobón Cárdenas',  iniciales: 'LT', cargoId: 'CGO-001', area: 'Operaciones',     sede: 'Cali',    fechaIngreso: '08/11/2022', fechaNacimiento: '06/05/1994' },
];

// Convenios externos (descuentos en aliados).
const CONVENIOS = [
  { id: 'CNV-001', nombre: 'Comfamiliar Valle del Cauca', categoriaId: 'cat-convenios', emoji: '🏞️', descuento: '100% acceso parques · cursos al 50%',           vigencia: '31/12/2026' },
  { id: 'CNV-002', nombre: 'Smartfit Cali',      categoriaId: 'cat-bienestar', emoji: '💪', descuento: '40% mensualidad · 2 cuotas sin interés',         vigencia: '31/12/2026' },
  { id: 'CNV-003', nombre: 'Falabella',             categoriaId: 'cat-financiero', emoji: '🛍️', descuento: '15% en electrodomésticos línea blanca',         vigencia: '30/06/2026' },
  { id: 'CNV-004', nombre: 'Olímpica Supermercados',categoriaId: 'cat-alimento',  emoji: '🛒', descuento: '5% en mercado · día Atica los miércoles',       vigencia: 'permanente' },
  { id: 'CNV-005', nombre: 'CinéColombia Cali',  categoriaId: 'cat-tiempo',    emoji: '🎬', descuento: '2x1 lunes a jueves · descuento crispetas 25%',   vigencia: 'permanente' },
  { id: 'CNV-006', nombre: 'Compensar EPS',         categoriaId: 'cat-salud',     emoji: '🩺', descuento: 'Plan complementario al 30% del valor habitual',   vigencia: 'permanente' },
  { id: 'CNV-007', nombre: 'EAFIT Educación Cont.', categoriaId: 'cat-educacion', emoji: '🎓', descuento: '20% en diplomados virtuales',                     vigencia: '31/12/2026' },
  { id: 'CNV-008', nombre: 'Ópticas GMO',           categoriaId: 'cat-salud',     emoji: '👓', descuento: '30% en lentes y monturas · examen sin costo',     vigencia: 'permanente' },
];

// Wallet de puntos · saldo + últimas transacciones por colaborador (mock).
const WALLET_PUNTOS = {
  E0247: { balance: 250,  total: 250,  transacciones: [
    { fecha: '02/05/2026', tipo: 'credito', monto: 50,  motivo: 'Kit Atica ingreso · BEN-023' },
    { fecha: '02/05/2026', tipo: 'credito', monto: 200, motivo: 'Bono nacimiento hijo · BEN-013' },
  ]},
  E0246: { balance: 1820, total: 2520, transacciones: [
    { fecha: '15/04/2026', tipo: 'credito', monto: 250, motivo: 'Aniversario 2 años · BEN-022' },
    { fecha: '20/04/2026', tipo: 'debito',  monto: 700, motivo: 'Canje · Voucher Falabella $50.000' },
    { fecha: '02/05/2026', tipo: 'credito', monto: 100, motivo: 'Reconocimiento de Carolina · Calidad' },
  ]},
  E0205: { balance: 940,  total: 1340, transacciones: [
    { fecha: '12/04/2026', tipo: 'credito', monto: 300, motivo: 'Gestor ambiental avanzado ambiental · BEN-018' },
    { fecha: '21/02/2026', tipo: 'credito', monto: 100, motivo: 'Día libre cumpleaños · BEN-019' },
    { fecha: '03/03/2026', tipo: 'debito',  monto: 400, motivo: 'Canje · Día simbólico extra' },
    { fecha: '18/04/2026', tipo: 'credito', monto: 150, motivo: 'Reconocimiento de Luis · Ingenio que transforma' },
  ]},
  E0198: { balance: 380,  total: 580,  transacciones: [
    { fecha: '18/05/2026', tipo: 'credito', monto: 100, motivo: 'Día libre cumpleaños · BEN-019' },
    { fecha: '02/05/2026', tipo: 'debito',  monto: 200, motivo: 'Canje · Bono Olímpica $30.000' },
    { fecha: '04/06/2026', tipo: 'credito', monto: 250, motivo: 'Aniversario 3 años · BEN-022' },
  ]},
  E0177: { balance: 2100, total: 3100, transacciones: [
    { fecha: '03/11/2025', tipo: 'credito', monto: 100, motivo: 'Día libre cumpleaños · BEN-019' },
    { fecha: '20/01/2026', tipo: 'credito', monto: 1000,motivo: 'Aniversario 6 años · BEN-022' },
    { fecha: '15/03/2026', tipo: 'debito',  monto: 700, motivo: 'Canje · Vale CinéColombia familiar' },
  ]},
  E0156: { balance: 1480, total: 2480, transacciones: [
    { fecha: '08/09/2025', tipo: 'credito', monto: 1000,motivo: 'Aniversario 4 años · BEN-022' },
    { fecha: '12/07/2026', tipo: 'credito', monto: 100, motivo: 'Día libre cumpleaños · BEN-019' },
    { fecha: '02/05/2026', tipo: 'debito',  monto: 1000,motivo: 'Canje · Auxilio gimnasio anual' },
  ]},
  E0142: { balance: 320,  total: 470,  transacciones: [
    { fecha: '14/02/2026', tipo: 'credito', monto: 250, motivo: 'Aniversario 2 años · BEN-022' },
    { fecha: '06/05/2026', tipo: 'credito', monto: 100, motivo: 'Día libre cumpleaños · BEN-019' },
    { fecha: '20/04/2026', tipo: 'debito',  monto: 30,  motivo: 'Canje · Pase doble cine' },
  ]},
  E0128: { balance: 540,  total: 540,  transacciones: [
    { fecha: '15/12/2025', tipo: 'credito', monto: 100, motivo: 'Día libre cumpleaños · BEN-019' },
    { fecha: '22/10/2025', tipo: 'credito', monto: 250, motivo: 'Aniversario 6 años · BEN-022' },
    { fecha: '02/05/2026', tipo: 'credito', monto: 190, motivo: 'Reconocimiento de Carolina · Compromiso' },
  ]},
  E0094: { balance: 1240, total: 2440, transacciones: [
    { fecha: '10/03/2026', tipo: 'credito', monto: 500, motivo: 'Aniversario 8 años · BEN-022' },
    { fecha: '28/06/2026', tipo: 'credito', monto: 100, motivo: 'Día libre cumpleaños · BEN-019' },
    { fecha: '15/04/2026', tipo: 'debito',  monto: 600, motivo: 'Canje · Mercado adicional Olímpica' },
  ]},
  E0067: { balance: 2480, total: 4480, transacciones: [
    { fecha: '14/04/2026', tipo: 'credito', monto: 2500,motivo: 'Aniversario 10 años · BEN-022' },
    { fecha: '02/05/2026', tipo: 'credito', monto: 100, motivo: 'Día libre cumpleaños · BEN-019' },
    { fecha: '12/04/2026', tipo: 'debito',  monto: 1000,motivo: 'Canje · Voucher EAFIT' },
  ]},
  E0023: { balance: 3120, total: 5120, transacciones: [
    { fecha: '01/02/2026', tipo: 'credito', monto: 5000,motivo: 'Aniversario 11 años · BEN-022' },
    { fecha: '19/09/2025', tipo: 'credito', monto: 100, motivo: 'Día libre cumpleaños · BEN-019' },
    { fecha: '15/03/2026', tipo: 'debito',  monto: 1500,motivo: 'Canje · Paquete bienestar Smartfit' },
  ]},
  E0312: { balance: 410,  total: 660,  transacciones: [
    { fecha: '08/11/2025', tipo: 'credito', monto: 250, motivo: 'Aniversario 3 años · BEN-022' },
    { fecha: '06/05/2026', tipo: 'credito', monto: 100, motivo: 'Día libre cumpleaños · BEN-019' },
    { fecha: '20/04/2026', tipo: 'credito', monto: 60,  motivo: 'Reconocimiento de Carolina · Calidad' },
  ]},
};

// Badges (gamificación) y asignaciones por empleado.
const BADGES = [
  { id: 'BDG-cumpleaños',  nombre: 'Cumpleañero Atica',         emoji: '🎂', descripcion: 'Disfrutaste tu día de cumpleaños este año.',           nivel: 'común',       puntos: 50  },
  { id: 'BDG-1y',          nombre: '1 año con la familia',        emoji: '🏅', descripcion: 'Cumpliste tu primer aniversario laboral.',             nivel: 'común',       puntos: 100 },
  { id: 'BDG-3y',          nombre: '3 años con la familia',       emoji: '🥉', descripcion: 'Tres años aportando a Atica.',                    nivel: 'plata',       puntos: 250 },
  { id: 'BDG-5y',          nombre: '5 años con la familia',       emoji: '🥈', descripcion: 'Lustro Atica · medalla plateada.',                     nivel: 'oro',         puntos: 500 },
  { id: 'BDG-10y',         nombre: '10 años con la familia',      emoji: '🏆', descripcion: 'Una década en Atica · medalla dorada.',                nivel: 'oro',         puntos: 1000},
  { id: 'BDG-pasion',      nombre: 'Ingenio que transforma',          emoji: '♻️', descripcion: 'Recibiste 3+ reconocimientos en ingenio que transforma.',  nivel: 'oro',         puntos: 200 },
  { id: 'BDG-equipo',      nombre: 'Compañero del mes',           emoji: '👥', descripcion: 'Reconocido por liderar un equipo este mes.',            nivel: 'plata',       puntos: 150 },
  { id: 'BDG-q-grader',    nombre: 'Maestro operario de planta',            emoji: '🎖️', descripcion: 'Certificación Gestor ambiental ambiental aprobada.',                  nivel: 'leyenda',     puntos: 500 },
  { id: 'BDG-bienestar',   nombre: 'Mente sana, material fresco',     emoji: '🧘', descripcion: 'Completaste el reto Mes del Bienestar.',                nivel: 'común',       puntos: 80  },
  { id: 'BDG-crecimiento', nombre: 'En crecimiento',              emoji: '📈', descripcion: 'Asistencia a 5+ formaciones en el año.',                nivel: 'plata',       puntos: 120 },
];

const EMPLEADO_BADGES = [
  { empleadoId: 'E0247', badgeId: 'BDG-1y',         fecha: '02/05/2026' },
  { empleadoId: 'E0205', badgeId: 'BDG-q-grader',   fecha: '12/04/2026' },
  { empleadoId: 'E0205', badgeId: 'BDG-3y',         fecha: '12/08/2025' },
  { empleadoId: 'E0205', badgeId: 'BDG-pasion',     fecha: '18/04/2026' },
  { empleadoId: 'E0177', badgeId: 'BDG-5y',         fecha: '20/01/2025' },
  { empleadoId: 'E0156', badgeId: 'BDG-3y',         fecha: '08/09/2024' },
  { empleadoId: 'E0156', badgeId: 'BDG-bienestar',  fecha: '20/03/2026' },
  { empleadoId: 'E0094', badgeId: 'BDG-5y',         fecha: '10/03/2023' },
  { empleadoId: 'E0094', badgeId: 'BDG-equipo',     fecha: '02/04/2026' },
  { empleadoId: 'E0067', badgeId: 'BDG-10y',        fecha: '14/04/2027' },
  { empleadoId: 'E0067', badgeId: 'BDG-equipo',     fecha: '02/05/2026' },
  { empleadoId: 'E0023', badgeId: 'BDG-10y',        fecha: '01/02/2025' },
  { empleadoId: 'E0023', badgeId: 'BDG-crecimiento',fecha: '12/04/2026' },
  { empleadoId: 'E0312', badgeId: 'BDG-1y',         fecha: '08/11/2023' },
  { empleadoId: 'E0312', badgeId: 'BDG-3y',         fecha: '08/11/2025' },
];

// Solicitudes de beneficios · estados: pendiente | aprobada | rechazada | entregada | vencida
const BENEFICIOS_SOLICITUDES = [
  { id: 'SOL-001', empleadoId: 'E0246', beneficioId: 'BEN-010', estado: 'pendiente',   fechaSolicitud: '04/05/2026 09:14', fechaResolucion: null,             aprobador: null,                       comentario: 'Solicito reembolso del mes de abril en Smartfit. Adjunto recibo.', vencimientoSLA: '06/05/2026 09:14' },
  { id: 'SOL-002', empleadoId: 'E0156', beneficioId: 'BEN-014', estado: 'pendiente',   fechaSolicitud: '03/05/2026 14:32', fechaResolucion: null,             aprobador: null,                       comentario: 'Útiles para mi hijo Mateo (8 años) · grado 3°.',                  vencimientoSLA: '08/05/2026 14:32' },
  { id: 'SOL-003', empleadoId: 'E0205', beneficioId: 'BEN-018', estado: 'aprobada',    fechaSolicitud: '01/04/2026 10:00', fechaResolucion: '03/04/2026 16:20', aprobador: 'Carolina Restrepo S.',     comentario: 'Aprobado para cohorte de mayo · cupo asegurado.',                   vencimientoSLA: '11/04/2026 10:00' },
  { id: 'SOL-004', empleadoId: 'E0094', beneficioId: 'BEN-017', estado: 'aprobada',    fechaSolicitud: '15/03/2026 11:00', fechaResolucion: '20/03/2026 09:45', aprobador: 'Carolina Restrepo S.',     comentario: 'Auxilio diplomado liderazgo EAFIT · cohorte septiembre.',          vencimientoSLA: '25/03/2026 11:00' },
  { id: 'SOL-005', empleadoId: 'E0142', beneficioId: 'BEN-021', estado: 'rechazada',   fechaSolicitud: '28/04/2026 08:30', fechaResolucion: '30/04/2026 10:00', aprobador: 'Luis Miguel Marín',        comentario: 'Solicitado fuera de plazo (5 días hábiles). Reagendar para junio.', vencimientoSLA: '30/04/2026 08:30' },
  { id: 'SOL-006', empleadoId: 'E0247', beneficioId: 'BEN-013', estado: 'aprobada',    fechaSolicitud: '02/05/2026 14:00', fechaResolucion: '02/05/2026 16:42', aprobador: 'Carolina Restrepo S.',     comentario: 'Bono nacimiento aprobado · pago en nómina del 15/05.',              vencimientoSLA: '05/05/2026 14:00' },
  { id: 'SOL-007', empleadoId: 'E0067', beneficioId: 'BEN-008', estado: 'pendiente',   fechaSolicitud: '01/05/2026 16:00', fechaResolucion: null,             aprobador: null,                       comentario: 'Crédito vivienda · 25M para entrada apartamento. Adjunto avalúo.',  vencimientoSLA: '08/05/2026 16:00' },
  { id: 'SOL-008', empleadoId: 'E0177', beneficioId: 'BEN-016', estado: 'aprobada',    fechaSolicitud: '10/04/2026 09:00', fechaResolucion: '17/04/2026 11:30', aprobador: 'Carolina Restrepo S.',     comentario: 'Tecnólogo en logística SENA · aprobado con compromiso de permanencia 1y.', vencimientoSLA: '17/04/2026 09:00' },
  { id: 'SOL-009', empleadoId: 'E0312', beneficioId: 'BEN-021', estado: 'pendiente',   fechaSolicitud: '05/05/2026 07:45', fechaResolucion: null,             aprobador: null,                       comentario: 'Día simbólico para el grado de mi hermana · 14 de mayo.',           vencimientoSLA: '07/05/2026 07:45' },
  { id: 'SOL-010', empleadoId: 'E0198', beneficioId: 'BEN-010', estado: 'aprobada',    fechaSolicitud: '02/04/2026 12:00', fechaResolucion: '04/04/2026 09:30', aprobador: 'Luis Miguel Marín',        comentario: 'Reembolso gimnasio marzo aprobado.',                                vencimientoSLA: '04/04/2026 12:00' },
  { id: 'SOL-011', empleadoId: 'E0205', beneficioId: 'BEN-021', estado: 'entregada',   fechaSolicitud: '15/02/2026 10:00', fechaResolucion: '16/02/2026 14:00', aprobador: 'Carolina Restrepo S.',     comentario: 'Día simbólico tomado el 03/03 · matrimonio del hermano.',           vencimientoSLA: '17/02/2026 10:00' },
  { id: 'SOL-012', empleadoId: 'E0246', beneficioId: 'BEN-014', estado: 'pendiente',   fechaSolicitud: '04/05/2026 16:30', fechaResolucion: null,             aprobador: null,                       comentario: 'Útiles para mis dos hijas (5 y 9 años).',                            vencimientoSLA: '09/05/2026 16:30' },
  { id: 'SOL-013', empleadoId: 'E0156', beneficioId: 'BEN-011', estado: 'pendiente',   fechaSolicitud: '05/05/2026 11:20', fechaResolucion: null,             aprobador: null,                       comentario: 'Solicito iniciar terapia · primer mes de las 6 sesiones anuales.', vencimientoSLA: '06/05/2026 11:20' },
  { id: 'SOL-014', empleadoId: 'E0023', beneficioId: 'BEN-017', estado: 'aprobada',    fechaSolicitud: '20/03/2026 08:00', fechaResolucion: '25/03/2026 11:00', aprobador: 'Sebastián Ruiz · CEO',     comentario: 'Diplomado EAFIT · cohorte agosto. Aprobado por gerencia.',          vencimientoSLA: '30/03/2026 08:00' },
];

// Campañas de bienestar (calendario corporativo).
const CAMPANAS_BIENESTAR = [
  { id: 'CAM-001', nombre: 'Mes de la salud cardíaca',       emoji: '❤️',
    fechaInicio: '01/05/2026', fechaFin: '31/05/2026', estado: 'activa',
    audiencia: 'Toda la operación', presupuesto: 4500000, beneficiosAsociados: ['BEN-001','BEN-011','BEN-012'],
    descripcion: 'Charlas con cardiólogo · jornadas de toma de presión · auxilio para chequeo cardiovascular.',
    kpis: { participacion: 184, meta: 240, satisfaccion: 91 } },
  { id: 'CAM-002', nombre: 'Semana del libro Atica',         emoji: '📚',
    fechaInicio: '23/04/2026', fechaFin: '30/04/2026', estado: 'finalizada',
    audiencia: 'Toda la operación', presupuesto: 1200000, beneficiosAsociados: ['BEN-016','BEN-017'],
    descripcion: 'Trueque de libros · charla con Gestor ambiental · descuentos en librería Comfamiliar.',
    kpis: { participacion: 312, meta: 280, satisfaccion: 88 } },
  { id: 'CAM-003', nombre: 'Octubre rosa',                    emoji: '🎀',
    fechaInicio: '01/10/2026', fechaFin: '31/10/2026', estado: 'planificada',
    audiencia: 'Mujeres + sensibilización general', presupuesto: 2800000, beneficiosAsociados: ['BEN-001','BEN-002','BEN-003'],
    descripcion: 'Tamizaje · taller de autocuidado · jornada de mamografía con Compensar.',
    kpis: { participacion: 0, meta: 180, satisfaccion: null } },
  { id: 'CAM-004', nombre: 'Finanzas personales en familia',  emoji: '💵',
    fechaInicio: '15/05/2026', fechaFin: '15/06/2026', estado: 'activa',
    audiencia: 'Liderazgo + administrativos', presupuesto: 1600000, beneficiosAsociados: ['BEN-008','BEN-014','BEN-016'],
    descripcion: 'Talleres con caja de compensación · simuladores de ahorro · feria de servicios financieros.',
    kpis: { participacion: 42, meta: 90, satisfaccion: 84 } },
  { id: 'CAM-005', nombre: 'Día de la familia 2026',          emoji: '🎪',
    fechaInicio: '05/09/2026', fechaFin: '05/09/2026', estado: 'planificada',
    audiencia: 'Toda la familia Atica', presupuesto: 18500000, beneficiosAsociados: ['BEN-015'],
    descripcion: 'Evento anual en Comfamiliar · juegos, almuerzo, conciertos infantiles.',
    kpis: { participacion: 0, meta: 1200, satisfaccion: null } },
  { id: 'CAM-006', nombre: 'Reto Septiembre activo',          emoji: '🏃',
    fechaInicio: '01/09/2026', fechaFin: '30/09/2026', estado: 'planificada',
    audiencia: 'Toda la operación', presupuesto: 950000, beneficiosAsociados: ['BEN-010','BEN-012'],
    descripcion: 'Conteo de pasos por equipos · premios para los 3 primeros · jornadas de yoga.',
    kpis: { participacion: 0, meta: 350, satisfaccion: null } },
];

// ─── Helpers Beneficios ───────────────────────────────────

// Calcula meses de antigüedad desde fechaIngreso (DD/MM/YYYY) hasta hoy.
const mesesAntiguedad = (fechaIngresoCO, hoy = _DP_TODAY) => {
  const d = parseDateCO(fechaIngresoCO);
  if (!d) return 0;
  return (hoy.getFullYear() - d.getFullYear()) * 12 + (hoy.getMonth() - d.getMonth())
    - (hoy.getDate() < d.getDate() ? 1 : 0);
};

// ¿El empleado es elegible para este beneficio?
// Combina la matriz cargo↔beneficio con las reglas de elegMin del catálogo.
const esElegibleBeneficio = (empleado, beneficio) => {
  if (!empleado || !beneficio) return false;
  // 1. Matriz: si el cargo tiene regla 'no_aplica' → no elegible
  const reglaMatriz = MATRIZ_BENEFICIOS_CARGO.find(m => m.cargoId === empleado.cargoId && m.beneficioId === beneficio.id);
  if (reglaMatriz && reglaMatriz.regla === 'no_aplica') return false;
  if (!reglaMatriz) return false; // no listado en matriz para ese cargo

  // 2. Reglas adicionales del beneficio
  const e = beneficio.elegMin || {};
  const cargo = getCargo(empleado.cargoId);
  const meses = mesesAntiguedad(empleado.fechaIngreso);

  if (e.antiguedadMeses != null && meses < e.antiguedadMeses) return false;
  if (e.niveles && cargo && !e.niveles.includes(cargo.nivel))   return false;
  if (e.areas   && !e.areas.includes(empleado.area))            return false;
  if (e.sedes   && !e.sedes.includes(empleado.sede))            return false;
  return true;
};

// Días hasta el próximo cumpleaños (positivo · si es hoy → 0).
const diasHastaCumple = (fechaNacCO, hoy = _DP_TODAY) => {
  const n = parseDateCO(fechaNacCO);
  if (!n) return null;
  const next = new Date(hoy.getFullYear(), n.getMonth(), n.getDate());
  if (next < new Date(hoy.getFullYear(), hoy.getMonth(), hoy.getDate())) {
    next.setFullYear(hoy.getFullYear() + 1);
  }
  return Math.round((next - new Date(hoy.getFullYear(), hoy.getMonth(), hoy.getDate())) / 86400000);
};

// Días hasta el próximo aniversario (positivo).
const diasHastaAniversario = (fechaIngresoCO, hoy = _DP_TODAY) => {
  const i = parseDateCO(fechaIngresoCO);
  if (!i) return null;
  const next = new Date(hoy.getFullYear(), i.getMonth(), i.getDate());
  if (next < new Date(hoy.getFullYear(), hoy.getMonth(), hoy.getDate())) {
    next.setFullYear(hoy.getFullYear() + 1);
  }
  return Math.round((next - new Date(hoy.getFullYear(), hoy.getMonth(), hoy.getDate())) / 86400000);
};

// Eventos de calendario de beneficios para los próximos N días.
const calcularEventosCalendario = (dias = 30, hoy = _DP_TODAY) => {
  const eventos = [];
  BENEFICIOS_EMPLEADOS_DEMO.forEach(emp => {
    const dCump = diasHastaCumple(emp.fechaNacimiento, hoy);
    if (dCump != null && dCump <= dias) {
      eventos.push({ tipo: 'cumple',     empleadoId: emp.id, dias: dCump, beneficioId: 'BEN-019' });
    }
    const dAniv = diasHastaAniversario(emp.fechaIngreso, hoy);
    if (dAniv != null && dAniv <= dias) {
      const anios = hoy.getFullYear() - parseDateCO(emp.fechaIngreso).getFullYear();
      eventos.push({ tipo: 'aniversario', empleadoId: emp.id, dias: dAniv, anios, beneficioId: 'BEN-022' });
    }
  });
  return eventos.sort((a, b) => a.dias - b.dias);
};

// Helpers de lookup
const getBeneficio    = (id) => BENEFICIOS_CATALOG.find(b => b.id === id);
const getCategoria    = (id) => BENEFICIOS_CATEGORIAS.find(c => c.id === id);
const getValor        = (id) => VALORES_AGUILA.find(v => v.id === id);
const getBadge        = (id) => BADGES.find(b => b.id === id);
const getEmpleadoBen  = (id) => BENEFICIOS_EMPLEADOS_DEMO.find(e => e.id === id);
const beneficiosDelCargo = (cargoId) => MATRIZ_BENEFICIOS_CARGO.filter(m => m.cargoId === cargoId);
const badgesDeEmpleado = (empleadoId) => EMPLEADO_BADGES
  .filter(eb => eb.empleadoId === empleadoId)
  .map(eb => ({ ...getBadge(eb.badgeId), fecha: eb.fecha }))
  .filter(b => b.id);

// Export al window
window.VALORES_AGUILA          = VALORES_AGUILA;
window.BENEFICIOS_CATEGORIAS   = BENEFICIOS_CATEGORIAS;
window.BENEFICIOS_CATALOG      = BENEFICIOS_CATALOG;
window.MATRIZ_BENEFICIOS_CARGO = MATRIZ_BENEFICIOS_CARGO;
window.BENEFICIOS_EMPLEADOS_DEMO = BENEFICIOS_EMPLEADOS_DEMO;
window.CONVENIOS               = CONVENIOS;
window.WALLET_PUNTOS           = WALLET_PUNTOS;
window.BADGES                  = BADGES;
window.EMPLEADO_BADGES         = EMPLEADO_BADGES;
window.BENEFICIOS_SOLICITUDES  = BENEFICIOS_SOLICITUDES;
window.CAMPANAS_BIENESTAR      = CAMPANAS_BIENESTAR;
window.esElegibleBeneficio     = esElegibleBeneficio;
window.mesesAntiguedad         = mesesAntiguedad;
window.diasHastaCumple         = diasHastaCumple;
window.diasHastaAniversario    = diasHastaAniversario;
window.calcularEventosCalendario = calcularEventosCalendario;
window.getBeneficio            = getBeneficio;
window.getCategoria            = getCategoria;
window.getValor                = getValor;
window.getBadge                = getBadge;
window.getEmpleadoBen          = getEmpleadoBen;
window.beneficiosDelCargo      = beneficiosDelCargo;

// ════════════════════════════════════════════════════════════════════════
// Ampliación de nómina · Cargo existente — fuente compartida
// Consumida por Solicitudes de personal, Organigrama aprobado y ATS.
// Independiente del catálogo administrativo de Cargos en Configuración
// (CARGOS_CATALOG_SEED) y de la matriz de competencias (CARGOS, arriba):
// este es el "Maestro de cargos" operativo para crear posiciones.
// ════════════════════════════════════════════════════════════════════════

const CARGOS_MAESTRO = [
  {
    id: 'CGO-004', nombre: 'Operario de Clasificación', area: 'Operaciones', nivel: 'Operativo',
    proceso: 'Operaciones', subproceso: 'Clasificación de residuos',
    perfil: 'Bachiller con experiencia en manejo de materiales aprovechables. Disponibilidad para trabajar en planta y rotar entre líneas de clasificación.',
    riesgoLaboral: 'Alto — riesgo mecánico, ergonómico y manejo de material aprovechable',
    rangoSalarial: '$ 1.423.500 — $ 1.620.000',
    competencias: ['Clasificación de residuos', 'Trabajo en equipo', 'Seguimiento de instrucciones', 'Atención al detalle'],
    capacitaciones: ['Inducción SG-SST', 'Manejo seguro de maquinaria de clasificación', 'Buenas Prácticas de Manufactura (BPM)'],
    examenesMedicos: ['Examen médico ocupacional de ingreso', 'Audiometría', 'Espirometría'],
    dotacionEPP: ['Guantes térmicos Kevlar 350°C', 'Botas dieléctricas con puntera', 'Casco industrial clase E', 'Protector auditivo copa 25 dB'],
  },
  {
    id: 'CGO-003', nombre: 'Op. Aprovechamiento', area: 'Aprovechamiento', nivel: 'Operativo',
    proceso: 'Operaciones', subproceso: 'Aprovechamiento',
    perfil: 'Bachiller con experiencia en procesos de aprovechamiento de material reciclable.',
    riesgoLaboral: 'Medio — riesgo ergonómico repetitivo y biológico',
    rangoSalarial: '$ 1.380.000 — $ 1.550.000',
    competencias: ['Separación de material aprovechable', 'Trabajo en equipo', 'Cumplimiento de metas de producción'],
    capacitaciones: ['Inducción SG-SST', 'Manejo de residuos aprovechables', 'Bioseguridad'],
    examenesMedicos: ['Examen médico ocupacional de ingreso', 'Optometría'],
    dotacionEPP: ['Guantes de nitrilo calibre 15', 'Tapabocas N95', 'Overol antifluido'],
  },
  {
    id: 'CGO-006', nombre: 'Téc. Mantenimiento', area: 'Mantenimiento', nivel: 'Técnico',
    proceso: 'Operaciones', subproceso: 'Mantenimiento',
    perfil: 'Técnico o tecnólogo en mantenimiento industrial, electromecánica o afines.',
    riesgoLaboral: 'Alto — trabajo en alturas, riesgo eléctrico y espacios confinados',
    rangoSalarial: '$ 1.750.000 — $ 2.100.000',
    competencias: ['Mantenimiento preventivo y correctivo', 'Lectura de planos técnicos', 'Trabajo seguro en alturas'],
    capacitaciones: ['Trabajo seguro en alturas', 'Bloqueo y etiquetado (LOTO)', 'Espacios confinados'],
    examenesMedicos: ['Examen médico ocupacional de ingreso', 'Examen de alturas'],
    dotacionEPP: ['Arnés de cuerpo completo', 'Guantes dieléctricos', 'Casco con barbuquejo', 'Botas dieléctricas'],
  },
  {
    id: 'CGO-009', nombre: 'Conductor', area: 'Logística', nivel: 'Operativo',
    proceso: 'Logística', subproceso: 'Recolección y transporte',
    perfil: 'Licencia de conducción categoría C2 vigente, experiencia en rutas de recolección.',
    riesgoLaboral: 'Medio — riesgo vial y manipulación de cargas',
    rangoSalarial: '$ 1.500.000 — $ 1.750.000',
    competencias: ['Conducción defensiva', 'Planeación de rutas', 'Atención al cliente'],
    capacitaciones: ['PESV — Plan Estratégico de Seguridad Vial', 'Manejo defensivo', 'Manipulación de cargas'],
    examenesMedicos: ['Examen médico ocupacional de ingreso', 'Examen visiométrico'],
    dotacionEPP: ['Chaleco reflectivo', 'Guantes de carnaza', 'Botas de seguridad con puntera'],
  },
  { id: 'CGO-000', nombre: 'Gerencia General',        area: 'Presidencia',       nivel: 'Directivo', proceso: 'Dirección general', subproceso: 'Presidencia' },
  { id: 'CGO-011', nombre: 'Gerente Regional',         area: 'Dirección regional', nivel: 'Directivo', proceso: 'Dirección general', subproceso: 'Dirección regional' },
  { id: 'CGO-012', nombre: 'Director de Operaciones',  area: 'Operaciones',      nivel: 'Directivo', proceso: 'Operaciones', subproceso: 'Dirección de operaciones' },
  { id: 'CGO-013', nombre: 'Supervisor de Planta',     area: 'Operaciones',      nivel: 'Liderazgo', proceso: 'Operaciones', subproceso: 'Supervisión de planta' },
];

// ─── Organigrama aprobado — posiciones (nunca empleados) ──────────────────

const POS_GERENCIA_GENERAL = {
  id: 'pos-0000012', codigo: 'POS-0000012', cargoId: 'CGO-000', cargo: 'Gerencia General',
  parentPositionId: null, parentPositionCode: null, parentPositionLabel: null,
  sociedad: 'ASEI', lineaNegocio: 'Presidencia', proceso: 'Dirección general', subproceso: 'Presidencia', area: 'Presidencia', sede: 'Cali', centroCosto: 'CC-ADM-001',
  estadoEstructural: 'activa', estadoOcupacion: 'ocupada',
  fechaEfectiva: '2020-01-01', solicitudOrigenId: null, fechaCreacion: '2020-01-01', esNueva: false,
};
const POS_GERENCIA_REGIONAL = {
  id: 'pos-0000127', codigo: 'POS-0000127', cargoId: 'CGO-011', cargo: 'Gerencia Regional Occidente',
  parentPositionId: POS_GERENCIA_GENERAL.id, parentPositionCode: POS_GERENCIA_GENERAL.codigo, parentPositionLabel: POS_GERENCIA_GENERAL.cargo,
  sociedad: 'ECOENTORNO', lineaNegocio: 'Aprovechamiento', proceso: 'Dirección general', subproceso: 'Dirección regional', area: 'Dirección regional', sede: 'Cali', centroCosto: 'CC-ADM-002',
  estadoEstructural: 'activa', estadoOcupacion: 'ocupada',
  fechaEfectiva: '2020-03-01', solicitudOrigenId: null, fechaCreacion: '2020-03-01', esNueva: false,
};
const POS_DIRECCION_OPERACIONES = {
  id: 'pos-0002341', codigo: 'POS-0002341', cargoId: 'CGO-012', cargo: 'Dirección de Operaciones',
  parentPositionId: POS_GERENCIA_REGIONAL.id, parentPositionCode: POS_GERENCIA_REGIONAL.codigo, parentPositionLabel: POS_GERENCIA_REGIONAL.cargo,
  sociedad: 'ECOENTORNO', lineaNegocio: 'Aprovechamiento', proceso: 'Operaciones', subproceso: 'Dirección de operaciones', area: 'Operaciones', sede: 'Cali', centroCosto: 'CC-OP-001',
  estadoEstructural: 'activa', estadoOcupacion: 'ocupada',
  fechaEfectiva: '2020-06-01', solicitudOrigenId: null, fechaCreacion: '2020-06-01', esNueva: false,
};
const POS_SUPERVISION_YUMBO = {
  id: 'pos-0008440', codigo: 'POS-0008440', cargoId: 'CGO-013', cargo: 'Supervisión de Planta',
  parentPositionId: POS_DIRECCION_OPERACIONES.id, parentPositionCode: POS_DIRECCION_OPERACIONES.codigo, parentPositionLabel: POS_DIRECCION_OPERACIONES.cargo,
  sociedad: 'ECOENTORNO', lineaNegocio: 'Aprovechamiento', proceso: 'Operaciones', subproceso: 'Supervisión de planta', area: 'Operaciones', sede: 'Planta Yumbo', centroCosto: 'CC-OP-014',
  estadoEstructural: 'activa', estadoOcupacion: 'ocupada',
  fechaEfectiva: '2021-02-01', solicitudOrigenId: null, fechaCreacion: '2021-02-01', esNueva: false,
};
const POS_SUPERVISION_CALI = {
  id: 'pos-0008441', codigo: 'POS-0008441', cargoId: 'CGO-013', cargo: 'Supervisión de Planta',
  parentPositionId: POS_DIRECCION_OPERACIONES.id, parentPositionCode: POS_DIRECCION_OPERACIONES.codigo, parentPositionLabel: POS_DIRECCION_OPERACIONES.cargo,
  sociedad: 'ASEI', lineaNegocio: 'Operaciones', proceso: 'Operaciones', subproceso: 'Supervisión de planta', area: 'Operaciones', sede: 'Planta Cali', centroCosto: 'CC-OP-010',
  estadoEstructural: 'activa', estadoOcupacion: 'ocupada',
  fechaEfectiva: '2021-02-01', solicitudOrigenId: null, fechaCreacion: '2021-02-01', esNueva: false,
};
const POS_SUPERVISION_BAQ = {
  id: 'pos-0008442', codigo: 'POS-0008442', cargoId: 'CGO-013', cargo: 'Supervisión de Planta',
  parentPositionId: POS_DIRECCION_OPERACIONES.id, parentPositionCode: POS_DIRECCION_OPERACIONES.codigo, parentPositionLabel: POS_DIRECCION_OPERACIONES.cargo,
  sociedad: 'ASEI', lineaNegocio: 'Operaciones', proceso: 'Operaciones', subproceso: 'Supervisión de planta', area: 'Operaciones', sede: 'Planta Barranquilla', centroCosto: 'CC-OP-011',
  estadoEstructural: 'activa', estadoOcupacion: 'ocupada',
  fechaEfectiva: '2021-05-01', solicitudOrigenId: null, fechaCreacion: '2021-05-01', esNueva: false,
};

// Genera N posiciones de Operario de Clasificación (todas activas · ocupadas)
// bajo una supervisión de planta, con código secuencial POS-0101xx.
const _generarOperariosClasificacion = (parentPos, cantidad, offsetInicial) => {
  const list = [];
  for (let i = 0; i < cantidad; i++) {
    const num = 10101 + offsetInicial + i;
    const codigo = `POS-0${num}`;
    list.push({
      id: codigo.toLowerCase(), codigo,
      cargoId: 'CGO-004', cargo: 'Operario de Clasificación',
      parentPositionId: parentPos.id, parentPositionCode: parentPos.codigo, parentPositionLabel: parentPos.cargo,
      sociedad: parentPos.sociedad, lineaNegocio: parentPos.lineaNegocio, proceso: 'Operaciones', subproceso: 'Clasificación de residuos',
      area: parentPos.area, sede: parentPos.sede, centroCosto: parentPos.centroCosto,
      estadoEstructural: 'activa', estadoOcupacion: 'ocupada',
      fechaEfectiva: '2023-01-15', solicitudOrigenId: null, fechaCreacion: '2023-01-15', esNueva: false,
    });
  }
  return list;
};

// 10 posiciones activas de Operario de Clasificación (headcount aprobado inicial)
// repartidas en 3 plantas — ninguna es POS-010347: esa nace únicamente al
// completarse la aprobación de AMP-2026-0048 (ver createApprovedPosition).
const POSICIONES_APROBADAS = [
  POS_GERENCIA_GENERAL, POS_GERENCIA_REGIONAL, POS_DIRECCION_OPERACIONES,
  POS_SUPERVISION_YUMBO, ..._generarOperariosClasificacion(POS_SUPERVISION_YUMBO, 2, 0),
  POS_SUPERVISION_CALI, ..._generarOperariosClasificacion(POS_SUPERVISION_CALI, 4, 2),
  POS_SUPERVISION_BAQ, ..._generarOperariosClasificacion(POS_SUPERVISION_BAQ, 4, 6),
];

const getHeadcountPorCargo = (cargoId) => {
  const activas = POSICIONES_APROBADAS.filter(p => p.cargoId === cargoId && p.estadoEstructural === 'activa');
  const aprobado = activas.length;
  const real = activas.filter(p => p.estadoOcupacion === 'ocupada').length;
  return { aprobado, real, vacantes: aprobado - real };
};

// ─── Plantilla de flujo — Ampliación de nómina · Cargo existente ──────────
// Misma fuente que consume la plantilla PLT-005 en Configuración → Plantillas
// de solicitudes, para que la vista de configuración y la ejecución real del
// flujo en Solicitudes de personal nunca queden desincronizadas.

const RESPONSABLES_POR_LINEA = {
  'Aprovechamiento': { director: 'Mauricio Andrés Salcedo Vargas · Director de Aprovechamiento', nacional: 'Claudia Patricia Reyes Londoño · Directora Nacional de Operaciones', gerente: 'Felipe Santiago Rengifo Ortiz · Gerente COO' },
  'Operaciones':     { director: 'Andrés Camilo Botero Salazar · Director de Operaciones',        nacional: 'Claudia Patricia Reyes Londoño · Directora Nacional de Operaciones', gerente: 'Felipe Santiago Rengifo Ortiz · Gerente COO' },
  'Mantenimiento':   { director: 'Luis Fernando Cárdenas Ospina · Director de Mantenimiento',      nacional: 'Claudia Patricia Reyes Londoño · Directora Nacional de Operaciones', gerente: 'Felipe Santiago Rengifo Ortiz · Gerente COO' },
  'Logística':       { director: 'María Camila Duarte Rincón · Directora de Logística',            nacional: 'Claudia Patricia Reyes Londoño · Directora Nacional de Operaciones', gerente: 'Felipe Santiago Rengifo Ortiz · Gerente COO' },
  default:           { director: 'Director del proceso',                                           nacional: 'Director Nacional de Operaciones',                                    gerente: 'Gerente del proceso' },
};

const AMPLIACION_NOMINA_CARGO_EXISTENTE_ETAPAS = [
  { nombre: 'Solicitud de personal', bloqueTipo: 'Formulario', responsable: 'Todos los colaboradores',
    nota: 'Envía confirmación automática al solicitante.' },
  { nombre: 'Revisar escala salarial', bloqueTipo: 'Revisión', responsable: 'Adriana Marcela Torres Villalba · Directora de Gestión Humana', accionable: true,
    nota: 'Confirma la línea de negocio y valida que la ampliación no afecte incorrectamente la escala salarial.' },
  { nombre: 'Autorización del director del proceso', bloqueTipo: 'Aprobación', rolDinamico: 'director', accionable: true },
  { nombre: 'Autorización del director nacional', bloqueTipo: 'Aprobación', rolDinamico: 'nacional', accionable: true },
  { nombre: 'Autorización de la gerencia del proceso', bloqueTipo: 'Aprobación', rolDinamico: 'gerente', accionable: true },
  { nombre: 'Autorización del CEO', bloqueTipo: 'Aprobación', responsable: 'Diego · CEO', accionable: true },
  { nombre: 'Creación de la posición aprobada', bloqueTipo: 'Resultado final', responsable: 'Sistema',
    nota: 'Crea la posición asociada al cargo existente — activa y vacante — y actualiza el headcount aprobado. No crea un cargo nuevo.' },
  { nombre: 'Apertura', bloqueTipo: 'Revisión', responsable: 'Reclutadores',
    nota: 'Revisa la información y envía la solicitud a las temporales. La requisición queda asociada a la posición creada.' },
  { nombre: 'RQ Temporales', bloqueTipo: 'Formulario', responsable: 'Reclutadores', requiereRQ: true,
    nota: 'No puede cerrarse sin el código RQ entregado por la temporal.' },
  { nombre: 'Actualizar HCM', bloqueTipo: 'Resultado final', responsable: 'Analista de Gestión Humana',
    nota: 'Confirma la actualización del control de planta y la vinculación entre solicitud, posición y requisición.' },
  { nombre: 'Alerta a Sistemas', bloqueTipo: 'Notificación', responsable: 'Coordinador Nacional de Tecnología',
    nota: 'Informa que se aprobó una ampliación de nómina para un cargo existente. No bloquea la posición ni la vacante.' },
];

// ─── Solicitudes · requisiciones ATS (en memoria, vive mientras dure la sesión) ──

const SOLICITUDES_PERSONAL_STORE = [];
const REQUISICIONES_ATS_STORE = [];

const crearRequisicionATS = (posicion, solicitud) => {
  const existing = REQUISICIONES_ATS_STORE.find(r => r.posicionCodigo === posicion.codigo);
  if (existing) return existing;
  const req = {
    id: `RQ-REF-${posicion.codigo}`, posicionCodigo: posicion.codigo, solicitudOrigenId: solicitud.id,
    estado: 'pendiente_codigo_rq', codigoRQ: null, creadaEn: posicion.fechaCreacion,
  };
  REQUISICIONES_ATS_STORE.push(req);
  return req;
};

// Idempotente: si ya existe una posición originada por esta solicitud, la retorna
// sin crear una nueva — aprobar dos veces (o recargar la interacción) no duplica.
const createApprovedPosition = (solicitud) => {
  let posicion = POSICIONES_APROBADAS.find(p => p.solicitudOrigenId === solicitud.id);
  let esCreacionNueva = false;
  if (!posicion) {
    const cargoMaestro = CARGOS_MAESTRO.find(c => c.id === solicitud.cargoId);
    posicion = {
      id: 'pos-010347', codigo: 'POS-010347',
      cargoId: solicitud.cargoId, cargo: solicitud.cargoNombre,
      parentPositionId: solicitud.posicionJefeId, parentPositionCode: solicitud.posicionJefeCodigo, parentPositionLabel: solicitud.posicionJefeLabel,
      sociedad: solicitud.sociedad, lineaNegocio: solicitud.lineaNegocio,
      proceso: cargoMaestro ? cargoMaestro.proceso : solicitud.lineaNegocio,
      subproceso: cargoMaestro ? cargoMaestro.subproceso : solicitud.area,
      area: solicitud.area, sede: solicitud.sede, centroCosto: solicitud.centroCosto,
      estadoEstructural: 'activa', estadoOcupacion: 'vacante',
      // Esta posición nace ligada al pipeline de Reclutamiento (RQ Temporales),
      // por eso no queda habilitada para contratación directa por defecto.
      estadoCobertura: 'no_habilitada', empleadoOcupanteId: null, empleadoOcupanteNombre: null, origenOcupacion: null,
      fechaEfectiva: solicitud.fechaRequeridaISO || '2026-10-01',
      solicitudOrigenId: solicitud.id, fechaCreacion: '2026-09-18', esNueva: true,
    };
    POSICIONES_APROBADAS.push(posicion);
    esCreacionNueva = true;
  }
  const requisicion = crearRequisicionATS(posicion, solicitud);
  if (esCreacionNueva && typeof window !== 'undefined' && window.dispatchEvent) {
    window.dispatchEvent(new CustomEvent('atica:position-created', { detail: { posicion, solicitud, requisicion } }));
  }
  return { posicion, requisicion };
};

const crearSolicitudAmpliacion = (form) => {
  // Código fijo para esta demo — si el usuario reintenta el envío, no duplica la solicitud.
  const existing = SOLICITUDES_PERSONAL_STORE.find(s => s.id === 'AMP-2026-0048');
  if (existing) return existing;

  const lookup = RESPONSABLES_POR_LINEA[form.lineaNegocio] || RESPONSABLES_POR_LINEA.default;
  const etapas = AMPLIACION_NOMINA_CARGO_EXISTENTE_ETAPAS.map((e, i) => ({
    ...e,
    responsableResuelto: e.rolDinamico ? lookup[e.rolDinamico] : e.responsable,
    estado: i === 0 ? 'completada' : (i === 1 ? 'pendiente' : 'bloqueada'),
    decididoPor: i === 0 ? 'Sistema' : null,
    fecha: i === 0 ? '18/09/2026' : null,
    comentario: null,
    codigoRQ: null,
  }));

  const solicitud = {
    id: 'AMP-2026-0048', tipo: 'Ampliación de nómina', modalidad: 'Cargo existente',
    cargoId: form.cargoId, cargoNombre: form.cargoNombre,
    cantidad: form.cantidad,
    sociedad: form.sociedad, lineaNegocio: form.lineaNegocio, area: form.area, sede: form.sede, centroCosto: form.centroCosto,
    posicionJefeId: form.posicionJefeId, posicionJefeCodigo: form.posicionJefeCodigo, posicionJefeLabel: form.posicionJefeLabel,
    fechaRequerida: form.fechaRequerida, fechaRequeridaISO: form.fechaRequeridaISO,
    jornada: form.jornada, tipoContrato: form.tipoContrato,
    motivo: form.motivo, justificacion: form.justificacion, proyectoRelacionado: form.proyectoRelacionado || '',
    archivoSoporte: form.archivoSoporte || null,
    fechaSolicitud: '18/09/2026', solicitante: 'Carolina Restrepo S. · Jefe de RRHH',
    jefeSolicitante: 'Carolina Restrepo S. · Jefe de RRHH',
    posicionAsociadaId: null, fechaAprobacion: null,
    // Esta solicitud sigue el camino de Reclutamiento (Apertura → RQ Temporales),
    // por eso no queda habilitada para contratación directa.
    metodoCobertura: 'reclutamiento', estadoCierre: 'abierta',
    estado: 'pendiente_aprobacion',
    posicionCreadaCodigo: null, requisicionId: null,
    etapas,
  };
  SOLICITUDES_PERSONAL_STORE.push(solicitud);
  return solicitud;
};

const decidirEtapaSolicitud = (solicitudId, decision, comentario) => {
  const solicitud = SOLICITUDES_PERSONAL_STORE.find(s => s.id === solicitudId);
  if (!solicitud) return null;
  const idx = solicitud.etapas.findIndex(e => e.estado === 'pendiente');
  if (idx === -1) return solicitud;
  const etapa = solicitud.etapas[idx];
  const fechaHoy = '18/09/2026';

  if (decision === 'rechazar') {
    etapa.estado = 'rechazada'; etapa.comentario = comentario; etapa.fecha = fechaHoy; etapa.decididoPor = etapa.responsableResuelto;
    solicitud.estado = 'rechazada';
    return solicitud;
  }
  if (decision === 'devolver') {
    etapa.estado = 'devuelta'; etapa.comentario = comentario; etapa.fecha = fechaHoy; etapa.decididoPor = etapa.responsableResuelto;
    solicitud.estado = 'devuelta';
    return solicitud;
  }

  // aprobar
  etapa.estado = 'completada'; etapa.comentario = comentario || null; etapa.fecha = fechaHoy; etapa.decididoPor = etapa.responsableResuelto;
  const next = solicitud.etapas[idx + 1];
  if (next) next.estado = 'pendiente';

  const esUltimaAprobacion = idx === 5; // índice 5 = etapa 6 · Autorización del CEO
  if (esUltimaAprobacion) {
    solicitud.estado = 'aprobada';
    solicitud.fechaAprobacion = fechaHoy;
    const { posicion, requisicion } = createApprovedPosition(solicitud);
    solicitud.posicionCreadaCodigo = posicion.codigo;
    solicitud.requisicionId = requisicion.id;
    if (solicitud.etapas[6]) { solicitud.etapas[6].estado = 'completada'; solicitud.etapas[6].fecha = fechaHoy; solicitud.etapas[6].decididoPor = 'Sistema'; }
    if (solicitud.etapas[7]) { solicitud.etapas[7].estado = 'completada'; solicitud.etapas[7].fecha = fechaHoy; solicitud.etapas[7].decididoPor = 'Reclutadores'; }
    if (solicitud.etapas[8]) { solicitud.etapas[8].estado = 'pendiente_rq'; }
  }
  return solicitud;
};

const registrarCodigoRQ = (solicitudId, codigoRQ) => {
  const solicitud = SOLICITUDES_PERSONAL_STORE.find(s => s.id === solicitudId);
  if (!solicitud || !codigoRQ || !codigoRQ.trim()) return solicitud;
  const fechaHoy = '18/09/2026';
  const etapaRQ = solicitud.etapas[8];
  if (etapaRQ) { etapaRQ.estado = 'completada'; etapaRQ.codigoRQ = codigoRQ.trim(); etapaRQ.fecha = fechaHoy; etapaRQ.decididoPor = 'Reclutadores'; }
  const req = REQUISICIONES_ATS_STORE.find(r => r.id === solicitud.requisicionId);
  if (req) { req.codigoRQ = codigoRQ.trim(); req.estado = 'en_temporales'; }
  if (solicitud.etapas[9])  { solicitud.etapas[9].estado  = 'completada'; solicitud.etapas[9].fecha  = fechaHoy; solicitud.etapas[9].decididoPor  = 'Analista de Gestión Humana'; }
  if (solicitud.etapas[10]) { solicitud.etapas[10].estado = 'completada'; solicitud.etapas[10].fecha = fechaHoy; solicitud.etapas[10].decididoPor = 'Sistema'; }
  solicitud.estado = 'ejecutada';
  return solicitud;
};

let _alertaNuevaPosicionDescartada = false;

// Reutiliza exactamente el mismo camino de datos que la UI (crear solicitud →
// 5 aprobaciones → código RQ) — solo automatiza los clics para poder mostrar
// el estado aprobado sin pasar por el formulario. No fabrica la posición
// directamente: si no se llama, POS-010347 no existe.
const simularFlujoCompletoDemo = () => {
  const existente = SOLICITUDES_PERSONAL_STORE.find(s => s.id === 'AMP-2026-0048');
  if (existente) return existente;
  const solicitud = crearSolicitudAmpliacion({
    cargoId: 'CGO-004', cargoNombre: 'Operario de Clasificación', cantidad: 1,
    sociedad: 'ECOENTORNO', lineaNegocio: 'Aprovechamiento', area: 'Operaciones',
    sede: 'Planta Yumbo, Valle del Cauca', centroCosto: 'CC-OP-014 · Planta Yumbo',
    posicionJefeId: POS_SUPERVISION_YUMBO.id, posicionJefeCodigo: POS_SUPERVISION_YUMBO.codigo, posicionJefeLabel: POS_SUPERVISION_YUMBO.cargo,
    fechaRequerida: '01/10/2026', fechaRequeridaISO: '2026-10-01',
    jornada: 'Turnos rotativos', tipoContrato: 'Término fijo',
    motivo: 'Incremento del volumen de material aprovechable',
    justificacion: 'Simulación demo del flujo completo de aprobación.',
    proyectoRelacionado: '', archivoSoporte: null,
  });
  for (let i = 0; i < 5; i++) decidirEtapaSolicitud(solicitud.id, 'aprobar', 'Aprobado (simulación demo)');
  registrarCodigoRQ(solicitud.id, 'RQ-2026-3381');
  return solicitud;
};

// ─── Contratación directa — reemplazo y ampliación sin pasar por Reclutamiento ──
// Extiende exactamente el mismo POSICIONES_APROBADAS / SOLICITUDES_PERSONAL_STORE
// de arriba (no crea arreglos nuevos). Una solicitud queda habilitada para este
// flujo solo si metodoCobertura === 'contratacion_directa_habilitada' — las que
// van por Reclutamiento (como AMP-2026-0048) usan 'reclutamiento' y nunca
// aparecen aquí. El código de posición sigue siendo el identificador definitivo.

const _dhEtapasHistoricas = (nombres) => nombres.map((nombre) => ({
  nombre, bloqueTipo: 'Aprobación', responsable: 'Historial', responsableResuelto: 'Historial',
  estado: 'completada', decididoPor: 'Sistema', fecha: '—', comentario: null, codigoRQ: null, nota: null,
}));

// Reemplazo · coincidencia única — la posición ya estaba vacante y habilitada
const POS_0009120 = {
  id: 'pos-0009120', codigo: 'POS-0009120', cargoId: 'CGO-006', cargo: 'Téc. Mantenimiento',
  parentPositionId: POS_DIRECCION_OPERACIONES.id, parentPositionCode: POS_DIRECCION_OPERACIONES.codigo, parentPositionLabel: POS_DIRECCION_OPERACIONES.cargo,
  sociedad: 'ASEI', lineaNegocio: 'Mantenimiento', proceso: 'Operaciones', subproceso: 'Mantenimiento',
  area: 'Mantenimiento', sede: 'Planta Cali', centroCosto: 'CC-MNT-004',
  estadoEstructural: 'activa', estadoOcupacion: 'vacante', estadoCobertura: 'habilitada',
  empleadoOcupanteId: null, empleadoOcupanteNombre: null, origenOcupacion: null,
  fechaEfectiva: '2026-08-20', solicitudOrigenId: null, fechaCreacion: '2024-02-01', esNueva: false,
};
// Control — misma familia de cargo, pero ocupada: nunca debe ser seleccionable
const POS_0009121 = {
  id: 'pos-0009121', codigo: 'POS-0009121', cargoId: 'CGO-006', cargo: 'Téc. Mantenimiento',
  parentPositionId: POS_DIRECCION_OPERACIONES.id, parentPositionCode: POS_DIRECCION_OPERACIONES.codigo, parentPositionLabel: POS_DIRECCION_OPERACIONES.cargo,
  sociedad: 'ASEI', lineaNegocio: 'Mantenimiento', proceso: 'Operaciones', subproceso: 'Mantenimiento',
  area: 'Mantenimiento', sede: 'Planta Cali', centroCosto: 'CC-MNT-004',
  estadoEstructural: 'activa', estadoOcupacion: 'ocupada', estadoCobertura: null,
  empleadoOcupanteId: 'E0156', empleadoOcupanteNombre: 'Paula Valencia Ríos', origenOcupacion: null,
  fechaEfectiva: '2022-04-01', solicitudOrigenId: null, fechaCreacion: '2022-04-01', esNueva: false,
};
// Control — vacante y "habilitada" pero con la estructura bloqueada: nunca seleccionable
const POS_0009130 = {
  id: 'pos-0009130', codigo: 'POS-0009130', cargoId: 'CGO-004', cargo: 'Operario de Clasificación',
  parentPositionId: POS_SUPERVISION_CALI.id, parentPositionCode: POS_SUPERVISION_CALI.codigo, parentPositionLabel: POS_SUPERVISION_CALI.cargo,
  sociedad: 'ASEI', lineaNegocio: 'Operaciones', proceso: 'Operaciones', subproceso: 'Clasificación de residuos',
  area: 'Operaciones', sede: 'Planta Cali', centroCosto: 'CC-OP-010',
  estadoEstructural: 'bloqueada', estadoOcupacion: 'vacante', estadoCobertura: 'habilitada',
  empleadoOcupanteId: null, empleadoOcupanteNombre: null, origenOcupacion: null,
  fechaEfectiva: '2026-07-01', solicitudOrigenId: null, fechaCreacion: '2023-01-15', esNueva: false,
  motivoBloqueo: 'Congelada por revisión del centro de costo CC-OP-010',
};
// Control — vacante pero sin autorización de cobertura: nunca seleccionable
const POS_0009140 = {
  id: 'pos-0009140', codigo: 'POS-0009140', cargoId: 'CGO-009', cargo: 'Conductor',
  parentPositionId: POS_DIRECCION_OPERACIONES.id, parentPositionCode: POS_DIRECCION_OPERACIONES.codigo, parentPositionLabel: POS_DIRECCION_OPERACIONES.cargo,
  sociedad: 'ECOLOGISTICA', lineaNegocio: 'Logística', proceso: 'Logística', subproceso: 'Recolección y transporte',
  area: 'Logística', sede: 'Planta Barranquilla', centroCosto: 'CC-LOG-002',
  estadoEstructural: 'activa', estadoOcupacion: 'vacante', estadoCobertura: 'no_habilitada',
  empleadoOcupanteId: null, empleadoOcupanteNombre: null, origenOcupacion: null,
  fechaEfectiva: '2026-08-10', solicitudOrigenId: null, fechaCreacion: '2021-05-01', esNueva: false,
};
// Reemplazo · varias coincidencias — dos posiciones equivalentes obligan a GH a elegir
const POS_0101200 = {
  id: 'pos-0101200', codigo: 'POS-0101200', cargoId: 'CGO-004', cargo: 'Operario de Clasificación',
  parentPositionId: POS_SUPERVISION_CALI.id, parentPositionCode: POS_SUPERVISION_CALI.codigo, parentPositionLabel: POS_SUPERVISION_CALI.cargo,
  sociedad: 'ASEI', lineaNegocio: 'Operaciones', proceso: 'Operaciones', subproceso: 'Clasificación de residuos',
  area: 'Operaciones', sede: 'Planta Cali', centroCosto: 'CC-OP-010',
  estadoEstructural: 'activa', estadoOcupacion: 'vacante', estadoCobertura: 'habilitada',
  empleadoOcupanteId: null, empleadoOcupanteNombre: null, origenOcupacion: null,
  fechaEfectiva: '2026-08-15', solicitudOrigenId: null, fechaCreacion: '2023-06-01', esNueva: false,
};
const POS_0101201 = {
  id: 'pos-0101201', codigo: 'POS-0101201', cargoId: 'CGO-004', cargo: 'Operario de Clasificación',
  parentPositionId: POS_SUPERVISION_BAQ.id, parentPositionCode: POS_SUPERVISION_BAQ.codigo, parentPositionLabel: POS_SUPERVISION_BAQ.cargo,
  sociedad: 'ASEI', lineaNegocio: 'Operaciones', proceso: 'Operaciones', subproceso: 'Clasificación de residuos',
  area: 'Operaciones', sede: 'Planta Barranquilla', centroCosto: 'CC-OP-011',
  estadoEstructural: 'activa', estadoOcupacion: 'vacante', estadoCobertura: 'habilitada',
  empleadoOcupanteId: null, empleadoOcupanteNombre: null, origenOcupacion: null,
  fechaEfectiva: '2026-08-18', solicitudOrigenId: null, fechaCreacion: '2023-06-01', esNueva: false,
};
// Ampliación · ya aprobada — su posición ya existe, activa y vacante (escenario B)
const POS_0101300 = {
  id: 'pos-0101300', codigo: 'POS-0101300', cargoId: 'CGO-003', cargo: 'Op. Aprovechamiento',
  parentPositionId: POS_DIRECCION_OPERACIONES.id, parentPositionCode: POS_DIRECCION_OPERACIONES.codigo, parentPositionLabel: POS_DIRECCION_OPERACIONES.cargo,
  sociedad: 'ECOENTORNO', lineaNegocio: 'Aprovechamiento', proceso: 'Operaciones', subproceso: 'Aprovechamiento',
  area: 'Aprovechamiento', sede: 'Planta Yumbo', centroCosto: 'CC-APR-006',
  estadoEstructural: 'activa', estadoOcupacion: 'vacante', estadoCobertura: 'habilitada',
  empleadoOcupanteId: null, empleadoOcupanteNombre: null, origenOcupacion: null,
  fechaEfectiva: '2026-08-01', solicitudOrigenId: 'AMP-2026-0052', fechaCreacion: '2026-08-01', esNueva: false,
};

POSICIONES_APROBADAS.push(POS_0009120, POS_0009121, POS_0009130, POS_0009140, POS_0101200, POS_0101201, POS_0101300);

// ─── Expansión internacional — República Dominicana y Guatemala ──────────
// Mismo modelo de posición (POSICIONES_APROBADAS), país distinto. Da al
// asistente Crear empleado suficientes posiciones vacantes fuera de
// Colombia para demostrar búsqueda/filtro por país.
const POS_LIDER_SEDE_DO = {
  id: 'pos-0500010', codigo: 'POS-0500010', cargoId: 'CGO-014', cargo: 'Líder de Sede',
  parentPositionId: POS_GERENCIA_GENERAL.id, parentPositionCode: POS_GERENCIA_GENERAL.codigo, parentPositionLabel: POS_GERENCIA_GENERAL.cargo,
  sociedad: 'BYC', lineaNegocio: 'Expansión internacional', proceso: 'Dirección general', subproceso: 'Dirección de sede', area: 'Administración', sede: 'Santo Domingo', centroCosto: 'CC-ADM-050',
  estadoEstructural: 'activa', estadoOcupacion: 'ocupada',
  fechaEfectiva: '2026-01-15', solicitudOrigenId: null, fechaCreacion: '2026-01-15', esNueva: false,
};
const POS_0500011 = {
  id: 'pos-0500011', codigo: 'POS-0500011', cargoId: 'CGO-004', cargo: 'Operario de Clasificación',
  parentPositionId: POS_LIDER_SEDE_DO.id, parentPositionCode: POS_LIDER_SEDE_DO.codigo, parentPositionLabel: POS_LIDER_SEDE_DO.cargo,
  sociedad: 'BYC', lineaNegocio: 'Expansión internacional', proceso: 'Operaciones', subproceso: 'Clasificación de residuos',
  area: 'Operaciones', sede: 'Santo Domingo', centroCosto: 'CC-OP-050',
  estadoEstructural: 'activa', estadoOcupacion: 'vacante', estadoCobertura: 'no_habilitada',
  empleadoOcupanteId: null, empleadoOcupanteNombre: null, origenOcupacion: null,
  fechaEfectiva: '2026-08-01', solicitudOrigenId: null, fechaCreacion: '2026-08-01', esNueva: false,
};
const POS_0500012 = {
  id: 'pos-0500012', codigo: 'POS-0500012', cargoId: 'CGO-021', cargo: 'Aux. Calidad',
  parentPositionId: POS_LIDER_SEDE_DO.id, parentPositionCode: POS_LIDER_SEDE_DO.codigo, parentPositionLabel: POS_LIDER_SEDE_DO.cargo,
  sociedad: 'BYC', lineaNegocio: 'Expansión internacional', proceso: 'Calidad', subproceso: 'Aseguramiento de calidad',
  area: 'Calidad', sede: 'Santo Domingo', centroCosto: 'CC-CAL-050',
  estadoEstructural: 'activa', estadoOcupacion: 'vacante', estadoCobertura: 'no_habilitada',
  empleadoOcupanteId: null, empleadoOcupanteNombre: null, origenOcupacion: null,
  fechaEfectiva: '2026-08-05', solicitudOrigenId: null, fechaCreacion: '2026-08-05', esNueva: false,
};

const POS_LIDER_SEDE_GT = {
  id: 'pos-0600010', codigo: 'POS-0600010', cargoId: 'CGO-014', cargo: 'Líder de Sede',
  parentPositionId: POS_GERENCIA_GENERAL.id, parentPositionCode: POS_GERENCIA_GENERAL.codigo, parentPositionLabel: POS_GERENCIA_GENERAL.cargo,
  sociedad: 'BYC', lineaNegocio: 'Expansión internacional', proceso: 'Dirección general', subproceso: 'Dirección de sede', area: 'Administración', sede: 'Guatemala', centroCosto: 'CC-ADM-060',
  estadoEstructural: 'activa', estadoOcupacion: 'ocupada',
  fechaEfectiva: '2026-01-20', solicitudOrigenId: null, fechaCreacion: '2026-01-20', esNueva: false,
};
const POS_0600011 = {
  id: 'pos-0600011', codigo: 'POS-0600011', cargoId: 'CGO-004', cargo: 'Operaria de Clasificación',
  parentPositionId: POS_LIDER_SEDE_GT.id, parentPositionCode: POS_LIDER_SEDE_GT.codigo, parentPositionLabel: POS_LIDER_SEDE_GT.cargo,
  sociedad: 'BYC', lineaNegocio: 'Expansión internacional', proceso: 'Operaciones', subproceso: 'Clasificación de residuos',
  area: 'Operaciones', sede: 'Guatemala', centroCosto: 'CC-OP-060',
  estadoEstructural: 'activa', estadoOcupacion: 'vacante', estadoCobertura: 'no_habilitada',
  empleadoOcupanteId: null, empleadoOcupanteNombre: null, origenOcupacion: null,
  fechaEfectiva: '2026-08-10', solicitudOrigenId: null, fechaCreacion: '2026-08-10', esNueva: false,
};
const POS_0600012 = {
  id: 'pos-0600012', codigo: 'POS-0600012', cargoId: 'CGO-009', cargo: 'Conductor',
  parentPositionId: POS_LIDER_SEDE_GT.id, parentPositionCode: POS_LIDER_SEDE_GT.codigo, parentPositionLabel: POS_LIDER_SEDE_GT.cargo,
  sociedad: 'BYC', lineaNegocio: 'Expansión internacional', proceso: 'Logística', subproceso: 'Recolección y transporte',
  area: 'Recolección', sede: 'Guatemala', centroCosto: 'CC-LOG-060',
  estadoEstructural: 'activa', estadoOcupacion: 'vacante', estadoCobertura: 'no_habilitada',
  empleadoOcupanteId: null, empleadoOcupanteNombre: null, origenOcupacion: null,
  fechaEfectiva: '2026-08-12', solicitudOrigenId: null, fechaCreacion: '2026-08-12', esNueva: false,
};

POSICIONES_APROBADAS.push(POS_LIDER_SEDE_DO, POS_0500011, POS_0500012, POS_LIDER_SEDE_GT, POS_0600011, POS_0600012);

// País y regional a partir de la sede — todas las posiciones existentes eran
// implícitamente de Colombia (nunca tuvieron el campo); se completa aquí en
// un solo lugar en vez de tocar cada literal, sin romper a quienes ya leen
// sociedad/área/sede directamente.
const SEDE_PAIS_REGIONAL = {
  'Cali': { pais: 'CO', regional: 'Regional Occidente' },
  'Planta Cali': { pais: 'CO', regional: 'Regional Occidente' },
  'Planta Yumbo': { pais: 'CO', regional: 'Regional Occidente' },
  'Planta Barranquilla': { pais: 'CO', regional: 'Regional Caribe' },
  'Santo Domingo': { pais: 'DO', regional: 'Regional Internacional' },
  'Guatemala': { pais: 'GT', regional: 'Regional Internacional' },
};
POSICIONES_APROBADAS.forEach((p) => {
  const meta = SEDE_PAIS_REGIONAL[p.sede] || { pais: 'CO', regional: 'Regional Occidente' };
  if (!p.pais) p.pais = meta.pais;
  if (!p.regional) p.regional = meta.regional;
});

SOLICITUDES_PERSONAL_STORE.push(
  {
    id: 'REEMP-2026-0031', tipo: 'Reemplazo', modalidad: 'Reemplazo por vacante',
    cargoId: 'CGO-006', cargoNombre: 'Téc. Mantenimiento', cantidad: 1,
    sociedad: 'ASEI', lineaNegocio: 'Mantenimiento', area: 'Mantenimiento', sede: 'Planta Cali', centroCosto: 'CC-MNT-004',
    posicionJefeId: POS_DIRECCION_OPERACIONES.id, posicionJefeCodigo: POS_DIRECCION_OPERACIONES.codigo, posicionJefeLabel: POS_DIRECCION_OPERACIONES.cargo,
    posicionAsociadaId: POS_0009120.id,
    fechaRequerida: '05/09/2026', fechaRequeridaISO: '2026-09-05',
    jornada: 'Tiempo completo', tipoContrato: 'Término fijo',
    motivo: 'Retiro voluntario del ocupante anterior',
    justificacion: 'La posición POS-0009120 quedó vacante tras el retiro de su ocupante; se autoriza cobertura inmediata.',
    proyectoRelacionado: '', archivoSoporte: null,
    fechaSolicitud: '18/08/2026', solicitante: 'Luis Fernando Cárdenas Ospina · Director de Mantenimiento',
    jefeSolicitante: 'Luis Fernando Cárdenas Ospina · Director de Mantenimiento',
    fechaAprobacion: '20/08/2026', metodoCobertura: 'contratacion_directa_habilitada', estadoCierre: 'abierta',
    estado: 'aprobada', posicionCreadaCodigo: null, requisicionId: null,
    etapas: _dhEtapasHistoricas(['Solicitud de reemplazo', 'Autorización del director de Mantenimiento', 'Habilitación de cobertura directa']),
  },
  {
    id: 'REEMP-2026-0044', tipo: 'Reemplazo', modalidad: 'Reemplazo por vacante',
    cargoId: 'CGO-004', cargoNombre: 'Operario de Clasificación', cantidad: 1,
    sociedad: 'ASEI', lineaNegocio: 'Operaciones', area: 'Operaciones', sede: 'Regional Occidente', centroCosto: 'Varios · Regional Occidente',
    posicionJefeId: POS_DIRECCION_OPERACIONES.id, posicionJefeCodigo: POS_DIRECCION_OPERACIONES.codigo, posicionJefeLabel: POS_DIRECCION_OPERACIONES.cargo,
    posicionAsociadaId: null,
    fechaRequerida: '10/09/2026', fechaRequeridaISO: '2026-09-10',
    jornada: 'Turnos rotativos', tipoContrato: 'Término fijo',
    motivo: 'Retiro de operario en la regional',
    justificacion: 'Existe más de una posición vacante equivalente en la regional; Gestión Humana debe elegir cuál cubrir.',
    proyectoRelacionado: '', archivoSoporte: null,
    fechaSolicitud: '19/08/2026', solicitante: 'Andrés Camilo Botero Salazar · Director de Operaciones',
    jefeSolicitante: 'Andrés Camilo Botero Salazar · Director de Operaciones',
    fechaAprobacion: '25/08/2026', metodoCobertura: 'contratacion_directa_habilitada', estadoCierre: 'abierta',
    estado: 'aprobada', posicionCreadaCodigo: null, requisicionId: null,
    etapas: _dhEtapasHistoricas(['Solicitud de reemplazo', 'Autorización del director de Operaciones', 'Habilitación de cobertura directa']),
  },
  {
    id: 'AMP-2026-0052', tipo: 'Ampliación de nómina', modalidad: 'Cargo existente',
    cargoId: 'CGO-003', cargoNombre: 'Op. Aprovechamiento', cantidad: 1,
    sociedad: 'ECOENTORNO', lineaNegocio: 'Aprovechamiento', area: 'Aprovechamiento', sede: 'Planta Yumbo', centroCosto: 'CC-APR-006',
    posicionJefeId: POS_DIRECCION_OPERACIONES.id, posicionJefeCodigo: POS_DIRECCION_OPERACIONES.codigo, posicionJefeLabel: POS_DIRECCION_OPERACIONES.cargo,
    posicionAsociadaId: POS_0101300.id,
    fechaRequerida: '01/09/2026', fechaRequeridaISO: '2026-09-01',
    jornada: 'Turnos rotativos', tipoContrato: 'Término fijo',
    motivo: 'Incremento de volumen de material aprovechable en Yumbo',
    justificacion: 'Ampliación aprobada por las 6 instancias del flujo; la posición ya fue creada y está vacante.',
    proyectoRelacionado: '', archivoSoporte: null,
    fechaSolicitud: '30/07/2026', solicitante: 'Mauricio Andrés Salcedo Vargas · Director de Aprovechamiento',
    jefeSolicitante: 'Mauricio Andrés Salcedo Vargas · Director de Aprovechamiento',
    fechaAprobacion: '01/08/2026', metodoCobertura: 'contratacion_directa_habilitada', estadoCierre: 'abierta',
    estado: 'aprobada', posicionCreadaCodigo: POS_0101300.codigo, requisicionId: null,
    etapas: _dhEtapasHistoricas([
      'Solicitud de personal', 'Revisar escala salarial', 'Autorización del director de Aprovechamiento',
      'Autorización del director nacional', 'Autorización de la gerencia', 'Autorización del CEO', 'Creación de la posición aprobada',
    ]),
  },
);

// Solicitudes aprobadas, habilitadas para contratación directa y aún abiertas.
const getSolicitudesDisponiblesParaCobertura = () => SOLICITUDES_PERSONAL_STORE.filter(s =>
  s.estado === 'aprobada' && s.metodoCobertura === 'contratacion_directa_habilitada' && s.estadoCierre === 'abierta'
);

// Busca posiciones vacantes y habilitadas compatibles con una solicitud.
// Si la solicitud tiene una posición asociada fija (reemplazo puntual, o la
// posición que creó una ampliación), solo esa cuenta — nunca se ofrecen
// alternativas no relacionadas. Sin posición asociada, se buscan todas las
// vacantes habilitadas del mismo cargo y sociedad (puede haber más de una).
const getPosicionesCompatibles = (solicitudId) => {
  const solicitud = SOLICITUDES_PERSONAL_STORE.find(s => s.id === solicitudId);
  if (!solicitud) return [];
  const elegibles = POSICIONES_APROBADAS.filter(p =>
    p.estadoEstructural === 'activa' && p.estadoOcupacion === 'vacante' && p.estadoCobertura === 'habilitada'
  );
  if (solicitud.posicionAsociadaId) {
    const pinned = elegibles.find(p => p.id === solicitud.posicionAsociadaId);
    return pinned ? [pinned] : [];
  }
  return elegibles.filter(p => p.cargoId === solicitud.cargoId && p.sociedad === solicitud.sociedad);
};

const _nextEmpleadoId = () => {
  const nums = (window.EMPLOYEES || [])
    .map(e => { const m = /^E0*(\d+)$/.exec(e.id); return m ? parseInt(m[1], 10) : 0; })
    .filter(n => n > 0);
  const next = (nums.length ? Math.max(...nums) : 300) + 1;
  return `E${String(next).padStart(4, '0')}`;
};

// Sigue la misma convención que EMPLOYEES: inicial del primer nombre + inicial
// del último apellido (ej. "Andrés Felipe Mejía" → "AM"), no las dos primeras palabras.
const _initialesDeNombre = (nombre) => {
  const partes = (nombre || '').trim().split(/\s+/).filter(Boolean);
  if (partes.length === 0) return '??';
  const primera = partes[0][0].toUpperCase();
  if (partes.length === 1) return primera;
  return primera + partes[partes.length - 1][0].toUpperCase();
};

// Contratación directa: liga un empleado nuevo a una posición vacante y
// habilitada, cierra la solicitud que la originó y NUNCA vuelve a tocar el
// headcount aprobado — solo cambian el ocupado y el vacante de esa posición.
const contratarEmpleadoDirecto = ({ nombre, tipoDocumento, documento, email, telefono, tipoContrato, fechaIngreso, solicitudId, posicionCodigo }) => {
  const solicitud = SOLICITUDES_PERSONAL_STORE.find(s => s.id === solicitudId);
  if (!solicitud) return { error: 'La solicitud seleccionada ya no existe.' };
  if (solicitud.estado !== 'aprobada') return { error: 'La solicitud debe estar aprobada para poder contratar.' };
  if (solicitud.estadoCierre === 'cerrada') return { error: 'Esta solicitud ya fue cerrada por otra contratación.' };

  const posicion = POSICIONES_APROBADAS.find(p => p.codigo === posicionCodigo);
  if (!posicion) return { error: 'La posición seleccionada ya no existe.' };
  if (posicion.estadoEstructural !== 'activa') return { error: 'La posición está bloqueada o inactiva y no puede ocuparse.' };
  if (posicion.estadoOcupacion === 'ocupada') return { error: 'La posición ya está ocupada por otro colaborador.' };
  if (posicion.estadoCobertura !== 'habilitada') return { error: 'La posición no tiene autorización de cobertura.' };

  const empleado = {
    id: _nextEmpleadoId(), initials: _initialesDeNombre(nombre), name: nombre,
    doc: `${tipoDocumento} ${documento}`, role: posicion.cargo, area: posicion.area, site: posicion.sede,
    contract: tipoContrato, hire: fechaIngreso, status: 'onboarding', country: 'CO',
    email: email || '', phone: telefono || '', origen: 'Contratación directa',
  };
  window.EMPLOYEES.push(empleado);

  posicion.estadoOcupacion = 'ocupada';
  posicion.estadoCobertura = null;
  posicion.empleadoOcupanteId = empleado.id;
  posicion.empleadoOcupanteNombre = empleado.name;
  posicion.origenOcupacion = 'contratacion_directa';
  posicion.origenSolicitudId = solicitud.id;
  posicion.origenSolicitudTipo = solicitud.tipo;

  solicitud.estadoCierre = 'cerrada';
  solicitud.posicionCubiertaCodigo = posicion.codigo;
  solicitud.empleadoContratadoId = empleado.id;

  if (typeof window !== 'undefined' && window.dispatchEvent) {
    window.dispatchEvent(new CustomEvent('atica:direct-hire', { detail: { empleado, posicion, solicitud } }));
  }
  return { empleado, posicion, solicitud };
};

// ─── Crear empleado — vinculación directa a una posición vacante ─────────
// Reemplaza el flujo de "Contratación directa" para el botón principal de
// Empleados: no depende de ninguna solicitud de personal. Cualquier posición
// estructuralmente activa y vacante es elegible — estadoCobertura no se
// evalúa aquí (ver getPosicionesDisponiblesParaContratacion). Los métodos de
// arriba (contratarEmpleadoDirecto y compañía) se conservan intactos para no
// romper Solicitudes de personal, que aún lee estadoCierre/posicionCubiertaCodigo.

const AUDITORIA_EMPLEADOS_STORE = [];

// Sin puntos, espacios ni guiones — para comparar números de documento.
const _normalizarDocumento = (s) => (s || '').replace(/[.\s-]/g, '').toUpperCase();

// Los empleados históricos guardan `doc` como "CC 1.087.456.231" (un solo
// string); los creados por este flujo también guardan tipoDocumento y
// numeroDocumento por separado. Soporta ambos formatos.
const _parseDocumentoEmpleado = (e) => {
  if (e.tipoDocumento && e.numeroDocumento) return { tipo: e.tipoDocumento.toUpperCase(), numero: _normalizarDocumento(e.numeroDocumento) };
  const m = /^(\S+)\s+(.+)$/.exec((e.doc || '').trim());
  if (!m) return { tipo: '', numero: _normalizarDocumento(e.doc) };
  return { tipo: m[1].toUpperCase(), numero: _normalizarDocumento(m[2]) };
};

// Usado tanto por el asistente (validación mientras el usuario escribe) como
// por crearEmpleadoEnPosicion (revalidación justo antes de confirmar).
const buscarEmpleadoPorDocumento = (tipoDocumento, numeroDocumento) => {
  if (!numeroDocumento || !numeroDocumento.trim()) return null;
  const numero = _normalizarDocumento(numeroDocumento);
  const tipo = (tipoDocumento || '').toUpperCase();
  return (window.EMPLOYEES || []).find((e) => {
    const parsed = _parseDocumentoEmpleado(e);
    return parsed.tipo === tipo && parsed.numero === numero;
  }) || null;
};

// Estructuralmente activa + vacante — sin evaluar estadoCobertura ni usar el
// estado "reservada" (no existe en el modelo de datos).
const getPosicionesDisponiblesParaContratacion = () => POSICIONES_APROBADAS.filter(p =>
  p.estadoEstructural === 'activa' && p.estadoOcupacion === 'vacante'
);

const crearEmpleadoEnPosicion = (input) => {
  const {
    posicionCodigo, tipoDocumento, numeroDocumento, primerNombre, otrosNombres,
    primerApellido, segundoApellido, telefono, email, fechaIngreso,
    tipoContrato, fechaFinContrato, tipoEmpleado, salario, grupoNomina, horario, modalidad,
  } = input;

  // 1) Validaciones — todas antes de tocar cualquier arreglo compartido.
  const posicion = POSICIONES_APROBADAS.find(p => p.codigo === posicionCodigo);
  if (!posicion) return { error: 'La posición seleccionada ya no existe.' };
  if (posicion.estadoEstructural !== 'activa' || posicion.estadoOcupacion !== 'vacante') {
    return { error: 'La posición seleccionada ya no se encuentra disponible. Selecciona otra posición para continuar.' };
  }
  if (!numeroDocumento || !numeroDocumento.trim()) return { error: 'El número de documento es obligatorio.' };
  if (!primerNombre || !primerNombre.trim()) return { error: 'El primer nombre es obligatorio.' };
  if (!primerApellido || !primerApellido.trim()) return { error: 'El primer apellido es obligatorio.' };
  if (!telefono || !telefono.trim()) return { error: 'El teléfono personal es obligatorio.' };
  if (!fechaIngreso) return { error: 'La fecha de ingreso es obligatoria.' };

  const hoyISO = new Date().toISOString().slice(0, 10);
  if (fechaIngreso > hoyISO) return { error: 'La fecha de ingreso no puede ser posterior a la fecha actual.' };

  const existente = buscarEmpleadoPorDocumento(tipoDocumento, numeroDocumento);
  if (existente) {
    if (existente.status === 'activo' || existente.status === 'onboarding') {
      return { error: 'Ya existe un empleado activo con este tipo y número de documento.' };
    }
    return { error: 'Esta persona cuenta con un historial laboral previo. No es posible crear un registro duplicado.' };
  }

  // 2) Mutaciones — se aplican juntas, solo si todo lo anterior pasó.
  const nombreCompleto = [primerNombre, otrosNombres, primerApellido, segundoApellido]
    .map(s => (s || '').trim()).filter(Boolean).join(' ');

  const empleado = {
    id: _nextEmpleadoId(),
    initials: _initialesDeNombre(`${primerNombre} ${primerApellido}`),
    name: nombreCompleto,
    doc: `${tipoDocumento} ${numeroDocumento.trim()}`,
    tipoDocumento, numeroDocumento: numeroDocumento.trim(),
    role: posicion.cargo, area: posicion.area, site: posicion.sede, country: posicion.pais || 'CO',
    contract: tipoContrato, hire: fechaIngreso, status: 'activo',
    email: (email || '').trim(), phone: telefono.trim(),
    origen: 'Crear empleado',
    positionId: posicion.id, positionCode: posicion.codigo,
    condicionesLaborales: {
      tipoContrato, fechaFinContrato: fechaFinContrato || null, tipoEmpleado: tipoEmpleado || '',
      salario: salario || '', grupoNomina: grupoNomina || '', horario: horario || '', modalidad: modalidad || '',
    },
    usuario: {
      username: _normalizarDocumento(numeroDocumento), estado: 'pendiente',
      canalPrincipal: 'Teléfono personal', telefono: telefono.trim(),
    },
    onboardingStatus: 'iniciado',
    automationStatus: 'generadas',
    sapSyncStatus: 'pendiente',
  };
  window.EMPLOYEES.push(empleado);

  posicion.estadoOcupacion = 'ocupada';
  posicion.estadoCobertura = null;
  posicion.empleadoOcupanteId = empleado.id;
  posicion.empleadoOcupanteNombre = empleado.name;
  posicion.origenOcupacion = 'crear_empleado';
  posicion.origenSolicitudId = null;
  posicion.origenSolicitudTipo = null;

  AUDITORIA_EMPLEADOS_STORE.push({
    id: `AUD-${empleado.id}`, empleadoId: empleado.id, campo: 'creación de empleado',
    valorAnterior: null, valorNuevo: posicion.codigo, tipoCambio: 'creacion',
    fecha: new Date().toISOString(), usuario: 'Carolina Restrepo S.', rol: 'admin',
    origen: 'Crear empleado', estadoSincronizacion: 'pendiente', idOperacion: `SYNC-${empleado.id}`,
  });

  if (typeof window !== 'undefined' && window.dispatchEvent) {
    window.dispatchEvent(new CustomEvent('atica:employee-created', { detail: { empleado, posicion } }));
  }

  // Sincronización SAP simulada — asíncrona, no bloquea la creación ya
  // confirmada. Un error de integración nunca revierte lo guardado en SOMOS.
  setTimeout(() => {
    empleado.sapSyncStatus = Math.random() < 0.15 ? 'fallida' : 'sincronizado';
    if (typeof window !== 'undefined' && window.dispatchEvent) {
      window.dispatchEvent(new CustomEvent('atica:employee-sap-sync', { detail: { empleadoId: empleado.id, status: empleado.sapSyncStatus } }));
    }
  }, 2200);

  return { empleado, posicion };
};

window.OrgDataStore = {
  getCargos: () => CARGOS_MAESTRO,
  getCargoById: (id) => CARGOS_MAESTRO.find(c => c.id === id),
  getPosiciones: () => POSICIONES_APROBADAS,
  getPosicionByCodigo: (codigo) => POSICIONES_APROBADAS.find(p => p.codigo === codigo),
  getHeadcountPorCargo,
  getSolicitudes: () => SOLICITUDES_PERSONAL_STORE,
  getSolicitudById: (id) => SOLICITUDES_PERSONAL_STORE.find(s => s.id === id),
  getRequisicionById: (id) => REQUISICIONES_ATS_STORE.find(r => r.id === id),
  getRequisicionPorPosicion: (codigo) => REQUISICIONES_ATS_STORE.find(r => r.posicionCodigo === codigo),
  crearSolicitudAmpliacion,
  decidirEtapaSolicitud,
  registrarCodigoRQ,
  createApprovedPosition,
  simularFlujoCompletoDemo,
  isAlertaNuevaPosicionDescartada: () => _alertaNuevaPosicionDescartada,
  descartarAlertaNuevaPosicion: () => { _alertaNuevaPosicionDescartada = true; },
  getSolicitudesDisponiblesParaCobertura,
  getPosicionesCompatibles,
  contratarEmpleadoDirecto,
  getPosicionesDisponiblesParaContratacion,
  crearEmpleadoEnPosicion,
  buscarEmpleadoPorDocumento,
  getAuditoriaEmpleados: () => AUDITORIA_EMPLEADOS_STORE,
};
window.AMPLIACION_NOMINA_CARGO_EXISTENTE_ETAPAS = AMPLIACION_NOMINA_CARGO_EXISTENTE_ETAPAS;
window.badgesDeEmpleado        = badgesDeEmpleado;
