feat(clientes-maestro): importar y exportar clientes del archivo maestro

- Tabla ClienteMaestro (27 campos, unique numCliente)
- Importador Excel: 1535 clientes del Reporte maestro
- API: listar/filtros/exportar/detalle/importar
- Frontend: tabla paginada, filtros, exportar Excel
- Dark mode completo, 52 tests verdes
This commit is contained in:
2026-09-07 09:11:09 +00:00
parent c2a8f4e929
commit 9021733455
18 changed files with 1428 additions and 10 deletions

View File

@@ -0,0 +1,66 @@
#!/usr/bin/env node
import { PrismaClient } from '@prisma/client'
import XLSX from 'xlsx'
const prisma = new PrismaClient()
const toStr = (v) => v == null ? null : String(v).trim() || null
const toNum = (v) => {
if (v == null) return null
const n = Number(String(v).replace(/[$,\s]/g, ''))
return Number.isFinite(n) ? n : null
}
const wb = XLSX.readFile('/root/Reporte maestro de clientesAGO26.xlsx')
const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1, defval: null })
console.log(`Filas: ${rows.length}`)
const map = new Map()
for (let i = 1; i < rows.length; i++) {
const nc = toStr(rows[i][0])
if (nc) map.set(nc, rows[i])
}
console.log(`Clientes: ${map.size}`)
let ok = 0, fail = 0
let idx = 0
for (const [nc, r] of map) {
idx++
const d = {
numCliente: nc,
nombre: toStr(r[1]) || 'SIN NOMBRE',
apellidoPaterno: toStr(r[2]) || '',
apellidoMaterno: toStr(r[3]),
sexo: toStr(r[4]),
telefono: toStr(r[5]),
email: toStr(r[6]),
estadoCivil: toStr(r[7]),
rangoEdad: toStr(r[8]),
ocupacion: toStr(r[16]),
rangoIngresos: toStr(r[15]),
credito: toStr(r[10]),
institucion: toStr(r[11]),
medioContacto: toStr(r[13]),
canal: toStr(r[14]),
desarrollo: toStr(r[19]),
manzana: toStr(r[20]),
lote: toStr(r[21]),
interior: toStr(r[22]),
tipoCasa: toStr(r[24]),
etapa: toStr(r[27]),
valorTotal: toNum(r[26]),
estatus: toStr(r[27]),
fechaLiberacion: r[28] instanceof Date ? r[28] : null,
vendedor: toStr(r[29]),
vendedorId: toStr(r[30]),
}
try {
await prisma.clienteMaestro.upsert({ where: { numCliente: nc }, update: d, create: d })
ok++
} catch (e) {
fail++
if (fail <= 5) console.log(`ERR ${nc}: ${e.message.slice(0, 120)}`)
}
if (idx % 500 === 0) console.log(` ${idx}/${map.size} ok=${ok} fail=${fail}`)
}
console.log(`\nListo: ${ok} ok, ${fail} fail / ${map.size}`)
await prisma.$disconnect()

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
/**
* Importa el archivo maestro de estatus de venta (Reporte_EstatusVenta.xlsx)
* Importa el archivo maestro de estatus de venta (/root/Reporte_EstatusVenta.xlsx)
* a la tabla CapturaDepartamento.
*
* Uso:
@@ -152,7 +152,7 @@ function detectarDesarrollo(nombreHoja) {
// ── Main ─────────────────────────────────────────────────────────────
async function main() {
const rutaExcel = process.argv[2] || join(__dirname, '..', '..', 'Reporte_EstatusVenta.xlsx')
const rutaExcel = process.argv[2] || '/root/Reporte_EstatusVenta.xlsx'
const usuarioId = process.argv[3] || 'importacion-maestro'
console.log(`Leyendo archivo: ${rutaExcel}`)
@@ -203,12 +203,19 @@ async function main() {
let hojaActualizados = 0
for (const fila of filasDatos) {
// Detectar unidad: primera columna con valor
const unidadCodigo = String(fila[0] ?? '').trim()
// 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')) continue
if (normalizeHeader(unidadCodigo).includes('TOTAL') || normalizeHeader(unidadCodigo).includes('DDI')) continue
for (let col = 0; col < headers.length; col++) {
const mapeo = mapaColumnas[col]