fix(captura): reimport del maestro con mapeo explicito por hoja (unidades canonicas, fechas YYYY-MM-DD)
This commit is contained in:
416
api-gateway/scripts/importar-captura-maestro.js
Normal file
416
api-gateway/scripts/importar-captura-maestro.js
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Reimporta el maestro de estatus de venta (/root/Reporte_EstatusVenta.xlsx)
|
||||||
|
* a la tabla CapturaDepartamento, con mapeo EXPLÍCITO por hoja.
|
||||||
|
*
|
||||||
|
* Uso:
|
||||||
|
* node scripts/importar-captura-maestro.js [--dry] [ruta.xlsx]
|
||||||
|
*
|
||||||
|
* Reemplaza a importar-maestro.js (mapeo difuso que dejó campos sin importar,
|
||||||
|
* unidades colapsadas y fechas mal parseadas).
|
||||||
|
*
|
||||||
|
* Reglas:
|
||||||
|
* - Limpia TODA CapturaDepartamento antes de insertar (salvo --dry).
|
||||||
|
* - Headers por nombre normalizado (acentos/espacios/puntos); primera
|
||||||
|
* aparición gana si un campo se repite en la misma hoja.
|
||||||
|
* - Fechas → 'YYYY-MM-DD' (lo que espera el input date de la UI).
|
||||||
|
* - Celdas de OBRA: fecha → campo `_fecha`; True/False → checkbox; 'n/a' → nada.
|
||||||
|
* - Unidades: formato canónico del cotizador
|
||||||
|
* (SENDA-MZA-LOTE-DEPTO, U-101, ATELIER-1, ESTANZA-1A, SOLANA-1A/B...).
|
||||||
|
* - Las hojas DG se procesan al final y sus valores prevalecen.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
import XLSX from 'xlsx'
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import { dirname, join } from 'path'
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
// ── .env ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if (!process.env.DATABASE_URL) {
|
||||||
|
try {
|
||||||
|
const env = readFileSync(join(__dirname, '..', '.env'), 'utf8')
|
||||||
|
const linea = env.split('\n').find((l) => l.startsWith('DATABASE_URL='))
|
||||||
|
if (linea) process.env.DATABASE_URL = linea.slice('DATABASE_URL='.length).trim().replace(/^"|"$/g, '')
|
||||||
|
} catch { /* usar el que venga del entorno */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
|
const DRY = process.argv.includes('--dry')
|
||||||
|
const RUTA = process.argv.slice(2).find((a) => !a.startsWith('--')) || '/root/Reporte_EstatusVenta.xlsx'
|
||||||
|
const USUARIO = 'importacion-captura-v2'
|
||||||
|
|
||||||
|
// ── Normalización de headers ─────────────────────────────────────────
|
||||||
|
|
||||||
|
function norm(h) {
|
||||||
|
return String(h ?? '')
|
||||||
|
.trim()
|
||||||
|
.toUpperCase()
|
||||||
|
.replace(/\./g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header normalizado → campo de captura
|
||||||
|
const FIELD_MAP = {
|
||||||
|
// OBRA (checkbox; si la celda trae fecha se escribe `${campo}_fecha`)
|
||||||
|
'CERTIFICACION GAS': { dept: 'OBRA', campo: 'certificacion_gas', tipo: 'boolean', obra: true },
|
||||||
|
'TO Y C4-FIANZA': { dept: 'OBRA', campo: 'to_c4_fianza', tipo: 'boolean', obra: true },
|
||||||
|
'TO Y C4 FIANZA': { dept: 'OBRA', campo: 'to_c4_fianza', tipo: 'boolean', obra: true },
|
||||||
|
'REGIMEN INSCRITO': { dept: 'OBRA', campo: 'regimen_inscrito', tipo: 'boolean', obra: true },
|
||||||
|
'NO OFICIAL': { dept: 'OBRA', campo: 'no_oficial', tipo: 'boolean', obra: true },
|
||||||
|
'CLAVE CATASTRAL': { dept: 'OBRA', campo: 'clave_catastral', tipo: 'boolean', obra: true },
|
||||||
|
'LISTAS PARA AVALUO': { dept: 'OBRA', campo: 'listas_avaluo', tipo: 'boolean', obra: true },
|
||||||
|
'LISTA PARA AVALUO': { dept: 'OBRA', campo: 'listas_avaluo', tipo: 'boolean', obra: true },
|
||||||
|
'HABITABILIDAD A PARTIR DE': { dept: 'OBRA', campo: 'habitabilidad', tipo: 'date' },
|
||||||
|
'HABILITABILIDAD': { dept: 'OBRA', campo: 'habitabilidad', tipo: 'date' },
|
||||||
|
'COMENTARIOS OBRA': { dept: 'OBRA', campo: 'comentarios_obra', tipo: 'text' },
|
||||||
|
|
||||||
|
// VENTAS
|
||||||
|
'CLIENTE': { dept: 'VENTAS', campo: 'cliente_id', tipo: 'text' },
|
||||||
|
'NOMBRE': { dept: 'VENTAS', campo: 'nombre_cliente', tipo: 'text' },
|
||||||
|
'UBICACION': { dept: 'VENTAS', campo: 'ubicacion', tipo: 'text' },
|
||||||
|
'MODELO': { dept: 'VENTAS', campo: 'modelo', tipo: 'text' },
|
||||||
|
'TIPO DE CREDITO': { dept: 'VENTAS', campo: 'tipo_credito', tipo: 'text' },
|
||||||
|
'ESTATUS': { dept: 'VENTAS', campo: 'estatus', tipo: 'text' },
|
||||||
|
'FECHA DE APARTADO': { dept: 'VENTAS', campo: 'fecha_apartado', tipo: 'date' },
|
||||||
|
'FECHA DE VENTA': { dept: 'VENTAS', campo: 'fecha_venta', tipo: 'date' },
|
||||||
|
'FECHA VENCIMIENTO DE CONTRATO': { dept: 'VENTAS', campo: 'fecha_vencimiento', tipo: 'date' },
|
||||||
|
'FECHA VENCIMIENTO': { dept: 'VENTAS', campo: 'fecha_vencimiento', tipo: 'date' },
|
||||||
|
'PRECIO DE VENTA': { dept: 'VENTAS', campo: 'precio_venta', tipo: 'number' },
|
||||||
|
|
||||||
|
// ESCRITURACION
|
||||||
|
'FECHA DE AUTORIZACION': { dept: 'ESCRITURACION', campo: 'fecha_autorizacion', tipo: 'date' },
|
||||||
|
'FECHA AUTORIZACION': { dept: 'ESCRITURACION', campo: 'fecha_autorizacion', tipo: 'date' },
|
||||||
|
'FECHA FIRMA': { dept: 'ESCRITURACION', campo: 'fecha_firma', tipo: 'date' },
|
||||||
|
'NOTARIA': { dept: 'ESCRITURACION', campo: 'notaria', tipo: 'text' },
|
||||||
|
'FECHA DE PAGO BANCO': { dept: 'ESCRITURACION', campo: 'fecha_pago_banco', tipo: 'date' },
|
||||||
|
'FECHA DE PAGO INFONAVIT': { dept: 'ESCRITURACION', campo: 'fecha_pago_infonavit', tipo: 'date' },
|
||||||
|
'FECHA DE PAGO FOVISSSTE': { dept: 'ESCRITURACION', campo: 'fecha_pago_fovissste', tipo: 'date' },
|
||||||
|
// Hojas DG: fechas de obra por unidad
|
||||||
|
'FECHA DE INICIO POR OBRA': { dept: 'ESCRITURACION', campo: 'inicio_obra_fecha', tipo: 'date' },
|
||||||
|
'FECHA DE ENTREGA POR OBRA': { dept: 'ESCRITURACION', campo: 'entrega_programada_fecha', tipo: 'date' },
|
||||||
|
'FECHA ESCRITURA': { dept: 'ESCRITURACION', campo: 'fecha_firma', tipo: 'date' },
|
||||||
|
|
||||||
|
// FINANZAS
|
||||||
|
'OTROS INGRESOS': { dept: 'FINANZAS', campo: 'otros_ingresos', tipo: 'number' },
|
||||||
|
'PAGADO TRANSFERENCIA/CHEQUE/EFECTIVO MN': { dept: 'FINANZAS', campo: 'pagado_transferencia', tipo: 'number' },
|
||||||
|
'PAGADO TRANSFERENCIA': { dept: 'FINANZAS', campo: 'pagado_transferencia', tipo: 'number' },
|
||||||
|
'INFONAVIT': { dept: 'FINANZAS', campo: 'infonavit', tipo: 'number' },
|
||||||
|
'BANCARIO': { dept: 'FINANZAS', campo: 'bancario', tipo: 'number' },
|
||||||
|
'FOVISSSTE': { dept: 'FINANZAS', campo: 'fovissste', tipo: 'number' },
|
||||||
|
'CREDITO PUENTE': { dept: 'FINANZAS', campo: 'credito_puente', tipo: 'number' },
|
||||||
|
'SALDO A FAVOR DEL CLIENTE': { dept: 'FINANZAS', campo: 'saldo_favor', tipo: 'number' },
|
||||||
|
'SALDO A FAVOR': { dept: 'FINANZAS', campo: 'saldo_favor', tipo: 'number' },
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers que NO son campos (solo para armar la unidad / ignorados)
|
||||||
|
const HEADERS_UNIDAD = new Set([
|
||||||
|
'UNIDADES', 'UNIDAD', 'DEPTO', 'DEPARTAMENTO', 'UBICACION',
|
||||||
|
'MZA', 'MZ', 'LOTE', 'TORRE', 'NIVEL', 'PROTOTIPO', 'CONTRATISTA',
|
||||||
|
'ETAPA', 'CONDOMINIO', 'DUPLO', 'CASA', 'FLUJO', 'COMENTARIO',
|
||||||
|
])
|
||||||
|
|
||||||
|
// ── Parsers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const MESES = { ENE: 1, FEB: 2, MAR: 3, ABR: 4, MAY: 5, JUN: 6, JUL: 7, AGO: 8, SEP: 9, OCT: 10, NOV: 11, DIC: 12 }
|
||||||
|
|
||||||
|
function isoDeUTC(d) {
|
||||||
|
const y = d.getUTCFullYear()
|
||||||
|
const m = String(d.getUTCMonth() + 1).padStart(2, '0')
|
||||||
|
const dia = String(d.getUTCDate()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${dia}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 'YYYY-MM-DD' desde Date | 'dd/mm/yy' | 'dd-mon.-yy' | 'lun dd/mm/yy'; null si inválida (fuera 2020-2040) */
|
||||||
|
function parseFecha(v) {
|
||||||
|
if (v instanceof Date) return isNaN(v.getTime()) ? null : isoDeUTC(v)
|
||||||
|
if (v == null) return null
|
||||||
|
let s = String(v).trim()
|
||||||
|
if (!s) return null
|
||||||
|
// quitar prefijo de día de la semana: 'lun 14/12/26'
|
||||||
|
s = s.replace(/^[a-zA-ZáéíóúÁÉÍÓÚ]{3,4}\.?\s+/, '')
|
||||||
|
const dmy = s.match(/^(\d{1,2})[/\-](\d{1,2})[/\-](\d{2,4})$/)
|
||||||
|
if (dmy) {
|
||||||
|
const dia = parseInt(dmy[1], 10)
|
||||||
|
const mes = parseInt(dmy[2], 10)
|
||||||
|
let anio = parseInt(dmy[3], 10)
|
||||||
|
if (anio < 100) anio += 2000
|
||||||
|
if (mes < 1 || mes > 12 || anio < 2020 || anio > 2040) return null
|
||||||
|
return `${anio}-${String(mes).padStart(2, '0')}-${String(dia).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
const dMes = s.match(/^(\d{1,2})[-\s]([a-zA-Z]{3})\.?[-\s](\d{2,4})$/)
|
||||||
|
if (dMes) {
|
||||||
|
const mes = MESES[dMes[2].toUpperCase()]
|
||||||
|
if (!mes) return null
|
||||||
|
let anio = parseInt(dMes[3], 10)
|
||||||
|
if (anio < 100) anio += 2000
|
||||||
|
if (anio < 2020 || anio > 2040) return null
|
||||||
|
return `${anio}-${String(mes).padStart(2, '0')}-${String(parseInt(dMes[1], 10)).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBool(v) {
|
||||||
|
if (typeof v === 'boolean') return v
|
||||||
|
if (v == null) return null
|
||||||
|
const s = String(v).trim().toUpperCase()
|
||||||
|
if (['TRUE', 'SI', 'X', '✓', '1'].includes(s)) return true
|
||||||
|
if (['FALSE', 'NO', '0'].includes(s)) return false
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNumero(v) {
|
||||||
|
if (v == null || v === '') return null
|
||||||
|
if (typeof v === 'number') return String(v)
|
||||||
|
const s = String(v).trim().replace(/[$,\s]/g, '')
|
||||||
|
if (!s) return null
|
||||||
|
const n = parseFloat(s)
|
||||||
|
return isNaN(n) ? null : String(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTexto(v) {
|
||||||
|
if (v == null) return null
|
||||||
|
const s = String(v).trim()
|
||||||
|
return s === '' ? null : s.replace(/\s+/g, ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Celda de columna OBRA: fecha → `${campo}_fecha`, True/False → checkbox, 'n/a' → nada */
|
||||||
|
function parseObra(v, dept, campo, out) {
|
||||||
|
if (v == null || v === '') return
|
||||||
|
const f = parseFecha(v)
|
||||||
|
if (f) { out.push({ dept, campo: `${campo}_fecha`, valor: f, tipo: 'date' }); return }
|
||||||
|
const b = parseBool(v)
|
||||||
|
if (b !== null) { out.push({ dept, campo, valor: b ? 'true' : 'false', tipo: 'boolean' }); return }
|
||||||
|
const s = String(v).trim().toUpperCase()
|
||||||
|
if (s === 'N/A' || s === 'NA' || s === '-') return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Config de hojas ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const HOJAS = [
|
||||||
|
{
|
||||||
|
hoja: 'Solana I y II', headerRow: 4, desarrollo: 'SOLANA',
|
||||||
|
unidad: {
|
||||||
|
necesita: ['UNIDADES'],
|
||||||
|
fmt: (v) => {
|
||||||
|
const m = String(v).match(/(\d+)\s*SOL\s*(I{1,2})\b/i)
|
||||||
|
if (m) return `SOLANA-${m[1]}${m[2].toUpperCase() === 'I' ? 'A' : 'B'}`
|
||||||
|
const n = String(v).match(/\d+/)
|
||||||
|
return n ? `SOLANA-${n[0]}` : null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hoja: 'Atelier', headerRow: 4, desarrollo: 'ATELIER',
|
||||||
|
unidad: {
|
||||||
|
necesita: ['UNIDADES'],
|
||||||
|
fmt: (v) => { const m = String(v).trim().match(/^(\d+)/); return m ? `ATELIER-${m[1]}` : null },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hoja: 'Estanza', headerRow: 4, desarrollo: 'ESTANZA',
|
||||||
|
unidad: {
|
||||||
|
necesita: ['UNIDADES'],
|
||||||
|
fmt: (v) => { const m = String(v).trim().match(/^([A-Za-z0-9]+)/); return m ? `ESTANZA-${m[1].toUpperCase()}` : null },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hoja: 'Senda', headerRow: 4, desarrollo: 'SENDA',
|
||||||
|
unidad: {
|
||||||
|
necesita: ['UBICACION', 'MZA', 'LOTE'],
|
||||||
|
fmt: (v, idx, fila) => `SENDA-${String(fila[idx.MZA]).trim()}-${String(fila[idx.LOTE]).trim()}-${String(fila[idx.UBICACION]).trim()}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hoja: 'Sole', headerRow: 4, desarrollo: 'SOLE',
|
||||||
|
unidad: {
|
||||||
|
necesita: ['UNIDAD'],
|
||||||
|
fmt: (v) => { const m = String(v).trim().match(/^(\d+)/); return m ? `SOLE-${m[1]}` : null },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hoja: 'Diagonal 38', headerRow: 2, desarrollo: 'DIAGONAL',
|
||||||
|
unidad: {
|
||||||
|
necesita: ['UNIDAD'],
|
||||||
|
fmt: (v) => { const m = String(v).trim().match(/^(\d+)/); return m ? `U-${m[1]}` : null },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hoja: 'Mutuo', headerRow: 8, desarrollo: 'MUTUO',
|
||||||
|
unidad: {
|
||||||
|
necesita: ['UNIDADES'],
|
||||||
|
fmt: (v) => { const m = String(v).trim().match(/^(\d+)/); return m ? `MUTUO-${m[1]}` : null },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hoja: 'SENDA DG', headerRow: 2, desarrollo: 'SENDA',
|
||||||
|
unidad: {
|
||||||
|
necesita: ['UBICACION', 'MZ', 'LT'],
|
||||||
|
fmt: (v, idx, fila) => `SENDA-${String(fila[idx.MZ]).trim()}-${String(fila[idx.LT]).trim()}-${String(fila[idx.UBICACION]).trim()}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hoja: 'SOLE DG', headerRow: 2, desarrollo: 'SOLE',
|
||||||
|
unidad: {
|
||||||
|
necesita: ['UBICACION'],
|
||||||
|
fmt: (v) => { const m = String(v).trim().match(/^(\d+)/); return m ? `SOLE-${m[1]}` : null },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// ── Lectura y agregación ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Devuelve el agregado: Map `${desarrollo}|${unidad}|${dept}|${campo}` → {valor, tipo} */
|
||||||
|
function leer() {
|
||||||
|
console.log(`Leyendo: ${RUTA} ${DRY ? '(DRY-RUN, no escribe)' : '(BORRA y reimporta)'}`)
|
||||||
|
const wb = XLSX.readFile(RUTA, { cellDates: true })
|
||||||
|
|
||||||
|
const agregado = new Map()
|
||||||
|
|
||||||
|
for (const cfg of HOJAS) {
|
||||||
|
const sheet = wb.Sheets[cfg.hoja]
|
||||||
|
if (!sheet) { console.log(`⚠ Hoja "${cfg.hoja}" no existe, saltando`); continue }
|
||||||
|
|
||||||
|
const filas = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: null })
|
||||||
|
const headersNorm = (filas[cfg.headerRow - 1] || []).map(norm)
|
||||||
|
|
||||||
|
// columnas campo (primera aparición gana dentro de la hoja)
|
||||||
|
const colMap = []
|
||||||
|
const vistos = new Set()
|
||||||
|
for (let c = 0; c < headersNorm.length; c++) {
|
||||||
|
const h = headersNorm[c]
|
||||||
|
if (!h || HEADERS_UNIDAD.has(h)) { colMap.push(null); continue }
|
||||||
|
const m = FIELD_MAP[h]
|
||||||
|
if (!m || vistos.has(`${m.dept}.${m.campo}`)) { colMap.push(null); continue }
|
||||||
|
vistos.add(`${m.dept}.${m.campo}`)
|
||||||
|
colMap.push(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
const idx = {}
|
||||||
|
let ok = true
|
||||||
|
for (const nombre of cfg.unidad.necesita) {
|
||||||
|
const i = headersNorm.indexOf(nombre)
|
||||||
|
if (i === -1) { console.log(`⚠ "${cfg.hoja}": falta columna "${nombre}" para la unidad, saltando`); ok = false; break }
|
||||||
|
idx[nombre] = i
|
||||||
|
}
|
||||||
|
if (!ok) continue
|
||||||
|
|
||||||
|
let filasOk = 0, valores = 0
|
||||||
|
for (let r = cfg.headerRow; r < filas.length; r++) {
|
||||||
|
const fila = filas[r]
|
||||||
|
if (!fila) continue
|
||||||
|
const raw = fila[idx[cfg.unidad.necesita[0]]]
|
||||||
|
if (raw == null || String(raw).trim() === '') continue
|
||||||
|
const unidad = cfg.unidad.fmt(raw, idx, fila)
|
||||||
|
if (!unidad || /TOTAL|DDI/i.test(unidad)) continue
|
||||||
|
|
||||||
|
const celdas = []
|
||||||
|
for (let c = 0; c < colMap.length; c++) {
|
||||||
|
const m = colMap[c]
|
||||||
|
if (!m) continue
|
||||||
|
const v = fila[c]
|
||||||
|
if (m.obra) {
|
||||||
|
parseObra(v, m.dept, m.campo, celdas)
|
||||||
|
} else if (m.campo === 'entrega_sac') {
|
||||||
|
const f = parseFecha(v)
|
||||||
|
const b = parseBool(v)
|
||||||
|
if (f || b === true) celdas.push({ dept: m.dept, campo: m.campo, valor: 'true', tipo: 'boolean' })
|
||||||
|
else if (b === false) celdas.push({ dept: m.dept, campo: m.campo, valor: 'false', tipo: 'boolean' })
|
||||||
|
} else {
|
||||||
|
let valor = null
|
||||||
|
if (m.tipo === 'date') valor = parseFecha(v)
|
||||||
|
else if (m.tipo === 'number') valor = parseNumero(v)
|
||||||
|
else valor = parseTexto(v)
|
||||||
|
if (valor != null) celdas.push({ dept: m.dept, campo: m.campo, valor, tipo: m.tipo })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (celdas.length === 0) continue
|
||||||
|
filasOk++
|
||||||
|
for (const c of celdas) {
|
||||||
|
agregado.set(`${cfg.desarrollo}|${unidad}|${c.dept}|${c.campo}`, { valor: c.valor, tipo: c.tipo })
|
||||||
|
valores++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(` ${cfg.hoja} → ${cfg.desarrollo}: ${filasOk} unidades, ${valores} valores`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nTotal valores únicos: ${agregado.size}`)
|
||||||
|
const porDesarrollo = {}
|
||||||
|
const unidadesPorDesarrollo = new Set()
|
||||||
|
for (const [key] of agregado) {
|
||||||
|
const des = key.split('|')[0]
|
||||||
|
porDesarrollo[des] = (porDesarrollo[des] || 0) + 1
|
||||||
|
unidadesPorDesarrollo.add(key.split('|').slice(0, 2).join('|'))
|
||||||
|
}
|
||||||
|
console.log('Valores por desarrollo:', Object.entries(porDesarrollo).map(([d, n]) => `${d}=${n}`).join(' '))
|
||||||
|
const uniPorDes = {}
|
||||||
|
for (const k of unidadesPorDesarrollo) {
|
||||||
|
const des = k.split('|')[0]
|
||||||
|
uniPorDes[des] = (uniPorDes[des] || 0) + 1
|
||||||
|
}
|
||||||
|
console.log('Unidades por desarrollo:', Object.entries(uniPorDes).map(([d, n]) => `${d}=${n}`).join(' '))
|
||||||
|
|
||||||
|
return agregado
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Escritura y verificación ─────────────────────────────────────────
|
||||||
|
|
||||||
|
async function escribir(agregado) {
|
||||||
|
console.log('\nBorrando registros previos de captura…')
|
||||||
|
const borrados = await prisma.capturaDepartamento.deleteMany({})
|
||||||
|
console.log(`Borrados: ${borrados.count}`)
|
||||||
|
|
||||||
|
const registros = [...agregado.entries()].map(([key, v]) => {
|
||||||
|
const [desarrollo, unidadCodigo, departamento, campo] = key.split('|')
|
||||||
|
return { desarrollo, unidadCodigo, departamento, campo, valor: v.valor, tipo: v.tipo, usuarioId: USUARIO }
|
||||||
|
})
|
||||||
|
|
||||||
|
const CHUNK = 500
|
||||||
|
for (let i = 0; i < registros.length; i += CHUNK) {
|
||||||
|
await prisma.capturaDepartamento.createMany({ data: registros.slice(i, i + CHUNK) })
|
||||||
|
}
|
||||||
|
console.log(`Insertados: ${registros.length}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verificar() {
|
||||||
|
const porDept = await prisma.capturaDepartamento.groupBy({ by: ['desarrollo', 'departamento'], _count: true, orderBy: [{ desarrollo: 'asc' }, { departamento: 'asc' }] })
|
||||||
|
console.log('\nVerificación (desarrollo/depto = registros):')
|
||||||
|
console.log(porDept.map((r) => `${r.desarrollo}/${r.departamento}=${r._count}`).join(' '))
|
||||||
|
|
||||||
|
const filasUnidad = await prisma.capturaDepartamento.findMany({
|
||||||
|
select: { desarrollo: true, unidadCodigo: true },
|
||||||
|
distinct: ['desarrollo', 'unidadCodigo'],
|
||||||
|
})
|
||||||
|
const conteo = {}
|
||||||
|
for (const u of filasUnidad) conteo[u.desarrollo] = (conteo[u.desarrollo] || 0) + 1
|
||||||
|
console.log('Unidades por desarrollo:', Object.entries(conteo).map(([d, n]) => `${d}=${n}`).join(' '))
|
||||||
|
|
||||||
|
const muestra = await prisma.capturaDepartamento.findMany({
|
||||||
|
where: { campo: { in: ['certificacion_gas_fecha', 'estatus', 'inicio_obra_fecha', 'entrega_programada_fecha', 'fecha_venta'] }, valor: { not: null } },
|
||||||
|
take: 10, orderBy: [{ desarrollo: 'asc' }, { unidadCodigo: 'asc' }],
|
||||||
|
})
|
||||||
|
console.log('\nMuestra:')
|
||||||
|
for (const m of muestra) console.log(` ${m.desarrollo} ${m.unidadCodigo} ${m.departamento}.${m.campo} = ${m.valor}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const agregado = leer()
|
||||||
|
if (DRY) {
|
||||||
|
console.log('\n(dry-run: nada escrito)')
|
||||||
|
} else {
|
||||||
|
await escribir(agregado)
|
||||||
|
await verificar()
|
||||||
|
}
|
||||||
|
await prisma.$disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('Error:', err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/**
|
|
||||||
* Importa el archivo maestro de estatus de venta (/root/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] || '/root/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: columna 1 (Depto) o combinación UBICACIÓN+MZA+LOTE
|
|
||||||
let unidadCodigo = String(fila[1] ?? '').trim() // columna 1 = Depto
|
|
||||||
if (!unidadCodigo) {
|
|
||||||
// Fallback: combinar UBICACIÓN (col 10) + MZA (col 11) + LOTE (col 12)
|
|
||||||
const ubic = String(fila[10] ?? '').trim()
|
|
||||||
const mza = String(fila[11] ?? '').trim()
|
|
||||||
const lote = String(fila[12] ?? '').trim()
|
|
||||||
if (ubic || mza || lote) unidadCodigo = [ubic, mza, lote].filter(Boolean).join('-')
|
|
||||||
}
|
|
||||||
if (!unidadCodigo) continue
|
|
||||||
|
|
||||||
// Skip rows that are clearly headers/section markers
|
|
||||||
if (normalizeHeader(unidadCodigo).includes('TOTAL') || normalizeHeader(unidadCodigo).includes('DDI')) 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)
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user