feat: conversión lead→oportunidad (Agendo) y alta ligada a apartado
- Mis leads: botón 'Agendo →' convierte el lead en oportunidad (etapa Agendo Cita, con sus datos, asesor = vendedor del lead o quien convierte), marca el lead Calificado y guarda el mapping LeadOportunidad; 409 si ya fue convertido - Mis oportunidades: se quitan No Asistio y Abandono (redundantes con Descartar); quedan Cotizar, Afluencia, Agendo Cita y Aparto - Mis apartados: 'Firma cliente (venta) →' abre el alta de cliente con TODO lo recolectado prellenado (ficha del apartado + contacto + oportunidad + cotización vinculada vía GET /altas/prefill-apartado/:id) - El alta con apartadoMondayId se LIGA al apartado existente y lo enriquece (curp, teléfono, email, precio final, enganche, forma de pago, unidad) en vez de crear un duplicado - monday.service: actualizarApartado + ApartadoMonday con curp, forma de pago, enganche y monto apartado - Tests: 470 API + 120 alta-clientes + 16 mi-dia, verde
This commit is contained in:
@@ -248,6 +248,40 @@ async function crearApartadoSiAplica(
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Apartado Monday del alta: con apartadoMondayId (alta arrancada desde un
|
||||
* apartado existente, botón "Firma Cliente (Venta)" de Mis apartados) se
|
||||
* LIGA y enriquece ese item; sin él, se crea uno nuevo como siempre.
|
||||
*/
|
||||
async function apartadoDeAlta(
|
||||
dg: { nombreCompleto: string; celular?: string | undefined; email?: string | undefined; curp?: string | undefined },
|
||||
vc: {
|
||||
ubicacion?: string | undefined
|
||||
valorVenta?: string | number | undefined
|
||||
enganche?: string | number | undefined
|
||||
tiposCredito?: unknown
|
||||
},
|
||||
capturadoPorId: string | null,
|
||||
apartadoMondayId: unknown,
|
||||
): Promise<string | null> {
|
||||
const existente = typeof apartadoMondayId === 'string' && apartadoMondayId !== '' ? apartadoMondayId : null
|
||||
if (existente === null) return crearApartadoSiAplica(dg, vc, capturadoPorId)
|
||||
const valorVenta = aNumero(vc.valorVenta)
|
||||
const enganche = aNumero(vc.enganche)
|
||||
// Solo se mandan los campos con valor (exactOptionalPropertyTypes)
|
||||
await mondayService.actualizarApartado(existente, {
|
||||
clienteNombre: dg.nombreCompleto,
|
||||
...(vc.ubicacion !== undefined && vc.ubicacion !== '' ? { unidadCodigo: vc.ubicacion } : {}),
|
||||
...(dg.celular !== undefined && dg.celular !== '' ? { telefono: dg.celular } : {}),
|
||||
...(dg.email !== undefined && dg.email !== '' ? { email: dg.email } : {}),
|
||||
...(dg.curp !== undefined && dg.curp !== '' ? { curp: dg.curp } : {}),
|
||||
...(valorVenta > 0 ? { precioFinal: valorVenta } : {}),
|
||||
...(enganche > 0 ? { enganche } : {}),
|
||||
...(formaPagoLabel(vc.tiposCredito) !== undefined ? { formaPagoLabel: formaPagoLabel(vc.tiposCredito) } : {}),
|
||||
})
|
||||
return existente
|
||||
}
|
||||
|
||||
/** POST /api/clientes — persiste el alta y la replica en Monday (Contactos y Apartado) */
|
||||
export async function crearCliente(req: Request, res: Response): Promise<void> {
|
||||
const body = crearClienteSchema.parse(req.body)
|
||||
@@ -290,7 +324,7 @@ export async function crearCliente(req: Request, res: Response): Promise<void> {
|
||||
...{ actividadEconomicaLabel: actividadEconomicaLabel(body.empleo?.tipoEmpleo) },
|
||||
})
|
||||
|
||||
mondayApartadoId = await crearApartadoSiAplica(dg, vc, req.usuarioToken?.sub ?? null)
|
||||
mondayApartadoId = await apartadoDeAlta(dg, vc, req.usuarioToken?.sub ?? null, body.apartadoMondayId)
|
||||
|
||||
await prisma.cliente.update({
|
||||
where: { id: cliente.id },
|
||||
@@ -493,8 +527,10 @@ export async function completarProgreso(req: Request, res: Response): Promise<vo
|
||||
}
|
||||
|
||||
if (mondayApartadoId === null) {
|
||||
// El capturista original del alta progresiva es quien queda como Vendedor
|
||||
mondayApartadoId = await crearApartadoSiAplica(dg, vc, cliente.capturadoPorId)
|
||||
// Con apartadoMondayId (alta desde un apartado existente) se liga y
|
||||
// enriquece ese item; si no, se crea. El capturista original del alta
|
||||
// progresiva es quien queda como Vendedor.
|
||||
mondayApartadoId = await apartadoDeAlta(dg, vc, cliente.capturadoPorId, body.apartadoMondayId)
|
||||
}
|
||||
|
||||
await prisma.cliente.update({
|
||||
@@ -607,3 +643,65 @@ export async function listarDocumentos(req: Request, res: Response): Promise<voi
|
||||
})
|
||||
res.json(documentos)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/altas/prefill-apartado/:itemId — todo lo ya recolectado de un
|
||||
* apartado de Monday para prellenar el alta de cliente (botón "Firma Cliente
|
||||
* (Venta)" de Mis apartados): ficha del apartado, contacto relacionado
|
||||
* (perfil completo), oportunidad relacionada (perfil de la afluencia) y la
|
||||
* cotización más reciente vinculada. Las relaciones se arman por teléfono
|
||||
* (últimos 10 dígitos) o email.
|
||||
*/
|
||||
export async function prefillApartadoEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const itemId = String(req.params['itemId'])
|
||||
const apartado = (await mondayService.getApartados()).find((a) => a.id === itemId)
|
||||
if (!apartado) throw new NotFoundError('Apartado no encontrado')
|
||||
|
||||
const tel = (apartado.telefono ?? '').replace(/\D/g, '').slice(-10)
|
||||
const email = (apartado.email ?? '').trim().toLowerCase()
|
||||
const coincide = (t: string, e: string): boolean =>
|
||||
(tel !== '' && t.replace(/\D/g, '').length >= 10 && t.replace(/\D/g, '').slice(-10) === tel) ||
|
||||
(email !== '' && e.trim().toLowerCase() === email)
|
||||
|
||||
const [contactos, oportunidades] = await Promise.all([
|
||||
mondayService.getContactos(),
|
||||
mondayService.getOportunidades(),
|
||||
])
|
||||
const contactoBase = contactos.find((c) => coincide(c.telefono, c.email))
|
||||
const contacto = contactoBase ? await mondayService.getContactoDetalle(contactoBase.id) : null
|
||||
const oportunidad = oportunidades.find((o) => coincide(o.telefono, o.email)) ?? null
|
||||
|
||||
// Cotización local más reciente: vinculada a la oportunidad o por teléfono
|
||||
let cotizacion = null
|
||||
if (oportunidad) {
|
||||
cotizacion = await prisma.cotizacion.findFirst({
|
||||
where: { mondayItemId: oportunidad.id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
}
|
||||
if (!cotizacion && tel !== '') {
|
||||
cotizacion = await prisma.cotizacion.findFirst({
|
||||
where: { clienteTelefono: { contains: tel } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
apartado,
|
||||
contacto,
|
||||
oportunidad,
|
||||
cotizacion: cotizacion
|
||||
? {
|
||||
tipoPropuesta: cotizacion.tipoPropuesta,
|
||||
acabados: cotizacion.acabados,
|
||||
apartado: cotizacion.apartado,
|
||||
precioLista: cotizacion.precioLista,
|
||||
precioFinal: cotizacion.precioFinal,
|
||||
enganche: cotizacion.enganche,
|
||||
numMensualidades: cotizacion.numMensualidades,
|
||||
mensualidad: cotizacion.mensualidad,
|
||||
pagoFinal: cotizacion.pagoFinal,
|
||||
}
|
||||
: null,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user