// src/excel/excel.service.ts import { Injectable, BadRequestException, Logger } from '@nestjs/common'; import { Workbook } from 'exceljs'; import { In, Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { CarreraUsuario, TipoUsuario, Usuario, UsuarioTipoUsuario } from '../entities/entities'; import { Genero } from '../entities/entities'; import { Carrera } from '../entities/entities'; export interface UsuarioRow { cuenta: string; nombreCompleto: string; clave: string; nomCarr: string; gen: string; // generación fechnac: string; // YYYYMMDD apellidopa: string; apellidoma: string; nombres: string; sexo: string; // m/f o texto tipo: string; correo: string; rfc: string; } @Injectable() export class ExcelService { private readonly logger = new Logger(ExcelService.name); constructor( @InjectRepository(Usuario) private readonly usuarioRepo: Repository, @InjectRepository(Genero) private readonly generoRepo: Repository, @InjectRepository(Carrera) private readonly carreraRepo: Repository, @InjectRepository(CarreraUsuario) private readonly carreraUsuarioRepo: Repository, @InjectRepository(UsuarioTipoUsuario) private readonly usuarioTipoUsuarioRepo: Repository, @InjectRepository(TipoUsuario) private readonly tipoUsuarioRepo: Repository, // … inyecta otros repositorios si los usarás ) { } /** Lee el buffer del Excel y devuelve un arreglo de filas tipadas */ private async parseFile(buffer: Buffer): Promise { const wb = new Workbook(); await wb.xlsx.load(buffer); const sheet = wb.worksheets[0]; const rows: UsuarioRow[] = []; // Asume que la primera fila es encabezados sheet.eachRow((row, idx) => { if (idx === 1) return; // salto encabezados // 1) Asegurarnos de que row.values no sea null/undefined if (!row.values) { this.logger.warn(`Fila ${idx + 1}: row.values vacío, se omite.`); return; } // 2) row.values[0] es null, así que slice(1) sí existe const [ cuenta, nombreCompleto, clave, nomCarr, gen, fechnac, apellidopa, apellidoma, nombres, sexo, tipo, correo, rfc, ] = (row.values as any[]).slice(1); rows.push({ cuenta, nombreCompleto, clave, nomCarr, gen, fechnac, apellidopa, apellidoma, nombres, sexo, tipo, correo, rfc, }); }); return rows; } /** Valida duplicados, celdas vacías y patrones básicos */ validateRows(rows: UsuarioRow[]) { const errors: string[] = []; const seen = new Set(); rows.forEach((r, i) => { const rowNum = i + 2; // por el encabezado // Campos que no pueden quedar vacíos ['cuenta', 'nombreCompleto'].forEach(field => { const raw = (r as any)[field]; const str = raw != null ? raw.toString().trim() : ''; if (!str) { errors.push(`Fila ${rowNum}: campo "${field}" está vacío.`); } }); // Duplicados de "cuenta" const cuentaStr = r.cuenta != null ? r.cuenta.toString().trim() : ''; if (seen.has(cuentaStr)) { errors.push(`Fila ${rowNum}: cuenta duplicada "${cuentaStr}".`); } else { seen.add(cuentaStr); } // Numérico en cuenta y generación const genStr = r.gen != null ? r.gen.toString().trim() : ''; if (genStr && !/^[0-9]{4}$/.test(genStr)) { errors.push(`Fila ${rowNum}: generación inválida "${genStr}".`); } // Fecha en formato YYYYMMDD const fechaStr = r.fechnac != null ? r.fechnac.toString().trim() : ''; if (!/^[0-9]{8}$/.test(fechaStr)) { errors.push(`Fila ${rowNum}: fecha de nacimiento inválida "${fechaStr}".`); } // RFC alfanumérico const rfcStr = r.rfc != null ? r.rfc.toString().trim() : ''; if (rfcStr && !/^[A-Z0-9]+$/.test(rfcStr)) { errors.push(`Fila ${rowNum}: RFC contiene caracteres no válidos.`); } }); return errors; } /** * Solo valida: retorna lista de errores (vacío = ok) */ async validateFile(buffer: Buffer) { const rows = await this.parseFile(buffer); const errs = this.validateRows(rows); return errs; } /** * Valida y luego persiste usuarios y relaciones básicas */ async loadFile(buffer: Buffer) { const rows = await this.parseFile(buffer); const errs = this.validateRows(rows); if (errs.length) { throw new BadRequestException({ errors: errs }); } for (const r of rows) { // 1) Género (crea o reutiliza) let genero = await this.generoRepo.findOne({ where: { genero: r.sexo } }); if (!genero) { genero = this.generoRepo.create({ genero: r.sexo }); await this.generoRepo.save(genero); } // 2) Carrera (por clave) let carrera = await this.carreraRepo.findOne({ where: { clave: r.clave }, }); if (!carrera) { carrera = this.carreraRepo.create({ clave: r.clave, carrera: r.nomCarr.slice(0, 100), }); await this.carreraRepo.save(carrera); } let tipo = await this.tipoUsuarioRepo.findOne({ where: { tipo_usuario: r.tipo.trim() }, }); if (!tipo) { tipo = this.tipoUsuarioRepo.create({ tipo_usuario: r.tipo.trim(), }); await this.tipoUsuarioRepo.save(tipo); } const generacion = /^[0-9]{4}$/.test(r.gen?.toString().trim()) ? parseInt(r.gen.toString().trim(), 10) : null; // 3) Usuario const usuario = this.usuarioRepo.create({ num_cuenta: r.cuenta, nombre: r.nombres.trim().split(' ')[0], // ajusta si quieres a_paterno: r.apellidopa, a_materno: r.apellidoma, rfc: r.rfc, fecha_nacimiento: r.fechnac, generacion: generacion, genero, }); await this.usuarioRepo.save(usuario); // 4) Relacion usuario–carrxera (tabla intermedia) const carreraUsuario = this.carreraUsuarioRepo.create({ usuario, carrera, }); await this.carreraUsuarioRepo.save(carreraUsuario); // 5) Tipo de usuario const usuarioTipo = this.usuarioTipoUsuarioRepo.create({ usuario, tipoUsuario: tipo, }); await this.usuarioTipoUsuarioRepo.save(usuarioTipo); } return { inserted: rows.length }; } // async generateCsv(): Promise { // const users = await this.usuarioRepo.find({ // relations: ['genero', 'carreraUsuarios'], // si quieres más info // select: ['num_cuenta', 'nombre', 'a_paterno', 'a_materno', 'rfc', 'fecha_nacimiento', 'generacion'], // }); // const header = [ // 'num_cuenta', 'nombre', 'a_paterno', 'a_materno', // 'carrera', 'rfc', 'fecha_nacimiento', 'generacion' // ].join('\t') + '\n'; // const rows = users.map(u => [ // u.num_cuenta, // u.nombre, // u.a_paterno, // u.a_materno, // u.rfc ?? '', // u.fecha_nacimiento, // (u.generacion != null ? u.generacion.toString() : '') // ].join('\t')).join('\n'); // return header + rows; // } async generateCsvCorreo(): Promise { const tipos = [ 'Diplomado', 'Extra Largo', 'Ampliación de Conocimiento', 'Servicio Social', 'Movilidad', 'Intercambio UNAM', 'Idiomas Sabatino', 'Idiomas R (UNAM)', 'Trabajadores', 'Profesor', 'Oyente', 'Posgrado', 'Reinscrito', 'Licenciatura', ]; const usuarios = await this.usuarioRepo.find({ where: { usuarioTipos: { tipoUsuario: { tipo_usuario: In(tipos), }, }, }, relations: [ 'genero', 'carreraUsuarios', // relación intermedia 'carreraUsuarios.carrera', // carrera asociada a través de la intermedia ], }); const header = [ 'num_cuenta', 'nombre', 'a_paterno', 'a_materno', 'carreras', 'rfc', 'fecha_nacimiento', 'generacion' ].join('\t') + '\n'; const rows = usuarios.map(u => { const carreras = (u.carreraUsuarios ?? []) .map(cu => cu.carrera?.carrera ?? '') // obtenemos solo el nombre .join(', '); // une todas las carreras en un solo string return [ u.num_cuenta, u.nombre, u.a_paterno, u.a_materno, carreras, u.rfc ?? '', u.fecha_nacimiento, u.generacion?.toString() ?? '' ].join('\t'); }).join('\n'); const tablaCompleta = header + rows; return tablaCompleta; } async generateCsvSolicita(): Promise { const tipos = [ 'Extra Largo', 'Ampliación de Conocimiento', 'Movilidad', 'Intercambio UNAM', 'Profesor', 'Posgrado', 'Reinscrito', 'Licenciatura', ]; const usuarios = await this.usuarioRepo.find({ where: { usuarioTipos: { tipoUsuario: { tipo_usuario: In(tipos), }, }, }, relations: [ 'genero', 'carreraUsuarios', // relación intermedia 'carreraUsuarios.carrera', // carrera asociada a través de la intermedia ], }); const usuariosTipo = await this.usuarioRepo.find({ relations: [ 'genero', 'carreraUsuarios', // relación intermedia 'carreraUsuarios.carrera', 'usuarioTipos', // relación con tipo de usuario 'usuarioTipos.tipoUsuario', // tipo de usuario asociado ], }); console.log('Usuarios encontrados:', usuariosTipo); console.log('Usuarios encontrados:', usuarios); const header = [ 'num_cuenta', 'nombre', 'a_paterno', 'a_materno', 'carreras', 'rfc' ].join('\t') + '\n'; const rows = usuarios.map(u => { const carreras = (u.carreraUsuarios ?? []) .map(cu => cu.carrera?.carrera ?? '') // obtenemos solo el nombre .join(', '); // une todas las carreras en un solo string return [ u.num_cuenta, u.nombre, u.a_paterno, u.a_materno, carreras, u.rfc ?? '', ].join('\t'); }).join('\n'); const tablaCompleta = header + rows; return tablaCompleta; } async generateCsvRed(): Promise { const tipos = [ 'Ampliación de Conocimiento', 'Servicio Social', 'Movilidad', 'Intercambio UNAM', 'Idiomas Sabatino', 'Idiomas R (UNAM)', 'Trabajadores', 'Profesor', 'Posgrado', 'Reinscrito', 'Licenciatura', ]; const usuarios = await this.usuarioRepo.find({ where: { usuarioTipos: { tipoUsuario: { tipo_usuario: In(tipos), }, }, }, relations: [ 'genero', 'carreraUsuarios', // relación intermedia 'carreraUsuarios.carrera', // carrera asociada a través de la intermedia ], }); const header = [ 'num_cuenta', 'fecha_nacimiento' ].join('\t') + '\n'; const rows = usuarios.map(u => { return [ u.num_cuenta, u.fecha_nacimiento, ].join('\t'); }).join('\n'); const tablaCompleta = header + rows; return tablaCompleta; } async generateCsvAT(): Promise { const tipos = [ 'Ampliación de Conocimiento', 'Servicio Social', 'Movilidad', 'Intercambio UNAM', 'Profesor', 'Posgrado', 'Reinscrito', 'Licenciatura', ]; const usuarios = await this.usuarioRepo.find({ where: { usuarioTipos: { tipoUsuario: { tipo_usuario: In(tipos), }, }, }, relations: [ 'genero', 'carreraUsuarios', // relación intermedia 'carreraUsuarios.carrera', // carrera asociada a través de la intermedia ], }); const header = [ 'num_cuenta', 'nombre', 'a_paterno', 'a_materno', 'carreras', 'rfc', 'fecha_nacimiento', 'generacion' ].join('\t') + '\n'; const rows = usuarios.map(u => { const carreras = (u.carreraUsuarios ?? []) .map(cu => cu.carrera?.carrera ?? '') // obtenemos solo el nombre .join(', '); // une todas las carreras en un solo string return [ u.num_cuenta, u.nombre, u.a_paterno, u.a_materno, carreras, u.fecha_nacimiento, u.generacion?.toString() ?? '' ].join('\t'); }).join('\n'); const tablaCompleta = header + rows; return tablaCompleta; } }