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.
This commit is contained in:
107
api-gateway/src/services/lead-scoring.service.ts
Normal file
107
api-gateway/src/services/lead-scoring.service.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user