89 lines
4.8 KiB
TypeScript
89 lines
4.8 KiB
TypeScript
/**
|
|
* VOCABULARIO canónico FRASA del backend: fases, estados, etapas, motivos,
|
|
* subcategorías, orígenes y desarrollos en un solo catálogo.
|
|
*
|
|
* Espejo de apps/mi-dia/src/lib/mi-dia.ts (mantener sincronizados). Los
|
|
* controllers siguen exportando sus constantes de validación zod derivadas de
|
|
* este mismo vocabulario:
|
|
* - leads.controller.ts → ESTADOS_LEAD_PERMITIDOS (FASES_LEAD sin NUEVO/DESCARTADO)
|
|
* - oportunidades.controller.ts → ETAPAS_OPORTUNIDAD_PERMITIDAS (= FASES_OPORTUNIDAD),
|
|
* MOTIVOS_NO_COMPRA, SUBCATEGORIAS_NO_COMPRA
|
|
* - apartados.controller.ts → FASES_APARTADO_PERMITIDAS (= FASES_APARTADO)
|
|
*/
|
|
export const VOCABULARIO = {
|
|
// Estados de la columna status del board Leads: ESTADOS_LEAD_PERMITIDOS del
|
|
// API + NUEVO (captura) y DESCARTADO (terminal)
|
|
FASES_LEAD: ['NUEVO', 'Intento de contacto', 'CONTACTADO', 'INTERESADO', 'AGENDA CITA', 'CALIFICADO', 'DESCARTADO'],
|
|
// Etapas del funnel de Oportunidades (labels REALES de la columna status 'Estado')
|
|
FASES_OPORTUNIDAD: ['Agendo Cita', 'Afluencia', 'Cotizador', 'Aparto', 'No Asistio', 'Abandono'],
|
|
// Fases del pipeline Enkontrol del tablero Apartado (labels REALES de la
|
|
// columna status 'Estado'); incluye las fases de escrituración
|
|
FASES_APARTADO: [
|
|
'Pago Apartado',
|
|
'Alta Enkontrol',
|
|
'Entrego Documentacion',
|
|
'Cita Clientes',
|
|
'Contrato Enkontrol',
|
|
'Firma Cliente',
|
|
'Cancelacion Apartado',
|
|
'Venta',
|
|
'Seguimiento',
|
|
'Integracion',
|
|
'Llamada Escrituras',
|
|
'X Escriturar',
|
|
'Banco/Infonavit',
|
|
'X Autorizar',
|
|
'Autorizado',
|
|
'Escriturado',
|
|
'Titulado',
|
|
'Cancelo',
|
|
],
|
|
// Fases de escrituración (pipeline posterior a 'Firma Cliente')
|
|
FASES_ESCRITURACION: ['Venta', 'Seguimiento', 'Integracion', 'X Autorizar', 'Autorizado', 'X Escriturar', 'Escriturado', 'Titulado', 'Cancelo'],
|
|
// Categorías del dropdown Motivo No Compra (vocabulario CRM FRASA)
|
|
MOTIVOS_NO_COMPRA: ['Precio', 'Producto', 'Ubicacion', 'Financiamiento', 'Competencia', 'Cliente', 'Timing'],
|
|
// Subcategorías por categoría (catálogo del tablero Motivos No Compra,
|
|
// sembrado en Frasa-CAS el 2026-09-04; 'Mesualidad' y 'Cambo de Planes'
|
|
// conservan las faltas de ortografía originales del tablero del cliente)
|
|
SUBCATEGORIAS: {
|
|
Precio: ['Precio Total', 'Enganche', 'Mesualidad'],
|
|
Producto: ['Distribucion', 'Tamaño', 'Recamaras', 'Baños', 'Amenidades', 'Acabados', 'Estacionamiento', 'Seguridad', 'Inventario'],
|
|
Ubicacion: ['Zona', 'Accesos', 'Distancia', 'Servicios', 'Seguridad', 'Escuela', 'Trabajo'],
|
|
Financiamiento: ['No Califica', 'Credito Insuficiente', 'Tasa de interes', 'Mensualidad Alta', 'Enganche Alto', 'Forma de Pago', 'Infonavit Insuficiente', 'No tiene liquidez'],
|
|
Competencia: ['Precio', 'Producto', 'Ubicacion', 'Promocion', 'Financiamiento'],
|
|
Cliente: ['Cambo de Planes', 'Decision Familiar', 'No estaba Listo', 'Emergencia Personal'],
|
|
Timing: ['Compra Posterior', 'Esperando Credito', 'Esperando Venta de Propiedad', 'Esperando Proyecto'],
|
|
},
|
|
// Labels de la columna 'Origen' (board Leads) / 'Origen del Lead'
|
|
// (afluencia), verificados y sembrados el 2026-09-04
|
|
ORIGENES: ['PASE', 'PROSPECCION ASESOR', 'ACTIVACION', 'FACEBOOK FRASA', 'REFERIDO', 'BROCKER', 'OTRO'],
|
|
// Labels oficiales de desarrollo (verificados el 2026-09-04; SOLE y MUTUO
|
|
// agregados 2026-09 — existen en captura e inventario)
|
|
DESARROLLOS: ['ATELIER', 'DIAGONAL', 'ESTANZA', 'SENDA', 'SOLANA', 'SOLE', 'MUTUO'],
|
|
// Campos de calificación que hacen "completa" una afluencia: sin ellos la
|
|
// oportunidad NO puede avanzar a 'Aparto' (regla de negocio del cliente)
|
|
CAMPOS_AFLUENCIA_REQUERIDOS: ['estadoCivil', 'tipoEmpleo', 'rangoSalarial', 'tipoEconomia', 'califica', 'loQuierePara'],
|
|
} as const
|
|
|
|
/** Campos de afluencia faltantes en un datosJson ('[]' = afluencia completa) */
|
|
export function camposAfluenciaFaltantes(datosJson: string | null): string[] {
|
|
let datos: Record<string, unknown> = {}
|
|
if (datosJson) {
|
|
try { datos = JSON.parse(datosJson) as Record<string, unknown> } catch { datos = {} }
|
|
}
|
|
return VOCABULARIO.CAMPOS_AFLUENCIA_REQUERIDOS.filter(
|
|
(c) => typeof datos[c] !== 'string' || (datos[c] as string).trim() === '',
|
|
)
|
|
}
|
|
|
|
/** ¿La fila tiene la afluencia completa? (gate de 'Aparto' y flag de Mi día) */
|
|
export function afluenciaCompleta(datosJson: string | null): boolean {
|
|
return camposAfluenciaFaltantes(datosJson).length === 0
|
|
}
|
|
|
|
export type FaseLead = (typeof VOCABULARIO.FASES_LEAD)[number]
|
|
export type FaseOportunidad = (typeof VOCABULARIO.FASES_OPORTUNIDAD)[number]
|
|
export type FaseApartado = (typeof VOCABULARIO.FASES_APARTADO)[number]
|
|
export type FaseEscrituracion = (typeof VOCABULARIO.FASES_ESCRITURACION)[number]
|
|
export type MotivoNoCompra = (typeof VOCABULARIO.MOTIVOS_NO_COMPRA)[number]
|