feat(purga): Monday eliminado completamente del sistema
14 servicios eliminados, 7 controllers local-only, crons locales, config sin MONDAY_*, frontend sin banners/avisos Monday. 552 tests API + 7 apps construidas.
This commit is contained in:
@@ -1,189 +0,0 @@
|
||||
/**
|
||||
* Importa los inventarios de ATELIER, ESTANZA, SENDA y SOLANA al board
|
||||
* "Unidades" de Monday, desde reference/cotizadores/{DEV}.json.
|
||||
*
|
||||
* - Idempotente: salta items cuyo nombre (código prefijado) ya existe.
|
||||
* - Crea la columna "Meses para firma" (numbers) si falta (la usa SOLANA).
|
||||
* - create_labels_if_missing: la columna status "Disponibilidad" gana las
|
||||
* etiquetas nuevas (VENDIDO, ESCRITURADO, APARTADO, BLOQUEADO).
|
||||
* - Delay entre mutaciones para cuidar el rate limit.
|
||||
*
|
||||
* Uso: npx tsx scripts/importar-unidades.ts (desde api-gateway/)
|
||||
*/
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { config } from '../src/config/index.js'
|
||||
import { MondayService } from '../src/services/monday.service.js'
|
||||
|
||||
const REF_DIR = path.resolve(process.cwd(), '..', 'reference', 'cotizadores')
|
||||
const DELAY_MS = 400
|
||||
|
||||
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
interface UnidadJson {
|
||||
codigo: string
|
||||
modelo: string | null
|
||||
fechaEntrega: string | null
|
||||
supEstacionamiento: number | null
|
||||
supConstruccion: number | null
|
||||
supExcedente: number | null
|
||||
status: string
|
||||
extras: Record<string, string | number | null>
|
||||
}
|
||||
|
||||
interface DefinicionDesarrollo {
|
||||
id: string
|
||||
archivo: string
|
||||
codigoMonday: (u: UnidadJson) => string
|
||||
precio: (u: UnidadJson) => number | null
|
||||
mesesParaFirma?: (u: UnidadJson) => number | null
|
||||
}
|
||||
|
||||
const DESARROLLOS: DefinicionDesarrollo[] = [
|
||||
{
|
||||
id: 'ATELIER',
|
||||
archivo: 'ATELIER.json',
|
||||
codigoMonday: (u) => `ATELIER-${u.codigo}`,
|
||||
precio: (u) => (typeof u.extras['precioLista'] === 'number' ? u.extras['precioLista'] : null),
|
||||
},
|
||||
{
|
||||
id: 'ESTANZA',
|
||||
archivo: 'ESTANZA.json',
|
||||
codigoMonday: (u) => `ESTANZA-${u.codigo.replace(/^CASA-/, '')}`,
|
||||
precio: (u) => (typeof u.extras['precioLista'] === 'number' ? u.extras['precioLista'] : null),
|
||||
},
|
||||
{
|
||||
id: 'SENDA',
|
||||
archivo: 'SENDA.json',
|
||||
codigoMonday: (u) => `SENDA-${u.codigo}`,
|
||||
precio: (u) => (typeof u.extras['precioPreventa'] === 'number' ? u.extras['precioPreventa'] : null),
|
||||
},
|
||||
{
|
||||
id: 'SOLANA',
|
||||
archivo: 'SOLANA.json',
|
||||
codigoMonday: (u) => `SOLANA-${u.codigo}`,
|
||||
precio: (u) => (typeof u.extras['precioPreventa'] === 'number' ? u.extras['precioPreventa'] : null),
|
||||
mesesParaFirma: (u) =>
|
||||
typeof u.extras['mesesParaFirma'] === 'number' ? u.extras['mesesParaFirma'] : null,
|
||||
},
|
||||
]
|
||||
|
||||
const svc = new MondayService()
|
||||
const boardId = config.monday.boards.unidades
|
||||
|
||||
/** Columnas del board mapeadas por título */
|
||||
async function columnasBoard(): Promise<Map<string, { id: string; title: string; type: string }>> {
|
||||
const data = await svc.graphql<{ boards: { columns: { id: string; title: string; type: string }[] }[] }>(
|
||||
`query ($ids: [ID!]) { boards(ids: $ids) { columns { id title type } } }`,
|
||||
{ ids: [boardId] },
|
||||
)
|
||||
return new Map((data.boards[0]?.columns ?? []).map((c) => [c.title, c]))
|
||||
}
|
||||
|
||||
async function asegurarColumnaMeses(): Promise<void> {
|
||||
const cols = await columnasBoard()
|
||||
if (cols.has('Meses para firma')) return
|
||||
const data = await svc.graphql<{ create_column: { id: string } }>(
|
||||
`mutation ($boardId: ID!, $title: String!) {
|
||||
create_column(board_id: $boardId, title: $title, column_type: numbers) { id }
|
||||
}`,
|
||||
{ boardId, title: 'Meses para firma' },
|
||||
)
|
||||
console.log(`Columna "Meses para firma" creada (${data.create_column.id})`)
|
||||
}
|
||||
|
||||
/** Nombres de TODOS los items del board (paginado por cursor) */
|
||||
async function itemsExistentes(): Promise<Set<string>> {
|
||||
const nombres = new Set<string>()
|
||||
let cursor: string | null = null
|
||||
do {
|
||||
const data: { boards: { items_page: { cursor: string | null; items: { name: string }[] } }[] } =
|
||||
cursor === null
|
||||
? await svc.graphql(
|
||||
`query ($ids: [ID!]) { boards(ids: $ids) { items_page(limit: 500) { cursor items { name } } } }`,
|
||||
{ ids: [boardId] },
|
||||
)
|
||||
: await svc.graphql(
|
||||
`query ($ids: [ID!], $cursor: String!) {
|
||||
boards(ids: $ids) { items_page(limit: 500, cursor: $cursor) { cursor items { name } } }
|
||||
}`,
|
||||
{ ids: [boardId], cursor },
|
||||
)
|
||||
const page = data.boards[0]?.items_page
|
||||
if (!page) break
|
||||
for (const item of page.items) nombres.add(item.name)
|
||||
cursor = page.cursor
|
||||
} while (cursor !== null)
|
||||
return nombres
|
||||
}
|
||||
|
||||
function columnValuesDe(
|
||||
def: DefinicionDesarrollo,
|
||||
u: UnidadJson,
|
||||
cols: Map<string, { id: string; title: string; type: string }>,
|
||||
): string {
|
||||
const valores: Record<string, unknown> = {}
|
||||
const poner = (titulo: string, valor: unknown) => {
|
||||
const col = cols.get(titulo)
|
||||
if (!col) throw new Error(`Columna "${titulo}" no existe en el board`)
|
||||
valores[col.id] = valor
|
||||
}
|
||||
const precio = def.precio(u)
|
||||
poner('Desarrollo', def.id)
|
||||
poner('Modelo', u.modelo ?? '')
|
||||
if (u.fechaEntrega) poner('Fecha entrega', { date: u.fechaEntrega })
|
||||
if (u.supConstruccion !== null) poner('Sup. construcción', u.supConstruccion)
|
||||
if (u.supExcedente !== null) poner('Sup. excedente', u.supExcedente)
|
||||
if (u.supEstacionamiento !== null) poner('Sup. estacionamiento', u.supEstacionamiento)
|
||||
if (precio !== null) {
|
||||
// Desarrollos de precio único: ambos precios replican el precio de lista
|
||||
poner('Precio sin acabados', precio)
|
||||
poner('Precio con acabados', precio)
|
||||
}
|
||||
poner('Disponibilidad', { label: u.status })
|
||||
const meses = def.mesesParaFirma?.(u)
|
||||
if (meses !== null && meses !== undefined) poner('Meses para firma', meses)
|
||||
return JSON.stringify(valores)
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
await asegurarColumnaMeses()
|
||||
const cols = await columnasBoard()
|
||||
const existentes = await itemsExistentes()
|
||||
console.log(`Board ${boardId}: ${existentes.size} items existentes`)
|
||||
|
||||
for (const def of DESARROLLOS) {
|
||||
const raw = JSON.parse(await readFile(path.join(REF_DIR, def.archivo), 'utf8')) as {
|
||||
unidades: UnidadJson[]
|
||||
}
|
||||
let creados = 0
|
||||
let saltados = 0
|
||||
for (const u of raw.unidades) {
|
||||
const nombre = def.codigoMonday(u)
|
||||
if (existentes.has(nombre)) {
|
||||
saltados++
|
||||
continue
|
||||
}
|
||||
const columnValues = columnValuesDe(def, u, cols)
|
||||
await svc.graphql<{ create_item: { id: string } }>(
|
||||
`mutation ($boardId: ID!, $itemName: String!, $columnValues: JSON!) {
|
||||
create_item(board_id: $boardId, item_name: $itemName, column_values: $columnValues, create_labels_if_missing: true) { id }
|
||||
}`,
|
||||
{ boardId, itemName: nombre, columnValues },
|
||||
)
|
||||
existentes.add(nombre)
|
||||
creados++
|
||||
if (creados % 25 === 0) console.log(` ${def.id}: ${creados} creados…`)
|
||||
await sleep(DELAY_MS)
|
||||
}
|
||||
console.log(`${def.id}: ${creados} creados, ${saltados} ya existían (total ${raw.unidades.length})`)
|
||||
}
|
||||
svc.invalidarCacheUnidades()
|
||||
console.log('Importación terminada.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Falló la importación:', err instanceof Error ? err.message : err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,240 +0,0 @@
|
||||
/**
|
||||
* 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)
|
||||
})
|
||||
@@ -1,196 +0,0 @@
|
||||
/**
|
||||
* 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)
|
||||
})
|
||||
@@ -1,368 +0,0 @@
|
||||
/**
|
||||
* Sincroniza la columna status "Etapa" del tablero Unidades (pipeline de 10
|
||||
* etapas: Apartado → Reserva → Vendido → Integración → En Análisis → Autorizado
|
||||
* → Notaría → Escriturado → Entrega → PostVenta) desde un JSON de etapas.
|
||||
*
|
||||
* Formatos de entrada aceptados:
|
||||
* - Simple: [{ "codigo": "ATELIER-1", "etapa": "NOTARIA" }, ...]
|
||||
* - Maestro: reference/maestro/maestro-unidades.json (codigo_nuestro +
|
||||
* etapa_maestro + mapeo.confianza; solo se usan confianza
|
||||
* alta/media). Es el default.
|
||||
*
|
||||
* Idempotente: solo escribe en Monday si la etapa cambió.
|
||||
*
|
||||
* Traspasos de departamento (dedup en AutomatizacionState, clave
|
||||
* `traspaso:<codigo>:<etapa>`):
|
||||
* - al llegar a Vendido → actividad "Venta de {codigo} lista para Escrituración"
|
||||
* - al llegar a Escriturado → actividad "{codigo} escriturado: pasa a SAC"
|
||||
* Una etapa alta implica las anteriores (p. ej. Escriturado cubre Vendido).
|
||||
*
|
||||
* Flags:
|
||||
* --archivo <path> JSON de etapas (default: reference/maestro/maestro-unidades.json)
|
||||
* --dry-run no escribe en Monday ni en la base; solo reporta
|
||||
* --semilla registra el dedup de traspasos SIN crear actividades.
|
||||
* Usarlo en la PRIMERA corrida (carga histórica): las ~50
|
||||
* unidades ya vendidas/escrituradas no deben generar
|
||||
* actividades retroactivas. Las corridas siguientes (sin
|
||||
* --semilla) solo crean actividad en transiciones nuevas.
|
||||
*
|
||||
* Uso: npm run sync:etapas -- [--archivo <path>] [--dry-run] [--semilla]
|
||||
*/
|
||||
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'
|
||||
import { ETAPAS_PIPELINE, normalizarEtapa, rangoEtapa, TRASPASOS, type EtapaPipeline } from '../src/services/etapas.js'
|
||||
import { prisma } from '../src/services/prisma.js'
|
||||
|
||||
const RAIZ = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const ARCHIVO_DEFAULT = path.join(RAIZ, 'reference/maestro/maestro-unidades.json')
|
||||
const PAUSA_MS = 400 // entre mutaciones, para no pegarle al rate limit de Monday
|
||||
|
||||
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
// ---------- Argumentos ----------
|
||||
|
||||
interface Opciones {
|
||||
archivo: string
|
||||
dryRun: boolean
|
||||
semilla: boolean
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): Opciones {
|
||||
const ops: Opciones = { archivo: ARCHIVO_DEFAULT, dryRun: false, semilla: false }
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i]
|
||||
if (arg === '--dry-run') ops.dryRun = true
|
||||
else if (arg === '--semilla') ops.semilla = true
|
||||
else if (arg === '--archivo') {
|
||||
const valor = argv[++i]
|
||||
if (!valor) throw new Error('--archivo requiere una ruta')
|
||||
ops.archivo = path.resolve(process.cwd(), valor)
|
||||
} else {
|
||||
throw new Error(`Argumento desconocido: ${arg}`)
|
||||
}
|
||||
}
|
||||
return ops
|
||||
}
|
||||
|
||||
// ---------- Entrada ----------
|
||||
|
||||
interface EtapaUnidad {
|
||||
codigo: string
|
||||
etapa: EtapaPipeline // ya normalizada al label oficial
|
||||
}
|
||||
|
||||
interface EntradaMaestro {
|
||||
codigo_nuestro?: string | null
|
||||
etapa_maestro?: string | null
|
||||
mapeo?: { confianza?: string }
|
||||
}
|
||||
|
||||
interface EntradaSimple {
|
||||
codigo?: string
|
||||
etapa?: string | null
|
||||
}
|
||||
|
||||
/** Lee el JSON de etapas y lo deja en pares {codigo, etapa oficial}; avisa lo que descarta */
|
||||
async function cargarEtapas(archivo: string): Promise<EtapaUnidad[]> {
|
||||
const json = JSON.parse(await readFile(archivo, 'utf8')) as unknown[]
|
||||
if (!Array.isArray(json)) throw new Error(`${archivo} no es un array JSON`)
|
||||
const esMaestro = json.some((e) => typeof e === 'object' && e !== null && 'codigo_nuestro' in e)
|
||||
const out: EtapaUnidad[] = []
|
||||
let descartadas = 0
|
||||
for (const cruda of json) {
|
||||
if (typeof cruda !== 'object' || cruda === null) {
|
||||
descartadas++
|
||||
continue
|
||||
}
|
||||
let codigo: string | undefined
|
||||
let etapaCruda: string | null | undefined
|
||||
if (esMaestro) {
|
||||
const e = cruda as EntradaMaestro
|
||||
const confianza = e.mapeo?.confianza ?? 'ninguna'
|
||||
if (confianza !== 'alta' && confianza !== 'media') {
|
||||
descartadas++
|
||||
continue
|
||||
}
|
||||
codigo = e.codigo_nuestro ?? undefined
|
||||
etapaCruda = e.etapa_maestro
|
||||
} else {
|
||||
const e = cruda as EntradaSimple
|
||||
codigo = e.codigo
|
||||
etapaCruda = e.etapa
|
||||
}
|
||||
if (!codigo) {
|
||||
descartadas++
|
||||
continue
|
||||
}
|
||||
const etapa = normalizarEtapa(etapaCruda)
|
||||
if (etapa === null) {
|
||||
// DISPONIBLE / vacío / desconocido: la unidad no tiene etapa del pipeline;
|
||||
// no se escribe nada (nunca se borra una etapa existente desde este script)
|
||||
descartadas++
|
||||
continue
|
||||
}
|
||||
out.push({ codigo, etapa })
|
||||
}
|
||||
if (descartadas > 0) console.log(` (${descartadas} entradas descartadas: sin match, sin etapa o confianza baja)`)
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------- Monday: columna Etapa ----------
|
||||
|
||||
const monday = new MondayService()
|
||||
|
||||
interface ColumnaBoard {
|
||||
id: string
|
||||
title: string
|
||||
type: string
|
||||
settings_str?: string
|
||||
}
|
||||
|
||||
async function columnasDe(boardId: string): Promise<ColumnaBoard[]> {
|
||||
const data = await monday.graphql<{ boards: { columns: ColumnaBoard[] }[] }>(
|
||||
`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 (de settings_str: {"labels":{"0":"Apartado",...}}) */
|
||||
function labelsDe(col: ColumnaBoard): Set<string> {
|
||||
try {
|
||||
const settings = JSON.parse(col.settings_str ?? '{}') as { labels?: Record<string, string> }
|
||||
return new Set(Object.values(settings.labels ?? {}))
|
||||
} catch {
|
||||
return new Set()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Garantiza que exista la columna status "Etapa" con los 10 labels oficiales.
|
||||
* Monday no crea labels vía defaults de create_column en este workspace, así
|
||||
* que los labels faltantes se siembran escribiéndolos en un item scratch con
|
||||
* create_labels_if_missing:true y borrando el item al final.
|
||||
*/
|
||||
async function asegurarColumnaEtapa(boardId: string, dryRun: boolean): Promise<void> {
|
||||
let col = (await columnasDe(boardId)).find((c) => c.title === 'Etapa')
|
||||
if (col && col.type !== 'status') throw new Error(`La columna "Etapa" existe pero es tipo ${col.type}, no status`)
|
||||
if (!col) {
|
||||
if (dryRun) {
|
||||
console.log(' [dry-run] se crearía la columna "Etapa" (status) con los 10 labels')
|
||||
return
|
||||
}
|
||||
await monday.graphql(
|
||||
`mutation ($boardId: ID!, $title: String!, $tipo: ColumnType!) {
|
||||
create_column(board_id: $boardId, title: $title, column_type: $tipo) { id }
|
||||
}`,
|
||||
{ boardId, title: 'Etapa', tipo: 'status' },
|
||||
)
|
||||
console.log(' + columna "Etapa" creada')
|
||||
col = (await columnasDe(boardId)).find((c) => c.title === 'Etapa')
|
||||
if (!col) throw new Error('No se encontró la columna "Etapa" recién creada')
|
||||
}
|
||||
|
||||
const faltantes = ETAPAS_PIPELINE.filter((l) => !labelsDe(col).has(l))
|
||||
if (faltantes.length === 0) {
|
||||
console.log(' columna "Etapa" ya tiene los 10 labels')
|
||||
return
|
||||
}
|
||||
if (dryRun) {
|
||||
console.log(` [dry-run] faltarían labels en "Etapa": ${faltantes.join(', ')}`)
|
||||
return
|
||||
}
|
||||
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: 'tmp-sync-etapas (borrar)' },
|
||||
)
|
||||
const scratchId = creado.create_item.id
|
||||
try {
|
||||
for (const label of faltantes) {
|
||||
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: scratchId, values: JSON.stringify({ [col.id]: { label } }) },
|
||||
)
|
||||
await sleep(PAUSA_MS)
|
||||
}
|
||||
} finally {
|
||||
await monday.graphql(`mutation ($itemId: ID!) { delete_item(item_id: $itemId) { id } }`, { itemId: scratchId })
|
||||
}
|
||||
console.log(` + labels sembrados en "Etapa": ${faltantes.join(', ')}`)
|
||||
}
|
||||
|
||||
// ---------- Traspasos de departamento ----------
|
||||
|
||||
const claveDedup = (codigo: string, etapa: string): string => `traspaso:${codigo}:${etapa}`
|
||||
|
||||
const nombreActividad = (codigo: string, etapa: EtapaPipeline): string =>
|
||||
etapa === 'Vendido' ? `Venta de ${codigo} lista para Escrituración` : `${codigo} escriturado: pasa a SAC`
|
||||
|
||||
const fechaHoy = (): string => new Date().toISOString().slice(0, 10)
|
||||
|
||||
interface ResumenTraspasos {
|
||||
actividadesCreadas: number
|
||||
registradasSemilla: number
|
||||
yaRegistradas: number
|
||||
errores: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Para la etapa vigente de una unidad, cubre los traspasos (Vendido →
|
||||
* Escrituración, Escriturado → SAC) con rango <= la etapa, con dedup persistente.
|
||||
*/
|
||||
async function cubrirTraspasos(
|
||||
codigo: string,
|
||||
etapaVigente: EtapaPipeline,
|
||||
ops: Opciones,
|
||||
resumen: ResumenTraspasos,
|
||||
): Promise<void> {
|
||||
const rango = rangoEtapa(etapaVigente)
|
||||
for (const [etapaTraspaso, depto] of Object.entries(TRASPASOS) as [EtapaPipeline, string][]) {
|
||||
if (rangoEtapa(etapaTraspaso) > rango) continue
|
||||
const clave = claveDedup(codigo, etapaTraspaso)
|
||||
const ya = await prisma.automatizacionState.findUnique({ where: { clave } })
|
||||
if (ya) {
|
||||
resumen.yaRegistradas++
|
||||
continue
|
||||
}
|
||||
if (ops.dryRun) {
|
||||
console.log(` [dry-run] traspaso pendiente: ${codigo} → ${depto} (${ops.semilla ? 'se registraría sin actividad' : 'crearía actividad'})`)
|
||||
continue
|
||||
}
|
||||
if (ops.semilla) {
|
||||
await prisma.automatizacionState.upsert({
|
||||
where: { clave },
|
||||
update: { valor: 'semilla' },
|
||||
create: { clave, valor: 'semilla' },
|
||||
})
|
||||
resumen.registradasSemilla++
|
||||
continue
|
||||
}
|
||||
try {
|
||||
await monday.crearActividad({
|
||||
nombre: nombreActividad(codigo, etapaTraspaso),
|
||||
tipoLabel: 'Tarea',
|
||||
fecha: fechaHoy(),
|
||||
estadoLabel: 'Abierto',
|
||||
responsableId: config.automatizaciones.coordinadorUserId > 0 ? config.automatizaciones.coordinadorUserId : undefined,
|
||||
})
|
||||
await prisma.automatizacionState.upsert({
|
||||
where: { clave },
|
||||
update: { valor: 'actividad-creada' },
|
||||
create: { clave, valor: 'actividad-creada' },
|
||||
})
|
||||
resumen.actividadesCreadas++
|
||||
console.log(` + actividad: ${nombreActividad(codigo, etapaTraspaso)}`)
|
||||
await sleep(PAUSA_MS)
|
||||
} catch (err) {
|
||||
// Sin dedup: la próxima corrida reintenta la actividad
|
||||
resumen.errores++
|
||||
console.error(` ! no se pudo crear la actividad de traspaso de ${codigo}:`, err instanceof Error ? err.message : err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Main ----------
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const ops = parseArgs(process.argv.slice(2))
|
||||
const boardId = config.monday.boards.unidades
|
||||
console.log(`sync-etapas: ${ops.archivo}${ops.dryRun ? ' [dry-run]' : ''}${ops.semilla ? ' [semilla]' : ''}`)
|
||||
|
||||
await asegurarColumnaEtapa(boardId, ops.dryRun)
|
||||
|
||||
const entradas = await cargarEtapas(ops.archivo)
|
||||
console.log(` ${entradas.length} unidades con etapa en el archivo`)
|
||||
|
||||
const unidades = await monday.getUnidades()
|
||||
const porCodigo = new Map(unidades.map((u) => [u.codigo, u]))
|
||||
|
||||
let escritas = 0
|
||||
let sinCambio = 0
|
||||
const noEncontradas: string[] = []
|
||||
const resumenTraspasos: ResumenTraspasos = { actividadesCreadas: 0, registradasSemilla: 0, yaRegistradas: 0, errores: 0 }
|
||||
|
||||
for (const { codigo, etapa } of entradas) {
|
||||
const unidad = porCodigo.get(codigo)
|
||||
if (!unidad) {
|
||||
noEncontradas.push(codigo)
|
||||
continue
|
||||
}
|
||||
const etapaActual = normalizarEtapa(unidad.etapa)
|
||||
if (etapaActual !== etapa) {
|
||||
if (ops.dryRun) {
|
||||
console.log(` [dry-run] ${codigo}: ${unidad.etapa ?? '(vacío)'} → ${etapa}`)
|
||||
} else {
|
||||
const itemId = await monday.itemIdDeUnidad(codigo)
|
||||
if (!itemId) {
|
||||
noEncontradas.push(codigo)
|
||||
continue
|
||||
}
|
||||
await monday.cambiarValorColumna(boardId, itemId, 'Etapa', etapa)
|
||||
escritas++
|
||||
console.log(` = ${codigo}: ${unidad.etapa ?? '(vacío)'} → ${etapa}`)
|
||||
await sleep(PAUSA_MS)
|
||||
}
|
||||
} else {
|
||||
sinCambio++
|
||||
}
|
||||
await cubrirTraspasos(codigo, etapa, ops, resumenTraspasos)
|
||||
}
|
||||
|
||||
console.log('\nResumen:')
|
||||
console.log(` etapas escritas: ${escritas}${ops.dryRun ? ' (dry-run: 0 reales)' : ''}`)
|
||||
console.log(` sin cambio: ${sinCambio}`)
|
||||
console.log(` no encontradas: ${noEncontradas.length}${noEncontradas.length > 0 ? ` (${noEncontradas.join(', ')})` : ''}`)
|
||||
console.log(` actividades creadas: ${resumenTraspasos.actividadesCreadas}`)
|
||||
console.log(` traspasos sembrados: ${resumenTraspasos.registradasSemilla}`)
|
||||
console.log(` traspasos ya dedup: ${resumenTraspasos.yaRegistradas}`)
|
||||
if (resumenTraspasos.errores > 0) console.log(` ERRORES en actividades: ${resumenTraspasos.errores}`)
|
||||
|
||||
// Conteo final por etapa (lectura fresca del board)
|
||||
if (!ops.dryRun) {
|
||||
monday.invalidarCacheUnidades()
|
||||
const frescas = await monday.getUnidades()
|
||||
const conteo = new Map<string, number>()
|
||||
for (const u of frescas) {
|
||||
const e = normalizarEtapa(u.etapa)
|
||||
if (e) conteo.set(e, (conteo.get(e) ?? 0) + 1)
|
||||
}
|
||||
console.log('\nUnidades por etapa (board):')
|
||||
for (const etapa of ETAPAS_PIPELINE) {
|
||||
const n = conteo.get(etapa) ?? 0
|
||||
if (n > 0) console.log(` ${etapa}: ${n}`)
|
||||
}
|
||||
const sinEtapa = frescas.length - [...conteo.values()].reduce((a, b) => a + b, 0)
|
||||
console.log(` (sin etapa): ${sinEtapa}`)
|
||||
}
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('sync-etapas falló:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user