2025-06-13 17:54:38 -06:00
|
|
|
|
// src/excel/excel.service.ts
|
|
|
|
|
|
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
|
|
|
|
|
|
import { Workbook } from 'exceljs';
|
2025-06-18 07:33:21 -06:00
|
|
|
|
import { In, Repository } from 'typeorm';
|
2025-06-13 17:54:38 -06:00
|
|
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
2025-06-18 12:54:26 -06:00
|
|
|
|
import { CarreraUsuario, TipoUsuario, Usuario, UsuarioTipoUsuario } from '../entities/entities';
|
2025-06-13 17:54:38 -06:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 16:23:01 -06:00
|
|
|
|
type RuleFn = (r: UsuarioRow, rowNum: number, errors: string[]) => void;
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-13 17:54:38 -06:00
|
|
|
|
@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>,
|
2025-06-18 12:54:26 -06:00
|
|
|
|
@InjectRepository(CarreraUsuario)
|
|
|
|
|
|
private readonly carreraUsuarioRepo: Repository<CarreraUsuario>,
|
|
|
|
|
|
@InjectRepository(UsuarioTipoUsuario)
|
|
|
|
|
|
private readonly usuarioTipoUsuarioRepo: Repository<UsuarioTipoUsuario>,
|
|
|
|
|
|
@InjectRepository(TipoUsuario)
|
|
|
|
|
|
private readonly tipoUsuarioRepo: Repository<TipoUsuario>,
|
2025-06-13 17:54:38 -06:00
|
|
|
|
// … inyecta otros repositorios si los usarás
|
2025-06-18 07:33:21 -06:00
|
|
|
|
) { }
|
2025-06-13 17:54:38 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Valida duplicados, celdas vacías y patrones básicos */
|
2025-06-18 07:33:21 -06:00
|
|
|
|
validateRows(rows: UsuarioRow[]) {
|
|
|
|
|
|
const errors: string[] = [];
|
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
2025-06-13 17:54:38 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
// Numérico en cuenta y generación
|
2025-06-13 17:54:38 -06:00
|
|
|
|
|
|
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
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}".`);
|
|
|
|
|
|
}
|
2025-06-13 17:54:38 -06:00
|
|
|
|
|
|
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
2025-06-13 17:54:38 -06:00
|
|
|
|
|
|
|
|
|
|
|
2025-06-18 16:23:01 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Describes which rules aplicar por tipo
|
|
|
|
|
|
private typeRules: Record<string, RuleFn[]> = {
|
|
|
|
|
|
// Regla común para todos los tipos
|
|
|
|
|
|
__default: [
|
|
|
|
|
|
this.ruleNonEmpty(['cuenta', 'nombreCompleto', 'tipo']),
|
|
|
|
|
|
this.ruleUniqueCuenta(),
|
|
|
|
|
|
this.ruleNumericField('gen', /^[0-9]{4}$/),
|
|
|
|
|
|
this.ruleDateField('fechnac'),
|
|
|
|
|
|
this.ruleRFC('rfc'),
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
|
|
// Añade reglas extra para Licenciatura
|
|
|
|
|
|
Licenciatura: [
|
|
|
|
|
|
this.ruleNonEmpty(['clave', 'nomCarr']),
|
|
|
|
|
|
(r, rowNum, errors) => {
|
|
|
|
|
|
if (!/^[0-9]{5}$/.test(r.clave)) {
|
|
|
|
|
|
errors.push(`Fila ${rowNum}: clave de Licenciatura debe tener 5 dígitos.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
|
|
// Añade reglas extra para Profesor
|
|
|
|
|
|
Profesor: [
|
|
|
|
|
|
(r, rowNum, errors) => {
|
|
|
|
|
|
if (!r.correo?.includes('@')) {
|
|
|
|
|
|
errors.push(`Fila ${rowNum}: Profesor debe tener correo válido.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
|
|
// … puedes definir más tipos aquí …
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** Métodos auxiliares que devuelven funciones de regla */
|
|
|
|
|
|
private ruleNonEmpty(fields: (keyof UsuarioRow)[]): RuleFn {
|
|
|
|
|
|
return (r, rowNum, errors) => {
|
|
|
|
|
|
for (const f of fields) {
|
|
|
|
|
|
const v = (r[f] ?? '').toString().trim();
|
|
|
|
|
|
if (!v) {
|
|
|
|
|
|
errors.push(`Fila ${rowNum}: campo "${f}" está vacío.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private ruleUniqueCuenta(): RuleFn {
|
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
|
return (r, rowNum, errors) => {
|
|
|
|
|
|
const c = (r.cuenta ?? '').toString().trim();
|
|
|
|
|
|
if (seen.has(c)) {
|
|
|
|
|
|
errors.push(`Fila ${rowNum}: cuenta duplicada "${c}".`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
seen.add(c);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private ruleNumericField(field: keyof UsuarioRow, regex: RegExp): RuleFn {
|
|
|
|
|
|
return (r, rowNum, errors) => {
|
|
|
|
|
|
const v = (r[field] ?? '').toString().trim();
|
|
|
|
|
|
if (v && !regex.test(v)) {
|
|
|
|
|
|
errors.push(`Fila ${rowNum}: campo "${field}" inválido: "${v}".`);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private ruleDateField(field: keyof UsuarioRow): RuleFn {
|
|
|
|
|
|
return (r, rowNum, errors) => {
|
|
|
|
|
|
const v = (r[field] ?? '').toString().trim();
|
|
|
|
|
|
if (v && !/^[0-9]{8}$/.test(v)) {
|
|
|
|
|
|
errors.push(`Fila ${rowNum}: fecha "${field}" debe ser YYYYMMDD: "${v}".`);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private ruleRFC(field: keyof UsuarioRow): RuleFn {
|
|
|
|
|
|
return (r, rowNum, errors) => {
|
|
|
|
|
|
const v = (r[field] ?? '').toString().trim();
|
|
|
|
|
|
if (v && !/^[A-Z0-9]+$/.test(v)) {
|
|
|
|
|
|
errors.push(`Fila ${rowNum}: RFC contiene caracteres inválidos: "${v}".`);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 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[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
sheet.eachRow((row, idx) => {
|
|
|
|
|
|
if (idx === 1) return; // salto encabezados
|
|
|
|
|
|
const vals = row.values as any[];
|
|
|
|
|
|
rows.push({
|
|
|
|
|
|
cuenta: vals[1] ?? '',
|
|
|
|
|
|
nombreCompleto: vals[2] ?? '',
|
|
|
|
|
|
clave: vals[3] ?? '',
|
|
|
|
|
|
nomCarr: vals[4] ?? '',
|
|
|
|
|
|
gen: vals[5] ?? '',
|
|
|
|
|
|
fechnac: vals[6] ?? '',
|
|
|
|
|
|
apellidopa: vals[7] ?? '',
|
|
|
|
|
|
apellidoma: vals[8] ?? '',
|
|
|
|
|
|
nombres: vals[9] ?? '',
|
|
|
|
|
|
sexo: vals[10] ?? '',
|
|
|
|
|
|
tipo: vals[11] ?? '',
|
|
|
|
|
|
correo: vals[12] ?? '',
|
|
|
|
|
|
rfc: vals[13] ?? '',
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return rows;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Valida cada fila según reglas comunes y específicas por tipo.
|
|
|
|
|
|
* Retorna un array de mensajes de error.
|
|
|
|
|
|
*/
|
|
|
|
|
|
validateRowsAdvanced(rows: UsuarioRow[]): string[] {
|
|
|
|
|
|
const errors: string[] = [];
|
|
|
|
|
|
rows.forEach((r, i) => {
|
|
|
|
|
|
const rowNum = i + 2;
|
|
|
|
|
|
// 1) Aplica reglas comunes
|
|
|
|
|
|
for (const rule of this.typeRules.__default) {
|
|
|
|
|
|
rule.call(this, r, rowNum, errors);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 2) Aplica reglas del tipo específico
|
|
|
|
|
|
const specificRules = this.typeRules[r.tipo] ?? [];
|
|
|
|
|
|
for (const rule of specificRules) {
|
|
|
|
|
|
rule.call(this, r, rowNum, errors);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return errors;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-13 17:54:38 -06:00
|
|
|
|
/**
|
|
|
|
|
|
* 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 12:54:26 -06:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
const generacion = /^[0-9]{4}$/.test(r.gen?.toString().trim())
|
|
|
|
|
|
? parseInt(r.gen.toString().trim(), 10)
|
|
|
|
|
|
: null;
|
2025-06-13 17:54:38 -06:00
|
|
|
|
// 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,
|
2025-06-18 07:33:21 -06:00
|
|
|
|
generacion: generacion,
|
2025-06-13 17:54:38 -06:00
|
|
|
|
genero,
|
2025-06-18 07:33:21 -06:00
|
|
|
|
|
2025-06-13 17:54:38 -06:00
|
|
|
|
});
|
|
|
|
|
|
await this.usuarioRepo.save(usuario);
|
|
|
|
|
|
|
2025-06-18 12:54:26 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-13 17:54:38 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { inserted: rows.length };
|
|
|
|
|
|
}
|
2025-06-16 11:26:17 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
// 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),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
relations: [
|
|
|
|
|
|
'genero',
|
|
|
|
|
|
'carreraUsuarios', // relación intermedia
|
|
|
|
|
|
'carreraUsuarios.carrera', // carrera asociada a través de la intermedia
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
|
|
});
|
2025-06-16 11:26:17 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
const header = [
|
|
|
|
|
|
'num_cuenta', 'nombre', 'a_paterno', 'a_materno',
|
|
|
|
|
|
'carreras', 'rfc', 'fecha_nacimiento', 'generacion'
|
|
|
|
|
|
].join('\t') + '\n';
|
2025-06-16 11:26:17 -06:00
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-06-16 11:26:17 -06:00
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
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),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
relations: [
|
|
|
|
|
|
'genero',
|
|
|
|
|
|
'carreraUsuarios', // relación intermedia
|
|
|
|
|
|
'carreraUsuarios.carrera', // carrera asociada a través de la intermedia
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
|
|
});
|
2025-06-16 11:26:17 -06:00
|
|
|
|
|
2025-06-18 12:54:26 -06:00
|
|
|
|
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);
|
|
|
|
|
|
|
2025-06-16 11:26:17 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
const header = [
|
|
|
|
|
|
'num_cuenta', 'nombre', 'a_paterno', 'a_materno',
|
|
|
|
|
|
'carreras', 'rfc'
|
|
|
|
|
|
].join('\t') + '\n';
|
2025-06-16 11:26:17 -06:00
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
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
|
2025-06-16 11:26:17 -06:00
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
return [
|
|
|
|
|
|
u.num_cuenta,
|
|
|
|
|
|
u.nombre,
|
|
|
|
|
|
u.a_paterno,
|
|
|
|
|
|
u.a_materno,
|
|
|
|
|
|
carreras,
|
|
|
|
|
|
u.rfc ?? '',
|
|
|
|
|
|
].join('\t');
|
|
|
|
|
|
}).join('\n');
|
2025-06-16 11:26:17 -06:00
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
const tablaCompleta = header + rows;
|
2025-06-16 11:26:17 -06:00
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
return tablaCompleta;
|
|
|
|
|
|
}
|
2025-06-16 11:26:17 -06:00
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
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),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
relations: [
|
|
|
|
|
|
'genero',
|
|
|
|
|
|
'carreraUsuarios', // relación intermedia
|
|
|
|
|
|
'carreraUsuarios.carrera', // carrera asociada a través de la intermedia
|
|
|
|
|
|
],
|
2025-06-18 12:54:26 -06:00
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
});
|
2025-06-16 11:26:17 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const header = [
|
2025-06-18 07:33:21 -06:00
|
|
|
|
'num_cuenta', 'fecha_nacimiento'
|
2025-06-16 11:26:17 -06:00
|
|
|
|
].join('\t') + '\n';
|
|
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
const rows = usuarios.map(u => {
|
|
|
|
|
|
return [
|
|
|
|
|
|
u.num_cuenta,
|
|
|
|
|
|
u.fecha_nacimiento,
|
|
|
|
|
|
].join('\t');
|
|
|
|
|
|
}).join('\n');
|
|
|
|
|
|
|
|
|
|
|
|
const tablaCompleta = header + rows;
|
2025-06-16 11:26:17 -06:00
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
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),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
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';
|
2025-06-16 11:26:17 -06:00
|
|
|
|
|
2025-06-18 07:33:21 -06:00
|
|
|
|
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;
|
2025-06-16 11:26:17 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-13 17:54:38 -06:00
|
|
|
|
}
|