Files
FRASA-CAS/api-gateway/src/services/lead-scoring.service.ts
Consult AS Technology db18685872 feat(ux): 17 mejoras de experiencia de usuario
Backend (10 endpoints nuevos):
- Recordatorios personales (CRUD + tabla RecordatorioVendedor)
- Lead scoring 0-100 (etapa+recencia+datos)
- Dashboard mora (pagos vencidos, monto, días atraso)
- Timeline de pagos (pagado/vence/futuro)
- Perfil completo contacto (pagos+cotizaciones+docs+score)
- Exportar contactos a CSV

Frontend Mi día:
- Resumen matutino (saludo+KPIs)
- Historial visual lead (timeline)
- Score badge 0-100 (verde/amarillo/rojo)
- Vista kanban (drag & drop)

Frontend Cobranza:
- Dashboard mora (tabla+WhatsApp)
- Timeline pagos visual (nodos coloreados)

Frontend Alta Clientes:
- Checklist documentos visual
- Foto cliente (cámara)

Frontend Landing:
- Perfil contacto completo
- Exportar contactos CSV

Frontend Mi día (extras):
- Comparativa mensual
- KPIs dashboard (leads hoy, ventas, conversión)

806 tests, 5 apps construidas.
2026-09-08 07:42:54 +00:00

108 lines
2.7 KiB
TypeScript

import { prisma } from './prisma.js'
/**
* Lead Scoring: asigna un score 0-100 a cada LeadCaptura basado en:
* - Etapa del pipeline (NUEVO=20, CONTACTADO=40, INTERESADO=60, AGENDO=80, CALIFICADO=90)
* - Recencia (último cambio/creación): <24h +10, <7d +5, >30d -20
* - Datos de contacto: tiene teléfono +5, email +5
* - Origen conocido (no vacío): +5
*/
const ETAPA_SCORE: Record<string, number> = {
NUEVO: 20,
CONTACTADO: 40,
INTERESADO: 60,
AGENDO: 80,
CALIFICADO: 90,
}
export interface LeadScore {
leadId: string
nombreLead: string
usuarioId: string
etapa: string
telefono: string | null
email: string | null
origen: string | null
score: number
}
/**
* Calcula el score 0-100 de un lead dado su registro de LeadCaptura.
* Función pura (testable sin BD).
*/
export function calcularScore(lead: {
etapa: string
telefono: string | null
email: string | null
origen: string | null
createdAt: Date
updatedAt: Date
}, ahora: Date = new Date()): number {
let score = 0
// 1. Etapa
const etapaNorm = (lead.etapa ?? '').trim().toUpperCase()
score += ETAPA_SCORE[etapaNorm] ?? 10
// 2. Recencia (basado en updatedAt como proxy de "última actividad")
const diffMs = ahora.getTime() - lead.updatedAt.getTime()
const diffHoras = diffMs / (1000 * 60 * 60)
if (diffHoras < 24) {
score += 10
} else if (diffHoras < 7 * 24) {
score += 5
} else if (diffHoras > 30 * 24) {
score -= 20
}
// 3. Datos de contacto
if (lead.telefono !== null && lead.telefono.trim() !== '') score += 5
if (lead.email !== null && lead.email.trim() !== '') score += 5
// 4. Origen conocido
if (lead.origen !== null && lead.origen.trim() !== '') score += 5
// Clamp 0-100
return Math.max(0, Math.min(100, score))
}
/**
* Obtiene los scores de todos los leads activos (no descartados), opcionalmente
* filtrados por usuarioId.
*/
export async function obtenerScores(usuarioId?: string): Promise<LeadScore[]> {
const where: Record<string, unknown> = {
board: { in: ['LEADS', 'OPORTUNIDADES'] },
}
if (usuarioId) {
where['usuarioId'] = usuarioId
}
const leads = await prisma.leadCaptura.findMany({ where })
const ahora = new Date()
return leads
.map((lead) => ({
leadId: lead.leadId,
nombreLead: lead.nombreLead,
usuarioId: lead.usuarioId,
etapa: lead.etapa,
telefono: lead.telefono,
email: lead.email,
origen: lead.origen,
score: calcularScore(
{
etapa: lead.etapa,
telefono: lead.telefono,
email: lead.email,
origen: lead.origen,
createdAt: lead.createdAt,
updatedAt: lead.updatedAt,
},
ahora,
),
}))
.sort((a, b) => b.score - a.score)
}