feat(mi-dia): botón Afluencia abre el formulario prellenado y actualiza la oportunidad
- Mis oportunidades: 'Afluencia →' navega a /client/?afluencia=<id> con nombre, contacto, desarrollo y modelo prellenados - Alta Afluencia: con oportunidadId el envío ACTUALIZA la oportunidad en Monday (perfil completo + etapa Afluencia) en vez de crear duplicado; sin oportunidadId el flujo de captura nueva no cambia - monday.service: actualizarOportunidadAfluencia (mismo builder de la creación + name); moverAGrupoSegunEstado tras actualizar - Tests: 457 API + 16 mi-dia + 116 alta-clientes, verde
This commit is contained in:
@@ -65,6 +65,10 @@ function mondayMock(
|
||||
this.oportunidadesCreadas.push(datos)
|
||||
return 'op-nueva-1'
|
||||
},
|
||||
oportunidadesActualizadas: [] as { id: string; datos: Record<string, unknown> }[],
|
||||
async actualizarOportunidadAfluencia(id: string, datos: Record<string, unknown>) {
|
||||
this.oportunidadesActualizadas.push({ id, datos })
|
||||
},
|
||||
async buscarLeadPorTelefono(_telefono: string) {
|
||||
return opts.duplicadoLead ?? null
|
||||
},
|
||||
@@ -260,6 +264,33 @@ describe('crearAfluencia (POST /api/afluencias) — la afluencia ES la oportunid
|
||||
espia.mockRestore()
|
||||
})
|
||||
|
||||
it('con oportunidadId (botón "Afluencia" de Mi día): ACTUALIZA la oportunidad, sin duplicado ni validación de teléfono', async () => {
|
||||
const monday = mondayMock()
|
||||
const r = await crearAfluencia(monday, {
|
||||
...datosBase,
|
||||
oportunidadId: 'op-existente-3',
|
||||
desarrollo: 'SENDA',
|
||||
modelo: 'SOHO',
|
||||
edad: 34,
|
||||
notas: 'Perfil completado en punto de venta',
|
||||
})
|
||||
expect(r).toEqual({ id: 'op-existente-3', oportunidadId: 'op-existente-3', actualizada: true })
|
||||
expect(monday.oportunidadesCreadas).toEqual([]) // no se duplica
|
||||
expect(monday.oportunidadesActualizadas).toEqual([
|
||||
{
|
||||
id: 'op-existente-3',
|
||||
datos: expect.objectContaining({
|
||||
nombre: 'Lead Prueba',
|
||||
telefono: '4771234567',
|
||||
desarrolloLabel: 'SENDA',
|
||||
modeloLabel: 'SOHO',
|
||||
edad: 34,
|
||||
notas: 'Perfil completado en punto de venta',
|
||||
}),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('rechaza un desarrollo que no sea label real de la columna Desarrollo (board Oportunidades)', async () => {
|
||||
const monday = mondayMock()
|
||||
await expect(crearAfluencia(monday, { ...datosBase, desarrollo: 'MONTEBELLO' })).rejects.toThrow(
|
||||
|
||||
@@ -62,6 +62,9 @@ const crearAfluenciaSchema = z.object({
|
||||
origen: z.string().trim().min(1).optional(),
|
||||
desarrollo: z.string().trim().min(1).optional(),
|
||||
notas: z.string().trim().min(1).optional(),
|
||||
// Con oportunidadId (botón "Afluencia" de Mis oportunidades en Mi día): se
|
||||
// ACTUALIZA esa oportunidad con el perfil capturado en vez de crear una nueva
|
||||
oportunidadId: z.string().trim().min(1).optional(),
|
||||
// Campos de calificación de la afluencia (opcionales; los labels fijos del
|
||||
// front coinciden con los sembrados en Monday, no se validan contra el board)
|
||||
modelo: z.string().trim().min(1).optional(),
|
||||
@@ -165,6 +168,7 @@ type MondayLeads = Pick<
|
||||
| 'cambiarValorColumna'
|
||||
| 'crearLead'
|
||||
| 'crearOportunidadAfluencia'
|
||||
| 'actualizarOportunidadAfluencia'
|
||||
| 'crearMotivoNoCompra'
|
||||
| 'buscarLeadPorTelefono'
|
||||
| 'buscarOportunidadPorTelefono'
|
||||
@@ -176,7 +180,7 @@ type MondayLeads = Pick<
|
||||
>
|
||||
|
||||
export type ResultadoCrearLead =
|
||||
| { id: string; oportunidadId: string | null }
|
||||
| { id: string; oportunidadId: string | null; actualizada?: boolean }
|
||||
| { duplicado: { id: string; nombre: string } }
|
||||
|
||||
/**
|
||||
@@ -237,6 +241,38 @@ export async function crearAfluencia(
|
||||
): Promise<ResultadoCrearLead> {
|
||||
const boardOportunidades = config.monday.boards.oportunidades
|
||||
|
||||
// Con oportunidadId: el formulario venía prellenado desde Mis oportunidades
|
||||
// (Mi día) → se ACTUALIZA esa oportunidad con el perfil completo y queda en
|
||||
// etapa 'Afluencia'; no se crea duplicado ni se valida teléfono.
|
||||
if (datos.oportunidadId !== undefined) {
|
||||
await monday.actualizarOportunidadAfluencia(datos.oportunidadId, {
|
||||
nombre: datos.nombre,
|
||||
telefono: datos.telefono,
|
||||
email: datos.email,
|
||||
desarrolloLabel: datos.desarrollo,
|
||||
origenLabel: datos.origen,
|
||||
modeloLabel: datos.modelo,
|
||||
medioContacto: datos.medioContacto,
|
||||
edad: datos.edad,
|
||||
estadoCivilLabel: datos.estadoCivil,
|
||||
tipoEmpleoLabel: datos.tipoEmpleo,
|
||||
ocupacion: datos.ocupacion,
|
||||
rangoSalarial: datos.rangoSalarial,
|
||||
creditoLabel: datos.credito,
|
||||
tipoEconomiaLabel: datos.tipoEconomia,
|
||||
calificaLabel: datos.califica,
|
||||
loQuierePara: datos.loQuierePara,
|
||||
notas: datos.notas,
|
||||
})
|
||||
try {
|
||||
await moverAGrupoSegunEstado(monday, boardOportunidades, datos.oportunidadId)
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`No se pudo mover al grupo correcto la oportunidad ${datos.oportunidadId}:`, err)
|
||||
}
|
||||
return { id: datos.oportunidadId, oportunidadId: datos.oportunidadId, actualizada: true }
|
||||
}
|
||||
|
||||
if (datos.telefono !== undefined) {
|
||||
const existente = await monday.buscarOportunidadPorTelefono(datos.telefono)
|
||||
if (existente) return { duplicado: { id: existente.id, nombre: existente.nombre } }
|
||||
@@ -429,6 +465,12 @@ export async function crearAfluenciaEndpoint(req: Request, res: Response): Promi
|
||||
})
|
||||
return
|
||||
}
|
||||
// Actualización de oportunidad existente (botón "Afluencia" de Mi día): no
|
||||
// hay captura nueva que registrar ni round-robin que correr
|
||||
if (resultado.actualizada === true) {
|
||||
res.json({ id: resultado.id, estado: 'Afluencia', oportunidadId: resultado.oportunidadId, actualizada: true })
|
||||
return
|
||||
}
|
||||
// Registro local del capturista para "Mis capturas" del panel Mi día.
|
||||
// leadId = el id de la oportunidad (la afluencia ES la oportunidad). Un
|
||||
// fallo aquí no rompe la respuesta: la afluencia ya quedó creada en Monday.
|
||||
|
||||
@@ -520,6 +520,48 @@ export class MondayService {
|
||||
return this.crearItem(boardId, datos.nombre, columnValues)
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza una oportunidad existente con el perfil completo de la afluencia
|
||||
* (mismo builder que crearOportunidadAfluencia) y la pone en etapa
|
||||
* 'Afluencia'. La usa el botón "Afluencia" de Mis oportunidades en Mi día:
|
||||
* el formulario se manda prellenado y el envío enriquece el item en vez de
|
||||
* duplicarlo. El nombre del item se actualiza por el column id 'name'.
|
||||
*/
|
||||
async actualizarOportunidadAfluencia(oportunidadId: string, datos: DatosOportunidadAfluencia): Promise<void> {
|
||||
const boardId = config.monday.boards.oportunidades
|
||||
const hoy = new Date()
|
||||
const fechaHoy = `${hoy.getFullYear()}-${String(hoy.getMonth() + 1).padStart(2, '0')}-${String(hoy.getDate()).padStart(2, '0')}`
|
||||
const valores = JSON.parse(
|
||||
await this.construirColumnValues(boardId, {
|
||||
Estado: 'Afluencia',
|
||||
Fecha: fechaHoy,
|
||||
'Cliente teléfono': datos.telefono,
|
||||
'Cliente email': datos.email,
|
||||
Desarrollo: datos.desarrolloLabel,
|
||||
'Modelo de Interés': datos.modeloLabel,
|
||||
'Origen del Lead': datos.origenLabel,
|
||||
'Medio de Contacto': datos.medioContacto,
|
||||
Edad: datos.edad,
|
||||
'Estado Civil': datos.estadoCivilLabel,
|
||||
'Tipo de Empleo': datos.tipoEmpleoLabel,
|
||||
Ocupación: datos.ocupacion,
|
||||
'Rango Salarial': datos.rangoSalarial,
|
||||
Crédito: datos.creditoLabel,
|
||||
'Tipo de Econimia': datos.tipoEconomiaLabel, // SIC: typo en el título real
|
||||
'Lead Califica': datos.calificaLabel,
|
||||
'Proposito de Compra': datos.loQuierePara,
|
||||
COMENTARIOS: datos.notas,
|
||||
}),
|
||||
) as Record<string, unknown>
|
||||
if (datos.nombre !== '') valores['name'] = datos.nombre
|
||||
await this.graphql(
|
||||
`mutation ($boardId: ID!, $itemId: ID!, $columnValues: JSON!) {
|
||||
change_multiple_column_values(board_id: $boardId, item_id: $itemId, column_values: $columnValues) { id }
|
||||
}`,
|
||||
{ boardId, itemId: oportunidadId, columnValues: JSON.stringify(valores) },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza una oportunidad existente con los datos de una cotización
|
||||
* (mismo builder de valores que crearOportunidad) y la mueve a 'Cotizador'.
|
||||
|
||||
Reference in New Issue
Block a user