/** * 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 = { 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 } 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> { const mapa = new Map() // 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 { 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 { 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 = {} 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) })