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.
108 lines
2.7 KiB
TypeScript
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)
|
|
}
|