14 servicios eliminados, 7 controllers local-only, crons locales, config sin MONDAY_*, frontend sin banners/avisos Monday. 552 tests API + 7 apps construidas.
214 lines
5.8 KiB
TypeScript
214 lines
5.8 KiB
TypeScript
import { prisma } from './prisma.js'
|
|
import { calcularScore } from './lead-scoring.service.js'
|
|
|
|
/**
|
|
* Perfil Completo del Contacto: datos locales del cliente/lead, historial de
|
|
* pagos, cotizaciones, documentos, lead score y última actividad.
|
|
*/
|
|
|
|
export interface RelacionCrm {
|
|
id: string
|
|
nombre: string
|
|
estado: string // estado/etapa actual
|
|
/** Quién lo atiende ahora; null si sin asignar */
|
|
atendidoPor: string | null
|
|
detalle: string // origen/desarrollo/unidad según el tablero ('' si no aplica)
|
|
}
|
|
|
|
export interface EventoHistorialContacto {
|
|
fechaISO: string
|
|
usuario: string | null
|
|
detalle: string
|
|
fuente: string
|
|
}
|
|
|
|
export interface DetalleContacto {
|
|
contacto: Record<string, string>
|
|
propiedad: {
|
|
id: string
|
|
unidadCodigo: string
|
|
desarrollo: string
|
|
estado: string
|
|
precioFinal: number
|
|
fechaCompra: string
|
|
fechaLimite: string
|
|
atendidoPor: string | null
|
|
} | null
|
|
relaciones: {
|
|
leads: RelacionCrm[]
|
|
oportunidades: RelacionCrm[]
|
|
apartados: RelacionCrm[]
|
|
}
|
|
historial: EventoHistorialContacto[]
|
|
}
|
|
|
|
/** Últimos 10 dígitos del teléfono (dedup/relación tolerante a lada y formato) */
|
|
export function claveTelefono(telefono: string): string {
|
|
const digitos = telefono.replace(/\D/g, '')
|
|
return digitos.length >= 10 ? digitos.slice(-10) : ''
|
|
}
|
|
|
|
// ── 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 }] }, // mondayContactoId: compatibilidad de datos
|
|
})
|
|
|
|
// 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,
|
|
}
|
|
}
|