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:
@@ -1,3 +1,5 @@
|
||||
import { prisma } from './prisma.js'
|
||||
import { calcularScore } from './lead-scoring.service.js'
|
||||
import { config } from '../config/index.js'
|
||||
import { detalleEventoUltimoCambio, esEstadoFinalApartado } from './mi-dia.service.js'
|
||||
import type { MondayService } from './monday.service.js'
|
||||
@@ -195,3 +197,167 @@ export async function detalleContacto(
|
||||
historial,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Perfil Completo del Contacto (modo local) ─────────────────────────
|
||||
|
||||
export interface PerfilCompletoContacto {
|
||||
contacto: Record<string, string>
|
||||
historialPagos: Array<{
|
||||
planId: string
|
||||
clienteNombre: string
|
||||
unidadCodigo: string
|
||||
proyecto: string
|
||||
numPago: number
|
||||
fechaVencimiento: string
|
||||
monto: number
|
||||
montoPagado: number
|
||||
estado: string
|
||||
}>
|
||||
cotizaciones: Array<{
|
||||
id: string
|
||||
unidadCodigo: string
|
||||
tipoPropuesta: string
|
||||
precioFinal: number
|
||||
createdAt: string
|
||||
}>
|
||||
documentos: Array<{
|
||||
id: string
|
||||
tipo: string
|
||||
nombreArchivo: string
|
||||
createdAt: string
|
||||
}>
|
||||
leadScore: number | null
|
||||
ultimaActividad: {
|
||||
titulo: string
|
||||
fecha: string
|
||||
tipo: string
|
||||
} | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Perfil completo de un contacto: combina datos del cliente/lead, historial
|
||||
* de pagos, cotizaciones, documentos, lead score y última actividad local.
|
||||
*/
|
||||
export async function perfilCompleto(contactoId: string): Promise<PerfilCompletoContacto | null> {
|
||||
// Buscar el cliente
|
||||
const cliente = await prisma.cliente.findFirst({
|
||||
where: { OR: [{ id: contactoId }, { mondayContactoId: contactoId }] },
|
||||
})
|
||||
|
||||
// Buscar la captura del lead
|
||||
const captura = await prisma.leadCaptura.findUnique({ where: { leadId: contactoId } })
|
||||
|
||||
if (!cliente && !captura) return null
|
||||
|
||||
const telefono = cliente?.celular ?? captura?.telefono ?? ''
|
||||
const email = (cliente?.email ?? captura?.email ?? '').trim().toLowerCase()
|
||||
const nombre = cliente?.nombreCompleto ?? captura?.nombreLead ?? 'Sin nombre'
|
||||
|
||||
// Contacto fields
|
||||
const contacto: Record<string, string> = {
|
||||
nombre,
|
||||
Teléfono: telefono,
|
||||
Email: email,
|
||||
}
|
||||
if (cliente) {
|
||||
contacto['CURP'] = cliente.curp
|
||||
contacto['RFC'] = cliente.rfc
|
||||
contacto['Proyecto'] = cliente.proyecto
|
||||
}
|
||||
if (captura) {
|
||||
contacto['Etapa'] = captura.etapa
|
||||
contacto['Origen'] = captura.origen ?? ''
|
||||
}
|
||||
|
||||
// Historial de pagos (por teléfono o nombre del cliente)
|
||||
const historialPagos: PerfilCompletoContacto['historialPagos'] = []
|
||||
if (cliente) {
|
||||
const planes = await prisma.planPago.findMany({
|
||||
where: { clienteNombre: cliente.nombreCompleto },
|
||||
include: { pagos: true },
|
||||
})
|
||||
for (const plan of planes) {
|
||||
for (const pago of plan.pagos) {
|
||||
const restante = pago.monto - pago.montoPagado
|
||||
const estado = restante <= 0.01 ? 'pagado' : 'pendiente'
|
||||
historialPagos.push({
|
||||
planId: plan.id,
|
||||
clienteNombre: plan.clienteNombre,
|
||||
unidadCodigo: plan.unidadCodigo,
|
||||
proyecto: plan.proyecto,
|
||||
numPago: pago.numPago,
|
||||
fechaVencimiento: pago.fechaVencimiento.toISOString().slice(0, 10),
|
||||
monto: pago.monto,
|
||||
montoPagado: pago.montoPagado,
|
||||
estado,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cotizaciones (por teléfono)
|
||||
const clave = cliente?.celular?.replace(/\D/g, '').slice(-10) ?? ''
|
||||
const cotizacionesRaw = clave !== ''
|
||||
? await prisma.cotizacion.findMany({
|
||||
where: { clienteTelefono: { contains: clave } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
: []
|
||||
|
||||
// Documentos
|
||||
const documentosRaw = cliente
|
||||
? await prisma.documento.findMany({
|
||||
where: { clienteId: cliente.id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
: []
|
||||
|
||||
// Lead score
|
||||
let leadScore: number | null = null
|
||||
if (captura) {
|
||||
leadScore = calcularScore({
|
||||
etapa: captura.etapa,
|
||||
telefono: captura.telefono,
|
||||
email: captura.email,
|
||||
origen: captura.origen,
|
||||
createdAt: captura.createdAt,
|
||||
updatedAt: captura.updatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// Última actividad
|
||||
let ultimaActividad: PerfilCompletoContacto['ultimaActividad'] = null
|
||||
if (captura) {
|
||||
const ultima = await prisma.actividadLocal.findFirst({
|
||||
where: { leadCapturaId: captura.leadId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
if (ultima) {
|
||||
ultimaActividad = {
|
||||
titulo: ultima.titulo,
|
||||
fecha: ultima.createdAt.toISOString(),
|
||||
tipo: ultima.tipo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
contacto,
|
||||
historialPagos,
|
||||
cotizaciones: cotizacionesRaw.map((c) => ({
|
||||
id: c.id,
|
||||
unidadCodigo: c.unidadCodigo,
|
||||
tipoPropuesta: c.tipoPropuesta,
|
||||
precioFinal: c.precioFinal,
|
||||
createdAt: c.createdAt.toISOString(),
|
||||
})),
|
||||
documentos: documentosRaw.map((d) => ({
|
||||
id: d.id,
|
||||
tipo: d.tipo,
|
||||
nombreArchivo: d.nombreArchivo,
|
||||
createdAt: d.createdAt.toISOString(),
|
||||
})),
|
||||
leadScore,
|
||||
ultimaActividad,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user