feat(purga): Monday eliminado completamente del sistema
14 servicios eliminados, 7 controllers local-only, crons locales, config sin MONDAY_*, frontend sin banners/avisos Monday. 552 tests API + 7 apps construidas.
This commit is contained in:
@@ -2,36 +2,20 @@ import { randomUUID } from 'node:crypto'
|
||||
import { AppError, NotFoundError } from '../utils/errors.js'
|
||||
import { claveApartadoCreado } from './automatizaciones.service.js'
|
||||
import { prisma } from './prisma.js'
|
||||
import type { TipoSync } from './sync-monday.service.js'
|
||||
|
||||
/**
|
||||
* CRM local (modo degradado): implementa el embudo completo contra SQLite
|
||||
* mientras Monday está caído y encola cada operación en SyncPendiente para que
|
||||
* el sync worker la replique cuando Monday vuelva.
|
||||
* CRM local: implementa el embudo completo contra SQLite (tabla LeadCaptura).
|
||||
*
|
||||
* - Entidad local del embudo: LeadCaptura (board LEADS | OPORTUNIDADES |
|
||||
* APARTADO + etapa). Ids locales: `loc_<uuid>` (ver `esIdLocal`).
|
||||
* - Las operaciones que tocan varias tablas van en $transaction; el encolado
|
||||
* viaja en la MISMA transacción para que nunca quede un cambio local sin su
|
||||
* réplica pendiente.
|
||||
* - Las operaciones que tocan varias tablas van en $transaction.
|
||||
* - SECUENCIAL a propósito (SQLite P1008): nada de Promise.all con escrituras.
|
||||
*
|
||||
* Los controllers rutean aquí cuando `saludMonday.estaDisponible()` es false,
|
||||
* cuando el id es `loc_*` (siempre local hasta que se sincronice) o como
|
||||
* fallback tras un AppError 502 de Monday.
|
||||
*/
|
||||
|
||||
/** Board local del embudo (columna LeadCaptura.board) */
|
||||
export type BoardLocal = 'LEADS' | 'OPORTUNIDADES' | 'APARTADO'
|
||||
|
||||
/** Board local → valor del payload CAMBIO_ETAPA (boards de config.monday.boards) */
|
||||
const BOARD_PAYLOAD: Record<BoardLocal, 'leads' | 'oportunidades' | 'apartado'> = {
|
||||
LEADS: 'leads',
|
||||
OPORTUNIDADES: 'oportunidades',
|
||||
APARTADO: 'apartado',
|
||||
}
|
||||
|
||||
/** true si el id es local (aún no existe en Monday) */
|
||||
/** true si el id es local (`loc_<uuid>`) en lugar de un id heredado del CRM anterior */
|
||||
export function esIdLocal(id: string): boolean {
|
||||
return id.startsWith('loc_')
|
||||
}
|
||||
@@ -40,14 +24,6 @@ function idLocal(): string {
|
||||
return `loc_${randomUUID()}`
|
||||
}
|
||||
|
||||
const hoyMas = (dias: number): string => {
|
||||
const d = new Date()
|
||||
d.setDate(d.getDate() + dias)
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const dia = String(d.getDate()).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${m}-${dia}`
|
||||
}
|
||||
|
||||
/** Captura rápida de lead (forma corta de /hoy) */
|
||||
export interface DatosLeadLocal {
|
||||
nombre: string
|
||||
@@ -93,50 +69,10 @@ export interface ResultadoBusquedaLocal {
|
||||
modeloInteres: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea un registro en la cola SyncPendiente. Es el ÚNICO punto de encolado del
|
||||
* modo local: los handlers del worker (sync-handlers.ts) consumen `tipo` +
|
||||
* `payloadJson` y actualizan los ids Monday usando `referenciaLocal`.
|
||||
*/
|
||||
export async function encolarSync(
|
||||
tipo: TipoSync,
|
||||
payload: Record<string, unknown>,
|
||||
referenciaLocal?: string | null,
|
||||
): Promise<void> {
|
||||
await prisma.syncPendiente.create({
|
||||
data: {
|
||||
tipo,
|
||||
payloadJson: JSON.stringify(payload),
|
||||
referenciaLocal: referenciaLocal ?? null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Versión best-effort para las réplicas que hoy solo se loguean (alta cliente,
|
||||
* cotización, checkbox de documento, liquidación): un fallo al encolar NUNCA
|
||||
* rompe el flujo del caller.
|
||||
*/
|
||||
export async function encolarSyncSeguro(
|
||||
tipo: TipoSync,
|
||||
payload: Record<string, unknown>,
|
||||
referenciaLocal?: string | null,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await encolarSync(tipo, payload, referenciaLocal)
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[sync] no se pudo encolar ${tipo}:`, err)
|
||||
}
|
||||
}
|
||||
|
||||
export class CrmLocalService {
|
||||
/**
|
||||
* Captura rápida local: LeadCaptura board=LEADS etapa 'NUEVO' + CREAR_LEAD
|
||||
* encolado. Dedup por teléfono entre los leads locales (mismo contrato 409
|
||||
* que el flujo Monday). Con usuarioId también se encola la auto-asignación
|
||||
* del capturista (ASIGNAR_PERSONA) para que quede como su vendedor al
|
||||
* sincronizarse.
|
||||
* Captura rápida local: LeadCaptura board=LEADS etapa 'NUEVO'. Dedup por
|
||||
* teléfono y email entre los leads locales (mismo contrato 409 de siempre).
|
||||
*/
|
||||
async crearLead(datos: DatosLeadLocal, usuarioId?: string): Promise<ResultadoCapturaLocal> {
|
||||
// Dedup por teléfono (normalizando: últimos 10 dígitos)
|
||||
@@ -157,111 +93,52 @@ export class CrmLocalService {
|
||||
if (existenteEmail) return { duplicado: { id: existenteEmail.leadId, nombre: existenteEmail.nombreLead } }
|
||||
}
|
||||
const id = idLocal()
|
||||
const datosMonday = {
|
||||
nombre: datos.nombre,
|
||||
telefono: datos.telefono,
|
||||
email: datos.email,
|
||||
origenLabel: datos.origen,
|
||||
desarrolloLabel: datos.desarrollo,
|
||||
notas: datos.notas,
|
||||
}
|
||||
const operaciones = [
|
||||
prisma.leadCaptura.create({
|
||||
data: {
|
||||
leadId: id,
|
||||
oportunidadId: null, // los leads no generan oportunidad
|
||||
usuarioId: usuarioId ?? 'desconocido',
|
||||
nombreLead: datos.nombre,
|
||||
telefono: datos.telefono ?? null,
|
||||
email: datos.email ?? null,
|
||||
origen: datos.origen ?? null,
|
||||
desarrollo: datos.desarrollo ?? null,
|
||||
board: 'LEADS',
|
||||
etapa: 'NUEVO',
|
||||
datosJson: JSON.stringify({ notas: datos.notas ?? null }),
|
||||
pendienteSync: true,
|
||||
},
|
||||
}),
|
||||
prisma.syncPendiente.create({
|
||||
data: { tipo: 'CREAR_LEAD', payloadJson: JSON.stringify({ datos: datosMonday }), referenciaLocal: id },
|
||||
}),
|
||||
]
|
||||
if (usuarioId !== undefined) {
|
||||
operaciones.push(
|
||||
prisma.syncPendiente.create({
|
||||
data: {
|
||||
tipo: 'ASIGNAR_PERSONA',
|
||||
payloadJson: JSON.stringify({ board: 'leads', usuarioId }),
|
||||
referenciaLocal: id,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
await prisma.$transaction(operaciones)
|
||||
return { id, oportunidadId: null }
|
||||
const fila = await prisma.leadCaptura.create({
|
||||
data: {
|
||||
leadId: id,
|
||||
oportunidadId: null, // los leads no generan oportunidad
|
||||
usuarioId: usuarioId ?? 'desconocido',
|
||||
nombreLead: datos.nombre,
|
||||
telefono: datos.telefono ?? null,
|
||||
email: datos.email ?? null,
|
||||
origen: datos.origen ?? null,
|
||||
desarrollo: datos.desarrollo ?? null,
|
||||
board: 'LEADS',
|
||||
etapa: 'NUEVO',
|
||||
datosJson: JSON.stringify({ notas: datos.notas ?? null }),
|
||||
pendienteSync: true,
|
||||
},
|
||||
})
|
||||
return { id: fila.leadId, oportunidadId: null }
|
||||
}
|
||||
|
||||
/**
|
||||
* Alta Afluencia local. Sin oportunidadId: LeadCaptura board=OPORTUNIDADES
|
||||
* etapa 'Afluencia' con el perfilamiento en datosJson (además de los campos
|
||||
* planos) + auto-mapping LeadOportunidad + CREAR_AFLUENCIA encolado. Con
|
||||
* oportunidadId (botón "Afluencia" de Mis oportunidades): actualiza la fila
|
||||
* local si existe y encola ACTUALIZAR_AFLUENCIA (el id puede ser de Monday).
|
||||
* planos) + auto-mapping LeadOportunidad. Con oportunidadId (botón
|
||||
* "Afluencia" de Mis oportunidades): actualiza la fila local si existe.
|
||||
*/
|
||||
async crearAfluencia(datos: DatosAfluenciaLocal, usuarioId?: string): Promise<ResultadoCapturaLocal> {
|
||||
const { oportunidadId, modelo, medioContacto, edad, estadoCivil, tipoEmpleo, ocupacion } = datos
|
||||
const { rangoSalarial, credito, tipoEconomia, califica, loQuierePara } = datos
|
||||
const datosMonday = {
|
||||
nombre: datos.nombre,
|
||||
telefono: datos.telefono,
|
||||
email: datos.email,
|
||||
desarrolloLabel: datos.desarrollo,
|
||||
origenLabel: datos.origen,
|
||||
modeloLabel: modelo,
|
||||
medioContacto,
|
||||
edad,
|
||||
estadoCivilLabel: estadoCivil,
|
||||
tipoEmpleoLabel: tipoEmpleo,
|
||||
ocupacion,
|
||||
rangoSalarial,
|
||||
creditoLabel: credito,
|
||||
tipoEconomiaLabel: tipoEconomia,
|
||||
calificaLabel: califica,
|
||||
loQuierePara,
|
||||
notas: datos.notas,
|
||||
}
|
||||
const { oportunidadId, modelo } = datos
|
||||
|
||||
if (oportunidadId !== undefined) {
|
||||
const existente = await prisma.leadCaptura.findUnique({ where: { leadId: oportunidadId } })
|
||||
const operaciones = []
|
||||
if (existente) {
|
||||
operaciones.push(
|
||||
prisma.leadCaptura.update({
|
||||
where: { leadId: oportunidadId },
|
||||
data: {
|
||||
nombreLead: datos.nombre,
|
||||
telefono: datos.telefono ?? null,
|
||||
email: datos.email ?? null,
|
||||
origen: datos.origen ?? null,
|
||||
desarrollo: datos.desarrollo ?? null,
|
||||
modelo: modelo ?? null,
|
||||
etapa: 'Afluencia',
|
||||
datosJson: JSON.stringify(datos),
|
||||
pendienteSync: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
operaciones.push(
|
||||
prisma.syncPendiente.create({
|
||||
await prisma.leadCaptura.update({
|
||||
where: { leadId: oportunidadId },
|
||||
data: {
|
||||
tipo: 'ACTUALIZAR_AFLUENCIA',
|
||||
payloadJson: JSON.stringify({ oportunidadId, datos: datosMonday }),
|
||||
referenciaLocal: oportunidadId,
|
||||
nombreLead: datos.nombre,
|
||||
telefono: datos.telefono ?? null,
|
||||
email: datos.email ?? null,
|
||||
origen: datos.origen ?? null,
|
||||
desarrollo: datos.desarrollo ?? null,
|
||||
modelo: modelo ?? null,
|
||||
etapa: 'Afluencia',
|
||||
datosJson: JSON.stringify(datos),
|
||||
pendienteSync: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
await prisma.$transaction(operaciones)
|
||||
})
|
||||
}
|
||||
return { id: oportunidadId, oportunidadId, actualizada: true }
|
||||
}
|
||||
|
||||
@@ -273,7 +150,7 @@ export class CrmLocalService {
|
||||
}
|
||||
|
||||
const id = idLocal()
|
||||
const operaciones = [
|
||||
await prisma.$transaction([
|
||||
prisma.leadCaptura.create({
|
||||
data: {
|
||||
leadId: id,
|
||||
@@ -291,33 +168,17 @@ export class CrmLocalService {
|
||||
pendienteSync: true,
|
||||
},
|
||||
}),
|
||||
// Auto-mapping leadId = oportunidadId (como el flujo Monday): el alta de
|
||||
// clientes encuentra así la oportunidad a mover a 'Aparto'
|
||||
// Auto-mapping leadId = oportunidadId: el alta de clientes encuentra así
|
||||
// la oportunidad a mover a 'Aparto'
|
||||
prisma.leadOportunidad.create({ data: { leadId: id, oportunidadId: id } }),
|
||||
prisma.syncPendiente.create({
|
||||
data: { tipo: 'CREAR_AFLUENCIA', payloadJson: JSON.stringify({ datos: datosMonday }), referenciaLocal: id },
|
||||
}),
|
||||
]
|
||||
if (usuarioId !== undefined) {
|
||||
operaciones.push(
|
||||
prisma.syncPendiente.create({
|
||||
data: {
|
||||
tipo: 'ASIGNAR_PERSONA',
|
||||
payloadJson: JSON.stringify({ board: 'oportunidades', usuarioId }),
|
||||
referenciaLocal: id,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
await prisma.$transaction(operaciones)
|
||||
])
|
||||
return { id, oportunidadId: id }
|
||||
}
|
||||
|
||||
/**
|
||||
* Fila local del item, creándola "sombra" si no existe y `sombra` es true.
|
||||
* Las sombras cubren el fallback 502 sobre ids de Monday antiguos (creados
|
||||
* antes del modo local): la acción queda reflejada en Mi día y el cambio se
|
||||
* encola igual. Sin `sombra` → 404.
|
||||
* Las sombras cubren ids heredados del CRM anterior sin fila local: la
|
||||
* acción queda reflejada en Mi día. Sin `sombra` → 404.
|
||||
*/
|
||||
private async filaLocal(itemId: string, board: BoardLocal, usuarioId: string | undefined, sombra: boolean) {
|
||||
const row = await prisma.leadCaptura.findUnique({ where: { leadId: itemId } })
|
||||
@@ -327,7 +188,7 @@ export class CrmLocalService {
|
||||
data: {
|
||||
leadId: itemId,
|
||||
usuarioId: usuarioId ?? 'desconocido',
|
||||
nombreLead: 'Registro previo (Monday)',
|
||||
nombreLead: 'Registro previo (CRM anterior)',
|
||||
board,
|
||||
etapa: 'NUEVO',
|
||||
mondayItemId: esIdLocal(itemId) ? null : itemId,
|
||||
@@ -338,8 +199,8 @@ export class CrmLocalService {
|
||||
|
||||
/**
|
||||
* Cambio de estado/etapa local (boards LEADS, OPORTUNIDADES y APARTADO):
|
||||
* actualiza `etapa` (+updatedAt vía Prisma) y encola CAMBIO_ETAPA con el
|
||||
* label real de la columna 'Estado' (y la fecha de la cita si aplica).
|
||||
* actualiza `etapa` (+updatedAt vía Prisma) y guarda la fecha de la cita en
|
||||
* datosJson si aplica.
|
||||
*/
|
||||
async cambiarEstado(
|
||||
itemId: string,
|
||||
@@ -348,8 +209,6 @@ export class CrmLocalService {
|
||||
opciones: { fecha?: string | undefined; usuarioId?: string | undefined; sombra?: boolean | undefined } = {},
|
||||
): Promise<void> {
|
||||
await this.filaLocal(itemId, board, opciones.usuarioId, opciones.sombra ?? false)
|
||||
const payload: Record<string, unknown> = { board: BOARD_PAYLOAD[board], label }
|
||||
if (opciones.fecha !== undefined) payload['fecha'] = opciones.fecha
|
||||
// Guardar fechaCita en datosJson para que la oportunidad la muestre en el calendario
|
||||
const updateData: Record<string, unknown> = { etapa: label, pendienteSync: true }
|
||||
if (opciones.fecha !== undefined && board === 'OPORTUNIDADES') {
|
||||
@@ -361,21 +220,15 @@ export class CrmLocalService {
|
||||
datos['fechaCita'] = opciones.fecha
|
||||
updateData['datosJson'] = JSON.stringify(datos)
|
||||
}
|
||||
await prisma.$transaction([
|
||||
prisma.leadCaptura.update({
|
||||
where: { leadId: itemId },
|
||||
data: updateData,
|
||||
}),
|
||||
prisma.syncPendiente.create({
|
||||
data: { tipo: 'CAMBIO_ETAPA', payloadJson: JSON.stringify(payload), referenciaLocal: itemId },
|
||||
}),
|
||||
])
|
||||
await prisma.leadCaptura.update({
|
||||
where: { leadId: itemId },
|
||||
data: updateData,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Descarte local: etapa terminal ('Descartado' en Leads, 'Perdido' en
|
||||
* Oportunidades), guarda motivo/submotivo y encola DESCARTE (el handler
|
||||
* replica el estado + el item del tablero Motivos No Compra).
|
||||
* Oportunidades); guarda motivo/submotivo.
|
||||
*/
|
||||
async descartar(
|
||||
itemId: string,
|
||||
@@ -392,31 +245,22 @@ export class CrmLocalService {
|
||||
const board: BoardLocal = fuente === 'lead' ? 'LEADS' : 'OPORTUNIDADES'
|
||||
const row = await this.filaLocal(itemId, board, opciones.usuarioId, opciones.sombra ?? false)
|
||||
const etapaTerminal = fuente === 'lead' ? 'Descartado' : 'Perdido'
|
||||
const payload: Record<string, unknown> = { fuente, motivo, nombreItem: row.nombreLead }
|
||||
if (opciones.subcategoria !== undefined) payload['subcategoria'] = opciones.subcategoria
|
||||
if (opciones.comentario !== undefined) payload['comentario'] = opciones.comentario
|
||||
await prisma.$transaction([
|
||||
prisma.leadCaptura.update({
|
||||
where: { leadId: itemId },
|
||||
data: {
|
||||
etapa: etapaTerminal,
|
||||
motivo,
|
||||
submotivo: opciones.subcategoria ?? null,
|
||||
pendienteSync: true,
|
||||
},
|
||||
}),
|
||||
prisma.syncPendiente.create({
|
||||
data: { tipo: 'DESCARTE', payloadJson: JSON.stringify(payload), referenciaLocal: itemId },
|
||||
}),
|
||||
])
|
||||
await prisma.leadCaptura.update({
|
||||
where: { leadId: itemId },
|
||||
data: {
|
||||
etapa: etapaTerminal,
|
||||
motivo,
|
||||
submotivo: opciones.subcategoria ?? null,
|
||||
pendienteSync: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Conversión local lead → oportunidad (botón "Agendo" de Mis leads): el lead
|
||||
* queda 'Calificado', se crea la oportunidad local en 'Agendo Cita' copiando
|
||||
* los datos, se liga LeadOportunidad + `oportunidadId` en la captura y se
|
||||
* encola CONVERSION. Si el lead ya fue convertido → 409 (como el flujo
|
||||
* Monday). Devuelve la fila local de la oportunidad.
|
||||
* los datos y se liga LeadOportunidad + `oportunidadId` en la captura. Si el
|
||||
* lead ya fue convertido → 409. Devuelve la fila local de la oportunidad.
|
||||
*/
|
||||
async convertirLead(leadIdLocal: string, _usuarioId?: string) {
|
||||
const lead = await prisma.leadCaptura.findUnique({ where: { leadId: leadIdLocal } })
|
||||
@@ -424,15 +268,6 @@ export class CrmLocalService {
|
||||
if (lead.oportunidadId !== null) throw new AppError(409, 'El lead ya fue convertido en oportunidad')
|
||||
|
||||
const oportunidadId = idLocal()
|
||||
const datosOportunidad = {
|
||||
nombre: lead.nombreLead,
|
||||
estadoLabel: 'Agendo Cita',
|
||||
telefono: lead.telefono ?? undefined,
|
||||
email: lead.email ?? undefined,
|
||||
origenLabel: lead.origen ?? undefined,
|
||||
desarrolloLabel: lead.desarrollo ?? undefined,
|
||||
modeloLabel: lead.modelo ?? undefined,
|
||||
}
|
||||
const resultados = await prisma.$transaction([
|
||||
prisma.leadCaptura.update({
|
||||
where: { leadId: leadIdLocal },
|
||||
@@ -456,13 +291,6 @@ export class CrmLocalService {
|
||||
},
|
||||
}),
|
||||
prisma.leadOportunidad.create({ data: { leadId: leadIdLocal, oportunidadId } }),
|
||||
prisma.syncPendiente.create({
|
||||
data: {
|
||||
tipo: 'CONVERSION',
|
||||
payloadJson: JSON.stringify({ oportunidadIdLocal: oportunidadId, datos: datosOportunidad }),
|
||||
referenciaLocal: leadIdLocal,
|
||||
},
|
||||
}),
|
||||
])
|
||||
return resultados[1]
|
||||
}
|
||||
@@ -471,11 +299,10 @@ export class CrmLocalService {
|
||||
* "Aparto" local: la oportunidad pasa a etapa 'Aparto' y se crea el apartado
|
||||
* local (board=APARTADO, etapa 'Pago Apartado', primer paso del pipeline
|
||||
* Enkontrol) con los datos conocidos. Idempotente con el mismo dedup
|
||||
* `apartado-creado:<opId>` que las automatizaciones. Encola CAMBIO_ETAPA +
|
||||
* CREAR_APARTADO (en ese orden: la etapa primero, como en Monday).
|
||||
* `apartado-creado:<opId>` de las automatizaciones.
|
||||
*/
|
||||
async apartar(oportunidadIdLocal: string, usuarioId?: string) {
|
||||
// Sombra para ids Monday sin fila local (fallback 502 sobre items viejos)
|
||||
// Sombra para ids heredados sin fila local
|
||||
const oportunidad = await this.filaLocal(
|
||||
oportunidadIdLocal,
|
||||
'OPORTUNIDADES',
|
||||
@@ -491,15 +318,6 @@ export class CrmLocalService {
|
||||
}
|
||||
|
||||
const apartadoId = idLocal()
|
||||
const datosApartado = {
|
||||
clienteNombre: oportunidad.nombreLead,
|
||||
unidadCodigo: '', // la unidad se captura en la cotización/alta
|
||||
fechaApartado: hoyMas(0),
|
||||
fechaLimite: hoyMas(15),
|
||||
estadoLabel: 'Pago Apartado',
|
||||
telefono: oportunidad.telefono ?? undefined,
|
||||
email: oportunidad.email ?? undefined,
|
||||
}
|
||||
const resultados = await prisma.$transaction([
|
||||
prisma.leadCaptura.update({
|
||||
where: { leadId: oportunidadIdLocal },
|
||||
@@ -525,28 +343,13 @@ export class CrmLocalService {
|
||||
update: { valor: apartadoId },
|
||||
create: { clave, valor: apartadoId },
|
||||
}),
|
||||
prisma.syncPendiente.create({
|
||||
data: {
|
||||
tipo: 'CAMBIO_ETAPA',
|
||||
payloadJson: JSON.stringify({ board: 'oportunidades', label: 'Aparto' }),
|
||||
referenciaLocal: oportunidadIdLocal,
|
||||
},
|
||||
}),
|
||||
prisma.syncPendiente.create({
|
||||
data: {
|
||||
tipo: 'CREAR_APARTADO',
|
||||
payloadJson: JSON.stringify({ datos: datosApartado }),
|
||||
referenciaLocal: apartadoId,
|
||||
},
|
||||
}),
|
||||
])
|
||||
return resultados[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Búsqueda local para el picker del alta de clientes (y el buscador del
|
||||
* cotizador): LeadCaptura por nombre/teléfono + Cliente por nombre/celular,
|
||||
* con la misma forma de respuesta que la búsqueda Monday.
|
||||
* cotizador): LeadCaptura por nombre/teléfono + Cliente por nombre/celular.
|
||||
*/
|
||||
async buscarLocales(termino: string): Promise<ResultadoBusquedaLocal[]> {
|
||||
const capturas = await prisma.leadCaptura.findMany({
|
||||
|
||||
Reference in New Issue
Block a user