feat(monday): alinear Frasa-CAS con CRM real del cliente (7 mejoras)
- 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
This commit is contained in:
240
api-gateway/scripts/setup-crm.ts
Normal file
240
api-gateway/scripts/setup-crm.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* 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)
|
||||
})
|
||||
196
api-gateway/scripts/sync-desarrollos.ts
Normal file
196
api-gateway/scripts/sync-desarrollos.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Sincroniza el tablero Desarrollos (catálogo maestro, estilo CRM FRASA) con
|
||||
* los inventarios reales:
|
||||
* - DIAGONAL: apps/cotizador/src/data/unidades.ts
|
||||
* - ATELIER / ESTANZA / SENDA / SOLANA: reference/cotizadores/*.json
|
||||
*
|
||||
* Calcula (solo unidades DISPONIBLE): Precio Desde/Hasta, M2 Construccion
|
||||
* (rango), Fecha Entrega (la más próxima) y Modelos; y fija el Tipo de
|
||||
* Inmueble (DIAGONAL/ATELIER = Departamentos; el resto = Casas).
|
||||
*
|
||||
* Idempotente: crea las columnas y los items que falten; actualiza valores.
|
||||
* Uso: npm run sync:desarrollos (desde api-gateway)
|
||||
*/
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { config } from '../src/config/index.js'
|
||||
import { MondayService } from '../src/services/monday.service.js'
|
||||
|
||||
const RAIZ = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
|
||||
const monday = new MondayService()
|
||||
|
||||
const TIPO_POR_DESARROLLO: Record<string, string> = {
|
||||
DIAGONAL: 'Departamentos',
|
||||
ATELIER: 'Departamentos',
|
||||
ESTANZA: 'Casas',
|
||||
SENDA: 'Casas',
|
||||
SOLANA: 'Casas',
|
||||
}
|
||||
|
||||
interface UnidadInventario {
|
||||
codigo: string
|
||||
modelo: string
|
||||
fechaEntrega: string | null
|
||||
supConstruccion: number | null
|
||||
precioSinAcabados: number | null
|
||||
precioConAcabados: number | null
|
||||
status: string
|
||||
extras?: { precioLista?: number | null; precioPreventa?: number | null } & Record<string, unknown>
|
||||
}
|
||||
|
||||
interface StatsDesarrollo {
|
||||
nombre: string
|
||||
tipoInmueble: string
|
||||
precioDesde: number | null
|
||||
precioHasta: number | null
|
||||
m2Construccion: string // 'min - max' o ''
|
||||
fechaEntrega: string | null // la más próxima entre DISPONIBLEs
|
||||
modelos: string // únicos, separados por coma
|
||||
disponibles: number
|
||||
}
|
||||
|
||||
/** Precio de referencia: con acabados, si no sin acabados, si no lista/preventa (extras) */
|
||||
function precioDe(u: UnidadInventario): number | null {
|
||||
return u.precioConAcabados ?? u.precioSinAcabados ?? u.extras?.precioLista ?? u.extras?.precioPreventa ?? null
|
||||
}
|
||||
|
||||
export function calcularStats(nombre: string, unidades: UnidadInventario[]): StatsDesarrollo {
|
||||
const disponibles = unidades.filter((u) => u.status.toUpperCase() === 'DISPONIBLE')
|
||||
const precios = disponibles.map(precioDe).filter((p): p is number => p !== null && p > 0)
|
||||
const m2 = disponibles.map((u) => u.supConstruccion).filter((s): s is number => s !== null && s > 0)
|
||||
const fechas = disponibles
|
||||
.map((u) => u.fechaEntrega)
|
||||
.filter((f): f is string => f !== null && f !== '')
|
||||
.sort()
|
||||
const modelos = [...new Set(disponibles.map((u) => u.modelo).filter((m) => m !== ''))].sort()
|
||||
const redondeo = (n: number): number => Math.round(n * 100) / 100
|
||||
return {
|
||||
nombre,
|
||||
tipoInmueble: TIPO_POR_DESARROLLO[nombre] ?? 'Casas',
|
||||
precioDesde: precios.length > 0 ? Math.min(...precios) : null,
|
||||
precioHasta: precios.length > 0 ? Math.max(...precios) : null,
|
||||
m2Construccion: m2.length > 0 ? `${redondeo(Math.min(...m2))} - ${redondeo(Math.max(...m2))}` : '',
|
||||
fechaEntrega: fechas[0] ?? null,
|
||||
modelos: modelos.join(', '),
|
||||
disponibles: disponibles.length,
|
||||
}
|
||||
}
|
||||
|
||||
async function cargarInventarios(): Promise<Map<string, UnidadInventario[]>> {
|
||||
const mapa = new Map<string, UnidadInventario[]>()
|
||||
// DIAGONAL vive en el seed del cotizador (TS)
|
||||
const mod = (await import(
|
||||
pathToFileURL2('../../apps/cotizador/src/data/unidades.ts')
|
||||
)) as { UNIDADES_SEED: UnidadInventario[] }
|
||||
mapa.set('DIAGONAL', mod.UNIDADES_SEED)
|
||||
for (const nombre of ['ATELIER', 'ESTANZA', 'SENDA', 'SOLANA']) {
|
||||
const crudo = await readFile(path.join(RAIZ, `reference/cotizadores/${nombre}.json`), 'utf8')
|
||||
const json = JSON.parse(crudo) as { unidades: UnidadInventario[] }
|
||||
mapa.set(nombre, json.unidades)
|
||||
}
|
||||
return mapa
|
||||
}
|
||||
|
||||
/** Ruta file:// absoluta para import dinámico de TS fuera del paquete */
|
||||
function pathToFileURL2(relDesdeScript: string): string {
|
||||
const abs = path.resolve(path.dirname(fileURLToPath(import.meta.url)), relDesdeScript)
|
||||
return `file://${abs}`
|
||||
}
|
||||
|
||||
// ---------- Monday ----------
|
||||
|
||||
interface Columna {
|
||||
id: string
|
||||
title: string
|
||||
type: string
|
||||
}
|
||||
|
||||
async function asegurarColumna(boardId: string, titulo: string, tipo: string, existentes: Columna[]): Promise<void> {
|
||||
if (existentes.some((c) => c.title === titulo)) return
|
||||
await monday.graphql(
|
||||
`mutation ($boardId: ID!, $title: String!, $tipo: ColumnType!) {
|
||||
create_column(board_id: $boardId, title: $title, column_type: $tipo) { id }
|
||||
}`,
|
||||
{ boardId, title: titulo, tipo },
|
||||
)
|
||||
console.log(` + columna "${titulo}" (${tipo})`)
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const boardId = config.monday.boards.desarrollos
|
||||
|
||||
const colsData = await monday.graphql<{ boards: { columns: Columna[] }[] }>(
|
||||
`query ($ids: [ID!]) { boards(ids: $ids) { columns { id title type } } }`,
|
||||
{ ids: [boardId] },
|
||||
)
|
||||
const columnas = colsData.boards[0]?.columns ?? []
|
||||
await asegurarColumna(boardId, 'Precio Desde', 'numbers', columnas)
|
||||
await asegurarColumna(boardId, 'Precio Hasta', 'numbers', columnas)
|
||||
await asegurarColumna(boardId, 'M2 Construccion', 'text', columnas)
|
||||
await asegurarColumna(boardId, 'Tipo de Inmueble', 'dropdown', columnas)
|
||||
await asegurarColumna(boardId, 'Fecha Entrega', 'date', columnas)
|
||||
await asegurarColumna(boardId, 'Modelos', 'text', columnas)
|
||||
|
||||
// Recargar columnas tras posibles creaciones y mapear por título
|
||||
const colsFrescas = await monday.graphql<{ boards: { columns: Columna[] }[] }>(
|
||||
`query ($ids: [ID!]) { boards(ids: $ids) { columns { id title type } } }`,
|
||||
{ ids: [boardId] },
|
||||
)
|
||||
const porTitulo = new Map((colsFrescas.boards[0]?.columns ?? []).map((c) => [c.title, c]))
|
||||
|
||||
const itemsData = await monday.graphql<{ boards: { items_page: { items: { id: string; name: string }[] } }[] }>(
|
||||
`query ($ids: [ID!]) { boards(ids: $ids) { items_page(limit: 100) { items { id name } } } }`,
|
||||
{ ids: [boardId] },
|
||||
)
|
||||
const itemsPorNombre = new Map(
|
||||
(itemsData.boards[0]?.items_page.items ?? []).map((i) => [i.name.trim().toUpperCase(), i.id]),
|
||||
)
|
||||
|
||||
const inventarios = await cargarInventarios()
|
||||
for (const [nombre, unidades] of inventarios) {
|
||||
const stats = calcularStats(nombre, unidades)
|
||||
const valores: Record<string, unknown> = {}
|
||||
const poner = (titulo: string, valor: unknown): void => {
|
||||
const col = porTitulo.get(titulo)
|
||||
if (col) valores[col.id] = valor
|
||||
}
|
||||
if (stats.precioDesde !== null) poner('Precio Desde', stats.precioDesde)
|
||||
if (stats.precioHasta !== null) poner('Precio Hasta', stats.precioHasta)
|
||||
if (stats.m2Construccion !== '') poner('M2 Construccion', stats.m2Construccion)
|
||||
poner('Tipo de Inmueble', { labels: [stats.tipoInmueble] })
|
||||
if (stats.fechaEntrega !== null) poner('Fecha Entrega', { date: stats.fechaEntrega })
|
||||
if (stats.modelos !== '') poner('Modelos', stats.modelos)
|
||||
const columnValues = JSON.stringify(valores)
|
||||
|
||||
const itemId = itemsPorNombre.get(nombre)
|
||||
if (itemId === undefined) {
|
||||
await monday.graphql(
|
||||
`mutation ($boardId: ID!, $name: String!, $values: JSON!) {
|
||||
create_item(board_id: $boardId, item_name: $name, column_values: $values, create_labels_if_missing: true) { id }
|
||||
}`,
|
||||
{ boardId, name: nombre, values: columnValues },
|
||||
)
|
||||
console.log(`+ ${nombre}: item creado (${stats.disponibles} disponibles)`)
|
||||
} else {
|
||||
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: columnValues },
|
||||
)
|
||||
console.log(`= ${nombre}: actualizado (${stats.disponibles} disponibles)`)
|
||||
}
|
||||
console.log(
|
||||
` ${stats.tipoInmueble} | $${stats.precioDesde ?? '-'} - $${stats.precioHasta ?? '-'} | ` +
|
||||
`m2 ${stats.m2Construccion || '-'} | entrega ${stats.fechaEntrega ?? '-'} | modelos: ${stats.modelos || '-'}`,
|
||||
)
|
||||
}
|
||||
console.log('\nSincronización de Desarrollos completa.')
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('sync-desarrollos falló:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user