import { claveTelefono, type DetalleContacto, type EventoHistorialContacto, type RelacionCrm } from './contactos-detalle.service.js' import { prisma } from './prisma.js' /** * Módulo Contactos 100% local: la lista y el detalle se arman desde las * tablas locales (Cliente + LeadCaptura + Documento + Cotizacion). El * historial se deriva de las capturas/altas/cotizaciones locales. */ /** Contacto de la lista con flag aditivo de procedencia (el front lo ignora o lo muestra) */ export interface ContactoListaLocal { id: string nombre: string telefono: string email: string estado: string updatedAt: string fuente?: 'local' } /** * Lista local: Clientes (alta de clientes) + capturas del embudo en boards * OPORTUNIDADES/APARTADO, deduplicados por teléfono (gana el Cliente: tiene la * ficha completa), más reciente primero, máx 100 (como getContactos). * Filtra por usuario actual: clientes capturados por él + leads asignados a él. */ export async function listarContactosLocal(usuarioId?: string): Promise { const clientes = await prisma.cliente.findMany({ where: usuarioId ? { capturadoPorId: usuarioId } : {}, orderBy: { createdAt: 'desc' }, take: 100, }) const capturas = await prisma.leadCaptura.findMany({ where: { board: { in: ['OPORTUNIDADES', 'APARTADO'] }, ...(usuarioId ? { usuarioId } : {}), }, orderBy: { updatedAt: 'desc' }, take: 100, }) const vistos = new Set() const lista: ContactoListaLocal[] = [] const agregar = (entrada: ContactoListaLocal): void => { const clave = claveTelefono(entrada.telefono) if (clave !== '') { if (vistos.has(clave)) return vistos.add(clave) } lista.push(entrada) } for (const c of clientes) { agregar({ id: c.id, nombre: c.nombreCompleto, telefono: c.celular ?? '', email: c.email ?? '', estado: c.proyecto.trim() !== '' ? 'Cliente' : '', updatedAt: c.createdAt.toISOString(), fuente: 'local', }) } for (const c of capturas) { agregar({ id: c.leadId, nombre: c.nombreLead, telefono: c.telefono ?? '', email: c.email ?? '', estado: c.etapa, updatedAt: c.updatedAt.toISOString(), fuente: 'local', }) } return lista.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, 100) } /** * Detalle local del contacto. El id puede ser cliente.id, leadId (loc_* o id * heredado) o mondayContactoId (compatibilidad de datos). Las relaciones se * arman por teléfono (últimos 10 dígitos) o email sobre LeadCaptura; el * historial se deriva de capturas + alta + documentos + cotizaciones locales. * null si no existe en ninguna tabla. */ export async function detalleContactoLocal(contactoId: string): Promise { let cliente = await prisma.cliente.findFirst({ where: { OR: [{ id: contactoId }, { mondayContactoId: contactoId }] }, }) const capturaBase = cliente ? null : await prisma.leadCaptura.findUnique({ where: { leadId: contactoId } }) if (cliente === null && capturaBase === null) return null const telefono = cliente?.celular ?? capturaBase?.telefono ?? '' const email = (cliente?.email ?? capturaBase?.email ?? '').trim().toLowerCase() const clave = claveTelefono(telefono) // Si el id era de una captura, el Cliente ligado se busca por celular if (cliente === null && clave !== '') { cliente = await prisma.cliente.findFirst({ where: { celular: { contains: clave } }, orderBy: { createdAt: 'desc' }, }) } const capturasTodas = await prisma.leadCaptura.findMany({ orderBy: { createdAt: 'desc' } }) const relacionadas = capturasTodas.filter( (c) => (clave !== '' && c.telefono !== null && claveTelefono(c.telefono) === clave) || (email !== '' && (c.email ?? '').trim().toLowerCase() === email), ) // Cotizaciones y documentos locales del contacto const cotizaciones = clave !== '' ? await prisma.cotizacion.findMany({ where: { clienteTelefono: { contains: clave } }, orderBy: { createdAt: 'desc' }, }) : [] const documentos = cliente ? await prisma.documento.findMany({ where: { clienteId: cliente.id }, orderBy: { createdAt: 'desc' } }) : [] // Nombres de capturistas/vendedores (ids locales → nombre para mostrar) const idsUsuarios = [ ...new Set([ ...relacionadas.map((c) => c.usuarioId), ...(cliente?.capturadoPorId ? [cliente.capturadoPorId] : []), ...cotizaciones.map((c) => c.vendedorId).filter((id): id is string => id !== null), ]), ] const usuarios = idsUsuarios.length ? await prisma.usuario.findMany({ where: { id: { in: idsUsuarios } }, select: { id: true, nombre: true } }) : [] const nombrePorId = new Map(usuarios.map((u) => [u.id, u.nombre])) const nombreDe = (id: string | null): string | null => (id !== null ? (nombrePorId.get(id) ?? null) : null) const unidadCliente = cliente?.unidadCodigo ?? cotizaciones[0]?.unidadCodigo ?? '' const aRelacion = (c: (typeof relacionadas)[number], detalle: string): RelacionCrm => ({ id: c.leadId, nombre: c.nombreLead, estado: c.etapa, atendidoPor: nombreDe(c.usuarioId), detalle, }) const relaciones = { leads: relacionadas .filter((c) => c.board === 'LEADS') .map((c) => aRelacion(c, c.origen ?? '')), oportunidades: relacionadas .filter((c) => c.board === 'OPORTUNIDADES') .map((c) => aRelacion(c, [c.desarrollo, c.modelo].filter((p) => p !== null && p !== '').join(' · '))), apartados: relacionadas .filter((c) => c.board === 'APARTADO') .map((c) => aRelacion(c, c.leadId === cliente?.mondayApartadoId ? unidadCliente : '')), } // Propiedad: ficha del Cliente (la comprada o en proceso); sin Cliente no hay let propiedad: DetalleContacto['propiedad'] = null if (cliente !== null) { const apartadoLocal = relacionadas.find((c) => c.leadId === cliente.mondayApartadoId) propiedad = { id: cliente.mondayApartadoId ?? cliente.id, unidadCodigo: unidadCliente, desarrollo: cliente.proyecto, estado: apartadoLocal?.etapa ?? '', precioFinal: cotizaciones[0]?.precioFinal ?? 0, fechaCompra: cliente.createdAt.toISOString().slice(0, 10), fechaLimite: '', atendidoPor: nombreDe(cliente.capturadoPorId), } } const historial: EventoHistorialContacto[] = [ ...relacionadas.map((c) => ({ fechaISO: c.createdAt.toISOString(), usuario: nombreDe(c.usuarioId), detalle: `Captura en la app (${c.nombreLead})`, fuente: 'App', })), ...(cliente !== null ? [ { fechaISO: cliente.createdAt.toISOString(), usuario: nombreDe(cliente.capturadoPorId), detalle: `Alta de cliente registrada${cliente.unidadCodigo !== null ? ` (${cliente.unidadCodigo})` : ''}`, fuente: 'App', }, ] : []), ...documentos.map((d) => ({ fechaISO: d.createdAt.toISOString(), usuario: null, detalle: `Documento subido (${d.tipo})`, fuente: 'App', })), ...cotizaciones.map((c) => ({ fechaISO: c.createdAt.toISOString(), usuario: nombreDe(c.vendedorId), detalle: `Cotización ${c.unidadCodigo} (${c.tipoPropuesta})`, fuente: 'App', })), ].sort((a, b) => b.fechaISO.localeCompare(a.fechaISO)) const contacto: Record = cliente !== null ? { nombre: cliente.nombreCompleto, telefono: cliente.celular ?? '', email: cliente.email ?? '', estado: 'Cliente', curp: cliente.curp, rfc: cliente.rfc, } : { nombre: capturaBase!.nombreLead, telefono: capturaBase!.telefono ?? '', email: capturaBase!.email ?? '', estado: capturaBase!.etapa, } return { contacto, propiedad, relaciones, historial } } // ── Exportar contactos a CSV ───────────────────────────────────────── /** * Genera CSV de todos los contactos (Cliente + LeadCaptura). * Columnas: Nombre, Teléfono, Email, Desarrollo, Estatus, Última actividad. */ export async function exportarCSV(filtros: { desarrollo?: string; estatus?: string } = {}): Promise { const clientes = await prisma.cliente.findMany({ orderBy: { createdAt: 'desc' } }) const capturas = await prisma.leadCaptura.findMany({ where: { board: { in: ['OPORTUNIDADES', 'APARTADO', 'LEADS'] }, }, orderBy: { updatedAt: 'desc' }, }) interface FilaCSV { Nombre: string Teléfono: string Email: string Desarrollo: string Estatus: string 'Última actividad': string } const filas: FilaCSV[] = [] const vistos = new Set() for (const c of clientes) { const clave = (c.celular ?? '').replace(/\D/g, '').slice(-10) if (clave !== '' && vistos.has(clave)) continue if (clave !== '') vistos.add(clave) filas.push({ Nombre: c.nombreCompleto, Teléfono: c.celular ?? '', Email: c.email ?? '', Desarrollo: c.proyecto, Estatus: 'Cliente', 'Última actividad': c.createdAt.toISOString().slice(0, 10), }) } for (const cap of capturas) { const clave = (cap.telefono ?? '').replace(/\D/g, '').slice(-10) if (clave !== '' && vistos.has(clave)) continue if (clave !== '') vistos.add(clave) filas.push({ Nombre: cap.nombreLead, Teléfono: cap.telefono ?? '', Email: cap.email ?? '', Desarrollo: cap.desarrollo ?? '', Estatus: cap.etapa, 'Última actividad': cap.updatedAt.toISOString().slice(0, 10), }) } // Aplicar filtros let filtradas = filas if (filtros.desarrollo) { const d = filtros.desarrollo.toUpperCase() filtradas = filtradas.filter((f) => f.Desarrollo.toUpperCase().includes(d)) } if (filtros.estatus) { const e = filtros.estatus.toUpperCase() filtradas = filtradas.filter((f) => f.Estatus.toUpperCase().includes(e)) } // Generar CSV const encabezado = 'Nombre,Teléfono,Email,Desarrollo,Estatus,Última actividad' const escape = (s: string) => (s.includes(',') || s.includes('"') ? `"${s.replace(/"/g, '""')}"` : s) const lineas = filtradas.map((f) => [f.Nombre, f.Teléfono, f.Email, f.Desarrollo, f.Estatus, f['Última actividad']].map(escape).join(',') ) return [encabezado, ...lineas].join('\n') }