/** * 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 { 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) } } catch { // settings no parseable: sin labels conocidos } return [] } async function asegurarColumna(boardId: string, titulo: string, tipo: string, defaults?: unknown): Promise { 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 { 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 { 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 { 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) })