feat(contactos): detalle completo del cliente con propiedad e historial
- GET /api/contactos/:id/detalle: todas las columnas del contacto, la propiedad comprada/en proceso (apartado relacionado por teléfono/email, enriquecido con el desarrollo de la unidad), quién atiende cada item relacionado (leads/oportunidades/apartados, columna people) y el historial completo de atención (activity log de los 4 tableros + capturas y alta local con nombre del capturista) - monday.service: getContactoDetalle (columnas por título), getHistorialItems (todos los eventos del activity log por items) y ApartadoMonday enriquecido (teléfono, email, precio final, fecha compra) - Landing: la lista de Contactos abre un modal por cliente con datos, propiedad, relaciones CRM (con quién atiende) y timeline de historial - Tests: 465 API + 12 landing, verde
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import type { Request, Response } from 'express'
|
||||
import { mondayService } from '../services/monday.service.js'
|
||||
import { detalleContacto, claveTelefono } from '../services/contactos-detalle.service.js'
|
||||
import { prisma } from '../services/prisma.js'
|
||||
import { NotFoundError } from '../utils/errors.js'
|
||||
|
||||
/**
|
||||
* GET /api/contactos (Bearer) → contactos del tablero Contactos de Monday
|
||||
@@ -8,3 +11,84 @@ import { mondayService } from '../services/monday.service.js'
|
||||
export async function listarContactosEndpoint(_req: Request, res: Response): Promise<void> {
|
||||
res.json(await mondayService.getContactos())
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/contactos/:id/detalle (Bearer) → ficha completa del contacto:
|
||||
* todas sus columnas, la propiedad comprada/en proceso (apartado relacionado),
|
||||
* quién atiende cada item relacionado (leads, oportunidades, apartados) y el
|
||||
* historial completo de atención (activity log de Monday + capturas locales).
|
||||
*/
|
||||
export async function detalleContactoEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const contactoId = String(req.params['id'])
|
||||
|
||||
const contacto = await mondayService.getContactoDetalle(contactoId)
|
||||
if (contacto === null) throw new NotFoundError('Contacto no encontrado')
|
||||
|
||||
// Nombres para "atendidoPor"/historial: usuarios del ecosistema + workspace
|
||||
const mapeados = await prisma.usuario.findMany({
|
||||
where: { mondayUserId: { not: null }, activo: true },
|
||||
select: { nombre: true, mondayUserId: true },
|
||||
})
|
||||
const nombresPorMondayId = new Map<number, string>(
|
||||
mapeados.filter((m) => m.mondayUserId !== null).map((m) => [m.mondayUserId!, m.nombre]),
|
||||
)
|
||||
try {
|
||||
for (const u of await mondayService.getUsuariosWorkspace()) {
|
||||
if (!nombresPorMondayId.has(u.id)) nombresPorMondayId.set(u.id, u.nombre)
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('No se pudieron listar los usuarios del workspace de Monday:', err)
|
||||
}
|
||||
|
||||
// Registro local: capturas con el mismo teléfono (Monday no guarda quién
|
||||
// capturó) y la ficha de cliente ligada a este contacto (o por celular).
|
||||
// LeadCaptura/Cliente no tienen relación Prisma a Usuario: se resuelve a mano.
|
||||
const clave = claveTelefono(contacto['Teléfono'] ?? '')
|
||||
const capturasTodas = await prisma.leadCaptura.findMany({
|
||||
where: { telefono: { not: null } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
const capturasCrudas = capturasTodas.filter((c) => c.telefono !== null && clave !== '' && claveTelefono(c.telefono) === clave)
|
||||
|
||||
const clienteCrudo = await prisma.cliente.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ mondayContactoId: contactoId },
|
||||
...(clave !== '' ? [{ celular: { contains: clave } }] : []),
|
||||
],
|
||||
},
|
||||
include: { _count: { select: { documentos: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
|
||||
// Nombres de capturistas (ids locales → nombre para mostrar)
|
||||
const idsUsuarios = [
|
||||
...new Set([
|
||||
...capturasCrudas.map((c) => c.usuarioId),
|
||||
...(clienteCrudo?.capturadoPorId ? [clienteCrudo.capturadoPorId] : []),
|
||||
]),
|
||||
]
|
||||
const usuariosLocal = idsUsuarios.length
|
||||
? await prisma.usuario.findMany({ where: { id: { in: idsUsuarios } }, select: { id: true, nombre: true } })
|
||||
: []
|
||||
const nombreLocal = new Map(usuariosLocal.map((u) => [u.id, u.nombre]))
|
||||
|
||||
const capturas = capturasCrudas.map((c) => ({
|
||||
leadId: c.leadId,
|
||||
nombreLead: c.nombreLead,
|
||||
createdAt: c.createdAt,
|
||||
usuario: nombreLocal.has(c.usuarioId) ? { nombre: nombreLocal.get(c.usuarioId)! } : null,
|
||||
}))
|
||||
const clienteLocal = clienteCrudo
|
||||
? {
|
||||
...clienteCrudo,
|
||||
capturadoPor:
|
||||
clienteCrudo.capturadoPorId && nombreLocal.has(clienteCrudo.capturadoPorId)
|
||||
? { nombre: nombreLocal.get(clienteCrudo.capturadoPorId)! }
|
||||
: null,
|
||||
}
|
||||
: null
|
||||
|
||||
res.json(await detalleContacto(mondayService, contactoId, nombresPorMondayId, capturas, clienteLocal))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user