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:
2026-09-08 07:42:54 +00:00
parent 5063ffe281
commit db18685872
34 changed files with 2950 additions and 33 deletions

View File

@@ -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,
}
}