feat(reportes): rango salarial en buckets + script de importacion de afluencia
- bucketSalario: el texto libre del rango salarial ('$80k', '4,500 DLLS',
'50-80 MIL') se agrupa en rangos por moneda para que la grafica sea
legible; valores sin monto se omiten.
- scripts/importar-afluencia-agosto.py: importa el historial de
afluencia al tablero Leads con normalizacion de catalogos, dedup por
telefono y desregistro temporal del webhook create_item (para no
disparar round-robin ni oportunidades con datos historicos).
This commit is contained in:
@@ -111,6 +111,44 @@ function bucketEdad(edad: number): string {
|
||||
return '55+'
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte el rango salarial de texto libre ('$80k', '4,500 DLLS', '50-80
|
||||
* MIL', '100,000.00-120,000.00') a un bucket graficable por moneda; los rangos
|
||||
* se promedian y K/MIL multiplica miles. null = sin monto interpretable
|
||||
* ('NO QUISO DECIR', '?', vacío) → se salta de la gráfica.
|
||||
*/
|
||||
export function bucketSalario(texto: string): string | null {
|
||||
const t = texto.toUpperCase()
|
||||
const tokens = [...t.matchAll(/\d[\d.,]*/g)].map((m) => m[0])
|
||||
if (tokens.length === 0) return null
|
||||
const montos = tokens
|
||||
.map((tok) => {
|
||||
// separador seguido de 3 dígitos = miles; el resto se trata como decimal
|
||||
let s = tok
|
||||
let prev = ''
|
||||
while (prev !== s) {
|
||||
prev = s
|
||||
s = s.replace(/(\d),(\d{3})(?!\d)/g, '$1$2')
|
||||
}
|
||||
const n = Number.parseFloat(s.replace(',', '.'))
|
||||
return Number.isFinite(n) ? n : null
|
||||
})
|
||||
.filter((n): n is number => n !== null)
|
||||
if (montos.length === 0) return null
|
||||
const enMiles = /\d\s*(K|MIL)\b/.test(t) && montos.every((n) => n < 1000)
|
||||
const ajustados = enMiles ? montos.map((n) => n * 1000) : montos
|
||||
const monto = ajustados.reduce((a, b) => a + b, 0) / ajustados.length
|
||||
if (/DLLS|DLS|USD/.test(t)) {
|
||||
if (monto < 3000) return 'Hasta $3k USD'
|
||||
if (monto < 5000) return '$3k–$5k USD'
|
||||
return 'Más de $5k USD'
|
||||
}
|
||||
if (monto < 30000) return 'Hasta $30k MXN'
|
||||
if (monto < 60000) return '$30k–$60k MXN'
|
||||
if (monto < 100000) return '$60k–$100k MXN'
|
||||
return 'Más de $100k MXN'
|
||||
}
|
||||
|
||||
/** Inicio del día local de `ahora` (para "nuevos hoy") */
|
||||
function inicioDelDia(d: Date): Date {
|
||||
return new Date(d.getFullYear(), d.getMonth(), d.getDate())
|
||||
@@ -195,7 +233,11 @@ export function construirResumen(datos: DatosResumen, ahora: Date): ResumenRepor
|
||||
porTipoEconomia: contarPorNoVacios(datos.leads.map((l) => l.tipoEconomia ?? '')),
|
||||
porCalifica: contarPorNoVacios(datos.leads.map((l) => l.califica ?? '')),
|
||||
porCredito: contarPorNoVacios(datos.leads.map((l) => l.credito ?? '')),
|
||||
porRangoSalarial: contarPorNoVacios(datos.leads.map((l) => l.rangoSalarial ?? '')),
|
||||
porRangoSalarial: contarPorNoVacios(
|
||||
datos.leads
|
||||
.map((l) => bucketSalario(l.rangoSalarial ?? ''))
|
||||
.filter((b): b is string => b !== null),
|
||||
),
|
||||
porMedioContacto: contarPorNoVacios(datos.leads.map((l) => l.medioContacto ?? '')),
|
||||
porLoQuierePara: contarPorNoVacios(datos.leads.map((l) => l.loQuierePara ?? '')),
|
||||
porEdad: contarPorNoVacios(
|
||||
|
||||
Reference in New Issue
Block a user