1254 lines
36 KiB
TypeScript
1254 lines
36 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
|
|
}
|
|
});
|
|
|
|
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, user: string) {
|
|
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,
|
|
);
|
|
}
|
|
|
|
let conteoTiposRed: number = 0;
|
|
let conteoTiposAt: number = 0;
|
|
let conteoTiposSolicita: number = 0;
|
|
let conteoTiposCorreo: number = 0;
|
|
|
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
|
|
try {
|
|
const movimiento = await this.movimientoService.logger(
|
|
queryRunner,
|
|
'LOAD',
|
|
'LOADING',
|
|
'Caraga masiva de usuarios hecha por: ' + user,
|
|
`CARGA MASIVA DE USUARIOS`,
|
|
);
|
|
|
|
// 🔹 1. Pre-cargar cuentas/rfc ya existentes
|
|
const cuentas = rows.map((r) => r.cuenta).filter(Boolean);
|
|
const rfcs = rows.map((r) => r.rfc).filter(Boolean);
|
|
|
|
const existentes = await queryRunner.manager.find(Usuario, {
|
|
where: [{ num_cuenta: In(cuentas) }, { rfc: In(rfcs) }],
|
|
select: ['num_cuenta', 'rfc'],
|
|
});
|
|
|
|
const setCuentas = new Set(existentes.map((u) => u.num_cuenta));
|
|
const setRfcs = new Set(existentes.map((u) => u.rfc));
|
|
|
|
// 🔹 2. Pre-cargar catálogos
|
|
const generosUnicos = [
|
|
...new Set(rows.map((r) => r.sexo).filter(Boolean)),
|
|
];
|
|
const carrerasUnicasClave = [
|
|
...new Set(rows.map((r) => r.clave).filter(Boolean)),
|
|
];
|
|
const carrerasUnicasNombre = [
|
|
...new Set(rows.map((r) => r.nomCarr).filter(Boolean)),
|
|
];
|
|
const tiposUnicos = [
|
|
...new Set(rows.map((r) => r.tipo?.trim()).filter(Boolean)),
|
|
];
|
|
|
|
const [generosExist, carrerasExist, tiposExist] = await Promise.all([
|
|
queryRunner.manager.find(Genero, {
|
|
where: { genero: In(generosUnicos) },
|
|
}),
|
|
queryRunner.manager.find(Carrera, {
|
|
where: [
|
|
{ clave: In(carrerasUnicasClave) },
|
|
{ carrera: In(carrerasUnicasNombre) },
|
|
],
|
|
}),
|
|
queryRunner.manager.find(TipoUsuario, {
|
|
where: { tipo_usuario: In(tiposUnicos) },
|
|
}),
|
|
]);
|
|
|
|
const mapGenero = new Map(generosExist.map((g) => [g.genero, g]));
|
|
const mapCarreraClave = new Map(carrerasExist.map((c) => [c.clave, c]));
|
|
const mapCarreraNom = new Map(carrerasExist.map((c) => [c.carrera, c]));
|
|
const mapTipo = new Map(tiposExist.map((t) => [t.tipo_usuario, t]));
|
|
|
|
// 🔹 3. Arreglos para inserciones masivas
|
|
const usuarios: Usuario[] = [];
|
|
const carrerasUsuario: CarreraUsuario[] = [];
|
|
const usuariosTipos: UsuarioTipoUsuario[] = [];
|
|
const serviciosActivos: ServActivos[] = [];
|
|
|
|
let contador = 0;
|
|
|
|
for (const r of rows) {
|
|
if (setCuentas.has(r.cuenta) || (r.rfc && setRfcs.has(r.rfc))) {
|
|
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;
|
|
}
|
|
|
|
// Genero
|
|
let genero = mapGenero.get(r.sexo);
|
|
if (!genero) {
|
|
genero = queryRunner.manager.create(Genero, { genero: r.sexo });
|
|
mapGenero.set(r.sexo, genero);
|
|
}
|
|
|
|
// Carrera
|
|
let carrera: Carrera | undefined;
|
|
if (r.clave && mapCarreraClave.has(r.clave)) {
|
|
carrera = mapCarreraClave.get(r.clave);
|
|
} else if (r.nomCarr && mapCarreraNom.has(r.nomCarr)) {
|
|
carrera = mapCarreraNom.get(r.nomCarr);
|
|
} else if (r.clave || r.nomCarr) {
|
|
carrera = queryRunner.manager.create(Carrera, {
|
|
clave: r.clave ?? '',
|
|
carrera: r.nomCarr?.slice(0, 100) ?? '',
|
|
});
|
|
mapCarreraClave.set(carrera.clave, carrera);
|
|
mapCarreraNom.set(carrera.carrera, carrera);
|
|
}
|
|
|
|
// Tipo
|
|
let tipo = mapTipo.get(r.tipo.trim());
|
|
if (!tipo) {
|
|
tipo = queryRunner.manager.create(TipoUsuario, {
|
|
tipo_usuario: r.tipo.trim(),
|
|
});
|
|
mapTipo.set(r.tipo.trim(), 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,
|
|
a_paterno: r.apellidopa,
|
|
a_materno: r.apellidoma,
|
|
rfc: r.rfc,
|
|
fecha_nacimiento: r.fechnac,
|
|
generacion,
|
|
genero,
|
|
movimiento,
|
|
});
|
|
|
|
usuarios.push(usuario);
|
|
|
|
if (carrera) {
|
|
const carreraUsuario = queryRunner.manager.create(CarreraUsuario, {
|
|
usuario,
|
|
carrera,
|
|
});
|
|
carrerasUsuario.push(carreraUsuario);
|
|
}
|
|
|
|
usuariosTipos.push(
|
|
queryRunner.manager.create(UsuarioTipoUsuario, {
|
|
usuario,
|
|
tipoUsuario: tipo,
|
|
}),
|
|
);
|
|
|
|
// Servicios activos
|
|
const servData: Partial<ServActivos> = {
|
|
usuario,
|
|
RedStatus: 'Inactivo',
|
|
ATStatus: 'Inactivo',
|
|
CorreoStatus: 'Inactivo',
|
|
PrestamosStatus: 'Inactivo',
|
|
};
|
|
|
|
switch (r.tipo.trim()) {
|
|
case 'Diplomado':
|
|
Object.assign(servData, { Correo: true });
|
|
conteoTiposCorreo++;
|
|
break;
|
|
case 'Extra Largo':
|
|
Object.assign(servData, { Correo: true, Prestamos: true });
|
|
conteoTiposCorreo++;
|
|
conteoTiposSolicita++;
|
|
break;
|
|
case 'Servicio Social':
|
|
Object.assign(servData, { Correo: true, AT: true, Red: true });
|
|
conteoTiposCorreo++;
|
|
conteoTiposAt++;
|
|
conteoTiposRed++;
|
|
break;
|
|
case 'Idiomas R (UNAM)':
|
|
case 'Idiomas Sabatino':
|
|
Object.assign(servData, { Correo: true, Red: true });
|
|
conteoTiposCorreo++;
|
|
conteoTiposRed++;
|
|
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,
|
|
});
|
|
conteoTiposCorreo++;
|
|
conteoTiposRed++;
|
|
conteoTiposAt++;
|
|
conteoTiposSolicita++;
|
|
break;
|
|
case 'Trabajadores':
|
|
Object.assign(servData, { Red: true });
|
|
conteoTiposRed++;
|
|
break;
|
|
}
|
|
|
|
serviciosActivos.push(
|
|
queryRunner.manager.create(ServActivos, servData),
|
|
);
|
|
}
|
|
|
|
// 🔹 4. Guardar en batch (orden correcto)
|
|
await queryRunner.manager.save([...mapGenero.values()]);
|
|
await queryRunner.manager.save([...mapCarreraClave.values()]);
|
|
await queryRunner.manager.save([...mapTipo.values()]);
|
|
|
|
// 1️⃣ Guardar usuarios primero — asigna IDs
|
|
await queryRunner.manager.save(usuarios);
|
|
|
|
// 2️⃣ Reconstruir relaciones con usuarios ya guardados
|
|
|
|
// CarreraUsuario
|
|
const carrerasUsuarioFinal: CarreraUsuario[] = [];
|
|
for (const cu of carrerasUsuario) {
|
|
const usuarioConId = usuarios.find(
|
|
(u) => u.num_cuenta === cu.usuario.num_cuenta,
|
|
);
|
|
if (usuarioConId && cu.carrera) {
|
|
carrerasUsuarioFinal.push(
|
|
queryRunner.manager.create(CarreraUsuario, {
|
|
usuario: usuarioConId,
|
|
carrera: cu.carrera,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
// UsuarioTipoUsuario
|
|
const usuariosTiposFinal: UsuarioTipoUsuario[] = [];
|
|
for (const ut of usuariosTipos) {
|
|
const usuarioConId = usuarios.find(
|
|
(u) => u.num_cuenta === ut.usuario.num_cuenta,
|
|
);
|
|
if (usuarioConId && ut.tipoUsuario) {
|
|
usuariosTiposFinal.push(
|
|
queryRunner.manager.create(UsuarioTipoUsuario, {
|
|
usuario: usuarioConId,
|
|
tipoUsuario: ut.tipoUsuario,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ServActivos
|
|
const serviciosActivosFinal: ServActivos[] = [];
|
|
for (const sa of serviciosActivos) {
|
|
const usuarioConId = usuarios.find(
|
|
(u) => u.num_cuenta === sa.usuario.num_cuenta,
|
|
);
|
|
if (usuarioConId) {
|
|
serviciosActivosFinal.push(
|
|
queryRunner.manager.create(ServActivos, {
|
|
...sa,
|
|
usuario: usuarioConId,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 3️⃣ Guardar relaciones dependientes en orden
|
|
await queryRunner.manager.save(carrerasUsuarioFinal);
|
|
await queryRunner.manager.save(usuariosTiposFinal);
|
|
await queryRunner.manager.save(serviciosActivosFinal);
|
|
|
|
return {
|
|
inserted: rows.length - contador,
|
|
id_movimiento: movimiento.id_mov,
|
|
errors: errs,
|
|
conteoTiposAt,
|
|
conteoTiposCorreo,
|
|
conteoTiposRed,
|
|
conteoTiposSolicita,
|
|
};
|
|
} catch (err) {
|
|
await queryRunner.rollbackTransaction();
|
|
throw err;
|
|
} finally {
|
|
await queryRunner.release();
|
|
}
|
|
}
|
|
|
|
async loadFile2(buffer: Buffer, user: string) {
|
|
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,
|
|
);
|
|
}
|
|
|
|
let conteoTiposRed: number = 0;
|
|
let conteoTiposAt: number = 0;
|
|
let conteoTiposSolicita: number = 0;
|
|
let conteoTiposCorreo: number = 0;
|
|
|
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
let idsUsuarios: number[] =[]
|
|
try {
|
|
const movimiento = await this.movimientoService.logger(
|
|
queryRunner,
|
|
'LOAD',
|
|
'LOADING',
|
|
'Caraga masiva de usuarios hecha por: ' + user,
|
|
`CARGA MASIVA DE USUARIOS`,
|
|
);
|
|
|
|
// 🔹 1. Pre-cargar cuentas/rfc ya existentes
|
|
const cuentas = rows.map((r) => r.cuenta).filter(Boolean);
|
|
const rfcs = rows.map((r) => r.rfc).filter(Boolean);
|
|
|
|
const existentes = await queryRunner.manager.find(Usuario, {
|
|
where: [{ num_cuenta: In(cuentas) }, { rfc: In(rfcs) }],
|
|
select: ['num_cuenta', 'rfc', 'id_usuario'],
|
|
});
|
|
|
|
const setCuentas = new Set(existentes.map((u) => u.num_cuenta));
|
|
const setRfcs = new Set(existentes.map((u) => u.rfc));
|
|
|
|
// 🔹 2. Pre-cargar catálogos
|
|
const generosUnicos = [
|
|
...new Set(rows.map((r) => r.sexo).filter(Boolean)),
|
|
];
|
|
const carrerasUnicasClave = [
|
|
...new Set(rows.map((r) => r.clave).filter(Boolean)),
|
|
];
|
|
const carrerasUnicasNombre = [
|
|
...new Set(rows.map((r) => r.nomCarr).filter(Boolean)),
|
|
];
|
|
const tiposUnicos = [
|
|
...new Set(rows.map((r) => r.tipo?.trim()).filter(Boolean)),
|
|
];
|
|
|
|
const [generosExist, carrerasExist, tiposExist] = await Promise.all([
|
|
queryRunner.manager.find(Genero, {
|
|
where: { genero: In(generosUnicos) },
|
|
}),
|
|
queryRunner.manager.find(Carrera, {
|
|
where: [
|
|
{ clave: In(carrerasUnicasClave) },
|
|
{ carrera: In(carrerasUnicasNombre) },
|
|
],
|
|
}),
|
|
queryRunner.manager.find(TipoUsuario, {
|
|
where: { tipo_usuario: In(tiposUnicos) },
|
|
}),
|
|
]);
|
|
|
|
const mapGenero = new Map(generosExist.map((g) => [g.genero, g]));
|
|
const mapCarreraClave = new Map(carrerasExist.map((c) => [c.clave, c]));
|
|
const mapCarreraNom = new Map(carrerasExist.map((c) => [c.carrera, c]));
|
|
const mapTipo = new Map(tiposExist.map((t) => [t.tipo_usuario, t]));
|
|
|
|
// 🔹 3. Arreglos para inserciones masivas
|
|
const usuarios: Usuario[] = [];
|
|
const carrerasUsuario: CarreraUsuario[] = [];
|
|
const usuariosTipos: UsuarioTipoUsuario[] = [];
|
|
const serviciosActivos: ServActivos[] = [];
|
|
|
|
let contador = 0;
|
|
|
|
for (const r of rows) {
|
|
if (setCuentas.has(r.cuenta) || (r.rfc && setRfcs.has(r.rfc))) {
|
|
const use= existentes.find( u => u.num_cuenta === r.cuenta || u.rfc === r.rfc)
|
|
if(use?.id_usuario)
|
|
idsUsuarios.push(use?.id_usuario)
|
|
contador++;
|
|
continue;
|
|
}
|
|
|
|
if (!r.tipo || !r.nombres || !r.apellidopa || !r.apellidoma) {
|
|
errs.push(`Fila con cuenta ${r.cuenta} tiene campos faltantes.`);
|
|
contador++;
|
|
continue;
|
|
}
|
|
|
|
// Genero
|
|
let genero = mapGenero.get(r.sexo);
|
|
if (!genero) {
|
|
genero = queryRunner.manager.create(Genero, { genero: r.sexo });
|
|
mapGenero.set(r.sexo, genero);
|
|
}
|
|
|
|
// Carrera
|
|
let carrera: Carrera | undefined;
|
|
if (r.clave && mapCarreraClave.has(r.clave)) {
|
|
carrera = mapCarreraClave.get(r.clave);
|
|
} else if (r.nomCarr && mapCarreraNom.has(r.nomCarr)) {
|
|
carrera = mapCarreraNom.get(r.nomCarr);
|
|
} else if (r.clave || r.nomCarr) {
|
|
carrera = queryRunner.manager.create(Carrera, {
|
|
clave: r.clave ?? '',
|
|
carrera: r.nomCarr?.slice(0, 100) ?? '',
|
|
});
|
|
mapCarreraClave.set(carrera.clave, carrera);
|
|
mapCarreraNom.set(carrera.carrera, carrera);
|
|
}
|
|
|
|
// Tipo
|
|
let tipo = mapTipo.get(r.tipo.trim());
|
|
if (!tipo) {
|
|
tipo = queryRunner.manager.create(TipoUsuario, {
|
|
tipo_usuario: r.tipo.trim(),
|
|
});
|
|
mapTipo.set(r.tipo.trim(), 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,
|
|
a_paterno: r.apellidopa,
|
|
a_materno: r.apellidoma,
|
|
rfc: r.rfc,
|
|
fecha_nacimiento: r.fechnac,
|
|
generacion,
|
|
genero,
|
|
movimiento,
|
|
});
|
|
|
|
usuarios.push(usuario);
|
|
|
|
if (carrera) {
|
|
const carreraUsuario = queryRunner.manager.create(CarreraUsuario, {
|
|
usuario,
|
|
carrera,
|
|
});
|
|
carrerasUsuario.push(carreraUsuario);
|
|
}
|
|
|
|
usuariosTipos.push(
|
|
queryRunner.manager.create(UsuarioTipoUsuario, {
|
|
usuario,
|
|
tipoUsuario: tipo,
|
|
}),
|
|
);
|
|
|
|
// Servicios activos
|
|
const servData: Partial<ServActivos> = {
|
|
usuario,
|
|
RedStatus: 'Inactivo',
|
|
ATStatus: 'Inactivo',
|
|
CorreoStatus: 'Inactivo',
|
|
PrestamosStatus: 'Inactivo',
|
|
};
|
|
|
|
switch (r.tipo.trim()) {
|
|
case 'Diplomado':
|
|
Object.assign(servData, { Correo: true });
|
|
conteoTiposCorreo++;
|
|
break;
|
|
case 'Extra Largo':
|
|
Object.assign(servData, { Correo: true, Prestamos: true });
|
|
conteoTiposCorreo++;
|
|
conteoTiposSolicita++;
|
|
break;
|
|
case 'Servicio Social':
|
|
Object.assign(servData, { Correo: true, AT: true, Red: true });
|
|
conteoTiposCorreo++;
|
|
conteoTiposAt++;
|
|
conteoTiposRed++;
|
|
break;
|
|
case 'Idiomas R (UNAM)':
|
|
case 'Idiomas Sabatino':
|
|
Object.assign(servData, { Correo: true, Red: true });
|
|
conteoTiposCorreo++;
|
|
conteoTiposRed++;
|
|
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,
|
|
});
|
|
conteoTiposCorreo++;
|
|
conteoTiposRed++;
|
|
conteoTiposAt++;
|
|
conteoTiposSolicita++;
|
|
break;
|
|
case 'Trabajadores':
|
|
Object.assign(servData, { Red: true });
|
|
conteoTiposRed++;
|
|
break;
|
|
}
|
|
|
|
serviciosActivos.push(
|
|
queryRunner.manager.create(ServActivos, servData),
|
|
);
|
|
}
|
|
|
|
// 🔹 4. Guardar en batch (orden correcto)
|
|
await queryRunner.manager.save([...mapGenero.values()]);
|
|
await queryRunner.manager.save([...mapCarreraClave.values()]);
|
|
await queryRunner.manager.save([...mapTipo.values()]);
|
|
|
|
// 1️⃣ Guardar usuarios primero — asigna IDs
|
|
await queryRunner.manager.save(usuarios);
|
|
idsUsuarios = usuarios.map(u => u.id_usuario);
|
|
idsUsuarios.push(
|
|
...existentes.map(e => e.id_usuario)
|
|
);
|
|
// 2️⃣ Reconstruir relaciones con usuarios ya guardados
|
|
|
|
// CarreraUsuario
|
|
const carrerasUsuarioFinal: CarreraUsuario[] = [];
|
|
for (const cu of carrerasUsuario) {
|
|
const usuarioConId = usuarios.find(
|
|
(u) => u.num_cuenta === cu.usuario.num_cuenta,
|
|
);
|
|
if (usuarioConId && cu.carrera) {
|
|
carrerasUsuarioFinal.push(
|
|
queryRunner.manager.create(CarreraUsuario, {
|
|
usuario: usuarioConId,
|
|
carrera: cu.carrera,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
// UsuarioTipoUsuario
|
|
const usuariosTiposFinal: UsuarioTipoUsuario[] = [];
|
|
for (const ut of usuariosTipos) {
|
|
const usuarioConId = usuarios.find(
|
|
(u) => u.num_cuenta === ut.usuario.num_cuenta,
|
|
);
|
|
if (usuarioConId && ut.tipoUsuario) {
|
|
usuariosTiposFinal.push(
|
|
queryRunner.manager.create(UsuarioTipoUsuario, {
|
|
usuario: usuarioConId,
|
|
tipoUsuario: ut.tipoUsuario,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ServActivos
|
|
const serviciosActivosFinal: ServActivos[] = [];
|
|
for (const sa of serviciosActivos) {
|
|
const usuarioConId = usuarios.find(
|
|
(u) => u.num_cuenta === sa.usuario.num_cuenta,
|
|
);
|
|
if (usuarioConId) {
|
|
serviciosActivosFinal.push(
|
|
queryRunner.manager.create(ServActivos, {
|
|
...sa,
|
|
usuario: usuarioConId,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 3️⃣ Guardar relaciones dependientes en orden
|
|
await queryRunner.manager.save(carrerasUsuarioFinal);
|
|
await queryRunner.manager.save(usuariosTiposFinal);
|
|
await queryRunner.manager.save(serviciosActivosFinal);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
inserted: rows.length - contador,
|
|
id_movimiento: movimiento.id_mov,
|
|
errors: errs,
|
|
conteoTiposAt,
|
|
conteoTiposCorreo,
|
|
conteoTiposRed,
|
|
conteoTiposSolicita,
|
|
};
|
|
} catch (err) {
|
|
await queryRunner.rollbackTransaction();
|
|
throw err;
|
|
} finally {
|
|
await queryRunner.release();
|
|
|
|
await this.servActivosRepo
|
|
.createQueryBuilder()
|
|
.update()
|
|
.set({
|
|
Red: false,
|
|
Prestamos: false,
|
|
AT: false,
|
|
Correo: false,
|
|
RedStatus: 'Inactivo',
|
|
PrestamosStatus: 'Inactivo',
|
|
ATStatus: 'Inactivo',
|
|
CorreoStatus: 'Inactivo',
|
|
})
|
|
.where('id_usuario NOT IN (SELECT id_usuario FROM usuario)')
|
|
.execute();
|
|
|
|
|
|
}
|
|
}
|
|
|
|
// 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',
|
|
Correo: true,
|
|
},
|
|
},
|
|
relations: [
|
|
'genero',
|
|
'carreraUsuarios',
|
|
'carreraUsuarios.carrera',
|
|
'usuarioTipos',
|
|
'usuarioTipos.tipoUsuario',
|
|
],
|
|
});
|
|
|
|
const header =
|
|
[
|
|
'num_cuenta',
|
|
'nombre',
|
|
'a_paterno',
|
|
'a_materno',
|
|
'carreras',
|
|
'rfc',
|
|
'fecha_nacimiento',
|
|
'generacion',
|
|
'tipos_usuario',
|
|
].join(',') + '\n';
|
|
|
|
const rows = usuarios
|
|
.map((u) => {
|
|
const carreras = (u.carreraUsuarios ?? [])
|
|
.map((cu) => cu.carrera?.carrera ?? '')
|
|
.join(' | ');
|
|
|
|
const tiposUsuario = (u.usuarioTipos ?? [])
|
|
.map((utu) => utu.tipoUsuario?.tipo_usuario ?? '')
|
|
.join(' | ');
|
|
|
|
return [
|
|
u.num_cuenta ?? '',
|
|
u.nombre ?? '',
|
|
u.a_paterno ?? '',
|
|
u.a_materno ?? '',
|
|
carreras,
|
|
u.rfc ?? '',
|
|
u.fecha_nacimiento ?? '',
|
|
u.generacion?.toString() ?? '',
|
|
tiposUsuario,
|
|
].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
|
|
'usuarioTipos',
|
|
'usuarioTipos.tipoUsuario',
|
|
],
|
|
});
|
|
|
|
const header =
|
|
[
|
|
'num_cuenta',
|
|
'nombre',
|
|
'a_paterno',
|
|
'a_materno',
|
|
'carreras',
|
|
'rfc',
|
|
'tipos_usuario',
|
|
].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
|
|
|
|
const tiposUsuario = (u.usuarioTipos ?? [])
|
|
.map((utu) => utu.tipoUsuario?.tipo_usuario ?? '')
|
|
.join(' | '); // une todos los tipos de usuario en un solo string
|
|
|
|
return [
|
|
u.num_cuenta ?? '',
|
|
u.nombre ?? '',
|
|
u.a_paterno ?? '',
|
|
u.a_materno ?? '',
|
|
carreras ?? '',
|
|
u.rfc ?? '',
|
|
tiposUsuario,
|
|
].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',
|
|
'Caso Especial',
|
|
];
|
|
|
|
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
|
|
},
|
|
},
|
|
relations: ['usuarioTipos', 'usuarioTipos.tipoUsuario'],
|
|
});
|
|
|
|
const header =
|
|
['num_cuenta', 'fecha_nacimiento', 'contraseña', 'tipos_usuario'].join(
|
|
',',
|
|
) + '\n';
|
|
|
|
const rows = usuarios
|
|
.map((u) => {
|
|
const tiposUsuario = (u.usuarioTipos ?? [])
|
|
.map((utu) => utu.tipoUsuario?.tipo_usuario ?? '')
|
|
.join(' | ');
|
|
|
|
return [
|
|
u.num_cuenta,
|
|
u.fecha_nacimiento,
|
|
u.contraseña ?? '',
|
|
tiposUsuario,
|
|
].join(',');
|
|
})
|
|
.join('\n');
|
|
|
|
const tablaCompleta = header + rows;
|
|
|
|
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
|
|
'usuarioTipos',
|
|
'usuarioTipos.tipoUsuario',
|
|
],
|
|
});
|
|
|
|
const header =
|
|
[
|
|
'num_cuenta',
|
|
'nombre',
|
|
'a_paterno',
|
|
'a_materno',
|
|
'carreras',
|
|
'fecha_nacimiento',
|
|
'generacion',
|
|
'tipos_usuario',
|
|
].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
|
|
|
|
const tiposUsuario = (u.usuarioTipos ?? [])
|
|
.map((utu) => utu.tipoUsuario?.tipo_usuario ?? '')
|
|
.join(' | '); // une todos los tipos de usuario en un solo string
|
|
|
|
return [
|
|
u.num_cuenta ?? '',
|
|
u.nombre ?? '',
|
|
u.a_paterno ?? '',
|
|
u.a_materno ?? '',
|
|
carreras ?? '',
|
|
u.fecha_nacimiento ?? '',
|
|
u.generacion?.toString() ?? '',
|
|
tiposUsuario,
|
|
].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);
|
|
}
|
|
}
|
|
|
|
async enviarCargaMasiva(data?: {
|
|
nuevos: number;
|
|
actualizados: number;
|
|
}): Promise<void> {
|
|
// Aquí puedes enviar el correo usnado MailService
|
|
|
|
this.enviarInforme(
|
|
'RED',
|
|
'Carga de Usuarios',
|
|
`Se ha realizado una carga de usuarios en el sistema. Nuevos: ${data?.nuevos ?? 0}, Actualizados: ${data?.actualizados ?? 0}`,
|
|
);
|
|
this.enviarInforme(
|
|
'AT',
|
|
'Carga de Usuarios',
|
|
`Se ha realizado una carga de usuarios en el sistema. Nuevos: ${data?.nuevos ?? 0}, Actualizados: ${data?.actualizados ?? 0}`,
|
|
);
|
|
this.enviarInforme(
|
|
'CORREO',
|
|
'Carga de Usuarios',
|
|
`Se ha realizado una carga de usuarios en el sistema. Nuevos: ${data?.nuevos ?? 0}, Actualizados: ${data?.actualizados ?? 0}`,
|
|
);
|
|
this.enviarInforme(
|
|
'SOLICITA',
|
|
'Carga de Usuarios',
|
|
`Se ha realizado una carga de usuarios en el sistema. Nuevos: ${data?.nuevos ?? 0}, Actualizados: ${data?.actualizados ?? 0}`,
|
|
);
|
|
|
|
}
|
|
|
|
async enviarInforme(
|
|
origen: string,
|
|
subject: string,
|
|
text: string,
|
|
html: string = '',
|
|
): Promise<void> {
|
|
try {
|
|
const user = await this.usuariosDelSistemaRepo.find({
|
|
where: { origen: { origen } },
|
|
});
|
|
user.forEach(async (u) => {
|
|
await this.mailService.sendMail({
|
|
to: u.correo ?? '',
|
|
subject,
|
|
text,
|
|
html,
|
|
});
|
|
});
|
|
} catch (error) {
|
|
throw new BadRequestException(
|
|
'No se encontraron usuarios para enviar correo o no se pudo enviar el correo',
|
|
);
|
|
}
|
|
}
|
|
|
|
async buscaAct() {
|
|
return await this.servActivosRepo.find({
|
|
where: [
|
|
{ AT: true },
|
|
{ Correo: true },
|
|
{ Prestamos: true },
|
|
{ Red: true },
|
|
],
|
|
});
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|