feat: Interesado en leads, lada MX/USA, orígenes FRASA y subcategoría de descarte
- Mis leads: botón Interesado (label sembrado en Monday, mueve a Leads Progreso); el botón Agenda cita ahora solo vive en Mis oportunidades - Descarte: además de la categoría, subcategoría del catálogo de Monday (workspace de referencia CRM FRASA); se guarda en la columna Subcategoria del tablero Motivos No Compra y el item queda en el grupo de su categoría - Teléfonos con selector de lada 🇲🇽 +52 / 🇺🇸 +1 en captura rápida (Mi día), Alta Afluencia y Alta de Cliente (tel. casa y celular); Monday escribe countryShortName MX/US y el número compacto (rechaza espacios); el dedup sigue comparando por los últimos 10 dígitos - Origen nuevo en captura rápida y afluencia: PASE, PROSPECCION ASESOR, ACTIVACION, FACEBOOK FRASA, REFERIDO, OTRO (sembrados en Leads.Origen y Oportunidades.Origen del Lead) - Tests: 461 API + 16 mi-dia + 116 alta-clientes, verde
This commit is contained in:
@@ -5,7 +5,7 @@ import { mondayService, type LeadMonday, type MondayService, type OportunidadMon
|
||||
import { moverAGrupoSegunEstado } from '../services/grupos-estado.service.js'
|
||||
import { automatizacionesService, CLAVE_RR_SKIP, prismaStateStore } from '../services/automatizaciones.service.js'
|
||||
import { prisma } from '../services/prisma.js'
|
||||
import { MOTIVOS_NO_COMPRA } from './oportunidades.controller.js'
|
||||
import { MOTIVOS_NO_COMPRA, SUBCATEGORIAS_NO_COMPRA } from './oportunidades.controller.js'
|
||||
import { AppError } from '../utils/errors.js'
|
||||
|
||||
/**
|
||||
@@ -23,19 +23,21 @@ import { AppError } from '../utils/errors.js'
|
||||
*/
|
||||
|
||||
// Transiciones que el vendedor puede hacer desde la app Mi día
|
||||
export const ESTADOS_LEAD_PERMITIDOS = ['CONTACTADO', 'Intento de contacto', 'AGENDA CITA', 'CALIFICADO'] as const
|
||||
export const ESTADOS_LEAD_PERMITIDOS = ['CONTACTADO', 'Intento de contacto', 'INTERESADO', 'AGENDA CITA', 'CALIFICADO'] as const
|
||||
|
||||
// Traducción de la whitelist (valores que mandan los frontends) a los labels
|
||||
// reales de la columna status 'Estado' de cada board
|
||||
const LABEL_ESTADO_LEADS: Record<(typeof ESTADOS_LEAD_PERMITIDOS)[number], string> = {
|
||||
CONTACTADO: 'Contactado',
|
||||
'Intento de contacto': 'Intento de contacto',
|
||||
INTERESADO: 'Interesado',
|
||||
'AGENDA CITA': 'Agenda Cita',
|
||||
CALIFICADO: 'Calificado',
|
||||
}
|
||||
const LABEL_ESTADO_OPORTUNIDAD: Record<(typeof ESTADOS_LEAD_PERMITIDOS)[number], string> = {
|
||||
CONTACTADO: 'Contactado',
|
||||
'Intento de contacto': 'Intento de contacto',
|
||||
INTERESADO: 'Interesado', // no existe en el board Oportunidades: se rechaza abajo
|
||||
'AGENDA CITA': 'Agendo Cita',
|
||||
CALIFICADO: 'Calificado',
|
||||
}
|
||||
@@ -93,9 +95,14 @@ const cambiarEstadoSchema = z.object({
|
||||
|
||||
const descartarSchema = z.object({
|
||||
motivo: z.enum(MOTIVOS_NO_COMPRA),
|
||||
// Subcategoría del catálogo de Monday (debe pertenecer a la categoría elegida)
|
||||
subcategoria: z.string().trim().min(1).optional(),
|
||||
comentario: z.string().trim().min(1).optional(),
|
||||
fuente: fuenteSchema,
|
||||
})
|
||||
}).refine(
|
||||
(b) => b.subcategoria === undefined || SUBCATEGORIAS_NO_COMPRA[b.motivo].includes(b.subcategoria),
|
||||
{ message: 'La subcategoría no pertenece a la categoría elegida', path: ['subcategoria'] },
|
||||
)
|
||||
|
||||
// ---------- Búsqueda dual (prellenado del alta de clientes) ----------
|
||||
|
||||
@@ -333,6 +340,10 @@ export async function cambiarEstadoLead(
|
||||
fuente: FuenteLead = 'oportunidad',
|
||||
): Promise<void> {
|
||||
const esLead = fuente === 'lead'
|
||||
// 'Interesado' solo existe en el tablero Leads (no es etapa del funnel)
|
||||
if (estado === 'INTERESADO' && !esLead) {
|
||||
throw new AppError(400, 'La etapa Interesado solo aplica a leads del tablero Leads')
|
||||
}
|
||||
const boardId = esLead ? config.monday.boards.leads : config.monday.boards.oportunidades
|
||||
const label = esLead ? LABEL_ESTADO_LEADS[estado] : LABEL_ESTADO_OPORTUNIDAD[estado]
|
||||
await monday.cambiarValorColumna(boardId, itemId, 'Estado', label)
|
||||
@@ -346,7 +357,8 @@ export async function cambiarEstadoLead(
|
||||
* - 'oportunidad': Estado 'Perdido' + dropdown Motivo No Compra (label ya
|
||||
* sembrado; si no existiera se omite) + comentario en COMENTARIOS.
|
||||
* - 'lead': Estado 'Descartado' + comentario en Notas.
|
||||
* Ambos registran el motivo en el tablero catálogo Motivos No Compra.
|
||||
* Ambos registran el motivo (y la subcategoría si viene) en el tablero
|
||||
* catálogo Motivos No Compra.
|
||||
*/
|
||||
export async function descartarLead(
|
||||
monday: MondayLeads,
|
||||
@@ -354,6 +366,7 @@ export async function descartarLead(
|
||||
motivo: string,
|
||||
comentario?: string,
|
||||
fuente: FuenteLead = 'oportunidad',
|
||||
subcategoria?: string,
|
||||
): Promise<string> {
|
||||
const nombreItem = await monday.obtenerNombreItem(itemId)
|
||||
if (fuente === 'lead') {
|
||||
@@ -377,8 +390,9 @@ export async function descartarLead(
|
||||
}
|
||||
}
|
||||
return monday.crearMotivoNoCompra({
|
||||
nombre: `${motivo} — ${nombreItem}`,
|
||||
nombre: `${motivo}${subcategoria !== undefined ? ` / ${subcategoria}` : ''} — ${nombreItem}`,
|
||||
categoria: motivo,
|
||||
...(subcategoria !== undefined ? { subcategoria } : {}),
|
||||
...(comentario !== undefined ? { descripcion: comentario } : {}),
|
||||
})
|
||||
}
|
||||
@@ -513,11 +527,11 @@ export async function cambiarEstadoLeadEndpoint(req: Request, res: Response): Pr
|
||||
res.json({ itemId, estado: body.estado, ...(body.fecha !== undefined ? { fecha: body.fecha } : {}) })
|
||||
}
|
||||
|
||||
/** POST /api/leads/:itemId/descartar { motivo, comentario?, fuente? } */
|
||||
/** POST /api/leads/:itemId/descartar { motivo, subcategoria?, comentario?, fuente? } */
|
||||
export async function descartarLeadEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const body = descartarSchema.parse(req.body)
|
||||
const itemId = String(req.params['itemId'])
|
||||
const motivoItemId = await descartarLead(mondayService, itemId, body.motivo, body.comentario, body.fuente)
|
||||
const motivoItemId = await descartarLead(mondayService, itemId, body.motivo, body.comentario, body.fuente, body.subcategoria)
|
||||
res.json({ itemId, estado: body.fuente === 'lead' ? 'Descartado' : 'Perdido', motivo: body.motivo, motivoItemId })
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user