- Vocabulario funnel: labels Intento de contacto/Abandono en Leads, etapas completas en Oportunidades (Cotizador, Ganado, Perdido...) - Apartados: pipeline Enkontrol (8 estados) + checkboxes CURP/RFC/Comprobante que la app de alta prende al subir documentos - Motivos No Compra: board catálogo + dropdown + endpoint /oportunidades/:id/perder - Perfil demográfico en Contactos mapeado desde el alta (estado civil, vivienda, propósito, fecha nac., rango de edad calculado) - Desarrollos: catálogo maestro sincronizado (precios, m2, tipo, entrega) - Actividades: board nuevo + creación automática en round-robin e inactividad - Cobranza: plan liquidado mueve el apartado a 'X Escriturar' en Monday - Scripts: setup:crm y sync:desarrollos (idempotentes) - Tests 119/119 verdes
241 lines
9.3 KiB
TypeScript
241 lines
9.3 KiB
TypeScript
/**
|
|
* Setup idempotente del workspace Frasa-CAS para alinearlo con el CRM FRASA real:
|
|
*
|
|
* - Leads: labels 'Intento de contacto' y 'Abandono' en el status Estado.
|
|
* - Oportunidades: labels de Etapa del funnel CRM + dropdown 'Motivo No Compra'.
|
|
* - Apartado: labels del pipeline Enkontrol + checkboxes CURP / RFC / Comprobante.
|
|
* - Contactos: dropdowns del perfil demográfico + date Fecha Nacimiento.
|
|
* - Tableros nuevos 'Motivos No Compra' y 'Actividades' (si MONDAY_BOARD_MOTIVOS /
|
|
* MONDAY_BOARD_ACTIVIDADES están vacíos en .env). Imprime los IDs creados.
|
|
*
|
|
* Uso: npm run setup:crm (desde api-gateway)
|
|
* Es seguro re-ejecutarlo: solo crea lo que falta.
|
|
*/
|
|
import { config } from '../src/config/index.js'
|
|
import { MondayService } from '../src/services/monday.service.js'
|
|
|
|
const monday = new MondayService()
|
|
|
|
// ---------- Vocabulario CRM FRASA (normalizado, sin los typos del original) ----------
|
|
|
|
const ETIQUETAS_LEADS_ESTADO = ['Intento de contacto', 'Abandono']
|
|
|
|
const ETIQUETAS_ETAPA_OPORTUNIDAD = [
|
|
'Afluencia',
|
|
'Cotizador',
|
|
'Agendo Cita',
|
|
'Aparto',
|
|
'Ganado',
|
|
'Perdido',
|
|
'Abandono',
|
|
'No Asistio',
|
|
]
|
|
|
|
export const MOTIVOS_NO_COMPRA = [
|
|
'Precio',
|
|
'Producto',
|
|
'Ubicacion',
|
|
'Financiamiento',
|
|
'Competencia',
|
|
'Cliente',
|
|
'Timing',
|
|
]
|
|
|
|
const ETIQUETAS_APARTADO_ESTADO = [
|
|
'Entrego Documentacion',
|
|
'Pago Apartado',
|
|
'Alta Enkontrol',
|
|
'Cita Clientes',
|
|
'Contrato Enkontrol',
|
|
'Firma Cliente (Venta)',
|
|
'Cancelacion',
|
|
'En Definicion',
|
|
// Postventa (cobranza): al liquidarse el plan de pagos
|
|
'X Escriturar',
|
|
'Escriturado',
|
|
]
|
|
|
|
const PERFIL_CONTACTOS: { titulo: string; tipo: string; labels?: string[] }[] = [
|
|
{ titulo: 'Sexo', tipo: 'dropdown', labels: ['Hombre', 'Mujer', 'No define'] },
|
|
{ titulo: 'Estado Civil', tipo: 'dropdown', labels: ['Soltero', 'Casado', 'Union Libre', 'Divorciado', 'Viudo'] },
|
|
{ titulo: 'Rango de Edad', tipo: 'dropdown', labels: ['Menor de 24', 'Entre 24 y 40', 'Mayor de 40'] },
|
|
{
|
|
titulo: 'Actividad Economica',
|
|
tipo: 'dropdown',
|
|
labels: ['Empleado', 'Empresario', 'Pensionado', 'Comerciante'],
|
|
},
|
|
{
|
|
titulo: 'Vivienda Actual',
|
|
tipo: 'dropdown',
|
|
labels: ['Propia', 'Rentada', 'Prestada', 'Comparte con familia'],
|
|
},
|
|
{ titulo: 'Proposito de Compra', tipo: 'dropdown', labels: ['Habitarla', 'Inversion'] },
|
|
{ titulo: 'Fecha Nacimiento', tipo: 'date' },
|
|
]
|
|
|
|
// ---------- Helpers ----------
|
|
|
|
interface Columna {
|
|
id: string
|
|
title: string
|
|
type: string
|
|
settings_str: string
|
|
}
|
|
|
|
async function columnas(boardId: string): Promise<Columna[]> {
|
|
const data = await monday.graphql<{ boards: { columns: Columna[] }[] }>(
|
|
`query ($ids: [ID!]) { boards(ids: $ids) { columns { id title type settings_str } } }`,
|
|
{ ids: [boardId] },
|
|
)
|
|
return data.boards[0]?.columns ?? []
|
|
}
|
|
|
|
/** Labels actuales de una columna status/dropdown (parsea settings_str) */
|
|
function labelsDe(col: Columna): string[] {
|
|
try {
|
|
const s = JSON.parse(col.settings_str) as { labels?: unknown }
|
|
if (Array.isArray(s.labels)) {
|
|
// dropdown: [{id, name}]
|
|
return s.labels.map((l) => (typeof l === 'object' && l !== null ? String((l as { name?: string }).name) : String(l)))
|
|
}
|
|
if (s.labels !== null && typeof s.labels === 'object') {
|
|
// status: {"0": "Label", ...}
|
|
return Object.values(s.labels as Record<string, string>)
|
|
}
|
|
} catch {
|
|
// settings no parseable: sin labels conocidos
|
|
}
|
|
return []
|
|
}
|
|
|
|
async function asegurarColumna(boardId: string, titulo: string, tipo: string, defaults?: unknown): Promise<void> {
|
|
const cols = await columnas(boardId)
|
|
if (cols.some((c) => c.title === titulo)) {
|
|
console.log(` = columna "${titulo}" ya existe (${boardId})`)
|
|
return
|
|
}
|
|
await monday.graphql(
|
|
`mutation ($boardId: ID!, $title: String!, $tipo: ColumnType!, $defaults: JSON) {
|
|
create_column(board_id: $boardId, title: $title, column_type: $tipo, defaults: $defaults) { id }
|
|
}`,
|
|
{ boardId, title: titulo, tipo, ...(defaults !== undefined ? { defaults: JSON.stringify(defaults) } : {}) },
|
|
)
|
|
console.log(` + columna "${titulo}" (${tipo}) creada en ${boardId}`)
|
|
}
|
|
|
|
/**
|
|
* Agrega labels faltantes a una columna status/dropdown usando un item scratch
|
|
* con create_labels_if_missing (change_column_metadata no siempre está disponible).
|
|
*/
|
|
async function asegurarEtiquetas(boardId: string, tituloColumna: string, etiquetas: string[]): Promise<void> {
|
|
const cols = await columnas(boardId)
|
|
const col = cols.find((c) => c.title === tituloColumna)
|
|
if (!col) throw new Error(`Columna "${tituloColumna}" no existe en ${boardId}`)
|
|
const actuales = new Set(labelsDe(col).map((l) => l.toLowerCase()))
|
|
const faltantes = etiquetas.filter((e) => !actuales.has(e.toLowerCase()))
|
|
if (faltantes.length === 0) {
|
|
console.log(` = "${tituloColumna}" ya tiene todas las etiquetas (${boardId})`)
|
|
return
|
|
}
|
|
// Item scratch: se crea, se le escriben los labels nuevos y se borra
|
|
const creado = await monday.graphql<{ create_item: { id: string } }>(
|
|
`mutation ($boardId: ID!, $name: String!) { create_item(board_id: $boardId, item_name: $name) { id } }`,
|
|
{ boardId, name: '__scratch_labels__' },
|
|
)
|
|
const itemId = creado.create_item.id
|
|
try {
|
|
for (const etiqueta of faltantes) {
|
|
const valor = col.type === 'dropdown' ? { labels: [etiqueta] } : { label: etiqueta }
|
|
await monday.graphql(
|
|
`mutation ($boardId: ID!, $itemId: ID!, $values: JSON!) {
|
|
change_multiple_column_values(board_id: $boardId, item_id: $itemId, column_values: $values, create_labels_if_missing: true) { id }
|
|
}`,
|
|
{ boardId, itemId, values: JSON.stringify({ [col.id]: valor }) },
|
|
)
|
|
console.log(` + etiqueta "${etiqueta}" en "${tituloColumna}" (${boardId})`)
|
|
}
|
|
} finally {
|
|
await monday.graphql(`mutation ($itemId: ID!) { delete_item(item_id: $itemId) { id } }`, { itemId })
|
|
}
|
|
}
|
|
|
|
async function crearTablero(nombre: string): Promise<string> {
|
|
const workspaces = await monday.graphql<{ workspaces: { id: string; name: string }[] }>(
|
|
`query { workspaces { id name } }`,
|
|
)
|
|
const ws = workspaces.workspaces.find((w) => w.name === config.monday.workspace)
|
|
const data = await monday.graphql<{ create_board: { id: string } }>(
|
|
`mutation ($name: String!, $kind: BoardKind!, $ws: ID) {
|
|
create_board(board_name: $name, board_kind: $kind, workspace_id: $ws) { id }
|
|
}`,
|
|
{ name: nombre, kind: 'public', ...(ws ? { ws: ws.id } : {}) },
|
|
)
|
|
return data.create_board.id
|
|
}
|
|
|
|
// ---------- Main ----------
|
|
|
|
async function main(): Promise<void> {
|
|
const b = config.monday.boards
|
|
|
|
console.log('== Leads: etiquetas de Estado ==')
|
|
await asegurarEtiquetas(b.leads, 'Estado', ETIQUETAS_LEADS_ESTADO)
|
|
|
|
console.log('== Oportunidades: Etapa (columna status "Estado") + Motivo No Compra ==')
|
|
await asegurarEtiquetas(b.oportunidades, 'Estado', ETIQUETAS_ETAPA_OPORTUNIDAD)
|
|
await asegurarColumna(b.oportunidades, 'Motivo No Compra', 'dropdown')
|
|
await asegurarEtiquetas(b.oportunidades, 'Motivo No Compra', MOTIVOS_NO_COMPRA)
|
|
|
|
console.log('== Apartado: pipeline Enkontrol + checkboxes de documentos ==')
|
|
await asegurarEtiquetas(b.apartado, 'Estado', ETIQUETAS_APARTADO_ESTADO)
|
|
for (const titulo of ['CURP', 'RFC', 'Comprobante']) {
|
|
await asegurarColumna(b.apartado, titulo, 'checkbox')
|
|
}
|
|
|
|
console.log('== Contactos: perfil demográfico ==')
|
|
for (const col of PERFIL_CONTACTOS) {
|
|
await asegurarColumna(b.contactos, col.titulo, col.tipo)
|
|
if (col.labels) await asegurarEtiquetas(b.contactos, col.titulo, col.labels)
|
|
}
|
|
|
|
console.log('== Tablero Motivos No Compra ==')
|
|
let motivosId = b.motivos
|
|
if (motivosId === '') {
|
|
motivosId = await crearTablero('Motivos No Compra')
|
|
console.log(` + tablero creado: ${motivosId} → MONDAY_BOARD_MOTIVOS="${motivosId}" en .env`)
|
|
} else {
|
|
console.log(` = ya configurado: ${motivosId}`)
|
|
}
|
|
await asegurarColumna(motivosId, 'Categoria', 'status')
|
|
await asegurarEtiquetas(motivosId, 'Categoria', MOTIVOS_NO_COMPRA)
|
|
await asegurarColumna(motivosId, 'Descripcion', 'long_text')
|
|
await asegurarColumna(motivosId, 'Accion Recomendada', 'long_text')
|
|
await asegurarColumna(motivosId, 'Responsable', 'people')
|
|
|
|
console.log('== Tablero Actividades ==')
|
|
let actividadesId = b.actividades
|
|
if (actividadesId === '') {
|
|
actividadesId = await crearTablero('Actividades')
|
|
console.log(` + tablero creado: ${actividadesId} → MONDAY_BOARD_ACTIVIDADES="${actividadesId}" en .env`)
|
|
} else {
|
|
console.log(` = ya configurado: ${actividadesId}`)
|
|
}
|
|
await asegurarColumna(actividadesId, 'Tipo', 'status')
|
|
await asegurarEtiquetas(actividadesId, 'Tipo', ['Llamada', 'Reunion', 'Tarea'])
|
|
await asegurarColumna(actividadesId, 'Responsable', 'people')
|
|
await asegurarColumna(actividadesId, 'Fecha', 'date')
|
|
await asegurarColumna(actividadesId, 'Estado', 'status')
|
|
await asegurarEtiquetas(actividadesId, 'Estado', ['Abierto', 'Listo'])
|
|
// board_relation NO se puede crear vía API ("column type is not supported yet"):
|
|
// la columna "Lead" (relación al tablero Leads) debe crearse a mano en la UI.
|
|
// El servicio la usa si existe y la omite si no.
|
|
console.log(' ! crea a mano la columna "Lead" (Conectar tableros → Leads) en el tablero Actividades')
|
|
|
|
console.log('\nListo. Si se crearon tableros nuevos, copia los IDs a .env y .env.example.')
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('setup-crm falló:', err)
|
|
process.exit(1)
|
|
})
|