// Cálculos portados de herramientas/sync_pipe.js (ETL estático) — misma lógica, servidor en vivo. export const CARTERA = ["Up-Selling", "Cross-Selling", "Plan de Cuenta"]; export const PESO_PLAN = { "Plan A - En firma": 0.9, "Plan B - En Negociación": 0.6, "Plan C - En seguimiento": 0.3, "Plan D - Recalificar": 0.15, "Sin plan de cierre": 0.1, }; // Los 4 tipos que cuentan para cumplimiento de meta (ver CLAUDE.md — se excluyen renovaciones y SAC). export const TIPOS_META = ["Venta - Cliente Nuevo", "Up-Selling", "Cross-Selling", "Plan de Cuenta"]; // Ejecutivos de venta reales (ver CLAUDE.md). Gabriela Sánchez y Ashley Bermúdez aparecen // en los datos de Dynamics pero no son ejecutivas de venta — se excluyen del selector. export const EJECUTIVOS_VENTA = [ "Mirna Obando Morras", "Maria Auxiliadora Marenco Abea", "Denis Urroz Manzanares", "Daybellys Perez Sequeira", "Martha Somarriba Sirias", ]; export function segmentoDe(tipo) { return CARTERA.includes(tipo) ? "Cartera Activa" : "Clientes Nuevos"; } // Agrupa arr por o[key], contando y sumando valfn(o). Ordena por conteo descendente. export function agg(arr, key, valfn) { const m = new Map(); arr.forEach((o) => { const k = o[key] || "(sin dato)"; if (!m.has(k)) m.set(k, { n: 0, v: 0 }); const e = m.get(k); e.n++; e.v += valfn ? valfn(o) : 0; }); return [...m.entries()].map(([k, x]) => ({ k, ...x })).sort((a, b) => b.n - a.n); } export function bucketActividad(diasSinActividad) { if (diasSinActividad == null) return "Sin dato"; if (diasSinActividad <= 14) return "Activa"; if (diasSinActividad <= 30) return "Tibia"; if (diasSinActividad <= 60) return "Fría"; return "Congelada"; } // row: objeto ya con etiquetas resueltas (tipo, plan como texto, no option-set numérico). // faseInfo: { fase, started } | undefined, de alfa_stagedurations. // today: Date, para calcular días en fase de forma determinística. export function derivePipelineRow(row, faseInfo, today) { const plan = row.plan || "Sin plan de cierre"; const peso = PESO_PLAN[plan] ?? 0.1; const est = row.est || 0; const diasEnFase = faseInfo?.started ? Math.round((today - new Date(faseInfo.started)) / 86400000) : null; const estanc = (row.dsa != null && row.dsa > 14) || (diasEnFase != null && diasEnFase > 30); return { ...row, plan, peso, fc: +(est * peso).toFixed(2), estanc, faseActual: faseInfo?.fase || null, diasEnFase, actividad: bucketActividad(row.dsa), segmento: segmentoDe(row.tipo), mesCierre: row.close ? row.close.slice(0, 7) : null, }; }