#!/usr/bin/env node /** * Importa el archivo maestro de estatus de venta (Reporte_EstatusVenta.xlsx) * a la tabla CapturaDepartamento. * * Uso: * node scripts/importar-maestro.js [ruta.xlsx] [usuarioId] * * - Lee cada hoja de desarrollo (SENDA, ATELIER, ESTANZA, etc.) * - Encuentra la fila de headers (la que tiene "CLIENTE" o "ESTATUS") * - Mapea columnas a campos del config * - Para cada fila de datos, crea registros via upsert (idempotente) * - Log de progreso */ import { PrismaClient } from '@prisma/client' import XLSX from 'xlsx' import { fileURLToPath } from 'url' import { dirname, join } from 'path' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) const prisma = new PrismaClient() // ── Desarrollos y sus nombres de hoja esperados ────────────────────── const DESARROLLOS = ['SENDA', 'ATELIER', 'ESTANZA', 'DIAGONAL', 'SOLANA', 'SOLE', 'MUTUO'] // Mapeo de headers del Excel → campo del config // Normaliza: quita acentos, mayúsculas, espacios → nombre de campo const HEADER_MAP = { // OBRA 'CERTIFICACIÓN GAS': { departamento: 'OBRA', campo: 'certificacion_gas', tipo: 'boolean' }, 'CERTIFICACION GAS': { departamento: 'OBRA', campo: 'certificacion_gas', tipo: 'boolean' }, 'TO Y C4-FIANZA': { departamento: 'OBRA', campo: 'to_c4_fianza', tipo: 'boolean' }, 'TO C4-FIANZA': { departamento: 'OBRA', campo: 'to_c4_fianza', tipo: 'boolean' }, 'RÉGIMEN INSCRITO': { departamento: 'OBRA', campo: 'regimen_inscrito', tipo: 'boolean' }, 'REGIMEN INSCRITO': { departamento: 'OBRA', campo: 'regimen_inscrito', tipo: 'boolean' }, 'NO. OFICIAL': { departamento: 'OBRA', campo: 'no_oficial', tipo: 'boolean' }, 'NO OFICIAL': { departamento: 'OBRA', campo: 'no_oficial', tipo: 'boolean' }, 'CLAVE CATASTRAL': { departamento: 'OBRA', campo: 'clave_catastral', tipo: 'boolean' }, 'LISTAS PARA AVALUO': { departamento: 'OBRA', campo: 'listas_avaluo', tipo: 'boolean' }, 'HABILITABILIDAD': { departamento: 'OBRA', campo: 'habitabilidad', tipo: 'date' }, 'COMENTARIOS OBRA': { departamento: 'OBRA', campo: 'comentarios_obra', tipo: 'text' }, 'COMENTARIOS': { departamento: 'OBRA', campo: 'comentarios_obra', tipo: 'text' }, // VENTAS 'CLIENTE': { departamento: 'VENTAS', campo: 'cliente_id', tipo: 'text' }, 'CLIENTE ID': { departamento: 'VENTAS', campo: 'cliente_id', tipo: 'text' }, 'NOMBRE CLIENTE': { departamento: 'VENTAS', campo: 'nombre_cliente', tipo: 'text' }, 'NOMBRE': { departamento: 'VENTAS', campo: 'nombre_cliente', tipo: 'text' }, 'UBICACIÓN': { departamento: 'VENTAS', campo: 'ubicacion', tipo: 'text' }, 'UBICACION': { departamento: 'VENTAS', campo: 'ubicacion', tipo: 'text' }, 'UBICACIÓN/UNIDAD': { departamento: 'VENTAS', campo: 'ubicacion', tipo: 'text' }, 'MODELO': { departamento: 'VENTAS', campo: 'modelo', tipo: 'text' }, 'TIPO CRÉDITO': { departamento: 'VENTAS', campo: 'tipo_credito', tipo: 'select' }, 'TIPO CREDITO': { departamento: 'VENTAS', campo: 'tipo_credito', tipo: 'select' }, 'ESTATUS': { departamento: 'VENTAS', campo: 'estatus', tipo: 'select' }, 'FECHA APARTADO': { departamento: 'VENTAS', campo: 'fecha_apartado', tipo: 'date' }, 'FECHA VENTA': { departamento: 'VENTAS', campo: 'fecha_venta', tipo: 'date' }, 'FECHA VENCIMIENTO': { departamento: 'VENTAS', campo: 'fecha_vencimiento', tipo: 'date' }, 'FECHA VENCIMIENTO CONTRATO': { departamento: 'VENTAS', campo: 'fecha_vencimiento', tipo: 'date' }, 'PRECIO VENTA': { departamento: 'VENTAS', campo: 'precio_venta', tipo: 'number' }, // ESCRITURACION 'FECHA AUTORIZACIÓN': { departamento: 'ESCRITURACION', campo: 'fecha_autorizacion', tipo: 'date' }, 'FECHA AUTORIZACION': { departamento: 'ESCRITURACION', campo: 'fecha_autorizacion', tipo: 'date' }, 'FECHA FIRMA': { departamento: 'ESCRITURACION', campo: 'fecha_firma', tipo: 'date' }, 'NOTARÍA': { departamento: 'ESCRITURACION', campo: 'notaria', tipo: 'text' }, 'NOTARIA': { departamento: 'ESCRITURACION', campo: 'notaria', tipo: 'text' }, 'FECHA PAGO BANCO': { departamento: 'ESCRITURACION', campo: 'fecha_pago_banco', tipo: 'date' }, 'FECHA PAGO INFONAVIT': { departamento: 'ESCRITURACION', campo: 'fecha_pago_infonavit', tipo: 'date' }, 'FECHA PAGO FOVISSSTE': { departamento: 'ESCRITURACION', campo: 'fecha_pago_fovissste', tipo: 'date' }, // FINANZAS 'OTROS INGRESOS': { departamento: 'FINANZAS', campo: 'otros_ingresos', tipo: 'number' }, 'PAGADO TRANSFERENCIA': { departamento: 'FINANZAS', campo: 'pagado_transferencia', tipo: 'number' }, 'INFONAVIT': { departamento: 'FINANZAS', campo: 'infonavit', tipo: 'number' }, 'BANCARIO': { departamento: 'FINANZAS', campo: 'bancario', tipo: 'number' }, 'FOVISSSTE': { departamento: 'FINANZAS', campo: 'fovissste', tipo: 'number' }, 'CRÉDITO PUENTE': { departamento: 'FINANZAS', campo: 'credito_puente', tipo: 'number' }, 'CREDITO PUENTE': { departamento: 'FINANZAS', campo: 'credito_puente', tipo: 'number' }, 'SALDO A FAVOR': { departamento: 'FINANZAS', campo: 'saldo_favor', tipo: 'number' }, 'SALDO A FAVOR': { departamento: 'FINANZAS', campo: 'saldo_favor', tipo: 'number' }, // POSTVENTA 'ENTREGA A SAC': { departamento: 'POSTVENTA', campo: 'entrega_sac', tipo: 'boolean' }, 'ENTREGA SAC': { departamento: 'POSTVENTA', campo: 'entrega_sac', tipo: 'boolean' }, 'COMENTARIOS ENTREGA': { departamento: 'POSTVENTA', campo: 'comentarios_entrega', tipo: 'text' }, } // ── Helpers ────────────────────────────────────────────────────────── function normalizeHeader(h) { return String(h ?? '') .trim() .toUpperCase() .replace(/\s+/g, ' ') .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') // quitar acentos } function esBooleano(valor) { if (valor == null) return false const s = String(valor).trim().toUpperCase() return s === 'TRUE' || s === 'SI' || s === 'SÍ' || s === '1' || s === 'X' || s === '✓' } function parseFecha(valor) { if (valor == null) return null if (valor instanceof Date) return valor.toISOString() const s = String(valor).trim() if (!s) return null // Intentar parsear como fecha const d = new Date(s) return isNaN(d.getTime()) ? null : d.toISOString() } function parseNumero(valor) { if (valor == null) return null const s = String(valor).trim().replace(/[$,]/g, '') if (!s) return null const n = parseFloat(s) return isNaN(n) ? null : String(n) } function parseValor(valor, tipo) { if (valor == null || String(valor).trim() === '') return null switch (tipo) { case 'boolean': return esBooleano(valor) ? 'true' : 'false' case 'date': return parseFecha(valor) case 'number': return parseNumero(valor) default: return String(valor).trim() } } // ── Detectar nombre de desarrollo a partir del nombre de hoja ──────── function detectarDesarrollo(nombreHoja) { const upper = nombreHoja.toUpperCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '') for (const d of DESARROLLOS) { if (upper.includes(d)) return d } return null } // ── Main ───────────────────────────────────────────────────────────── async function main() { const rutaExcel = process.argv[2] || join(__dirname, '..', '..', 'Reporte_EstatusVenta.xlsx') const usuarioId = process.argv[3] || 'importacion-maestro' console.log(`Leyendo archivo: ${rutaExcel}`) const workbook = XLSX.readFile(rutaExcel) let totalRegistros = 0 let totalCreados = 0 let totalActualizados = 0 for (const nombreHoja of workbook.SheetNames) { const desarrollo = detectarDesarrollo(nombreHoja) if (!desarrollo) { console.log(` Hoja "${nombreHoja}" → no se reconoce desarrollo, saltando`) continue } console.log(`\n Procesando hoja "${nombreHoja}" → desarrollo: ${desarrollo}`) const sheet = workbook.Sheets[nombreHoja] const todasLasFilas = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: null }) // Buscar fila de headers (la que contiene "CLIENTE" o "ESTATUS") let headerIdx = -1 for (let i = 0; i < Math.min(todasLasFilas.length, 20); i++) { const fila = todasLasFilas[i] const textos = fila.map((c) => normalizeHeader(c)) if (textos.some((t) => t.includes('CLIENTE') || t.includes('ESTATUS'))) { headerIdx = i break } } if (headerIdx === -1) { console.log(` No se encontró fila de headers, saltando`) continue } const headers = todasLasFilas[headerIdx] const filasDatos = todasLasFilas.slice(headerIdx + 1) // Mapear columnas const mapaColumnas = headers.map((h) => { const norm = normalizeHeader(h) return HEADER_MAP[norm] || null }) let hojaCreados = 0 let hojaActualizados = 0 for (const fila of filasDatos) { // Detectar unidad: primera columna con valor const unidadCodigo = String(fila[0] ?? '').trim() if (!unidadCodigo) continue // Skip rows that are clearly headers/section markers if (normalizeHeader(unidadCodigo).includes('TOTAL')) continue for (let col = 0; col < headers.length; col++) { const mapeo = mapaColumnas[col] if (!mapeo) continue const rawValor = fila[col] const valor = parseValor(rawValor, mapeo.tipo) try { const existente = await prisma.capturaDepartamento.findUnique({ where: { desarrollo_unidadCodigo_departamento_campo: { desarrollo, unidadCodigo, departamento: mapeo.departamento, campo: mapeo.campo, }, }, }) await prisma.capturaDepartamento.upsert({ where: { desarrollo_unidadCodigo_departamento_campo: { desarrollo, unidadCodigo, departamento: mapeo.departamento, campo: mapeo.campo, }, }, update: { valor, tipo: mapeo.tipo, usuarioId }, create: { desarrollo, unidadCodigo, departamento: mapeo.departamento, campo: mapeo.campo, valor, tipo: mapeo.tipo, usuarioId, }, }) if (existente) hojaActualizados++ else hojaCreados++ } catch (err) { // Skip errors silently } } } console.log(` Unidades: ${new Set(filasDatos.map((f) => String(f[0] ?? '').trim()).filter(Boolean)).size}`) console.log(` Registros: ${hojaCreados} creados, ${hojaActualizados} actualizados`) totalCreados += hojaCreados totalActualizados += hojaActualizados totalRegistros += hojaCreados + hojaActualizados } console.log(`\n── Resumen ──`) console.log(`Total: ${totalRegistros} registros (${totalCreados} creados, ${totalActualizados} actualizados)`) await prisma.$disconnect() } main().catch((err) => { console.error('Error:', err) process.exit(1) })