// src/excel/excel.service.ts import { Injectable, BadRequestException, Logger } from '@nestjs/common'; import { Workbook } from 'exceljs'; import { DataSource, In, Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { CarreraUsuario, TipoUsuario, Usuario, UsuariosDelSistema, UsuarioTipoUsuario } from '../entities/entities'; import { Genero } from '../entities/entities'; import { Carrera } from '../entities/entities'; import { MovimientoService } from 'src/movimiento/movimiento.service'; import { ServActivos } from 'src/usuarios/entities/servActivos.entitie'; import { MailService } from 'src/mail/mail.service'; 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, @InjectRepository(ServActivos) private readonly servActivosRepo: Repository, @InjectRepository(UsuariosDelSistema) private readonly usuariosDelSistemaRepo: Repository, private readonly movimientoService: MovimientoService, private readonly mailService: MailService, private readonly dataSource: DataSource // … 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(); const rowsGood: UsuarioRow[] = []; let flagError: boolean = false; rows.forEach((r, i) => { const rowNum = i + 2; // por el encabezado // Campos que no pueden quedar vacíos ['cuenta', 'tipo'].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.`); flagError = true; } }); // El número de cuenta debe ser de nueve dígitos para los alumnos if (r.cuenta && !/^\d{9}$/.test(r.cuenta.toString().trim()) && r.tipo.trim() === 'Licenciatura') { errors.push(`Fila ${rowNum}: cuenta "${r.cuenta}" debe tener 9 dígitos.`); flagError = true; } // Duplicados de "cuenta" const cuentaStr = r.cuenta != null ? r.cuenta.toString().trim() : ''; if (seen.has(cuentaStr)) { errors.push(`Fila ${rowNum}: cuenta duplicada "${cuentaStr}".`); flagError = true; } else { seen.add(cuentaStr); } 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}".`); flagError = true; } // 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}".`); flagError = true; } // 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.`); flagError = true; } if (!flagError) { rowsGood.push(r) } else { flagError = false; // reset para la siguiente fila } }); console.log(rowsGood); return { errors, rowsGood }; } /** * Solo valida: retorna lista de errores (vacío = ok) */ async validateFile(buffer: Buffer) { const rows = await this.parseFile(buffer); const status = this.validateRows(rows); return status; } /** * Valida y luego persiste usuarios y relaciones básicas */ async loadFile(buffer: Buffer) { const file = await this.parseFile(buffer); const status = this.validateRows(file); const errs = status.errors; const rows = status.rowsGood; if (rows.length === 0) { throw new BadRequestException('No se encontraron filas válidas para cargar. ' + errs); } const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const movimiento = await this.movimientoService.logger( queryRunner, 'LOAD', 'LOADING', undefined, 'CARGA MASIVA DE USUARIOS' ); let contador = 0; for (const r of rows) { const whereCond: Array<{ num_cuenta?: string; rfc?: string }> = [{ num_cuenta: r.cuenta }]; if (r.rfc) { whereCond.push({ rfc: r.rfc }); } const userExists = await queryRunner.manager.findOne(Usuario, { where: whereCond, }); if (userExists) { errs.push(`Cuenta ${r.cuenta} ya existe, se omite.`); contador++; continue; } if (!r.tipo || !r.nombres || !r.apellidopa || !r.apellidoma) { errs.push(`Fila con cuenta ${r.cuenta} tiene campos faltantes.`); contador++; continue; } let genero = await queryRunner.manager.findOne(Genero, { where: { genero: r.sexo } }); if (!genero) { genero = queryRunner.manager.create(Genero, { genero: r.sexo }); await queryRunner.manager.save(genero); } let carrera if (r.clave || r.nomCarr) { carrera = await queryRunner.manager.findOne(Carrera, { where: { clave: r.clave } }); if (!carrera) { carrera = queryRunner.manager.create(Carrera, { clave: r.clave, carrera: r.nomCarr.slice(0, 100), }); await queryRunner.manager.save(carrera); } } else { carrera = null; } let tipo = await queryRunner.manager.findOne(TipoUsuario, { where: { tipo_usuario: r.tipo.trim() }, }); if (!tipo) { tipo = queryRunner.manager.create(TipoUsuario, { tipo_usuario: r.tipo.trim() }); await queryRunner.manager.save(tipo); } const generacion = /^[0-9]{4}$/.test(r.gen?.toString().trim()) ? parseInt(r.gen.toString().trim(), 10) : null; const usuario = queryRunner.manager.create(Usuario, { num_cuenta: r.cuenta, nombre: r.nombres.trim().split(' ')[0], a_paterno: r.apellidopa, a_materno: r.apellidoma, rfc: r.rfc, fecha_nacimiento: r.fechnac, generacion, genero, movimiento, }); const user = await queryRunner.manager.save(usuario); if (carrera) { const carreraUsuario = queryRunner.manager.create(CarreraUsuario, { usuario: user, carrera, }); await queryRunner.manager.save(carreraUsuario); } const usuarioTipo = queryRunner.manager.create(UsuarioTipoUsuario, { usuario: user, tipoUsuario: tipo, }); await queryRunner.manager.save(usuarioTipo); const servData = { usuario: user, RedStatus: 'Inactivo', ATStatus: 'Inactivo', CorreoStatus: 'Inactivo', PrestamosStatus: 'Inactivo', }; switch (r.tipo.trim()) { case 'Diplomado': Object.assign(servData, { Correo: true }); break; case 'Extra Largo': Object.assign(servData, { Correo: true, Prestamos: true }); break; case 'Servicio Social': Object.assign(servData, { Correo: true, AT: true, Red: true }); break; case 'Idiomas R (UNAM)': case 'Idiomas Sabatino': Object.assign(servData, { Correo: true, Red: true }); break; case 'Reinscrito': case 'Posgrado': case 'Intercambio UNAM': case 'Movilidad': case 'Ampliación de Conocimiento': case 'Licenciatura': case 'Profesor': Object.assign(servData, { Correo: true, AT: true, Red: true, Prestamos: true }); break; case 'Trabajadores': Object.assign(servData, { Red: true }); break; } const serv = queryRunner.manager.create(ServActivos, servData); await queryRunner.manager.save(serv); } await queryRunner.commitTransaction(); return { inserted: rows.length - contador, id_movimiento: movimiento.id_mov, errors: errs }; } catch (err) { await queryRunner.rollbackTransaction(); throw err; } finally { await queryRunner.release(); } } // 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), }, }, servActivo: { CorreoStatus: 'Inactivo', // solo los que no tienen servicio activo Correo: true, // solo los que tienen el servicio de correo activo } }, 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(',') + '\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(','); }).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), }, }, servActivo: { PrestamosStatus: 'Inactivo', // solo los que no tienen servicio activo Prestamos: true, // solo los que tienen el servicio de préstamos activo } }, 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(',') + '\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(','); }).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), }, }, servActivo: { RedStatus: 'Inactivo', Red: true, // solo los que tienen el servicio de red activo } }, relations: [ 'genero', 'carreraUsuarios', // relación intermedia 'carreraUsuarios.carrera', // carrera asociada a través de la intermedia ], }); const header = [ 'num_cuenta', 'fecha_nacimiento' ].join(',') + '\n'; const rows = usuarios.map(u => { return [ u.num_cuenta, u.fecha_nacimiento, ].join(','); }).join('\n'); const tablaCompleta = header + rows; console.log(tablaCompleta); console.log('Usuarios encontrados:', usuarios); 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), }, }, servActivo: { ATStatus: 'Inactivo', AT: true }, }, 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', 'fecha_nacimiento', 'generacion' ].join(',') + '\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(','); }).join('\n'); const tablaCompleta = header + rows; return tablaCompleta; } //Funcion que se le debe hacer su propio módulo async activarServicios(id_movimiento: number, origen: string): Promise { if (!['AT', 'SOLICITA', 'CORREO', 'RED'].includes(origen)) { throw new BadRequestException(`Origen ${origen} no válido`); } const usuarios = await this.usuarioRepo.find({ where: { movimiento: { id_mov: id_movimiento } }, }); if (!usuarios || usuarios.length === 0) { throw new BadRequestException('No se encontró el usuario asociado al movimiento'); } for (const usuario of usuarios) { const servActivos = await this.servActivosRepo.findOne({ where: { usuario: { id_usuario: usuario.id_usuario } }, }); if (!servActivos) { throw new BadRequestException( `No se encontraron servicios activos para el usuario con ID ${usuario.id_usuario}`, ); } const timestamp = 'Activo ' + new Date().toISOString(); if (origen === 'AT' && servActivos.AT) { servActivos.ATStatus = timestamp; } else if (origen === 'RED' && servActivos.Red) { servActivos.RedStatus = timestamp; } else if (origen === 'CORREO' && servActivos.Correo) { servActivos.CorreoStatus = timestamp; } else if (origen === 'SOLICITA' && servActivos.Prestamos) { servActivos.PrestamosStatus = timestamp; } else { continue; } await this.servActivosRepo.save(servActivos); console.log(`Servicio ${servActivos}`); } } async enviarCargaMasiva(): Promise { const usuarios_red = await this.usuariosDelSistemaRepo.findOne({ where: { origen: { origen: 'RED' } } }); const usuarios_at = await this.usuariosDelSistemaRepo.findOne({ where: { origen: { origen: 'AT' } } }); const usuarios_correo = await this.usuariosDelSistemaRepo.findOne({ where: { origen: { origen: 'CORREO' } } }); const usuarios_solicita = await this.usuariosDelSistemaRepo.findOne({ where: { origen: { origen: 'SOLICITA' } } }); if (!usuarios_red || !usuarios_at || !usuarios_correo || !usuarios_solicita) { throw new BadRequestException('No se encontraron usuarios para enviar correo'); } // Aquí puedes enviar el correo usando MailService await this.mailService.sendMail({ to: usuarios_red.correo ?? '', subject: 'Carga de Usuarios', text: 'Se ha realizado una carga de usuarios en el sistema.', html: '', }); await this.mailService.sendMail({ to: usuarios_at.correo ?? '', subject: 'Carga de Usuarios', text: 'Se ha realizado una carga de usuarios en el sistema.', html: '', }); await this.mailService.sendMail({ to: usuarios_correo.correo ?? '', subject: 'Carga de Usuarios', text: 'Se ha realizado una carga de usuarios en el sistema.', html: '', }); await this.mailService.sendMail({ to: usuarios_solicita.correo ?? '', subject: 'Carga de Usuarios', text: 'Se ha realizado una carga de usuarios en el sistema.', html: '', }); console.log(`Correo enviados`); } }