---
proyecto: P4 · HR + Agentes IA
subproyecto: Dashboard Talento Humano
version: 1.13.0
fecha_inicio: 2026-04-22
ultima_actualizacion: 2026-05-19
autor: Ricardo Martínez (BAI · Apoyo HR)
herramienta_desarrollo: Claude Code (CLI + SDK)
estado: Producción — operativo
tipo: ficha_trabajo_realizado
---

# P4 · Dashboard Talento Humano — Trabajo Realizado

Documentación exhaustiva de todo lo construido en el componente de visualización de Recursos Humanos del proyecto P4. Este dashboard es el entregable real y operativo de la participación de Ricardo Martínez en P4.

---

## Resumen ejecutivo

**Qué es:** Aplicación web de inteligencia de negocio para el área de Talento Humano de Faranda Hotels. Permite consultar, filtrar y analizar el estado de los 23 hoteles en términos de empleados activos, salarios por centro de costo, nómina, ingresos, retiros y rotación de personal.

**Estado:** En producción. Sirve desde el Mac del analista vía LaunchAgent automático en puerto 8080.

**Cobertura de datos:** 39.912 registros históricos (Enero 2022 – Abril 2026, 52 meses) + API en tiempo real para el mes en curso.

**Hoteles cubiertos:** 23 hoteles del grupo Faranda (Colombia), excluyendo ELMA, FNS y FCZN.

---

## Arquitectura del sistema

```
┌──────────────────────────────────────────────────────────────┐
│  Fuentes de datos                                             │
│  ├── API externa (mes actual)                                 │
│  │   ├── GET  /api/v1/rrhh/empleados-activos-detalle         │
│  │   ├── POST /api/v1/rrhh/ingresos-retiros                  │
│  │   └── POST /api/v1/nomina/reporte  (Payroll)              │
│  └── PostgreSQL histórico (meses anteriores)                 │
│      └── "Base de Datos Talento Humano"                       │
│          ├── historico_empleados (39.912 registros)           │
│          ├── historico_ir        (4.929 registros)            │
│          ├── nomina_conceptos    (mapeo de conceptos)         │
│          └── tablas de unificación (ceco, hotel, cargo)       │
└────────────────────┬─────────────────────────────────────────┘
                     │
┌────────────────────▼─────────────────────────────────────────┐
│  servidor.py — Proxy Python (puerto 8080)                     │
│  ├── Resuelve CORS entre dashboard y API externa              │
│  ├── Ejecuta queries PostgreSQL directamente                  │
│  └── 18+ endpoints propios                                    │
└────────────────────┬─────────────────────────────────────────┘
                     │
┌────────────────────▼─────────────────────────────────────────┐
│  dashboard-empleados.html                                     │
│  ├── HTML puro + ECharts 5.4.3 (sin frameworks)              │
│  └── 5 tabs: Análisis · Empleados · Salarios · IR · Payroll  │
└──────────────────────────────────────────────────────────────┘
```

**Por qué esta arquitectura:**
- Sin frameworks → cero dependencias de build, el archivo abre directamente en el navegador.
- Proxy local → evita CORS sin tocar la API de producción.
- PostgreSQL histórico → la API solo devuelve el mes actual; los 51 meses anteriores viven en local.

---

## Archivos del proyecto

| Archivo | Ruta relativa | Descripción |
|---|---|---|
| Dashboard principal | `dashboard-empleados.html` | App completa (frontend + lógica JS) |
| Servidor proxy | `servidor.py` | Python http.server + endpoints PostgreSQL |
| Excel CECOs | `Unificacion_CECOs.xlsx` | 184 mappings CECO original → unificado |
| Excel Hoteles | `Unificacion_Hoteles.xlsx` | Tabla de unificación de nombres de hoteles |
| Excel Cargos | `Unificacion_Cargos.xlsx` | Tabla de unificación de cargos |
| Excel Conceptos | `Conceptos_Nomina_Mapeo.xlsx` | Mapeo de conceptos de nómina (grupo, subgrupo, display) |
| Script conceptos | `generar_conceptos_excel.py` | 522 peticiones API para descubrir conceptos únicos |
| CHANGELOG | `CHANGELOG.md` | Historial completo de cambios por versión |
| LaunchAgent prod | `~/Library/LaunchAgents/com.bia.dashboard-talento.plist` | Inicio automático en Mac (puerto 8080) |
| LaunchAgent dev | `~/Library/LaunchAgents/com.bia.dashboard-talento-dev.plist` | Servidor de desarrollo (puerto 8081) |
| Log servidor | `logs/servidor.log` | Log de peticiones |
| Log errores | `logs/servidor_error.log` | Log de errores del servidor |

**Ruta absoluta del proyecto:**
```
/Users/ricardomartinez/BIA CLAUDE PROYECTOS/Proyectos Claude IA/Talento Humano Api Dashboard/
```

---

## Paleta cromática (Brand Guidelines Faranda Hotels — Marzo 2024)

| Variable CSS | HEX | Uso |
|---|---|---|
| `--deep-blue` | `#001A5A` | Header, fondos oscuros, textos principales |
| `--deep-blue2` | `#33487B` | Gradientes del header |
| `--deep-blue3` | `#66769C` | Textos secundarios |
| `--ocean-blue` | `#0033A0` | Botones, barras de gráficos, bordes activos |
| `--ocean-blue2` | `#335CB3` | Gradientes secundarios |
| `--sky-blue` | `#87BAE6` | Labels, íconos, scrollbar, tab activo |
| `--sky-blue2` | `#CFE3F5` | Hover de filas, fondos suaves |
| `--gold` | `#D59F00` | Solo decorativo: línea inferior header, borde logo |
| `--bg` | `#f0f4fa` | Fondo general del contenido |
| `--border` | `#CCD1DE` | Bordes de tablas y cards |

---

## Base de datos — PostgreSQL

**Nombre de base de datos:** `Base de Datos Talento Humano`  
**Host:** localhost · Puerto: 5432 · Usuario: postgres

### Tablas

#### `historico_empleados`
Snapshot mensual de empleados activos. **39.912 registros.** Período: Enero 2022 – Abril 2026 (52 meses). Origen: 61 archivos Excel consolidados.

| Columna | Tipo | Notas |
|---|---|---|
| `fecha` | date | Primer día del mes (ej: 2026-04-01) |
| `razon_social` | varchar | Código corto (GHS, SERVMYS, COLH…) |
| `codigo_empleado` | numeric | Cédula — 5% nulos |
| `nombre_completo` | varchar | Nombre completo del empleado |
| `fecha_nacimiento` | varchar | Parseo especial Abr 2026 (formato "Friday, March 12, 2004") |
| `sexo` | varchar | INITCAP aplicado al cargar |
| `fecha_ingreso` | date | |
| `fecha_vencimiento` | date | Nulo en contratos indefinidos (46.5%) |
| `centro_de_costo` | varchar | |
| `cargo` | varchar | Puede venir en mayúsculas o mixto según mes |
| `sueldo_base` | integer | |
| `operacion` | varchar | "En Operacion" / "Socio" |
| `tipo_contrato_combinado` | varchar | 5 categorías |

#### `historico_ir`
Movimientos de ingresos y retiros. **4.929 registros.** Período: histórico hasta Abril 2026.

| Columna | Tipo | Notas |
|---|---|---|
| `razon_social` | varchar | Código corto |
| `nombre_completo` | varchar | |
| `ingreso` | date | Fecha de ingreso |
| `cargo` | varchar | |
| `tipo_movimiento` | varchar | 'I' / 'R' / 'IR' |
| `sueldo_base` | numeric | |
| `tipo_contrato` | varchar | |
| `termino` | date | Fecha de retiro (null si ingreso) |
| `operacion` | varchar | |
| `causa_retiro` | varchar | |

#### `empresas`
27 códigos de razón social del grupo Faranda. Incluye columna `nit` — 18 hoteles con NIT configurado para Payroll.

#### `nomina_conceptos`
Mapeo de conceptos de nómina para el tab Payroll.

| Columna | Tipo | Notas |
|---|---|---|
| `codigo` | varchar(40) PK | Código único. Sufijo `_v2/_v3` para mismo código API con distinto nombre |
| `nombre` | varchar | Nombre del concepto en la API |
| `tipo` | varchar | 'Devengo' / 'Deducción' |
| `grupo` | varchar | Agrupación principal (ej: "Salario Base", "Auxilio de Transporte") |
| `subgrupo` | varchar | Sub-agrupación |
| `nombre_display` | varchar | Nombre corto para mostrar en tabla |
| `incluir` | boolean | Si el concepto se muestra en el dashboard |
| `actualizado_en` | timestamp | |

#### `ceco_unificacion`
Mapeo de CECOs originales a nombres unificados. **184 mappings activos.**

| Columna | Tipo | Notas |
|---|---|---|
| `ceco_original` | varchar (PK) | Código tal cual viene del PMS |
| `ceco_unificado` | varchar | Nombre amigable para mostrar |
| `fuente` | varchar | Origen del dato |
| `empleados_api` | integer | Cantidad de empleados activos en API |
| `registros_pg` | integer | Registros en histórico |
| `notas` | text | Observaciones |
| `actualizado_en` | timestamp | Última actualización |

#### `hotel_unificacion`
Mapeo completo de nombres de hoteles.

| Columna | Tipo | Notas |
|---|---|---|
| `razon_social` | varchar (PK) | Código corto del histórico |
| `hotel_name_api` | varchar | Nombre en la API externa |
| `bu_codigo` | varchar | Código de unidad de negocio |
| `bu_nombre` | varchar | Nombre de unidad de negocio (para split) |
| `registros_historico` | integer | |
| `meses_historico` | integer | |
| `activos_api` | integer | Activos en API — define prioridad en `HOTEL_MAP` |
| `nombre_personalizado` | varchar | Nombre final en el dashboard |
| `notas` | text | |
| `actualizado_en` | timestamp | |

#### `cargo_unificacion`
Normalización de cargos.

| Columna | Tipo | Notas |
|---|---|---|
| `cargo_original` | varchar (PK) | Cargo tal cual viene de la fuente |
| `cargo_unificado` | varchar | Nombre normalizado para mostrar |
| `num_hoteles` | integer | En cuántos hoteles aparece |
| `registros_pg` | integer | Registros en histórico |
| `ultima_fecha` | date | |
| `hoteles` | text | Lista de hoteles |
| `actualizado_en` | timestamp | |

---

## API externa

**Base URL:** `https://api.innfinancial.cloud/api/v1`  
**Auth:** Header `X-API-Key: ri_<REDACTED>`

| Endpoint | Método | Descripción |
|---|---|---|
| `/rrhh/empleados-activos-detalle` | GET | Todos los empleados activos del mes actual. Devuelve `{grand_activos, hoteles[{hotel_nit, hotel_name, activos, empleados[]}]}` |
| `/rrhh/ingresos-retiros` | POST | Ingresos y retiros por rango de fechas. Devuelve `{hoteles[{hotel_name, unidades_negocio[{rows[]}]}]}` |
| `/nomina/reporte` | POST | Nómina del mes por hotel. Requiere `{hotel_nit, ano, mes, incluir_empleados:true}`. Devuelve KPIs + empleados con conceptos |
| `/nomina/hoteles` | GET | Lista de hoteles con NITs disponibles en la API |

**Estructura empleados (empleados-activos-detalle):**
```
empleados[]
  ├── identificacion
  ├── nombre
  ├── fecha_nacimiento
  ├── sexo
  ├── fecha_ingreso
  ├── fecha_vence_contrato
  ├── sueldo_base
  ├── cargo
  ├── bu_codigo
  ├── bu_nombre
  ├── contrato
  └── centro_costo
```

**Estructura rows (ingresos-retiros):**
```
hoteles[]
  └── unidades_negocio[]
        └── rows[]
              ├── tipo (I/R)
              ├── nombres
              ├── cargo
              ├── sueldo
              ├── tipo_contrato
              ├── causa_retiro
              ├── ingreso
              └── vencimiento
```

**Body POST ingresos-retiros:**
```json
{
  "tipo_nomina_id": 4,
  "fecha_inicial": "YYYY-MM-DD",
  "fecha_final": "YYYY-MM-DD",
  "estado": "T",
  "agrupar": { "cco": false, "bu": true, "cla": false, "causa": false, "cargo": false },
  "saltos": { "cco": false, "bu": false, "cla": false, "causa": false, "cargo": false },
  "filtros": { "contrato": ["","zzz"], "empleado": ["","zzz"], "centro_costo": ["","zzz"],
               "bu": ["","zzz"], "clasificacion": ["","zzz"], "cargo": ["","zzz"],
               "nivel": ["","zzz"], "causa_retiro": ["","zzz"] },
  "orden": ["C", "N", "I"],
  "forma_pago": 999
}
```

---

## Endpoints proxy (servidor.py)

| Método | Ruta | Descripción |
|---|---|---|
| GET | `/proxy/rrhh/...` | Proxy transparente GET a API externa |
| POST | `/proxy/rrhh/ingresos-retiros` | Proxy POST — ingresos y retiros |
| GET | `/proxy/nomina/hoteles-nit` | JOIN hotel_unificacion + empresas → hoteles con NIT |
| GET | `/proxy/nomina/conceptos-map` | Tabla nomina_conceptos + codigo_api via regexp_replace |
| POST | `/proxy/nomina/reporte` | Proxy POST — nómina mes por NIT |
| GET | `/proxy/historico/empleados` | Empleados históricos por fecha (COALESCE fecha_nacimiento, INITCAP sexo) |
| GET | `/proxy/historico/salarios` | Salarios CECO con LY — acepta: razon, centro, cargo, nombre |
| GET | `/proxy/historico/matriz` | Árbol CECO→Cargo→Empleado — acepta: razon, centro, cargo, nombre |
| GET | `/proxy/historico/activos-count` | Conteo activos por snapshot — acepta: razon, operac, centro, cargo, nombre |
| GET | `/proxy/historico/ir` | IR histórico desde historico_ir — acepta: razon, operac, cargo, nombre |
| GET | `/proxy/historico/ir-mensual` | IR agrupado por mes: `{mes, ingresos, retiros}` |
| GET | `/proxy/historico/cohorte-retencion` | Retención por cohorte (NOT EXISTS mes anterior), pct_1…pct_12 |
| GET | `/proxy/historico/rotacion-heatmap` | % retiros por hotel/mes |
| GET | `/proxy/historico/activos-hotel` | Activos por razon_social en fecha dada |
| GET | `/proxy/historico/antiguedad` | Bins de antigüedad (0-6m, 6-12m, 1-2a, 2-3a, 3-4a, 4+a) |
| GET | `/proxy/historico/cecos-map` | Tabla ceco_unificacion completa |
| GET | `/proxy/historico/hoteles-map` | Tabla hotel_unificacion completa |
| GET | `/proxy/historico/cargos-map` | Tabla cargo_unificacion completa |
| POST | `/proxy/historico/cecos-sync` | Inserta CECOs nuevos desde API automáticamente |
| POST | `/proxy/admin/sync-hoteles` | Lee Unificacion_Hoteles.xlsx → hotel_unificacion |
| POST | `/proxy/admin/sync-cargos` | Lee Unificacion_Cargos.xlsx → cargo_unificacion |
| POST | `/proxy/admin/sync-cecos` | Lee Unificacion_CECOs.xlsx → ceco_unificacion |
| POST | `/proxy/admin/sync-ir` | Lee Excel IR histórico → historico_ir (TRUNCATE + INSERT) |
| POST | `/proxy/admin/sync-conceptos` | Lee Conceptos_Nomina_Mapeo.xlsx → nomina_conceptos (upsert) |

**Helpers internos en servidor.py:**
- `razon_where(razon, col)` — construye cláusula `= %s` o `IN (...)` para razones sociales CSV
- `_parse_list(qs, key)` — parsea parámetro CSV a lista de valores en mayúsculas
- `_where_in(col, vals, a)` — genera `UPPER(col) IN (...)` dinámica con N placeholders
- `_where_cargo(col, vals, a)` — busca cargo original o unificado, case-insensitive con `UPPER()` en ambos lados

---

## Dashboard — estructura y funcionalidades

### Layout global (Material Design 3)
```
┌──────────────────────────────────────────┐
│  Header oscuro (gradient deep-blue)      │
│  ├── Logo Faranda + Filtros de header    │
│  └── Tabs (underline, sky-blue activo)   │
├──────────────────────────────────────────┤
│  Content (fondo --bg claro)              │
│  ├── KPI Bar (cards blancas MD3)         │
│  └── Panel del tab activo               │
└──────────────────────────────────────────┘
```

**Material Design 3 aplicado:** Roboto font, border-radius 16px, shadow doble capa, filtros y botones en pill (border-radius 20px), tabs con font-weight 500/700.

### Filtros del header (cascada multi-select)
```
Razón Social (multi) → Centro de Costo (multi) → Cargo (multi) → Empleado (single) → Operación → Período → ✕ Limpiar
```

- **Razón Social, Centro de Costo, Cargo:** `MultiSelect` (checkboxes, "N seleccionados", "Todas" limpia)
- **Empleado:** `CustomSelect` (single, con buscador)
- **Operación:** select simple — default "En Operacion"
- **Período:** `selYear` / `selMonth` — aplica a todos los tabs
- **Cascada:** cambiar hotel reconstruye centros → cambiar centro reconstruye cargos
- **Botón 🔄 sync CECOs:** junto al label "Centro de Costo" — sincroniza Unificacion_CECOs.xlsx → PostgreSQL

**Regla crítica:** Nunca leer `.value` directamente en filtros multi-select. Usar `getHotelFilter()`, `getCentroFilter()`, `getCargoFilter()` que devuelven arrays.

---

### Tab: 🔍 Análisis

**KPI Banner:** Total empleados · Hombres · Mujeres · Promedio de edad · Promedio antigüedad · Salario promedio

**Gráficos:**
- **Empleados por Hotel:** donut con leyenda scrollable (desktop) / barras horizontales descendentes (mobile). Etiquetas y líneas solo para hoteles ≥ 5% — evita solapamiento.
- **Empleados por Centro de Costo:** barras horizontales con degradado RANK_BLUES (14 tonos azul oscuro → claro). 20 barras en desktop. Altura dinámica `26px × n` barras. Click en barra → **drill-down de cargos** de ese CECO con botón "← Volver". Hint `🔍 Clic para ver cargos`.
- **Distribución por Tipo de Contrato:** donut (flex fill, sin espacio sobrante)
- **Pirámide de Antigüedad:** barras por rango (0-6m · 6-12m · 1-2a · 2-3a · 3-4a · 4+a). Usa `historico_empleados` para meses pasados; calcula desde `filteredEmps._meses` para el mes actual de API.

---

### Tab: 👤 Empleados

**Desktop:** tabla paginada con `table-layout:fixed`, anchos suman 100%.
- Columnas: Nombre (con badge M/F + title hover) · Hotel · Centro de Costo · Cargo (limpiado) · Tipo Contrato (pill coloreado) · Fecha Ingreso · F. Vence · Sueldo Base · Edad · Antigüedad
- Sort por cualquier columna (cargo ordena por `cleanCargo()`)
- Barra de búsqueda `max-width: min(420px, 40%)` + botón ✕ circular para limpiar
- `display:flex` en `<div class="td-nombre-inner">` (NO en `<td>`) — preserva `table-layout:fixed`

**Mobile (≤640px):** tarjetas `.emp-cards` con:
- Nombre en Title Case (`toTitle()`) + badge sexo
- Hotel · CECO · Cargo (limpio) · Contrato
- Borde color por tipo de contrato: indefinido (azul) / fijo (gold) / SENA (verde)
- F.Vence solo si no es indefinido
- Buscador mobile con sugerencias tipificadas (CECO/Cargo/Hotel/Empleado) en tiempo real
- Sort-bar mobile

---

### Tab: 💰 Salarios CECO

**Desktop:**
- Gráfico combo: barras salario actual vs LY (Last Year) + línea variación %. Escala eje Y desde `minBase × 0.4` (no desde 0). Leyenda `type:'scroll'` + `grid.top: 46`. Tooltip dinámico `fmtM()` — M / K / raw.
- Donut distribución de masa salarial: **Top 10 CECOs + "Otros (N)"** en gris agrupando el resto. Click en "Otros" → bottom sheet con grid 3-col.
- Tabla árbol: CECO (L1) → Cargo (L2) → Empleados (L3). `ceco-table-wrap` con `max-height: 480px` + `thead sticky`.

**Mobile (≤640px):**
- Tarjetas `.sal-cards` (una por CECO) — tabla `.sal-detail-card` oculta
- Gráfico CECO: barras horizontales con fuente dinámica (`offsetHeight/n*0.72`)
- Bottom sheet al tap `👥 N ›` → panel desde abajo con árbol Cargo → Empleados (CECO drill-down)
- Bottom sheet "Otros (N)" → grid 3 columnas con todos los CECOs agrupados

**Cache de variables:**
```js
_salMatrizCache   // árbol CECO→Cargo→Empleado
_donOtrosCache    // CECOs agrupados en "Otros"
_donTotalEmp      // total empleados para % donut
```

---

### Tab: 🔄 Ingresos y Retiros

**Fuente de datos:**
- **Meses ≤ Abril 2026:** PostgreSQL histórico (`GET /proxy/historico/ir`)
- **Mayo 2026 en adelante:** API externa (`POST /proxy/rrhh/ingresos-retiros`)
- Controlado por `IR_HIST_MAX = { year: 2026, month: 4 }` e `irUsaApi(y, m)`

**KPI Banner:**
| KPI | Descripción | Comparativo |
|---|---|---|
| Activos Inicio Mes | Snapshot del primer día del mes anterior | LY + Var |
| Ingresos | Total ingresos del período | LY + Var |
| Retiros | Total retiros del período | LY + Var |
| Activos Fin Mes | Activos Inicio + Ingresos − Retiros | LY + Var |
| % Rotación Deseada Anual | `(retiros_deseada / activos_inicio) × 12 × 100` | |
| % Rotación Indeseada Anual | `(retiros_indeseada / activos_inicio) × 12 × 100` | |

**Clasificación de rotación:**
- **Deseada:** Vencimiento de Contrato · Justa Causa · Term. Aprendizaje
- **Indeseada:** Voluntario · Sin Justa Causa · Injusta Causa

**Tablas (toggle pill único Ingresos / Retiros):**
- **Ingresos:** Nombre · Hotel · Cargo · Tipo Contrato · Fecha Ingreso · Fecha Término — ordenable por columna
- **Retiros:** Nombre · Hotel · Cargo · Tipo Contrato · Fecha Retiro · Razón · Tiempo de Servicio — ordenable por columna

**Gráficos (3 filas):**
1. Barras Rotación Deseada vs Indeseada (% y conteo) + Donut causas de retiro
2. Variación Neta Mensual (barras ingresos ↑ verde / retiros ↓ rojo + línea variación) + YoY por Hotel (barras divergentes)
3. Tendencia Retiros con Banda de Alerta (línea + media + media+σ + markArea) + Análisis de Cohorte (retención pct_1…pct_12)

**Mobile (≤640px):** accordion `.ir-acc-cards` — cada empleado como tarjeta expandible. Period picker mobile usa `getBoundingClientRect()` para posición correcta.

---

### Tab: 💼 Payroll (Nómina)

**Fuente de datos:** `POST /api/v1/nomina/reporte` por NIT de hotel. Siempre desde la API (sin histórico propio por ahora).

**KPI Cards:** Empleados en nómina · Días promedio trabajados · Total Devengos · Gasto Total Nómina — todos con comparativa LY y %VarLY.

**Tabla árbol de conceptos:** Grupo → Subgrupo → Concepto. Columnas: Concepto · Año Actual · Año Anterior · Var $. Incluye conceptos que solo aparecen en LY (mostrados con `—` en Año Actual).

**Gráfico:** Donut distribución de devengos por grupo.

**Tabla detalle empleados:** Nombre · Cargo · CECO · Días · Devengo · Deducción · Neto.

**Filtros:**
- Filtro BU integrado para Belvedere / FC Bogotá (mismo NIT 900345450, split por `bu_codigo = 'BE'` / `'Local'`). Reconstruye conceptos directamente desde `e.conceptos[]` de los empleados filtrados.
- Filtro CECO/Cargo: conectado a los filtros globales del header (`getCentroFilter()` + `getCargoFilter()`). Re-filtra sin re-fetch al cambiar CECO/cargo; re-fetch solo al cambiar hotel.

**Sistema de conceptos:**
- **`_v2/_v3`:** mismo código API con nombre diferente en otro hotel recibe clave BD con sufijo. Excel tiene col "Código BD" y "Código API". Filas en amarillo.
- **Índice doble:** `_prConceptos[codigo_db]` + `_prConceptosByApi[codigo_api|nombre.upper()]`. Función `_prLookup(codigo, nombre)` busca primero por API+nombre antes de fallback por código.

**Generación del mapeo:** `generar_conceptos_excel.py` — 18 NITs × 29 meses (Ene 2024 – May 2026) = 522 peticiones en paralelo. Detecta combinaciones únicas `(codigo, nombre)` por hotel.

---

## Mapas de hoteles (JavaScript)

```js
HOTEL_MAP          = hotel_name_api → nombre_personalizado
                     (prioridad por activos_api — evita overwrite por hoteles inactivos)

BU_HOTEL_MAP       = "hotel_name_api|bu_nombre" → nombre_personalizado
                     (hoteles con mismo hotel_name_api diferenciados por BU)

HOTEL_EXCLUIDOS    = Set de hotel_name_api/razon_social marcados "No mostrar" (ELMA, FNS)

HOTEL_RAZON_MAP    = hotel_name_api → razon_social
RAZON_HOTEL_MAP    = razon_social → hotel_name_api
RAZON_NOMBRE_MAP   = razon_social → nombre_personalizado
                     (hoteles SIN hotel_name_api: SERVMYS, SERVTCLUB, SPIA…)

NOMBRE_RAZONES_MAP = nombre_personalizado → [razon_social list]
CARGO_MAP          = cargo_original → cargo_unificado
CECO_MAP           = ceco_original (normalizado) → ceco_unificado (184 mappings)
```

**Hoteles con split por `bu_nombre` (BU_HOTEL_MAP):**
| hotel_name_api | bu_nombre | nombre_personalizado |
|---|---|---|
| Faranda_Bogota | CLUB HOUSE | FC Bogota |
| Faranda_Bogota | BELVEDERE | Belvedere |
| Hotel_Canabal | Local | Casa Canabal |
| Hotel_Canabal | Faranda Cucuta | FC Cucuta |
| GHS | Local | GHS |
| GHS | The Club | The Club |
| Col_Hoteles | FARANDA BARRANQUILLA | FC Barranquilla |
| Hotel_Caribe | HOTEL CARIBE | Hotel Caribe |

**Lógica de resolución de nombre (aplica en loadData, loadHistoricoEmps e irFlattenRows):**
```js
// Prioridad: BU_HOTEL_MAP > HOTEL_MAP > cleanHotel() > raw
hotel_name: BU_HOTEL_MAP[`${h.hotel_name}|${(e.bu_nombre||'').toUpperCase()}`]
         || HOTEL_MAP[h.hotel_name]
         || cleanHotel(h.hotel_name)
         || h.hotel_name
```

---

## Responsive CSS — breakpoints

| Breakpoint | Aplica a |
|---|---|
| `@media (max-width: 1024px)` | Tablet: columnas colapsadas, gráficos redimensionados |
| `@media (max-width: 768px)` | Tablet pequeño: `sal-cards` activadas, `sal-detail-card` oculta |
| `@media (max-width: 640px)` | Mobile: `emp-cards`, `ir-acc-cards`, buscador mobile |
| `@media (max-width: 380px)` | Small mobile: reducción adicional de padding y fuentes |

**Regla:** Todo cambio futuro de UI debe incluir CSS para los 4 breakpoints.

**Patrones mobile implementados (NO modificar sin razón):**
| Tab | Desktop | Mobile (≤640px) |
|---|---|---|
| Empleados | tabla `.tbl-wrap` | tarjetas `.emp-cards` |
| Salarios CECO | gráfico barras+líneas `#ch-sal` | tarjetas `.sal-cards` |
| Salarios CECO | tabla `.sal-detail-card` | oculta (`display:none`) |
| Ingresos y Retiros | tablas toggle `.ir-table-wrap` | accordion `.ir-acc-cards` |
| CECO donut | donut (desktop ≥640) | barras horizontales (<640) |
| Period picker | dropdown CSS | `getBoundingClientRect()` + estilos inline |

---

## Sistema del buscador mobile

```js
let _mSearchType = null; // 'ceco' | 'cargo' | 'hotel' | 'emp' | null

// null = escritura libre → OR sobre todos los campos
// 'ceco'/'cargo'/'hotel'/'emp' = filtro exacto solo ese campo
// Se define al elegir sugerencia en mPickSuggest()
// Se limpia en mSearchInput() y mSearchClear()
```

**Flujo:**
1. Usuario escribe → `mBuildSuggest()` construye sugerencias desde `filteredEmps` (respeta filtros de header)
2. Sugerencias tipificadas: CECO (azul), Cargo (verde), Hotel (naranja), Empleado (morado)
3. Al elegir sugerencia: `_mSearchType` = tipo elegido
4. `applyFilters()` filtra solo el campo correspondiente
5. `getMQServerParams(mq)`: respeta `_mSearchType` — para texto libre, orden: CECO → Cargo → Nombre

---

## Helpers JavaScript clave

```js
// Title Case para nombres de empleados
function toTitle(str) {
  if (!str) return '';
  return str.toLowerCase().replace(/\b\w/g, c => c.toUpperCase());
}
// Usado en: tabla desktop, emp-cards mobile, dropdown empleado,
//           salRenderMatriz, cecoSheetOpen, salarios CECO L1/L2/L3

// Normalización de cargo (usa CARGO_MAP + toTitle)
function cleanCargo(cargo) { return CARGO_MAP[cargo] || cargo; }

// Normalización de CECO (usa CECO_MAP + toTitle)
function cleanCentro(ceco) { return CECO_MAP[ceco] || ceco; }

// Formato de moneda dinámico
function fmtM(val) {
  if (val >= 1e6) return `$${(val/1e6).toFixed(1)}M`;
  if (val >= 1e3) return `$${(val/1e3).toFixed(0)}K`;
  return `$${val.toFixed(0)}`;
}

// Determina si un período usa API externa o histórico PostgreSQL
function irUsaApi(y, m) {
  if (MODO_HISTORICO) return false;
  return y > IR_HIST_MAX.year || (y === IR_HIST_MAX.year && m > IR_HIST_MAX.month);
}
```

---

## Variables JS globales clave

```js
// Constantes de período
const _hoy        = new Date();
const IR_HIST_MAX = { year: 2026, month: 4 };  // último mes en historico_ir
const API_MONTH   = MODO_HISTORICO
  ? { year: IR_HIST_MAX.year, month: IR_HIST_MAX.month }
  : { year: _hoy.getFullYear(), month: _hoy.getMonth() + 1 };
const SAL_MAX     = { year: API_MONTH.year, month: API_MONTH.month };
const MODO_HISTORICO = false;  // true = desconecta API externa completamente

// Estado global
let selYear  = API_MONTH.year, selMonth = API_MONTH.month;
let _periodChanging = false;     // flag anti-doble-carga
let salCharts = [null, null];    // [combo, donut]
let irCharts  = [null, null, null, null, null, null];  // [rotacion, causas, mensual, yoy, tendencia, cohorte]
let _irTblMode = 'ing';          // toggle tabla I&R: 'ing'|'ret'

// Mapas de datos
let CARGO_MAP = {}
let RAZON_NOMBRE_MAP = {}
let BU_HOTEL_MAP = {}            // "hotel_name_api|bu_nombre" → nombre_personalizado
let HOTEL_MAP = {}
let NOMBRE_RAZONES_MAP = {}
let HOTEL_RAZON_MAP = {}
let SOCIOS_IDS = new Set()       // identificaciones marcadas como Socio

// Cache IR
let _irAllCY = [], _irAllLY = [], _irCacheKey = '';

// Payroll
let _prHotels = [], _prConceptos = {}, _prConceptosByApi = {};
let _prCur = null, _prLY = null;
let _prLastHotelKey = '', _prInited = false;
```

---

## LaunchAgent — inicio automático

**Archivo producción:** `~/Library/LaunchAgents/com.bia.dashboard-talento.plist`
- Arranca `servidor.py` al encender el Mac
- Se reinicia si se cae (`KeepAlive: true`)
- Puerto: 8080
- Logs: `logs/servidor.log` y `logs/servidor_error.log`

**Archivo desarrollo:** `~/Library/LaunchAgents/com.bia.dashboard-talento-dev.plist`
- Arranca el servidor del worktree en puerto 8081

**Gestión del LaunchAgent:**
```bash
# Activar
launchctl load ~/Library/LaunchAgents/com.bia.dashboard-talento.plist

# Detener y recargar (tras cambios en servidor.py)
launchctl unload ~/Library/LaunchAgents/com.bia.dashboard-talento.plist
launchctl load   ~/Library/LaunchAgents/com.bia.dashboard-talento.plist

# Iniciar manualmente si el LaunchAgent falla
kill $(lsof -ti:8080)
cd "/Users/ricardomartinez/BIA CLAUDE PROYECTOS/Proyectos Claude IA/Talento Humano Api Dashboard"
python3 servidor.py
```

---

## Control de versiones (Git)

**Ramas:**
- `main` — producción (v1.12.0 mergeado)
- `claude/angry-diffie-524755` — rama dev con v1.13.0 (Tab Payroll) pendiente de merge
- Worktrees: `.claude/worktrees/`

**Nota:** El servidor en el worktree usa puerto 8081 para no conflictuar con producción (8080). Al hacer merge a main, verificar que `PORT=8080` en `servidor.py`.

---

## Historial de versiones — todo lo construido

### v1.0.0 — 2026-04-22 — Base inicial

- Dashboard HTML base con ECharts 5.4.3
- Servidor proxy `servidor.py` (Python http.server, puerto 8080) — resuelve CORS
- Autenticación API via header `X-API-Key`
- Endpoints proxy base: GET transparente + POST ingresos-retiros
- **Tab Análisis:** KPIs principales (total empleados, género, edad, antigüedad, salario); distribución por cargo, CECO, tipo de contrato, antigüedad
- **Tab Empleados:** tabla paginada con detalle de empleado
- Filtros de header: Razón Social, Centro de Costo, Cargo, Empleado, Operación, Período
- Mapas base: `HOTEL_MAP`, `HOTEL_EXCLUIDOS`, `RAZON_NOMBRE_MAP`
- `cleanHotel()` y `cleanCentro()` para normalización

---

### v1.1.0 — 2026-04-24 — PostgreSQL histórico

- Carga de **38.891 registros históricos** (Enero 2022 — Marzo 2026): 51 meses, 61 archivos Excel consolidados
- Tabla `historico_empleados` + tabla `empresas` (27 códigos razon_social)
- Endpoint `/proxy/historico/empleados` — con COALESCE fecha_nacimiento e INITCAP sexo
- Endpoint `POST /proxy/historico/cecos-sync` — inserta CECOs nuevos desde API
- Helper `razon_where()` — cláusula dinámica para razones sociales CSV
- **Paleta cromática Faranda Hotels** desde Brand Guidelines Marzo 2024
- Separación de fuente: mes actual = API; meses anteriores = PostgreSQL

---

### v1.2.0 — 2026-04-25 — Tab Salarios CECO

- **Tab Salarios CECO completo:** combo barras/línea actual vs LY, donut masa salarial, tabla detalle
- Endpoints: `/proxy/historico/salarios`, `/proxy/historico/matriz`, cecos-map, cargos-map, hoteles-map
- Tablas PostgreSQL: `ceco_unificacion`, `cargo_unificacion`, `hotel_unificacion`
- Sync desde Excel: `sync-hoteles` y `sync-cargos`
- `cleanCargo()` con `CARGO_MAP`. `SOCIOS_IDS` Set de cédulas.
- `HOTEL_MAP` prioriza por `activos_api` — evita sobrescritura por hoteles inactivos

---

### v1.3.0 — 2026-04-27 — Tab Ingresos y Retiros

- **Tab I&R completo:** Banner 6 KPIs con LY, Tabla Ingresos, Tabla Retiros, Gráfico Rotación (barras), Gráfico Causas (donut)
- Clasificación rotación Deseada / Indeseada. Fórmula anualizada.
- Endpoint `/proxy/historico/ir` — desde `historico_ir` con LY
- Endpoint `/proxy/historico/activos-count` — conteo activos por snapshot
- `irFlattenRows()` — aplana `hoteles[] → unidades_negocio[] → rows[]`
- Caché IR por período. Período global `selYear`/`selMonth`. Flag `_periodChanging`.

---

### v1.4.0 — 2026-04-30 — BU_HOTEL_MAP + Responsive + LaunchAgent + Git

- **`BU_HOTEL_MAP`** — mapa `"hotel_name_api|bu_nombre" → nombre_personalizado`. Split: FC Bogota / Belvedere, Casa Canabal / FC Cucuta, GHS / The Club
- **LaunchAgent macOS** — inicio automático con `KeepAlive: true`
- **Filtros globales** — Cargo, Empleado y CECO aplican en todos los tabs
- **CSS responsive completo** — 4 breakpoints (1024, 768, 640, 380px) en todos los tabs
- `irFilterRows()` — función centralizada de filtrado en cliente
- Lógica Activos Inicio/Fin corregida — Inicio = snapshot mes anterior
- Fix FC Bogota no aparecía en dropdown
- **Repositorio Git inicializado**

---

### v1.5.0 — 2026-05-04 — API_MONTH dinámico + Carga Abril 2026

- **Carga Abril 2026 — `historico_empleados`:** +1.021 registros → **39.912 total** (52 meses). Nota: `fecha_nacimiento` en formato "Friday, March 12, 2004" → `pd.to_datetime()`.
- **Carga Abril 2026 — `historico_ir`:** +109 movimientos → 4.929 total
- **`API_MONTH` y `SAL_MAX` dinámicos** — `new Date()` en JS; se actualiza solo cada mes
- **`IR_HIST_MAX = { year: 2026, month: 4 }`** — frontera: PostgreSQL ≤ Abril, API ≥ Mayo

---

### v1.6.0 — 2026-05-07 — Multi-select + Gráficas mejoradas

- **Multi-select** para Razón Social, Centro de Costo y Cargo (clase `MultiSelect` independiente)
- Helpers: `getHotelFilter()`, `getCentroFilter()`, `getCargoFilter()`, `getHotelRazonesMulti()`
- servidor.py: `_parse_list()`, `_where_in()`, `_where_cargo()` — cláusulas dinámicas multi-valor
- **Tab Análisis — Hotel:** barras → donut con leyenda scrollable (desktop) + barras mobile
- **Tab Análisis — Cargos:** barras → embudo con gradiente azul, etiquetas blancas internas
- **Tab Análisis — CECOs:** ancho eje Y dinámico según nombre más largo
- **Salarios combo:** escala desde `minBase × 0.4`, `fmtM()` dinámico
- `rebuildCentros()` en multi-select; `applyFilters()` filtra por arrays

---

### v1.7.0 — 2026-05-08 — Responsive mobile completo

- **Responsive Tab Análisis** — gráficos redimensionados, stat-col 2 columnas mobile
- **Responsive Tab Empleados** — cards `.emp-cards` con borde color, sort-bar, F.Vence condicional
- **LaunchAgent dev** — puerto 8081 para worktree

**Bugs corregidos:**
- KPI Activos CECO incorrecto en mobile: `getMQServerParams` ignoraba `_mSearchType`
- `_where_cargo` case-sensitive: corregido a `UPPER()` en ambos lados (Abr 2026 = capitalización mixta)
- Buscador mobile: hotel añadido como tipo ('hotel', tag naranja)
- `mBuildSuggest` usa `filteredEmps` (respeta filtros de header)
- Desbordamiento texto en emp-cards: `min-width:0`, fuentes `clamp()`, `word-break`

---

### v1.8.0 — 2026-05-08 — UX Tab Empleados

- Helper `toTitle(str)` — Title Case aplicado en tabla, tarjetas, dropdown, Salarios CECO
- Badge sexo (M/F) inline en columna Nombre desktop
- `title` por celda en Nombre, Hotel, CECO, Cargo
- Barra de búsqueda desktop: `max-width: min(420px, 40%)`
- Sort por Cargo usa `cleanCargo()` — consistente con lo mostrado

---

### v1.9.0 — 2026-05-08 — UX desktop Salarios e I&R

- **Salarios CECO:** `ceco-table-wrap` con `max-height: 480px` + `thead sticky`. Leyenda `type:'scroll'` + `grid.top: 46`.
- **I&R:** Sort por columnas con caché (`_irIngCache`/`_irRetCache`). `toTitle()` en nombres. `max-height: 380px`.
- **Tab Análisis:** fixes donut Contrato, barras Edad, donut Salarios, tooltip Hotel con M/F y %.
- **Matriz CECO:** `toTitle()`, acento `border-left: 3px`, botón toggle circular, padding empleados.

---

### v1.10.0 — 2026-05-11 — MODO_HISTORICO + MD3 + fixes

- **`MODO_HISTORICO`** — flag que desconecta completamente la API. Para desarrollo/testing sin API.
- **KPI Activos Fin LY** — `id="k-fin-sub"` + comparativa año anterior
- **Botón ✕ buscador** — botón circular MD3 que aparece al escribir
- **Material Design 3 completo:** Roboto, border-radius 16px, shadow capa doble, pills 20px, scrollbar 6px

---

### v1.11.0 — 2026-05-12 — Drill-down + 4 gráficos I&R + Toggle + Reorder

- **Drill-down CECOs** — click barra → cargos del CECO con RANK_BLUES + "← Volver"
- **Top 15 Cargos eliminado** — card, función `renderCargos()` y CSS `#ch-cargos` removidos
- **Tab Análisis reordenado** — Hotel sube a fila 1; Contrato baja con Pirámide
- **I&R — 4 nuevos gráficos:** Variación Neta Mensual · YoY por Hotel · Tendencia+Banda · Cohorte Retención
- **I&R — Toggle tabla** — pill único Ingresos/Retiros reemplaza dos tablas apiladas (`_irTblMode`)
- **CECOs:** 20 barras, altura dinámica `26px×n`, RANK_BLUES, `barCategoryGap 20%`
- **Donut Contrato:** flex fill — sin espacio sobrante
- Endpoints nuevos: `/proxy/historico/ir-mensual`, `/proxy/historico/cohorte-retencion`

---

### v1.12.0 — 2026-05-13 — Sync CECOs + fixes tabla + period picker

- **Endpoint `POST /proxy/admin/sync-cecos`** — lee `Unificacion_CECOs.xlsx` → `ceco_unificacion`
- **Botón 🔄 sync CECOs** — junto a "Centro de Costo" en el header; `syncCecosXlsx()` recarga CECO_MAP
- **184 CECOs mapeados** — `Unificacion_CECOs.xlsx` completado (A&B → Alimentos Y Bebidas, etc.)
- **Gráfico Antigüedad mes actual** — calcula bins desde `filteredEmps._meses` sin tocar PostgreSQL
- **Fix tabla Empleados** — `display:flex` movido de `<td>` a `<div class="td-nombre-inner">`. Anchos 100%.
- **Fix period picker mobile** — `getBoundingClientRect()` + estilos inline. `top = rect.bottom + 6px`.
- **CECOs y Cargos Title Case** — `cleanCentro()` y `cleanCargo()` aplican `toTitle()`.

---

### v1.13.0 — 2026-05-14 — Tab Payroll (Nómina)

- **Tab 💼 Payroll completo:** KPIs, tabla árbol Conceptos, donut devengos, tabla empleados
- **18 NITs cargados** en `empresas.nit` desde Excel `Nit Hoteles Faranda.xlsx`
- **Tabla `nomina_conceptos`** en PostgreSQL con sistema `_v2/_v3`
- **`GET /proxy/nomina/hoteles-nit`** — JOIN hotel_unificacion + empresas
- **`GET /proxy/nomina/conceptos-map`** — con `codigo_api` via `regexp_replace`
- **`POST /proxy/admin/sync-conceptos`** — upsert desde `Conceptos_Nomina_Mapeo.xlsx`
- **`generar_conceptos_excel.py`** — 18 NITs × 29 meses = 522 peticiones paralelas
- **Filtro BU Belvedere / FC Bogotá** — mismo NIT, split por `bu_codigo`
- **Filtro CECO/Cargo** — conectado a filtros globales del header sin re-fetch

---

## Pendientes activos

### 🔴 Alta prioridad

#### 1. Nóminas históricas → PostgreSQL
- **Archivo:** `OneDrive/.../Base de Datos Historicos/Nominas de Empleados Historicos.xlsx`
- **Estructura:** 27.796 filas × 416 columnas · 27 meses (Ene 2024 – Mar 2026)
- **Modelo elegido:** EAV — `nomina_header` + `nomina_lineas` + `conceptos`
- **Problemas a resolver antes de cargar:**
  1. Fechas en texto largo ("Monday, January 1, 2024") → parsear a DATE
  2. 180 filas duplicadas (Cedula+Fecha) — válidas: pago normal + liquidación → PK: `(Cedula, Fecha, Estado)`
  3. `Cargo` solo 3.500/27.796 filas llenas — cruzar con `historico_empleados`
  4. `Area` con duplicados semánticos: "Hotel Bantú" vs "Bantú" → tabla de mapeo
  5. 401 filas con `Area` nula (SERVIHOTELES, MANISA KAVALA)
  6. 905 filas sin Sexo ni Fecha Nacimiento
  7. 1 fila con `Sueldo Mes` negativo (-466.908) — reversa válida
  8. 397 columnas de conceptos: 235 con <1% datos — confirma EAV
  9. Nombres de conceptos inconsistentes → tabla de normalización

### 🟡 Media prioridad

#### 2. Merge worktree → main (v1.13.0)
- Tab Payroll completo en worktree `angry-diffie-524755`; ejecutar `/merge` cuando confirme

#### 3. `empresas.nombre` incompleto
- 27 códigos razon_social sin nombre completo de empresa (BIAN, CCAN, GHS, etc.)

### 🟢 Mejoras opcionales

#### 4. Detección automática conceptos nuevos en Payroll
- Detección pasiva en `prLoad()` + banner amarillo al detectar nuevo
- Botón "🔍 Buscar nuevos" → endpoint `/check-nuevos-conceptos` (~18 llamadas)

#### 5. Tab Salarios CECO — mejoras menores
- `max-height` en `sal-detail-card` — puede crecer sin límite al expandir todo
- `p.nombre` en `cecoSheetOpen` pendiente de `toTitle()` en edge cases

#### 6. Refactor limpio del HTML
- El dashboard acumula parches de múltiples sesiones; refactor mejoraría mantenibilidad

#### 7. Opciones de Operación para histórico
- `historico_empleados.operacion` tiene "RETIROS" no disponible como opción en el filtro

#### 8. Tabla `unidades_negocio`
- Pendiente de poblar (id, empresa_codigo, nombre, ciudad, pais, codigo_api, activa)

---

## Problemas resueltos (referencia)

| Problema | Solución |
|---|---|
| CORS entre dashboard y API externa | Proxy `servidor.py` local |
| API solo devuelve mes actual | PostgreSQL histórico (39.912 registros) |
| Hoteles con mismo `hotel_name_api` pero distintos | `BU_HOTEL_MAP` con clave compuesta `"hotel_name_api\|bu_nombre"` |
| `fecha_nacimiento` Abr 2026 en formato texto largo | `pd.to_datetime(flexible=True)` al cargar Excel |
| `cargo` con capitalización mixta rompía filtros | `_where_cargo` con `UPPER()` en ambos lados |
| KPI Activos CECO incorrecto en mobile | `getMQServerParams` respeta `_mSearchType` |
| FC Bogota no aparecía en dropdown | `HOTEL_MAP` prioriza por `activos_api`; dropdown incluye `NOMBRE_RAZONES_MAP` |
| `API_MONTH` hardcodeado cada mes | `new Date()` en JS → automático |
| Servidor muere al cerrar terminal | LaunchAgent macOS con `KeepAlive: true` |
| Puerto 8080 conflicto post-merge | Verificar `PORT=8080` en `servidor.py` antes de cada merge |
| `display:flex` en `<td>` rompía `table-layout:fixed` | Movido a `<div class="td-nombre-inner">` interno |
| Period picker mobile posición incorrecta | `getBoundingClientRect()` + estilos inline en `togglePeriodPicker()` |
| API key expirada (HTTP 403) | Actualizar `API_KEY` en `servidor.py`; key actual: `ri_<REDACTED>` |
| Concepto duplicado entre hoteles (mismo código, distinto nombre) | Sistema `_v2/_v3` en `nomina_conceptos` con índice doble |

---

## Infraestructura PostgreSQL — Trabajo transversal de Ricardo

### Entorno PostgreSQL completo

| Servidor | Host | Puerto | Usuario |
|---|---|---|---|
| Local Mac (principal) | localhost | 5432 | postgres |
| Remoto producción | 172.19.0.160 | 5432 | rmartinez |

**Credenciales remoto:** usuario `rmartinez` · contraseña `luna-roja-tranquila-33`

### Bases de datos en localhost:5432

| Base de datos | Uso |
|---|---|
| `Bases de Datos Hoteles` | Tabla `Oportunidades Colombia Año 2026` (2.210 registros, carga diaria desde OneDrive) |
| `Bases de Datos Historicos` | Histórico comercial. `historico_oportunidades` (3.486 registros) |
| `Bases de Datos Historicos Comercial - rmartinez` | Copia local del remoto. 7 tablas · 575.949 registros · 112 MB |
| `Base de Datos Talento Humano` | Dashboard HR. `historico_empleados` (39.912) + tablas de unificación |

> **Nota:** Todos los nombres tienen mayúsculas y espacios — siempre usar comillas dobles en SQL.

### MCP PostgreSQL — Acceso directo desde Claude Code

| Nombre MCP | Host | Latencia |
|---|---|---|
| `pg-historicos-comercial` | localhost:5432 | ~8 ms |
| `pg-remoto-172` | 172.19.0.160:5432 | ~293 ms |

**Registro:** `~/.claude.json` (scope: user — globales). Wrappers en `~/.claude/mcp-wrappers/`.

---

## P5 · Ecosistema Comercial — Avances de Ricardo Martínez

### Carga histórica comercial — COMPLETADO ✅

**7 tablas · 575.949 registros** cargados en PostgreSQL. Base: `Bases de Datos Historicos`.

| Tabla | Registros |
|---|---|
| `forecast_historico` | 6.339 |
| `historico_agentes_fns` | 700 |
| `historico_empresas_fns` | 18.981 |
| `historico_empresas_opera` | 21.077 |
| `historico_empresas_zeus` | 518.150 |
| `historico_omnibees` | 7.216 |
| `historico_oportunidades` | 3.486 |

Scripts en `C:\Proyectos Claude IA\PostgrestSQL Proyect\`. Detectan cambios por hash MD5. Tareas Windows a las 9:00 AM.

### Vista UNION ALL — COMPLETADO ✅
`Vista Oportunidades Historico y 2026` en `Bases de Datos Hoteles` via `postgres_fdw`.

### Carga automática Oportunidades 2026 — OPERATIVA ✅
Script `cargar_oportunidades.py` carga diariamente desde `OPORTUNIDADES FARANDA 2025.xlsx` (hoja `GRUPOS AÑO 2026`). TRUNCATE + INSERT, ~1 segundo.

---

## Cómo iniciar el servidor

**Producción (LaunchAgent, automático al encender Mac):**
```bash
# Si necesita reiniciarse manualmente:
launchctl unload ~/Library/LaunchAgents/com.bia.dashboard-talento.plist
launchctl load   ~/Library/LaunchAgents/com.bia.dashboard-talento.plist
```

**Manual (si el LaunchAgent falla o hay conflicto de puerto):**
```bash
kill $(lsof -ti:8080)
cd "/Users/ricardomartinez/BIA CLAUDE PROYECTOS/Proyectos Claude IA/Talento Humano Api Dashboard"
python3 servidor.py
```

**Con preview_start (Claude Code):**
1. Verificar: `lsof -i :8080`
2. Matar si hay proceso: `kill $(lsof -ti:8080)`
3. Llamar `preview_start` con nombre "Servidor Proxy Dashboard"

---

*Dashboard Talento Humano · P4 HR + Agentes IA · Faranda Hotels 2026*  
*Desarrollado por Ricardo Martínez (BAI) · Herramienta: Claude Code*  
*Última actualización: 2026-05-19 · Versión: 1.13.0*
