Files
cargaMasiva_api/src/excel/excel.service.ts
T

758 lines
20 KiB
TypeScript

// 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<Usuario>,
@InjectRepository(Genero)
private readonly generoRepo: Repository<Genero>,
@InjectRepository(Carrera)
private readonly carreraRepo: Repository<Carrera>,
@InjectRepository(CarreraUsuario)
private readonly carreraUsuarioRepo: Repository<CarreraUsuario>,
@InjectRepository(UsuarioTipoUsuario)
private readonly usuarioTipoUsuarioRepo: Repository<UsuarioTipoUsuario>,
@InjectRepository(TipoUsuario)
private readonly tipoUsuarioRepo: Repository<TipoUsuario>,
@InjectRepository(ServActivos)
private readonly servActivosRepo: Repository<ServActivos>,
@InjectRepository(UsuariosDelSistema)
private readonly usuariosDelSistemaRepo: Repository<UsuariosDelSistema>,
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<UsuarioRow[]> {
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<string>();
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<string> {
// 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<string> {
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<string> {
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<string> {
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<string> {
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<void> {
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<void> {
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`);
}
}