// Dashboard — Inicio, con contenido condicionado por rol principal
// (Gestión Humana / Jefe / Colaborador). Ver nota de mapeo de roles junto a
// DASH_ROLE_META, cerca del final del archivo.
//
// Dos universos de fecha conviven aquí, a propósito:
//  1) "Estado del personal" (exclusivo de Gestión Humana) sigue anclado al
//     corte estático 5 de mayo de 2026 — es un snapshot curado de headcount
//     por posición (mismo modelo del Organigrama), no un feed de eventos con
//     fecha real, así que no tiene sentido moverlo con el reloj real.
//  2) El resto del Dashboard (saludo, KPI de GH/Jefe, Empleados nuevos,
//     Últimas publicaciones, Cumpleaños) usa la fecha/hora real del
//     navegador normalizada a America/Bogota — así el saludo, las ventanas
//     móviles de 15 días y los cumpleaños siempre son coherentes con "hoy",
//     sin importar cuándo se abra el prototipo. Los mocks de esa segunda
//     mitad (DASH_EMPLOYEES, PUBLICACIONES, AUSENCIAS_EQUIPO) se generan
//     como offsets relativos a esa fecha real — ver bloque "Fecha y hora".

// ─────────────────────────────────────────── Fecha y hora (America/Bogota) ─
const _bogotaField = (opts) => new Intl.DateTimeFormat('es-CO', { timeZone: 'America/Bogota', ...opts }).format(new Date());
const _bogotaParts = () => {
  const fmt = new Intl.DateTimeFormat('en-CA', {
    timeZone: 'America/Bogota', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', hour12: false,
  });
  const parts = {};
  fmt.formatToParts(new Date()).forEach(p => { if (p.type !== 'literal') parts[p.type] = p.value; });
  return parts;
};
const _dashHoyParts = _bogotaParts();
// Medianoche (UTC) del día calendario de hoy en Bogotá — ancla estable para
// toda aritmética de días (Bogotá no observa horario de verano).
const DASH_HOY = new Date(Date.UTC(+_dashHoyParts.year, +_dashHoyParts.month - 1, +_dashHoyParts.day));
const DASH_HOY_HORA = +_dashHoyParts.hour;

const _addDays = (date, n) => { const d = new Date(date.getTime()); d.setUTCDate(d.getUTCDate() + n); return d; };
const _fmtDDMMYYYY = (date) => `${String(date.getUTCDate()).padStart(2, '0')}/${String(date.getUTCMonth() + 1).padStart(2, '0')}/${date.getUTCFullYear()}`;
const _daysBetween = (a, b) => Math.round((b.getTime() - a.getTime()) / 86400000);
// Ventana móvil de N días calendario incluyendo hoy: [hoy-(n-1), hoy].
const _dentroDeVentana = (fecha, dias) => { const diff = _daysBetween(fecha, DASH_HOY); return diff >= 0 && diff < dias; };
const _cubreHoy = (inicio, fin) => inicio.getTime() <= DASH_HOY.getTime() && DASH_HOY.getTime() <= fin.getTime();

const DASH_WEEKDAY = _bogotaField({ weekday: 'long' });
const DASH_DAY = _bogotaField({ day: 'numeric' });
const DASH_MONTH_LONG = _bogotaField({ month: 'long' });
const DASH_YEAR = _bogotaField({ year: 'numeric' });
const DASH_FECHA_LARGA = `${DASH_WEEKDAY} ${DASH_DAY} de ${DASH_MONTH_LONG} de ${DASH_YEAR}`;
const DASH_DIA_MES_CORTO = `${DASH_DAY} de ${DASH_MONTH_LONG}`;
const _saludoPorHora = () => (DASH_HOY_HORA < 12 ? 'Buenos días' : DASH_HOY_HORA < 19 ? 'Buenas tardes' : 'Buenas noches');

// ─────────────────────────────────────────── Movimientos mensuales ─────────
// Ingresos y retiros reales del periodo, por empresa. Única fuente para
// "Evolución de personal" en Estado del personal (snapshot estático, ver
// nota de arriba — no se usa para los KPI de GH/Jefe, que son dinámicos).
const DASH_MESES = ['Dic', 'Ene', 'Feb', 'Mar', 'Abr', 'May'];
const MOVIMIENTOS_MENSUALES = [
  { mes: 'Dic', empresa: 'ASEI', ingresos: 6, retiros: 3 },
  { mes: 'Dic', empresa: 'ECOENTORNO', ingresos: 5, retiros: 3 },
  { mes: 'Dic', empresa: 'ECOLOGISTICA', ingresos: 1, retiros: 1 },
  { mes: 'Dic', empresa: 'IA4 SAS', ingresos: 1, retiros: 1 },
  { mes: 'Dic', empresa: 'BYC', ingresos: 1, retiros: 0 },

  { mes: 'Ene', empresa: 'ASEI', ingresos: 7, retiros: 3 },
  { mes: 'Ene', empresa: 'ECOENTORNO', ingresos: 6, retiros: 2 },
  { mes: 'Ene', empresa: 'ECOLOGISTICA', ingresos: 1, retiros: 1 },
  { mes: 'Ene', empresa: 'IA4 SAS', ingresos: 1, retiros: 1 },
  { mes: 'Ene', empresa: 'BYC', ingresos: 1, retiros: 0 },

  { mes: 'Feb', empresa: 'ASEI', ingresos: 5, retiros: 4 },
  { mes: 'Feb', empresa: 'ECOENTORNO', ingresos: 4, retiros: 3 },
  { mes: 'Feb', empresa: 'ECOLOGISTICA', ingresos: 1, retiros: 1 },
  { mes: 'Feb', empresa: 'IA4 SAS', ingresos: 1, retiros: 1 },
  { mes: 'Feb', empresa: 'BYC', ingresos: 1, retiros: 0 },

  { mes: 'Mar', empresa: 'ASEI', ingresos: 8, retiros: 2 },
  { mes: 'Mar', empresa: 'ECOENTORNO', ingresos: 8, retiros: 2 },
  { mes: 'Mar', empresa: 'ECOLOGISTICA', ingresos: 2, retiros: 1 },
  { mes: 'Mar', empresa: 'IA4 SAS', ingresos: 1, retiros: 0 },
  { mes: 'Mar', empresa: 'BYC', ingresos: 1, retiros: 0 },

  { mes: 'Abr', empresa: 'ASEI', ingresos: 6, retiros: 3 },
  { mes: 'Abr', empresa: 'ECOENTORNO', ingresos: 6, retiros: 2 },
  { mes: 'Abr', empresa: 'ECOLOGISTICA', ingresos: 1, retiros: 1 },
  { mes: 'Abr', empresa: 'IA4 SAS', ingresos: 1, retiros: 0 },
  { mes: 'Abr', empresa: 'BYC', ingresos: 1, retiros: 0 },

  { mes: 'May', empresa: 'ASEI', ingresos: 7, retiros: 3 },
  { mes: 'May', empresa: 'ECOENTORNO', ingresos: 7, retiros: 2 },
  { mes: 'May', empresa: 'ECOLOGISTICA', ingresos: 2, retiros: 1 },
  { mes: 'May', empresa: 'IA4 SAS', ingresos: 1, retiros: 0 },
  { mes: 'May', empresa: 'BYC', ingresos: 1, retiros: 0 },
];

const _movPorMes = (empresa) => DASH_MESES.map((mes) => {
  const filas = MOVIMIENTOS_MENSUALES.filter(m => m.mes === mes && (empresa === 'todas' || m.empresa === empresa));
  return { mes, ingresos: filas.reduce((s, f) => s + f.ingresos, 0), retiros: filas.reduce((s, f) => s + f.retiros, 0) };
});

// ─────────────────────────────────────────── Headcount por posición ─────────
// Grupos organizacionales curados a mano; cada uno se expande en posiciones
// individuales (una fila = una posición, nunca "cantidad" en un solo registro).
// autorizado = ocupado + vacante siempre, por construcción. Las 4 categorías de
// vacante (disponible, reservada, en_reclutamiento, bloqueada) son mutuamente
// excluyentes y suman exactamente el total de vacantes de cada grupo. Todas las
// posiciones de este conjunto están estructuralmente activas — es decir, una
// posición "bloqueada" aquí es una vacante autorizada sin cobertura habilitada,
// no una posición inactiva; sigue contando dentro del headcount autorizado.
const EP_GRUPOS = [
  { sociedad: 'ASEI', regional: 'Regional Occidente', oficina: '2201 — ASEI Cali', area: 'Operaciones', cargo: 'Operario de Clasificación', autorizado: 18, ocupado: 15, reservada: 1, reclutamiento: 1, bloqueada: 0 },
  { sociedad: 'ASEI', regional: 'Regional Occidente', oficina: '2201 — ASEI Cali', area: 'Mantenimiento', cargo: 'Téc. Mantenimiento', autorizado: 6, ocupado: 5, reservada: 0, reclutamiento: 1, bloqueada: 0 },
  { sociedad: 'ASEI', regional: 'Regional Caribe', oficina: '2302 — ASEI Barranquilla', area: 'Operaciones', cargo: 'Operario de Clasificación', autorizado: 10, ocupado: 8, reservada: 1, reclutamiento: 0, bloqueada: 0 },
  { sociedad: 'ASEI', regional: 'Regional Caribe', oficina: '2302 — ASEI Barranquilla', area: 'Recolección', cargo: 'Conductor', autorizado: 8, ocupado: 7, reservada: 0, reclutamiento: 1, bloqueada: 0 },
  { sociedad: 'ASEI', regional: 'Regional Centro', oficina: '2101 — ASEI Bogotá', area: 'Administración', cargo: 'Aux. Recursos Humanos', autorizado: 5, ocupado: 5, reservada: 0, reclutamiento: 0, bloqueada: 0 },
  { sociedad: 'ASEI', regional: 'Regional Centro', oficina: '2101 — ASEI Bogotá', area: 'Compras', cargo: 'Coord. Compras', autorizado: 3, ocupado: 2, reservada: 0, reclutamiento: 1, bloqueada: 0 },
  { sociedad: 'ASEI', regional: 'Regional Occidente', oficina: '2201 — ASEI Cali', area: 'Comercial', cargo: 'Líder Comercial', autorizado: 4, ocupado: 3, reservada: 0, reclutamiento: 1, bloqueada: 0 },
  { sociedad: 'ASEI', regional: 'Regional Occidente', oficina: '2201 — ASEI Cali', area: 'Administración', cargo: 'Jefe de RRHH', autorizado: 1, ocupado: 1, reservada: 0, reclutamiento: 0, bloqueada: 0 },

  { sociedad: 'ECOENTORNO', regional: 'Regional Occidente', oficina: '2401 — ECOENTORNO Cali', area: 'Aprovechamiento', cargo: 'Op. Aprovechamiento', autorizado: 22, ocupado: 19, reservada: 1, reclutamiento: 1, bloqueada: 0 },
  { sociedad: 'ECOENTORNO', regional: 'Regional Occidente', oficina: '2402 — ECOENTORNO Yumbo', area: 'Aprovechamiento', cargo: 'Op. Aprovechamiento', autorizado: 14, ocupado: 12, reservada: 1, reclutamiento: 0, bloqueada: 1 },
  { sociedad: 'ECOENTORNO', regional: 'Regional Occidente', oficina: '2402 — ECOENTORNO Yumbo', area: 'Aprovechamiento', cargo: 'Líder de Aprovechamiento', autorizado: 3, ocupado: 3, reservada: 0, reclutamiento: 0, bloqueada: 0 },
  { sociedad: 'ECOENTORNO', regional: 'Regional Antioquia', oficina: '2501 — ECOENTORNO Medellín', area: 'Calidad', cargo: 'Aux. Calidad', autorizado: 6, ocupado: 5, reservada: 0, reclutamiento: 1, bloqueada: 0 },
  { sociedad: 'ECOENTORNO', regional: 'Regional Centro', oficina: '2403 — ECOENTORNO Bogotá', area: 'Comercial', cargo: 'Líder Comercial', autorizado: 3, ocupado: 3, reservada: 0, reclutamiento: 0, bloqueada: 0 },

  { sociedad: 'ECOLOGISTICA', regional: 'Regional Occidente', oficina: '2601 — ECOLOGISTICA Cali', area: 'Mantenimiento', cargo: 'Téc. Mantenimiento', autorizado: 5, ocupado: 4, reservada: 0, reclutamiento: 1, bloqueada: 0 },
  { sociedad: 'ECOLOGISTICA', regional: 'Regional Caribe', oficina: '2602 — ECOLOGISTICA Barranquilla', area: 'Recolección', cargo: 'Conductor', autorizado: 9, ocupado: 8, reservada: 1, reclutamiento: 0, bloqueada: 0 },

  { sociedad: 'IA4 SAS', regional: 'Regional Centro', oficina: '2701 — IA4 SAS Bogotá', area: 'Compras', cargo: 'Coord. Compras', autorizado: 4, ocupado: 3, reservada: 0, reclutamiento: 1, bloqueada: 0 },
  { sociedad: 'IA4 SAS', regional: 'Regional Antioquia', oficina: '2702 — IA4 SAS Medellín', area: 'Calidad', cargo: 'Aux. Calidad', autorizado: 5, ocupado: 4, reservada: 1, reclutamiento: 0, bloqueada: 0 },

  { sociedad: 'BYC', regional: 'Regional Internacional', oficina: '2801 — BYC Santo Domingo', area: 'Administración', cargo: 'Líder de Sede', autorizado: 3, ocupado: 3, reservada: 0, reclutamiento: 0, bloqueada: 0 },
  { sociedad: 'BYC', regional: 'Regional Internacional', oficina: '2801 — BYC Santo Domingo', area: 'Operaciones', cargo: 'Operario de Clasificación', autorizado: 6, ocupado: 5, reservada: 0, reclutamiento: 0, bloqueada: 0 },
];

const _NOMBRES_POOL = [
  'Andrés Felipe Rojas', 'Laura Camila Torres', 'Julián Esteban Gómez', 'María Paula Restrepo',
  'Carlos Iván Muñoz', 'Diana Marcela López', 'Jorge Luis Cárdenas', 'Natalia Andrea Pérez',
  'Sebastián Ortiz Vargas', 'Valentina Duarte Rincón', 'Camilo Andrés Salazar', 'Paula Andrea Ríos',
  'Felipe Santiago Bermúdez', 'Mónica Lucía Aguirre', 'Kevin Steven Guerrero', 'Tatiana Marcela Sánchez',
];
let _nombreCursor = 0;
const _siguienteNombre = () => _NOMBRES_POOL[_nombreCursor++ % _NOMBRES_POOL.length];
const _inicialesDe = (nombre) => { const partes = nombre.split(' '); return (partes[0][0] + partes[partes.length - 1][0]).toUpperCase(); };

let _posSeq = 20001;
const _expandirGrupoHeadcount = (g) => {
  const filas = [];
  const base = { sociedad: g.sociedad, regional: g.regional, oficina: g.oficina, area: g.area, cargo: g.cargo };
  for (let i = 0; i < g.ocupado; i++) {
    const nombre = _siguienteNombre();
    filas.push({ ...base, codigo: `POS-${_posSeq++}`, estadoOcupacion: 'ocupada', estadoCobertura: null, ocupanteNombre: nombre, ocupanteIniciales: _inicialesDe(nombre) });
  }
  const vacante = g.autorizado - g.ocupado;
  const disponible = Math.max(0, vacante - (g.reservada || 0) - (g.reclutamiento || 0) - (g.bloqueada || 0));
  const coberturas = [
    ...Array(g.reservada || 0).fill('reservada'),
    ...Array(g.reclutamiento || 0).fill('en_reclutamiento'),
    ...Array(g.bloqueada || 0).fill('bloqueada'),
    ...Array(disponible).fill('disponible'),
  ];
  coberturas.forEach((cov) => {
    filas.push({ ...base, codigo: `POS-${_posSeq++}`, estadoOcupacion: 'vacante', estadoCobertura: cov, ocupanteNombre: null, ocupanteIniciales: null });
  });
  return filas;
};

const HEADCOUNT_POSICIONES = EP_GRUPOS.flatMap(_expandirGrupoHeadcount);

const EP_ESTADO_META = {
  ocupada: { label: 'Ocupada', pill: 'green' },
  disponible: { label: 'Vacante disponible', pill: 'rose' },
  reservada: { label: 'Vacante reservada', pill: 'amber-soft' },
  en_reclutamiento: { label: 'En reclutamiento', pill: 'navy' },
  bloqueada: { label: 'Bloqueada', pill: '' },
};
const _epEstadoKey = (p) => (p.estadoOcupacion === 'ocupada' ? 'ocupada' : p.estadoCobertura);

// ─────────────────────────────────────────── Onboarding (KPI de GH) ─────────
const DASH_ONBOARDINGS = [
  { initials: 'AM', name: 'Andrés Felipe Mejía', role: 'Op. Clasificación', area: 'Operaciones',     site: 'Cali',         pct: 18, days: 1,  status: 'iniciando' },
  { initials: 'LG', name: 'Laura Gómez',         role: 'Operaria Planta',   area: 'Operaciones',     site: 'Cali',         pct: 35, days: 3,  status: 'normal' },
  { initials: 'JR', name: 'Juan David Ruiz',     role: 'Aux. Calidad',      area: 'Calidad',         site: 'Barranquilla', pct: 57, days: 5,  status: 'normal' },
  { initials: 'SO', name: 'Sandra Milena Ortiz', role: 'Coord. Compras',    area: 'Compras',         site: 'Bogotá',       pct: 71, days: 8,  status: 'normal' },
  { initials: 'DT', name: 'Diego Toro',          role: 'Aux. Aprovechamiento', area: 'Aprovechamiento', site: 'Cali',      pct: 89, days: 11, status: 'normal' },
  { initials: 'PV', name: 'Paula Valencia',      role: 'Mantenimiento',     area: 'Mantenimiento',   site: 'Cali',         pct: 45, days: 14, status: 'atrasado' },
  { initials: 'NC', name: 'Nicolás Cárdenas',    role: 'Conductor',         area: 'Recolección',     site: 'Barranquilla', pct: 92, days: 13, status: 'cierre' },
];

// ─────────────────────────────────────────── Empleados (roster local del Dashboard) ─
// Fuente propia para saludo / KPI de GH y Jefe / Empleados nuevos / Cumpleaños.
// No reutiliza window.EMPLOYEES (employee-list.jsx) porque ese roster no trae
// jerarquía (jefeId), sociedad/oficina en el formato de Estado del personal,
// ni fecha de cumpleaños — y sus fechas de ingreso son fijas a 2026, lo que
// rompería las ventanas móviles de 15 días si "hoy" es la fecha real. Aquí
// las fechas de ingreso/retiro/cumpleaños se generan como offsets desde
// DASH_HOY para que el prototipo siempre tenga contenido representativo.
// Documentado como simplificación explícita (ver CLAUDE.md / vista de este
// módulo) — no se duplica para inflar ningún KPI, cada persona aparece una
// sola vez y alimenta todas las secciones por igual.
const DASH_EMPLOYEES = [
  // ── Las 3 personas ancla de los roles del Dashboard ──
  { id: 'gh-carolina', name: 'Carolina Restrepo', initials: 'CR', cargo: 'Jefe de RRHH', area: 'Administración', sociedad: 'ASEI', oficina: '2201 — ASEI Cali', jefeId: null, estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -2500), fechaRetiro: null, cumpleDia: 14, cumpleMes: 11, mostrarCumple: true, grupos: [] },
  { id: 'jefe-luis', name: 'Luis Miguel Marín', initials: 'LM', cargo: 'Líder de Aprovechamiento', area: 'Aprovechamiento', sociedad: 'ECOENTORNO', oficina: '2401 — ECOENTORNO Cali', jefeId: 'gh-carolina', estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -1800), fechaRetiro: null, cumpleDia: 3, cumpleMes: 2, mostrarCumple: true, grupos: ['comite-sst'] },
  { id: 'colab-andres', name: 'Andrés Felipe Mejía', initials: 'AM', cargo: 'Operario de Clasificación', area: 'Operaciones', sociedad: 'ASEI', oficina: '2201 — ASEI Cali', jefeId: null, estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -3), fechaRetiro: null, cumpleDia: 21, cumpleMes: 8, mostrarCumple: true, grupos: ['brigada-emergencia'] },

  // ── Equipo de Luis Miguel Marín — 2 niveles (para "mi equipo") ──
  { id: 'eq-diego', name: 'Diego Andrés Toro', initials: 'DT', cargo: 'Aux. Aprovechamiento', area: 'Aprovechamiento', sociedad: 'ECOENTORNO', oficina: '2401 — ECOENTORNO Cali', jefeId: 'jefe-luis', estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -400), fechaRetiro: null, cumpleDia: 9, cumpleMes: 6, mostrarCumple: true },
  { id: 'eq-maria', name: 'María Fernanda López', initials: 'ML', cargo: 'Op. Aprovechamiento', area: 'Aprovechamiento', sociedad: 'ECOENTORNO', oficina: '2401 — ECOENTORNO Cali', jefeId: 'jefe-luis', estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -600), fechaRetiro: null, cumpleDia: 17, cumpleMes: 1, mostrarCumple: true },
  { id: 'eq-kevin', name: 'Kevin Steven Guerrero', initials: 'KG', cargo: 'Op. Aprovechamiento', area: 'Aprovechamiento', sociedad: 'ECOENTORNO', oficina: '2401 — ECOENTORNO Cali', jefeId: 'jefe-luis', estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -10), fechaRetiro: null, cumpleDia: 5, cumpleMes: 12, mostrarCumple: true },
  { id: 'eq-tatiana', name: 'Tatiana Marcela Sánchez', initials: 'TS', cargo: 'Op. Aprovechamiento', area: 'Aprovechamiento', sociedad: 'ECOENTORNO', oficina: '2401 — ECOENTORNO Cali', jefeId: 'eq-maria', estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -200), fechaRetiro: null, cumpleDia: DASH_HOY.getUTCDate(), cumpleMes: DASH_HOY.getUTCMonth() + 1, mostrarCumple: true },
  { id: 'eq-camilo', name: 'Camilo Andrés Salazar', initials: 'CS', cargo: 'Op. Aprovechamiento', area: 'Aprovechamiento', sociedad: 'ECOENTORNO', oficina: '2401 — ECOENTORNO Cali', jefeId: 'eq-maria', estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -90), fechaRetiro: null, cumpleDia: 28, cumpleMes: 3, mostrarCumple: true },
  { id: 'eq-paula', name: 'Paula Andrea Ríos', initials: 'PR', cargo: 'Aux. Aprovechamiento', area: 'Aprovechamiento', sociedad: 'ECOENTORNO', oficina: '2401 — ECOENTORNO Cali', jefeId: 'eq-diego', estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -150), fechaRetiro: null, cumpleDia: 30, cumpleMes: 9, mostrarCumple: true },
  { id: 'eq-felipe', name: 'Felipe Santiago Bermúdez', initials: 'FB', cargo: 'Aux. Aprovechamiento', area: 'Aprovechamiento', sociedad: 'ECOENTORNO', oficina: '2401 — ECOENTORNO Cali', jefeId: 'eq-diego', estado: 'retirado', fechaIngreso: _addDays(DASH_HOY, -500), fechaRetiro: _addDays(DASH_HOY, -8), cumpleDia: 12, cumpleMes: 7, mostrarCumple: true },

  // ── Resto de la compañía (para KPI globales de GH y Empleados nuevos) ──
  { id: 'gh-laura', name: 'Laura Gómez Restrepo', initials: 'LG', cargo: 'Operaria de Planta', area: 'Operaciones', sociedad: 'ASEI', oficina: '2201 — ASEI Cali', jefeId: null, estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -1), fechaRetiro: null, cumpleDia: DASH_HOY.getUTCDate(), cumpleMes: DASH_HOY.getUTCMonth() + 1, mostrarCumple: true },
  { id: 'gh-juan', name: 'Juan David Ruiz', initials: 'JR', cargo: 'Auxiliar de Calidad', area: 'Calidad', sociedad: 'ECOLOGISTICA', oficina: '2602 — ECOLOGISTICA Barranquilla', jefeId: null, estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -5), fechaRetiro: null, cumpleDia: 19, cumpleMes: 4, mostrarCumple: true },
  { id: 'gh-sandra', name: 'Sandra Milena Ortiz', initials: 'SO', cargo: 'Coord. Compras', area: 'Compras', sociedad: 'IA4 SAS', oficina: '2701 — IA4 SAS Bogotá', jefeId: null, estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -7), fechaRetiro: null, cumpleDia: 2, cumpleMes: 10, mostrarCumple: true },
  { id: 'gh-valentina', name: 'Valentina Castaño', initials: 'VC', cargo: 'Aux. Aprovechamiento', area: 'Aprovechamiento', sociedad: 'ECOENTORNO', oficina: '2402 — ECOENTORNO Yumbo', jefeId: null, estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -9), fechaRetiro: null, cumpleDia: 8, cumpleMes: 5, mostrarCumple: true },
  { id: 'gh-jhonatan', name: 'Jhonatan Herrera', initials: 'JH', cargo: 'Conductor', area: 'Recolección', sociedad: 'ECOLOGISTICA', oficina: '2602 — ECOLOGISTICA Barranquilla', jefeId: null, estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -12), fechaRetiro: null, cumpleDia: 25, cumpleMes: 6, mostrarCumple: true },
  { id: 'gh-manuela', name: 'Manuela Marín', initials: 'MM', cargo: 'Analista SST', area: 'Administración', sociedad: 'ASEI', oficina: '2101 — ASEI Bogotá', jefeId: null, estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -13), fechaRetiro: null, cumpleDia: 11, cumpleMes: 3, mostrarCumple: true },
  { id: 'gh-esteban', name: 'Esteban Ossa', initials: 'EO', cargo: 'Op. Clasificación', area: 'Operaciones', sociedad: 'ASEI', oficina: '2201 — ASEI Cali', jefeId: null, estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -14), fechaRetiro: null, cumpleDia: 6, cumpleMes: 1, mostrarCumple: true },
  { id: 'gh-karen', name: 'Karen Rentería', initials: 'KR', cargo: 'Aux. Bodega', area: 'Operaciones', sociedad: 'ASEI', oficina: '2201 — ASEI Cali', jefeId: null, estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -20), fechaRetiro: null, cumpleDia: 23, cumpleMes: 8, mostrarCumple: false },

  // ── Retiros recientes (para el KPI de retirados y como histórico) ──
  { id: 'gh-retirado1', name: 'Mónica Lucía Aguirre', initials: 'MA', cargo: 'Aux. Administrativo', area: 'Administración', sociedad: 'ASEI', oficina: '2101 — ASEI Bogotá', jefeId: null, estado: 'retirado', fechaIngreso: _addDays(DASH_HOY, -700), fechaRetiro: _addDays(DASH_HOY, -3), cumpleDia: 15, cumpleMes: 2, mostrarCumple: true },
  { id: 'gh-retirado2', name: 'Jorge Luis Cárdenas', initials: 'JC', cargo: 'Conductor', area: 'Recolección', sociedad: 'ECOLOGISTICA', oficina: '2602 — ECOLOGISTICA Barranquilla', jefeId: null, estado: 'retirado', fechaIngreso: _addDays(DASH_HOY, -900), fechaRetiro: _addDays(DASH_HOY, -11), cumpleDia: 7, cumpleMes: 9, mostrarCumple: true },
  { id: 'gh-retirado3', name: 'Natalia Andrea Pérez', initials: 'NP', cargo: 'Operaria de Clasificación', area: 'Operaciones', sociedad: 'ASEI', oficina: '2201 — ASEI Cali', jefeId: null, estado: 'retirado', fechaIngreso: _addDays(DASH_HOY, -1200), fechaRetiro: _addDays(DASH_HOY, -40), cumpleDia: 4, cumpleMes: 12, mostrarCumple: true },

  // ── Casos borde intencionales (ingreso futuro / registro anulado / cumple oculto) ──
  { id: 'gh-futuro', name: 'Sebastián Ortiz Vargas', initials: 'SV', cargo: 'Operario de Clasificación', area: 'Operaciones', sociedad: 'ASEI', oficina: '2201 — ASEI Cali', jefeId: null, estado: 'activo', fechaIngreso: _addDays(DASH_HOY, 5), fechaRetiro: null, cumpleDia: 1, cumpleMes: 1, mostrarCumple: true },
  { id: 'gh-anulado', name: 'Julián Esteban Gómez', initials: 'JG', cargo: 'Operario de Clasificación', area: 'Operaciones', sociedad: 'ASEI', oficina: '2201 — ASEI Cali', jefeId: null, estado: 'anulado', fechaIngreso: _addDays(DASH_HOY, -2), fechaRetiro: null, cumpleDia: 10, cumpleMes: 10, mostrarCumple: true },
  { id: 'gh-privado', name: 'Ricardo Andrés Vanegas', initials: 'RV', cargo: 'Op. Clasificación', area: 'Operaciones', sociedad: 'ASEI', oficina: '2201 — ASEI Cali', jefeId: null, estado: 'activo', fechaIngreso: _addDays(DASH_HOY, -100), fechaRetiro: null, cumpleDia: DASH_HOY.getUTCDate(), cumpleMes: DASH_HOY.getUTCMonth() + 1, mostrarCumple: false },
];

const DASH_EMPLEADOS_POR_ID = Object.fromEntries(DASH_EMPLOYEES.map(e => [e.id, e]));

// Todos los descendientes (directos + indirectos) de un empleado, sin
// duplicados y a salvo de ciclos accidentales en el mock — recorrido
// iterativo con set de visitados. Reutilizable para cualquier jefe.
const getDescendants = (empId, empleados = DASH_EMPLOYEES) => {
  const hijosDe = {};
  empleados.forEach(e => { if (e.jefeId) (hijosDe[e.jefeId] = hijosDe[e.jefeId] || []).push(e); });
  const visitados = new Set();
  const resultado = [];
  const pila = [...(hijosDe[empId] || [])];
  while (pila.length) {
    const e = pila.pop();
    if (visitados.has(e.id)) continue;
    visitados.add(e.id);
    resultado.push(e);
    (hijosDe[e.id] || []).forEach(hijo => { if (!visitados.has(hijo.id)) pila.push(hijo); });
  }
  return resultado;
};

// ─────────────────────────────────────────── Equipo de Luis Miguel Marín ─────
// Posiciones y solicitudes acotadas al equipo del Jefe — modelo local y
// pequeño a propósito: HEADCOUNT_POSICIONES (arriba) no tiene jefe asociado
// a cada posición, así que no sirve para "ocupación de mi equipo". Documentado
// como simplificación explícita, igual que DASH_EMPLOYEES.
const TEAM_POSICIONES_LUIS = [
  { codigo: 'TPOS-01', cargo: 'Op. Aprovechamiento', estadoEstructural: 'activa', estadoOcupacion: 'ocupada', ocupanteId: 'eq-maria' },
  { codigo: 'TPOS-02', cargo: 'Op. Aprovechamiento', estadoEstructural: 'activa', estadoOcupacion: 'ocupada', ocupanteId: 'eq-kevin' },
  { codigo: 'TPOS-03', cargo: 'Op. Aprovechamiento', estadoEstructural: 'activa', estadoOcupacion: 'ocupada', ocupanteId: 'eq-tatiana' },
  { codigo: 'TPOS-04', cargo: 'Op. Aprovechamiento', estadoEstructural: 'activa', estadoOcupacion: 'ocupada', ocupanteId: 'eq-camilo' },
  { codigo: 'TPOS-05', cargo: 'Op. Aprovechamiento', estadoEstructural: 'activa', estadoOcupacion: 'vacante', estadoCobertura: 'disponible', ocupanteId: null },
  { codigo: 'TPOS-06', cargo: 'Op. Aprovechamiento', estadoEstructural: 'activa', estadoOcupacion: 'vacante', estadoCobertura: 'en_reclutamiento', ocupanteId: null },
  { codigo: 'TPOS-07', cargo: 'Aux. Aprovechamiento', estadoEstructural: 'activa', estadoOcupacion: 'ocupada', ocupanteId: 'eq-diego' },
  { codigo: 'TPOS-08', cargo: 'Aux. Aprovechamiento', estadoEstructural: 'activa', estadoOcupacion: 'ocupada', ocupanteId: 'eq-paula' },
  { codigo: 'TPOS-09', cargo: 'Aux. Aprovechamiento', estadoEstructural: 'activa', estadoOcupacion: 'vacante', estadoCobertura: 'disponible', ocupanteId: null }, // antigua posición de Felipe Bermúdez
  { codigo: 'TPOS-10', cargo: 'Aux. Aprovechamiento', estadoEstructural: 'activa', estadoOcupacion: 'vacante', estadoCobertura: 'bloqueada', ocupanteId: null },
  { codigo: 'TPOS-11', cargo: 'Aux. Aprovechamiento', estadoEstructural: 'anulada', estadoOcupacion: 'vacante', estadoCobertura: 'disponible', ocupanteId: null }, // excluida del denominador
];

const SOLICITUDES_PERSONAL_LUIS = [
  { id: 'SOL-2601', posicionCodigo: 'TPOS-05', responsableId: 'jefe-luis', estado: 'enviada', fecha: _addDays(DASH_HOY, -6) },
  { id: 'SOL-2602', posicionCodigo: 'TPOS-06', responsableId: 'jefe-luis', estado: 'en_reclutamiento', fecha: _addDays(DASH_HOY, -18) },
  { id: 'SOL-2603', posicionCodigo: 'TPOS-09', responsableId: 'jefe-luis', estado: 'aprobada_pendiente_cobertura', fecha: _addDays(DASH_HOY, -4) },
  { id: 'SOL-2604', posicionCodigo: 'TPOS-10', responsableId: 'jefe-luis', estado: 'borrador', fecha: _addDays(DASH_HOY, -1) },
  { id: 'SOL-2599', posicionCodigo: 'TPOS-01', responsableId: 'jefe-luis', estado: 'cubierta', fecha: _addDays(DASH_HOY, -95) },
];
const SOLICITUDES_ESTADOS_ACTIVOS = ['enviada', 'en_aprobacion', 'aprobada_pendiente_cobertura', 'en_reclutamiento', 'en_seleccion'];

const AUSENCIAS_EQUIPO_LUIS = [
  { empId: 'eq-camilo', tipo: 'vacaciones', estado: 'aprobada', inicio: _addDays(DASH_HOY, -2), fin: _addDays(DASH_HOY, 3) },
  { empId: 'eq-tatiana', tipo: 'permiso', estado: 'pendiente', inicio: DASH_HOY, fin: DASH_HOY },
  { empId: 'eq-paula', tipo: 'incapacidad', estado: 'aprobada', inicio: _addDays(DASH_HOY, 10), fin: _addDays(DASH_HOY, 15) },
  { empId: 'eq-diego', tipo: 'licencia', estado: 'rechazada', inicio: _addDays(DASH_HOY, -1), fin: _addDays(DASH_HOY, 2) },
];

// ─────────────────────────────────────────── Empleados nuevos (compartido) ─
const NewHires = () => {
  const items = DASH_EMPLOYEES
    .filter(e => e.estado === 'activo' && e.fechaIngreso.getTime() <= DASH_HOY.getTime())
    .sort((a, b) => b.fechaIngreso.getTime() - a.fechaIngreso.getTime())
    .slice(0, 10);

  return (
    <div className="card">
      <div className="card-head">
        <div>
          <h3>Empleados nuevos</h3>
          <div className="sub">Le damos la bienvenida a nuestros nuevos colaboradores</div>
        </div>
      </div>
      <div className="card-body" style={{ paddingTop: 4, paddingBottom: 6 }}>
        {items.length === 0 ? (
          <div className="muted" style={{ fontSize: 12.5, padding: '18px 4px', textAlign: 'center' }}>
            No se han registrado nuevos colaboradores recientemente.
          </div>
        ) : items.map((p) => (
          <div key={p.id} className="list-row">
            <div className="avatar" style={{ width: 32, height: 32, fontSize: 12 }}>{p.initials}</div>
            <div style={{ minWidth: 0, flex: 1 }}>
              <div className="ttl">{p.name}</div>
              <div className="sub">{p.cargo} · {p.sociedad}</div>
            </div>
            <div className="right" style={{ fontSize: 12.5, color: 'var(--text-muted)', fontWeight: 600, whiteSpace: 'nowrap' }}>{_fmtDDMMYYYY(p.fechaIngreso)}</div>
          </div>
        ))}
      </div>
    </div>
  );
};

// ─────────────────────────────────────────── Publicaciones (compartido, con audiencia) ─
// Local a este módulo — comms.jsx guarda su propio COMUNICADOS con un modelo
// de audiencia libre (texto), pensado para su propia UI de "Nuevo comunicado".
// Aquí necesitamos un modelo tipado (global/individual/grupo/sociedad/
// oficina/área) para poder filtrar programáticamente por el usuario activo,
// así que se mantiene un conjunto propio y documentado en vez de forzar ese
// modelo dentro de comms.jsx (mayor riesgo, cero beneficio para este widget).
const PUBLICACIONES = [
  { id: 'PB-01', categoria: 'SG-SST', titulo: 'Reinducción SG-SST 2026 obligatoria', resumen: 'Ingresa al módulo de Capacitaciones y completa la ruta "Reinducción General Atica 2026" antes del cierre de mes.', autor: 'Carolina Restrepo', autorIniciales: 'CR', publishedAt: _addDays(DASH_HOY, -1), state: 'publicado', audienceType: 'global', vigenciaHasta: null, unread: true },
  { id: 'PB-02', categoria: 'SST', titulo: 'Nueva política de dotación para línea de Clasificación', resumen: 'El guante de nitrilo reforzado reemplaza al guante estándar en las líneas de Clasificación y Aprovechamiento.', autor: 'Julián Andrés Pardo', autorIniciales: 'JP', publishedAt: _addDays(DASH_HOY, -3), state: 'publicado', audienceType: 'global', vigenciaHasta: null, unread: true },
  { id: 'PB-03', categoria: 'Institucional', titulo: 'Feliz aniversario, Atica — 12 años transformando residuos', resumen: 'Habrá celebración el próximo viernes en cada sede para todo el equipo de planta, recolección y administración.', autor: 'Diana Isabel Ruiz', autorIniciales: 'DR', publishedAt: _addDays(DASH_HOY, -6), state: 'publicado', audienceType: 'global', vigenciaHasta: null, unread: false },
  { id: 'PB-04', categoria: 'Institucional', titulo: 'Actualización del Reglamento Interno de Trabajo', resumen: 'Tras la Reforma Laboral, revisamos el RIT. Te pedimos leerlo y firmar electrónicamente la constancia de lectura.', autor: 'Carolina Restrepo', autorIniciales: 'CR', publishedAt: _addDays(DASH_HOY, -9), state: 'publicado', audienceType: 'global', vigenciaHasta: null, unread: false },
  { id: 'PB-05', categoria: 'Bienestar', titulo: 'Nuevo convenio con EPS Sura · descuentos en odontología', resumen: '25% de descuento en tratamientos odontológicos para colaboradores y beneficiarios, vigente desde este mes.', autor: 'Patricia Gómez', autorIniciales: 'PG', publishedAt: _addDays(DASH_HOY, -20), state: 'publicado', audienceType: 'global', vigenciaHasta: null, unread: false },
  { id: 'PB-06', categoria: 'Nómina', titulo: 'Cierre del periodo contable — directrices', resumen: 'Toda factura o nota de gasto debe estar radicada antes del cierre del corte quincenal.', autor: 'Carolina Restrepo', autorIniciales: 'CR', publishedAt: _addDays(DASH_HOY, -25), state: 'publicado', audienceType: 'global', vigenciaHasta: null, unread: false },

  { id: 'PB-07', categoria: 'Documentos', titulo: 'Firma pendiente de tu contrato', resumen: 'Tu contrato quedó cargado en Documentos. Complétalo desde el Portal antes de tu quinto día de ingreso.', autor: 'Patricia Gómez', autorIniciales: 'PG', publishedAt: _addDays(DASH_HOY, -2), state: 'publicado', audienceType: 'individual', audienceValue: 'colab-andres', vigenciaHasta: null, unread: true },
  { id: 'PB-08', categoria: 'Comité', titulo: 'Convocatoria reunión COPASST · este mes', resumen: 'Se convoca a los miembros del comité a la sesión ordinaria de seguimiento de indicadores.', autor: 'Julián Andrés Pardo', autorIniciales: 'JP', publishedAt: _addDays(DASH_HOY, -4), state: 'publicado', audienceType: 'grupo', audienceValue: 'comite-sst', vigenciaHasta: null, unread: true },
  { id: 'PB-09', categoria: 'Operaciones', titulo: 'Mantenimiento programado · trituradora 2', resumen: 'Suspensión planificada de la línea industrial el sábado, de 06:00 a 14:00. Turnos reasignados a la jornada nocturna.', autor: 'Luis Miguel Marín', autorIniciales: 'LM', publishedAt: _addDays(DASH_HOY, -5), state: 'publicado', audienceType: 'sociedad', audienceValue: 'ECOENTORNO', vigenciaHasta: null, unread: false },
  { id: 'PB-10', categoria: 'Logística', titulo: 'Cambio de horario del comedor · sede Cali', resumen: 'A partir de la próxima semana el comedor de la oficina 2201 amplía su horario de atención al turno de la tarde.', autor: 'Patricia Gómez', autorIniciales: 'PG', publishedAt: _addDays(DASH_HOY, -7), state: 'publicado', audienceType: 'oficina', audienceValue: '2201 — ASEI Cali', vigenciaHasta: null, unread: false },
  { id: 'PB-11', categoria: 'Operaciones', titulo: 'Nuevo protocolo de manejo de materiales', resumen: 'Se actualiza el protocolo de manejo de materiales para el área de Aprovechamiento, con nuevas señaléticas.', autor: 'Luis Miguel Marín', autorIniciales: 'LM', publishedAt: _addDays(DASH_HOY, -8), state: 'publicado', audienceType: 'area', audienceValue: 'Aprovechamiento', vigenciaHasta: null, unread: false },
  { id: 'PB-12', categoria: 'Documentos', titulo: 'Recordatorio personal de examen periódico', resumen: 'Tu examen médico periódico está próximo a vencer, agenda tu cita desde SST.', autor: 'Julián Andrés Pardo', autorIniciales: 'JP', publishedAt: _addDays(DASH_HOY, -1), state: 'publicado', audienceType: 'individual', audienceValue: 'gh-esteban', vigenciaHasta: null, unread: true },

  { id: 'PB-13', categoria: 'Institucional', titulo: 'Borrador: comunicado de resultados Q3', resumen: 'Texto en edición, aún no aprobado para publicación.', autor: 'Carolina Restrepo', autorIniciales: 'CR', publishedAt: DASH_HOY, state: 'borrador', audienceType: 'global', vigenciaHasta: null, unread: false },
  { id: 'PB-14', categoria: 'Bienestar', titulo: 'Programado: jornada de bienestar Q3', resumen: 'Jornada de bienestar trimestral, aún no ha iniciado su vigencia de publicación.', autor: 'Patricia Gómez', autorIniciales: 'PG', publishedAt: _addDays(DASH_HOY, 3), state: 'publicado', audienceType: 'global', vigenciaHasta: null, unread: false },
  { id: 'PB-15', categoria: 'Bienestar', titulo: 'Oferta vencida de bienestar', resumen: 'Promoción de gimnasios aliados ya no vigente.', autor: 'Patricia Gómez', autorIniciales: 'PG', publishedAt: _addDays(DASH_HOY, -30), state: 'publicado', audienceType: 'global', vigenciaHasta: _addDays(DASH_HOY, -1), unread: false },
  { id: 'PB-16', categoria: 'Institucional', titulo: 'Comunicado archivado de campaña anterior', resumen: 'Campaña ya cerrada, archivada por el equipo de Comunicaciones.', autor: 'Carolina Restrepo', autorIniciales: 'CR', publishedAt: _addDays(DASH_HOY, -60), state: 'archivado', audienceType: 'global', vigenciaHasta: null, unread: false },
  { id: 'PB-17', categoria: 'Documentos', titulo: 'Aviso individual para otro colaborador', resumen: 'Recordatorio dirigido exclusivamente a otra persona del equipo de Compras.', autor: 'Patricia Gómez', autorIniciales: 'PG', publishedAt: _addDays(DASH_HOY, -1), state: 'publicado', audienceType: 'individual', audienceValue: 'gh-sandra', vigenciaHasta: null, unread: false },
];

// vista: 'gh' | 'jefe' | 'colaborador' · usuario: registro de DASH_EMPLOYEES
const publicacionesVisiblesPara = (vista, usuario) => PUBLICACIONES
  .filter(p => p.state === 'publicado')
  .filter(p => p.publishedAt.getTime() <= DASH_HOY.getTime())
  .filter(p => !p.vigenciaHasta || p.vigenciaHasta.getTime() >= DASH_HOY.getTime())
  .filter(p => {
    if (p.audienceType === 'global') return true;
    if (vista === 'gh') return false; // GH solo ve publicaciones globales
    if (!usuario) return false;
    if (p.audienceType === 'individual') return p.audienceValue === usuario.id;
    if (p.audienceType === 'grupo') return (usuario.grupos || []).includes(p.audienceValue);
    if (p.audienceType === 'sociedad') return p.audienceValue === usuario.sociedad;
    if (p.audienceType === 'oficina') return p.audienceValue === usuario.oficina;
    if (p.audienceType === 'area') return p.audienceValue === usuario.area;
    return false;
  })
  .sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime())
  .slice(0, 5);

const LatestPublications = ({ vista, usuario }) => {
  const items = publicacionesVisiblesPara(vista, usuario);
  return (
    <div className="card">
      <div className="card-head">
        <div>
          <h3>Últimas publicaciones</h3>
          <div className="sub">Entérate de lo que está sucediendo</div>
        </div>
      </div>
      <div className="card-body" style={{ paddingTop: 4, paddingBottom: 14, display: 'flex', flexDirection: 'column', gap: 14 }}>
        {items.length === 0 ? (
          <div className="muted" style={{ fontSize: 12.5, padding: '18px 4px', textAlign: 'center' }}>
            No hay publicaciones recientes para mostrar.
          </div>
        ) : items.map((p) => (
          <div key={p.id} style={{ display: 'flex', gap: 10 }}>
            <div className="avatar" style={{ width: 32, height: 32, fontSize: 12, flexShrink: 0 }}>{p.autorIniciales}</div>
            <div style={{ minWidth: 0, flex: 1 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                {p.unread && <span aria-hidden="true" style={{ width: 7, height: 7, borderRadius: '50%', background: 'var(--teal-500)', flexShrink: 0 }} />}
                <div style={{ fontWeight: 700, fontSize: 13.5 }}>{p.titulo}</div>
              </div>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '3px 0 4px', flexWrap: 'wrap' }}>
                <span className="pill navy" style={{ fontSize: 10.5 }}>{p.categoria}</span>
                <span style={{ fontSize: 12, color: 'var(--text-muted)' }}>Publicado el {_fmtDDMMYYYY(p.publishedAt)} por {p.autor}</span>
                {p.unread && <span className="muted-xs" style={{ color: 'var(--teal-800)', fontWeight: 700 }}>No leído</span>}
              </div>
              <div style={{ fontSize: 12.5, color: 'var(--text-muted)', lineHeight: 1.5 }}>{p.resumen}</div>
            </div>
          </div>
        ))}
        <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 2 }}>
          <button className="btn primary" onClick={() => window.__navigateToModule && window.__navigateToModule('comms')}>Ver todas</button>
        </div>
      </div>
    </div>
  );
};

// ─────────────────────────────────────────── Cumpleaños de hoy (compartido, ancho completo) ─
const BirthdayCard = ({ p }) => (
  <div className="dash-bday-card">
    <div className="avatar" style={{ width: 44, height: 44, fontSize: 15 }}>{p.initials}</div>
    <div style={{ minWidth: 0 }}>
      <div className="dash-bday-name">🎂 {p.name}</div>
      <div className="dash-bday-meta">{p.sociedad}{p.cargo ? ` · ${p.cargo}` : ''}</div>
    </div>
  </div>
);

const MAX_MENSAJE_FELICITACION = 280;

const GreetingModal = ({ cumpleañeros, onClose, triggerRef }) => {
  const [seleccion, setSeleccion] = React.useState(() => new Set(cumpleañeros.length === 1 ? [cumpleañeros[0].id] : []));
  const [mensaje, setMensaje] = React.useState('');
  const [estado, setEstado] = React.useState('editando'); // editando | enviando | enviado

  const toggle = (id) => setSeleccion(prev => {
    const next = new Set(prev);
    next.has(id) ? next.delete(id) : next.add(id);
    return next;
  });

  const mensajeValido = mensaje.trim().length > 0 && mensaje.length <= MAX_MENSAJE_FELICITACION;
  const puedeEnviar = seleccion.size > 0 && mensajeValido && estado === 'editando';

  const enviar = () => {
    if (!puedeEnviar) return;
    setEstado('enviando');
    setTimeout(() => {
      // Mensajes privados independientes por destinatario — nunca se agregan
      // a PUBLICACIONES/COMUNICADOS ni a ningún feed compartido; solo el
      // remitente ve la confirmación de envío, en este modal.
      seleccion.forEach((empId) => {
        window.__dashPrivateGreetings = window.__dashPrivateGreetings || [];
        window.__dashPrivateGreetings.push({ toEmpId: empId, mensaje: mensaje.trim(), sentAt: new Date() });
      });
      setEstado('enviado');
    }, 900);
  };

  const handleClose = () => { onClose(); if (triggerRef && triggerRef.current) triggerRef.current.focus(); };

  return (
    <Modal open onClose={handleClose} title="Enviar felicitación" subtitle={DASH_DIA_MES_CORTO} width={460}
      footer={estado === 'enviado' ? (
        <button className="btn primary" onClick={handleClose}>Cerrar</button>
      ) : (
        <>
          <button className="btn" onClick={handleClose} disabled={estado === 'enviando'}>Cancelar</button>
          <button className="btn primary" onClick={enviar} disabled={!puedeEnviar}>
            {estado === 'enviando' ? 'Enviando…' : 'Enviar felicitación'}
          </button>
        </>
      )}>
      <div aria-live="polite">
        {estado === 'enviado' ? (
          <div style={{ textAlign: 'center', padding: '18px 8px' }}>
            <div className="task-icon green" style={{ width: 40, height: 40, margin: '0 auto 10px' }}><IconCheck size={18} /></div>
            <div style={{ fontWeight: 600, fontSize: 14 }}>Tu felicitación fue enviada correctamente.</div>
            <p className="muted-xs" style={{ marginTop: 6 }}>Solo {seleccion.size === 1 ? 'la persona seleccionada podrá' : 'las personas seleccionadas podrán'} ver este mensaje.</p>
          </div>
        ) : (
          <>
            <div className="form-field" style={{ marginBottom: 14 }}>
              <label>Destinatarios</label>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 4, marginTop: 4 }}>
                {cumpleañeros.map((p) => (
                  <label key={p.id} className="dash-greet-recipient" style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 10px', borderRadius: 8, cursor: 'pointer' }}>
                    <input type="checkbox" checked={seleccion.has(p.id)} onChange={() => toggle(p.id)} style={{ accentColor: 'var(--navy-800)', width: 16, height: 16 }} />
                    <div className="avatar" style={{ width: 28, height: 28, fontSize: 11 }}>{p.initials}</div>
                    <span style={{ fontSize: 13, fontWeight: 500 }}>{p.name}</span>
                  </label>
                ))}
              </div>
            </div>
            <div className="form-field">
              <label htmlFor="dash-greet-msg">Mensaje</label>
              <textarea
                id="dash-greet-msg"
                className="form-input"
                rows={4}
                maxLength={MAX_MENSAJE_FELICITACION + 40}
                value={mensaje}
                onChange={(e) => setMensaje(e.target.value)}
                placeholder="Escribe un mensaje de felicitación…"
                disabled={estado === 'enviando'}
              />
              <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 4 }}>
                <span className="muted-xs" style={{ fontWeight: mensaje.length > MAX_MENSAJE_FELICITACION ? 700 : 500, color: mensaje.length > MAX_MENSAJE_FELICITACION ? 'var(--rose-600)' : 'var(--text-muted)' }}>
                  {mensaje.length}/{MAX_MENSAJE_FELICITACION} caracteres
                </span>
              </div>
            </div>
          </>
        )}
      </div>
    </Modal>
  );
};

const Birthdays = ({ usuarioActivo }) => {
  const [modalOpen, setModalOpen] = React.useState(false);
  const triggerRef = React.useRef(null);

  const hoyDia = DASH_HOY.getUTCDate();
  const hoyMes = DASH_HOY.getUTCMonth() + 1;
  const cumpleañeros = DASH_EMPLOYEES.filter(e => e.estado === 'activo' && e.mostrarCumple !== false && e.cumpleDia === hoyDia && e.cumpleMes === hoyMes);

  return (
    <div className="card">
      <div className="card-head">
        <div>
          <h3>Cumpleaños de hoy</h3>
          <div className="sub">Hoy {DASH_DIA_MES_CORTO} felicitamos a nuestros compañeros en su cumpleaños</div>
        </div>
      </div>
      <div className="card-body" style={{ paddingTop: 4, paddingBottom: 14 }}>
        {cumpleañeros.length === 0 ? (
          <div className="muted" style={{ fontSize: 12.5, padding: '18px 4px', textAlign: 'center' }}>
            Hoy no tenemos cumpleaños registrados.
          </div>
        ) : (
          <>
            <div className="dash-bday-list">
              {cumpleañeros.map((p) => <BirthdayCard key={p.id} p={p} />)}
            </div>
            <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 14 }}>
              <button ref={triggerRef} className="btn primary" onClick={() => setModalOpen(true)}>Enviar felicitación</button>
            </div>
          </>
        )}
      </div>
      {modalOpen && (
        <GreetingModal cumpleañeros={cumpleañeros} onClose={() => setModalOpen(false)} triggerRef={triggerRef} />
      )}
    </div>
  );
};

// ─────────────────────────────────────────── KPI rediseñadas ─────────
// Namespace propio (dkpi-*) — no reutiliza .kpi / .kpi-icon / .kpi-chart, que
// también consume dashboard-new-user.jsx.

const DkpiDistribution = ({ segments, label }) => {
  const total = segments.reduce((s, x) => s + x.value, 0) || 1;
  return (
    <div className="dkpi-dist" role="img" aria-label={label}>
      <div className="dkpi-dist-track">
        {segments.filter(s => s.value > 0).map((s, i) => (
          <span key={i} style={{ width: `${(s.value / total) * 100}%`, background: s.color }} title={`${s.label}: ${s.value}`} />
        ))}
      </div>
      <div className="dkpi-dist-legend">
        {segments.map((s, i) => (
          <span key={i} className="dkpi-dist-item"><i style={{ background: s.color }} />{s.label} · {s.value}</span>
        ))}
      </div>
    </div>
  );
};

const DKpi = ({ tone, icon, label, value, footnote, chart }) => {
  const Ico = window[icon] || window.IconBarChart;
  return (
    <div className={`dkpi-card dkpi-${tone}`}>
      <div className="dkpi-top">
        <div className={`dkpi-icon dkpi-icon-${tone}`}><Ico size={16} /></div>
        <div className="dkpi-label">{label}</div>
      </div>
      <div className="dkpi-value">{value}</div>
      {footnote && <div className="dkpi-footnote">{footnote}</div>}
      {chart && <div className="dkpi-chart-wrap">{chart}</div>}
    </div>
  );
};

// ── KPI de Gestión Humana — globales, no dependen de los filtros de Estado
// del personal (ver requisito explícito: alcance = toda la compañía siempre).
const GhKpiRow = () => {
  const activos = DASH_EMPLOYEES.filter(e => e.estado === 'activo' && e.fechaIngreso.getTime() <= DASH_HOY.getTime()).length;
  const nuevos15 = DASH_EMPLOYEES.filter(e => e.estado !== 'anulado' && _dentroDeVentana(e.fechaIngreso, 15)).length;
  const retirados15 = DASH_EMPLOYEES.filter(e => e.estado === 'retirado' && e.fechaRetiro && _dentroDeVentana(e.fechaRetiro, 15)).length;

  const onboardingElegibles = DASH_ONBOARDINGS; // todos los estados presentes ya son elegibles (iniciando/normal/atrasado/cierre)
  const iniciando = onboardingElegibles.filter(o => o.status === 'iniciando').length;
  const atrasado = onboardingElegibles.filter(o => o.status === 'atrasado').length;
  const cierre = onboardingElegibles.filter(o => o.status === 'cierre').length;

  return (
    <div className="grid row-kpi">
      <DKpi tone="teal" icon="IconUsers" label="Colaboradores activos" value={activos} footnote="Con vínculo activo a la fecha de hoy" />
      <DKpi tone="sky" icon="IconArrowUpRight" label="Nuevos en los últimos 15 días" value={nuevos15} footnote="Ventana móvil de 15 días, incluye hoy" />
      <DKpi tone="aubergine" icon="IconArrowDownRight" label="Retirados en los últimos 15 días" value={retirados15} footnote="Ventana móvil de 15 días, incluye hoy" />
      <DKpi
        tone="amber" icon="IconRocket" label="Onboardings en proceso" value={onboardingElegibles.length}
        footnote={`${iniciando} por iniciar · ${atrasado} atrasados · ${cierre} en cierre`}
        chart={
          <DkpiDistribution
            label="Distribución de onboardings por estado"
            segments={[
              { label: 'Por iniciar', value: iniciando, color: 'var(--teal-500)' },
              { label: 'En progreso', value: onboardingElegibles.filter(o => o.status === 'normal').length, color: 'var(--sky-500)' },
              { label: 'Atrasado', value: atrasado, color: 'var(--rose-600)' },
              { label: 'En cierre', value: cierre, color: 'var(--green-600)' },
            ]}
          />
        }
      />
    </div>
  );
};

// ── KPI del Jefe — acotados a los descendientes (directos + indirectos) del
// jefe activo. Ver getDescendants() más arriba.
const JefeKpiRow = ({ jefe }) => {
  const equipo = getDescendants(jefe.id);
  if (equipo.length === 0) {
    return (
      <div className="card" style={{ marginBottom: 0 }}>
        <div className="card-body muted" style={{ textAlign: 'center', padding: 24, fontSize: 12.5 }}>
          Aún no tienes colaboradores asignados a tu equipo.
        </div>
      </div>
    );
  }

  const equipoIds = new Set(equipo.map(e => e.id));
  const equipoActivos = equipo.filter(e => e.estado === 'activo').length;

  const posicionesActivas = TEAM_POSICIONES_LUIS.filter(p => p.estadoEstructural === 'activa');
  const autorizadas = posicionesActivas.length;
  const ocupadas = posicionesActivas.filter(p => p.estadoOcupacion === 'ocupada').length;
  const pctOcupacion = autorizadas > 0 ? Math.round((ocupadas / autorizadas) * 100) : 0;

  const solicitudesActivas = SOLICITUDES_PERSONAL_LUIS.filter(s => SOLICITUDES_ESTADOS_ACTIVOS.includes(s.estado)).length;

  const ausentesHoy = new Set(
    AUSENCIAS_EQUIPO_LUIS
      .filter(a => a.estado === 'aprobada' && equipoIds.has(a.empId) && _cubreHoy(a.inicio, a.fin))
      .map(a => a.empId)
  ).size;

  return (
    <div className="grid row-kpi">
      <DKpi tone="teal" icon="IconUsers" label="Colaboradores en mi equipo" value={equipoActivos} footnote="Reportes directos e indirectos, sin contarte a ti" />
      <DKpi tone="sky" icon="IconBarChart" label="Porcentaje de ocupación de mi equipo" value={`${pctOcupacion}%`} footnote={`${ocupadas} de ${autorizadas} posiciones autorizadas`} />
      <DKpi tone="aubergine" icon="IconSearch" label="Solicitudes de personal activas" value={solicitudesActivas} footnote={solicitudesActivas === 0 ? 'Sin solicitudes activas en este momento' : 'En aprobación, reclutamiento o selección'} />
      <DKpi tone="amber" icon="IconCalendar" label="Personas ausentes en mi equipo" value={ausentesHoy} footnote={ausentesHoy === 0 ? 'Nadie de tu equipo está ausente hoy' : 'Vacaciones, permisos, licencias o incapacidades vigentes hoy'} />
    </div>
  );
};

// ─────────────────────────────────────────── Estado del personal (exclusivo GH) ─────────

const EP_FILTROS_VACIOS = { empresa: 'todas', oficina: 'todas', area: 'todas' };

const EpFilterSelect = ({ label, value, isDefault, onChange, options }) => (
  <div className={`filter-select ${isDefault ? '' : 'active'}`}>
    <span className="filter-label">{label}</span>
    <select value={value} onChange={e => onChange(e.target.value)}>
      {options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
    </select>
    <IconChevronDown size={13} />
  </div>
);

const EpComposition = ({ posiciones }) => {
  const autorizado = posiciones.length;
  const ocupado = posiciones.filter(p => p.estadoOcupacion === 'ocupada').length;
  const vacante = autorizado - ocupado;
  const disponible = posiciones.filter(p => p.estadoCobertura === 'disponible').length;
  const reservada = posiciones.filter(p => p.estadoCobertura === 'reservada').length;
  const reclutamiento = posiciones.filter(p => p.estadoCobertura === 'en_reclutamiento').length;
  const bloqueada = posiciones.filter(p => p.estadoCobertura === 'bloqueada').length;
  const pct = (n) => autorizado > 0 ? Math.round((n / autorizado) * 1000) / 10 : 0;
  const pctOcupacion = autorizado > 0 ? Math.round((ocupado / autorizado) * 100) : 0;

  if (autorizado === 0) {
    return <div className="muted" style={{ fontSize: 12.5, padding: '18px 4px', textAlign: 'center' }}>Ningún registro coincide con los filtros seleccionados.</div>;
  }

  const CATS = [
    { key: 'ocupada', label: 'Ocupadas', value: ocupado, color: 'var(--teal-800)' },
    { key: 'disponible', label: 'Vacantes disponibles', value: disponible, color: 'var(--rose-600)' },
    { key: 'reservada', label: 'Vacantes reservadas', value: reservada, color: 'var(--amber-600)' },
    { key: 'en_reclutamiento', label: 'En reclutamiento', value: reclutamiento, color: 'var(--navy-800)' },
    { key: 'bloqueada', label: 'Bloqueadas', value: bloqueada, color: 'var(--text-muted)' },
  ];

  return (
    <div className="ep-composition">
      <div className="ep-summary-grid">
        <div className="ep-summary-cell"><span className="ep-summary-num">{autorizado}</span><span className="muted-xs">Headcount autorizado</span></div>
        <div className="ep-summary-cell"><span className="ep-summary-num" style={{ color: 'var(--teal-800)' }}>{ocupado}</span><span className="muted-xs">Headcount real</span></div>
        <div className="ep-summary-cell"><span className="ep-summary-num" style={{ color: 'var(--rose-600)' }}>{vacante}</span><span className="muted-xs">Posiciones vacantes</span></div>
        <div className="ep-summary-cell"><span className="ep-summary-num">{pctOcupacion}%</span><span className="muted-xs">% de ocupación</span></div>
      </div>

      <div className="ep-seg-bar" role="img" aria-label={`${ocupado} ocupadas, ${disponible} vacantes disponibles, ${reservada} reservadas, ${reclutamiento} en reclutamiento y ${bloqueada} bloqueadas, de ${autorizado} posiciones autorizadas`}>
        {CATS.map(c => c.value > 0 && <span key={c.key} style={{ width: `${(c.value / autorizado) * 100}%`, background: c.color }} />)}
      </div>

      <div className="ep-legend-row">
        {CATS.map(c => (
          <span key={c.key} className="ep-legend-item">
            <i style={{ background: c.color }} />{c.label} <b>{c.value}</b> <span className="muted-xs">({pct(c.value)}%)</span>
          </span>
        ))}
      </div>
      <p className="muted-xs" style={{ margin: '4px 0 0' }}>Headcount autorizado = Headcount real + Posiciones vacantes · las reservadas, en reclutamiento y bloqueadas son subconjuntos de las vacantes, no cantidades adicionales.</p>
    </div>
  );
};

const EP_AGRUPAR_OPCIONES = [
  { value: 'area', label: 'Área' },
  { value: 'sociedad', label: 'Sociedad' },
  { value: 'regional', label: 'Regional' },
];

const EpDistribucion = ({ posiciones, onExplorar }) => {
  const [agruparPor, setAgruparPor] = React.useState('area');
  const [vista, setVista] = React.useState('grafico');

  const grupos = {};
  posiciones.forEach(p => {
    const key = p[agruparPor];
    if (!grupos[key]) grupos[key] = { key, ocupado: 0, vacante: 0, total: 0 };
    grupos[key].total++;
    if (p.estadoOcupacion === 'ocupada') grupos[key].ocupado++; else grupos[key].vacante++;
  });
  const filas = Object.values(grupos).sort((a, b) => b.total - a.total);
  const max = Math.max(...filas.map(f => f.total), 1);

  if (filas.length === 0) {
    return <div className="muted" style={{ fontSize: 12.5, padding: '18px 4px', textAlign: 'center' }}>Ningún registro coincide con los filtros seleccionados.</div>;
  }

  return (
    <div>
      <div className="ep-subtoolbar">
        <EpFilterSelect label="Agrupar por" value={agruparPor} isDefault={agruparPor === 'area'} onChange={setAgruparPor} options={EP_AGRUPAR_OPCIONES} />
        <div className="seg-pill">
          <button className={`seg-pill-btn ${vista === 'grafico' ? 'active' : ''}`} onClick={() => setVista('grafico')}>Gráfico</button>
          <button className={`seg-pill-btn ${vista === 'tabla' ? 'active' : ''}`} onClick={() => setVista('tabla')}>Tabla</button>
        </div>
      </div>

      <div className="ep-legend-row" style={{ marginBottom: 10 }}>
        <span className="ep-legend-item"><i style={{ background: 'var(--teal-800)' }} />Ocupadas</span>
        <span className="ep-legend-item"><i style={{ background: 'var(--rose-600)' }} />Vacantes</span>
      </div>

      {vista === 'grafico' ? (
        <div className="ep-dist-list">
          {filas.map(f => (
            <button key={f.key} className="ep-dist-row" onClick={() => onExplorar(agruparPor, f.key)} title={`Ver el detalle de ${f.key}`}>
              <span className="ep-dist-label">{f.key}</span>
              <span className="ep-dist-track" style={{ width: `${(f.total / max) * 100}%` }}>
                <span className="ep-dist-seg ocupada" style={{ width: `${(f.ocupado / f.total) * 100}%` }} />
                <span className="ep-dist-seg vacante" style={{ width: `${(f.vacante / f.total) * 100}%` }} />
              </span>
              <span className="ep-dist-num">{f.total}</span>
            </button>
          ))}
        </div>
      ) : (
        <table className="data-table">
          <thead><tr><th>{EP_AGRUPAR_OPCIONES.find(o => o.value === agruparPor).label}</th><th>Autorizado</th><th>Ocupado</th><th>Vacante</th><th></th></tr></thead>
          <tbody>
            {filas.map(f => (
              <tr key={f.key}>
                <td style={{ fontWeight: 500 }}>{f.key}</td>
                <td>{f.total}</td>
                <td>{f.ocupado}</td>
                <td>{f.vacante}</td>
                <td style={{ textAlign: 'right' }}>
                  <button className="btn" style={{ padding: '4px 10px', fontSize: 12 }} onClick={() => onExplorar(agruparPor, f.key)}>Ver detalle</button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
};

const EpEvolucion = ({ empresa }) => {
  const [rango, setRango] = React.useState(6);
  const datos = _movPorMes(empresa).slice(-rango);
  const max = Math.max(...datos.map(d => Math.max(d.ingresos, d.retiros)), 1);
  const totalIng = datos.reduce((s, d) => s + d.ingresos, 0);
  const totalRet = datos.reduce((s, d) => s + d.retiros, 0);

  return (
    <div>
      <div className="ep-subtoolbar">
        <div className="seg-pill">
          <button className={`seg-pill-btn ${rango === 3 ? 'active' : ''}`} onClick={() => setRango(3)}>Últimos 3 meses</button>
          <button className={`seg-pill-btn ${rango === 6 ? 'active' : ''}`} onClick={() => setRango(6)}>Últimos 6 meses</button>
        </div>
        <span className="muted-xs">{empresa === 'todas' ? 'Todas las empresas' : empresa} · movimientos del periodo, no acumulados</span>
      </div>

      <div className="ep-legend-row" style={{ marginBottom: 6 }}>
        <span className="ep-legend-item"><i style={{ background: 'var(--teal-800)' }} />Ingresos <b>{totalIng}</b></span>
        <span className="ep-legend-item"><i style={{ background: 'var(--aubergine-800)' }} />Retiros <b>{totalRet}</b></span>
      </div>

      <div className="ep-evo-chart">
        {datos.map(d => (
          <div key={d.mes} className="ep-evo-col">
            <div className="ep-evo-bars">
              <span className="ep-evo-bar ingreso" style={{ height: `${(d.ingresos / max) * 100}%` }} title={`Ingresos ${d.mes}: ${d.ingresos}`} />
              <span className="ep-evo-bar retiro" style={{ height: `${(d.retiros / max) * 100}%` }} title={`Retiros ${d.mes}: ${d.retiros}`} />
            </div>
            <div className="ep-evo-vals">{d.ingresos}/{d.retiros}</div>
            <div className="ep-evo-mes">{d.mes}</div>
          </div>
        ))}
      </div>
    </div>
  );
};

const EpDetalle = ({ posiciones, quickFilter, onClearQuickFilter }) => {
  const [q, setQ] = React.useState('');
  const [estado, setEstado] = React.useState('todos');
  const [detalle, setDetalle] = React.useState(null);

  const base = quickFilter ? posiciones.filter(p => p[quickFilter.dim] === quickFilter.val) : posiciones;
  const filtradas = base
    .filter(p => !q || p.codigo.toLowerCase().includes(q.toLowerCase()) || p.cargo.toLowerCase().includes(q.toLowerCase()))
    .filter(p => estado === 'todos' || _epEstadoKey(p) === estado);

  return (
    <div>
      <div className="ep-subtoolbar" style={{ flexWrap: 'wrap' }}>
        <input className="form-input" style={{ maxWidth: 220, fontSize: 12.5, padding: '6px 10px' }} placeholder="Buscar por código o cargo" value={q} onChange={e => setQ(e.target.value)} />
        <select value={estado} onChange={e => setEstado(e.target.value)} style={{ fontSize: 12.5, padding: '6px 10px' }}>
          <option value="todos">Todos los estados</option>
          {Object.entries(EP_ESTADO_META).map(([k, m]) => <option key={k} value={k}>{m.label}</option>)}
        </select>
        {quickFilter && (
          <button className="ep-clear-filters" onClick={onClearQuickFilter}>
            Filtrado por "{quickFilter.val}" ×
          </button>
        )}
        <span className="muted-xs" style={{ marginLeft: 'auto' }}>{filtradas.length} de {base.length} posiciones</span>
      </div>

      <div className="ep-table-wrap">
        <table className="data-table">
          <thead>
            <tr><th>Código</th><th>Cargo</th><th>Sociedad</th><th>Oficina de venta</th><th>Área</th><th>Estado</th><th>Ocupante</th></tr>
          </thead>
          <tbody>
            {filtradas.map(p => {
              const meta = EP_ESTADO_META[_epEstadoKey(p)] || { label: _epEstadoKey(p), pill: '' };
              return (
                <tr key={p.codigo} onClick={() => setDetalle(p)} style={{ cursor: 'pointer' }}>
                  <td className="mono" style={{ fontWeight: 700 }}>{p.codigo}</td>
                  <td>{p.cargo}</td>
                  <td>{p.sociedad}</td>
                  <td className="mono muted">{p.oficina}</td>
                  <td>{p.area}</td>
                  <td><span className={`pill ${meta.pill}`}>{meta.label}</span></td>
                  <td>{p.ocupanteNombre || '—'}</td>
                </tr>
              );
            })}
            {filtradas.length === 0 && (
              <tr><td colSpan={7} className="muted" style={{ textAlign: 'center', padding: 20, fontSize: 12.5 }}>Ninguna posición coincide con la búsqueda.</td></tr>
            )}
          </tbody>
        </table>
      </div>

      {detalle && (
        <Modal open onClose={() => setDetalle(null)} title={detalle.codigo} subtitle={detalle.cargo} width={480}
          footer={<>
            <button className="btn" onClick={() => setDetalle(null)}>Cerrar</button>
            <button className="btn primary" onClick={() => { setDetalle(null); window.__navigateToModule && window.__navigateToModule('orgchart', { tab: 'aprobado' }); }}>
              Ver Organigrama
            </button>
          </>}>
          <div className="amp-resumen-grid">
            <div><span className="muted-xs">Sociedad</span><div>{detalle.sociedad}</div></div>
            <div><span className="muted-xs">Regional</span><div>{detalle.regional}</div></div>
            <div><span className="muted-xs">Oficina de venta</span><div className="mono">{detalle.oficina}</div></div>
            <div><span className="muted-xs">Área</span><div>{detalle.area}</div></div>
            <div><span className="muted-xs">Estado</span><div><span className={`pill ${(EP_ESTADO_META[_epEstadoKey(detalle)] || {}).pill}`}>{(EP_ESTADO_META[_epEstadoKey(detalle)] || {}).label}</span></div></div>
            <div><span className="muted-xs">Ocupante</span><div>{detalle.ocupanteNombre || 'Sin asignar'}</div></div>
          </div>
        </Modal>
      )}
    </div>
  );
};

const EP_TABS = [
  { id: 'distribucion', label: 'Distribución organizacional' },
  { id: 'evolucion', label: 'Evolución de personal' },
  { id: 'detalle', label: 'Detalle de posiciones' },
];

const EstadoDelPersonal = ({ filtros, setFiltros, posicionesFiltradas }) => {
  const [tab, setTab] = React.useState('distribucion');
  const [quickFilter, setQuickFilter] = React.useState(null);

  const setFiltro = (k, v) => setFiltros(f => {
    const next = { ...f, [k]: v };
    if (k === 'empresa') { next.oficina = 'todas'; next.area = 'todas'; }
    if (k === 'oficina') { next.area = 'todas'; }
    return next;
  });
  const limpiarFiltros = () => { setFiltros(EP_FILTROS_VACIOS); setQuickFilter(null); };

  const empresasOpts = [...new Set(HEADCOUNT_POSICIONES.map(p => p.sociedad))];
  const oficinasOpts = [...new Set(HEADCOUNT_POSICIONES
    .filter(p => filtros.empresa === 'todas' || p.sociedad === filtros.empresa)
    .map(p => p.oficina))];
  const areasOpts = [...new Set(HEADCOUNT_POSICIONES
    .filter(p => filtros.empresa === 'todas' || p.sociedad === filtros.empresa)
    .filter(p => filtros.oficina === 'todas' || p.oficina === filtros.oficina)
    .map(p => p.area))];

  const explorarGrupo = (dim, val) => { setQuickFilter({ dim, val }); setTab('detalle'); };

  return (
    <div className="card">
      <div className="card-head">
        <div>
          <h3>Estado del personal</h3>
          <div className="sub">Headcount autorizado, ocupación y movimientos — fuente: Organigrama (posiciones)</div>
        </div>
      </div>

      <div className="ep-filters">
        <IconFilter size={14} className="ep-filters-icon" />
        <EpFilterSelect label="Sociedad" value={filtros.empresa} isDefault={filtros.empresa === 'todas'} onChange={v => setFiltro('empresa', v)}
          options={[{ value: 'todas', label: 'Todas' }, ...empresasOpts.map(v => ({ value: v, label: v }))]} />
        <EpFilterSelect label="Oficina de venta" value={filtros.oficina} isDefault={filtros.oficina === 'todas'} onChange={v => setFiltro('oficina', v)}
          options={[{ value: 'todas', label: 'Todas' }, ...oficinasOpts.map(v => ({ value: v, label: v }))]} />
        <EpFilterSelect label="Área" value={filtros.area} isDefault={filtros.area === 'todas'} onChange={v => setFiltro('area', v)}
          options={[{ value: 'todas', label: 'Todas' }, ...areasOpts.map(v => ({ value: v, label: v }))]} />
        {(filtros.empresa !== 'todas' || filtros.oficina !== 'todas' || filtros.area !== 'todas') && (
          <button className="ep-clear-filters" onClick={limpiarFiltros}>Limpiar filtros</button>
        )}
      </div>

      <div className="ep-block">
        <EpComposition posiciones={posicionesFiltradas} />
      </div>

      <div className="tabs" style={{ marginTop: 22 }}>
        {EP_TABS.map(t => (
          <button key={t.id} className={`tab ${tab === t.id ? 'active' : ''}`} onClick={() => setTab(t.id)}>{t.label}</button>
        ))}
      </div>

      <div className="ep-block">
        {tab === 'distribucion' && <EpDistribucion posiciones={posicionesFiltradas} onExplorar={explorarGrupo} />}
        {tab === 'evolucion' && <EpEvolucion empresa={filtros.empresa} />}
        {tab === 'detalle' && <EpDetalle posiciones={posicionesFiltradas} quickFilter={quickFilter} onClearQuickFilter={() => setQuickFilter(null)} />}
      </div>
    </div>
  );
};

// ─────────────────────────────────────────── Page ─────────
// Mapeo del rol técnico (currentRole, ver app.jsx / RoleSwitcher en
// icontec-extras.jsx) a la variante de Dashboard y a la persona activa. Se
// conservan los identificadores técnicos existentes (admin/jefe/nuevo/…)
// para no romper el resto del sistema de permisos — solo se traduce aquí,
// a propósito acotado a este módulo (ver instrucción de alcance).
const DASH_ROLE_META = {
  admin: { view: 'gh',          empId: 'gh-carolina' },
  jefe:  { view: 'jefe',        empId: 'jefe-luis' },
  nuevo: { view: 'colaborador', empId: 'colab-andres' },
};

const Dashboard = ({ currentRole }) => {
  const roleMeta = DASH_ROLE_META[currentRole] || DASH_ROLE_META.admin;
  const usuarioActivo = DASH_EMPLEADOS_POR_ID[roleMeta.empId];
  const view = roleMeta.view;

  const [filtros, setFiltros] = React.useState(EP_FILTROS_VACIOS);
  const posicionesFiltradas = HEADCOUNT_POSICIONES
    .filter(p => filtros.empresa === 'todas' || p.sociedad === filtros.empresa)
    .filter(p => filtros.oficina === 'todas' || p.oficina === filtros.oficina)
    .filter(p => filtros.area === 'todas' || p.area === filtros.area);

  const primerNombre = usuarioActivo.name.split(' ')[0];

  return (
    <div className="main">
      <div className="page-head">
        <div>
          <h1 className="page-title">{_saludoPorHora()}, {primerNombre}</h1>
          <div className="page-subtitle">Hoy es <b>{DASH_FECHA_LARGA}</b></div>
        </div>
      </div>

      {view === 'gh' && <GhKpiRow />}
      {view === 'jefe' && <JefeKpiRow jefe={usuarioActivo} />}

      <div className="section-title">Novedades</div>
      <div className="grid row-1-1" style={{ marginBottom: 24 }}>
        <NewHires />
        <LatestPublications vista={view} usuario={usuarioActivo} />
      </div>

      <div style={{ marginBottom: 24 }}>
        <Birthdays usuarioActivo={usuarioActivo} />
      </div>

      {view === 'gh' && (
        <>
          <div className="section-title">Estado del personal</div>
          <div style={{ marginBottom: 24 }}>
            <EstadoDelPersonal filtros={filtros} setFiltros={setFiltros} posicionesFiltradas={posicionesFiltradas} />
          </div>
        </>
      )}
    </div>
  );
};

window.Dashboard = Dashboard;
window.getDescendants = getDescendants;
