Se agregaron los campos de los que se deben de mandar a origen de usuario

This commit is contained in:
evenegas
2025-06-18 07:33:21 -06:00
parent 8703a292e3
commit 4984c47029
12 changed files with 598 additions and 229 deletions
+130 -29
View File
@@ -3,7 +3,7 @@ import { Controller, Post, Get, UseGuards, Request, Res, UploadedFile, UseInterc
import { AuthGuard } from '@nestjs/passport';
import { ApiBearerAuth } from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express';
import { Response } from 'express';
import e, { Response } from 'express';
import { ExcelDocumentation } from './excel.documentation';
import { ExcelService } from './excel.service';
import { MovimientoService } from '../movimiento/movimiento.service';
@@ -15,7 +15,7 @@ export class ExcelController {
constructor(
private readonly excelService: ExcelService,
private readonly movimientoService: MovimientoService,
) {}
) { }
@Post('verify')
@UseInterceptors(FileInterceptor('file'))
@@ -24,7 +24,7 @@ export class ExcelController {
const userId: number = req.user.userId; // ahora sí existe
const errors = await this.excelService.validateFile(file.buffer);
await this.movimientoService.log(
userId,
'VERIFY',
errors.length ? 'FAILED' : 'SUCCESS',
errors.join('; ')
@@ -35,12 +35,20 @@ export class ExcelController {
@Post('load')
@UseInterceptors(FileInterceptor('file'))
@ExcelDocumentation.loadExcel()
async loadExcel(@UploadedFile() file: Express.Multer.File, @Request() req) {
const userId: number = req.user.userId;
async loadExcel(@Request() req, @UploadedFile() file: Express.Multer.File) {
const origen = req.user.origen; // ahora sí existe
if (origen != 'LOAD') {
await this.movimientoService.log(
'LOAD',
'FAILED',
'Origen no permitido para carga'
);
throw new Error('Origen no permitido para carga.');
}
try {
const result = await this.excelService.loadFile(file.buffer);
await this.movimientoService.log(
userId,
'LOAD',
'SUCCESS',
undefined,
@@ -49,7 +57,7 @@ export class ExcelController {
return result;
} catch (err) {
await this.movimientoService.log(
userId,
'LOAD',
'FAILED',
err.message
@@ -61,31 +69,124 @@ export class ExcelController {
@Get('download')
@ExcelDocumentation.downloadExcel()
async downloadData(@Request() req, @Res() res: Response) {
const userId: number = req.user.userId;
try {
const csv = await this.excelService.generateCsv();
const origen = req.user.origen; // ahora sí existe
console.log('Origen de descarga:', origen);
console.log('Usuario:', req.user);
if (origen == 'SOLICITA') {
try {
const csv = await this.excelService.generateCsvSolicita();
await this.movimientoService.log(
origen,
'SUCCESS',
undefined,
`size=${csv.length}`
);
return res
.status(HttpStatus.OK)
.header('Content-Type', 'text/tab-separated-values')
.header('Content-Disposition', 'attachment; filename="usuarios_solicita.tsv"')
.send(csv);
} catch (err) {
await this.movimientoService.log(
origen,
'FAILED',
'Origen no permitido para descarga'
);
return res
.status(HttpStatus.FORBIDDEN)
.json({ statusCode: 403, message: 'Origen no permitido para descarga.' });
}
} else if (origen == 'CORREO') {
try {
const csv = await this.excelService.generateCsvCorreo();
await this.movimientoService.log(
origen,
'SUCCESS',
undefined,
`size=${csv.length}`
);
return res
.status(HttpStatus.OK)
.header('Content-Type', 'text/tab-separated-values')
.header('Content-Disposition', 'attachment; filename="usuarios.tsv"')
.send(csv);
} catch (err) {
await this.movimientoService.log(
origen,
'FAILED',
err.message
);
return res
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.json({ statusCode: 500, message: 'Error generando descarga.' });
}
} else if (origen == 'AT') {
try {
const csv = await this.excelService.generateCsvAT();
await this.movimientoService.log(
origen,
'SUCCESS',
undefined,
`size=${csv.length}`
);
return res
.status(HttpStatus.OK)
.header('Content-Type', 'text/tab-separated-values')
.header('Content-Disposition', 'attachment; filename="usuarios_at.tsv"')
.send(csv);
} catch (err) {
await this.movimientoService.log(
origen,
'FAILED',
err.message
);
return res
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.json({ statusCode: 500, message: 'Error generando descarga.' });
}
} else if (origen == 'RED') {
try {
const csv = await this.excelService.generateCsvRed();
await this.movimientoService.log(
origen,
'SUCCESS',
undefined,
`size=${csv.length}`
);
return res
.status(HttpStatus.OK)
.header('Content-Type', 'text/tab-separated-values')
.header('Content-Disposition', 'attachment; filename="usuarios_red.tsv"')
.send(csv);
} catch (err) {
await this.movimientoService.log(
origen,
'FAILED',
err.message
);
return res
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.json({ statusCode: 500, message: 'Error generando descarga.' });
}
} else {
await this.movimientoService.log(
userId,
'DOWNLOAD',
'SUCCESS',
undefined,
`size=${csv.length}`
);
return res
.status(HttpStatus.OK)
.header('Content-Type', 'text/tab-separated-values')
.header('Content-Disposition', 'attachment; filename="usuarios.tsv"')
.send(csv);
} catch (err) {
await this.movimientoService.log(
userId,
'DOWNLOAD',
origen,
'FAILED',
err.message
'Origen no permitido para descarga'
);
return res
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.json({ statusCode: 500, message: 'Error generando descarga.' });
.status(HttpStatus.FORBIDDEN)
.json({ statusCode: 403, message: 'Origen no permitido para descarga.' });
}
}
}
}
+332 -76
View File
@@ -1,7 +1,7 @@
// src/excel/excel.service.ts
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
import { Workbook } from 'exceljs';
import { Repository } from 'typeorm';
import { In, Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Usuario } from '../entities/entities';
import { Genero } from '../entities/entities';
@@ -35,7 +35,7 @@ export class ExcelService {
@InjectRepository(Carrera)
private readonly carreraRepo: Repository<Carrera>,
// … 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[]> {
@@ -92,54 +92,54 @@ export class ExcelService {
}
/** Valida duplicados, celdas vacías y patrones básicos */
validateRows(rows: UsuarioRow[]) {
const errors: string[] = [];
const seen = new Set<string>();
validateRows(rows: UsuarioRow[]) {
const errors: string[] = [];
const seen = new Set<string>();
rows.forEach((r, i) => {
const rowNum = i + 2; // por el encabezado
rows.forEach((r, i) => {
const rowNum = i + 2; // por el encabezado
// Campos que no pueden quedar vacíos
['cuenta','nombreCompleto','clave','fechnac'].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.`);
// 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.`);
}
});
// 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
if (!/^[0-9]+$/.test(cuentaStr)) {
errors.push(`Fila ${rowNum}: cuenta debe ser numérica.`);
}
const genStr = r.gen != null ? r.gen.toString().trim() : '';
if (!/^[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;
}
return errors;
}
/**
@@ -181,6 +181,9 @@ export class ExcelService {
await this.carreraRepo.save(carrera);
}
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,
@@ -189,8 +192,9 @@ export class ExcelService {
a_materno: r.apellidoma,
rfc: r.rfc,
fecha_nacimiento: r.fechnac,
generacion: parseInt(r.gen, 10),
generacion: generacion,
genero,
});
await this.usuarioRepo.save(usuario);
@@ -207,48 +211,300 @@ export class ExcelService {
// 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
],
select: {
num_cuenta: true,
nombre: true,
a_paterno: true,
a_materno: true,
rfc: true,
fecha_nacimiento: true,
generacion: true,
carreraUsuarios: true,
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','correo','rfc','fecha_nacimiento','generacion' ],
},
});
const header = [
'num_cuenta','nombre','a_paterno','a_materno',
'correo','rfc','fecha_nacimiento','generacion'
'num_cuenta', 'nombre', 'a_paterno', 'a_materno',
'carreras', 'rfc', 'fecha_nacimiento', 'generacion'
].join('\t') + '\n';
const rows = users.map(u => [
u.num_cuenta,
u.nombre,
u.a_paterno,
u.a_materno,
u.correo ?? '',
u.rfc ?? '',
u.fecha_nacimiento,
(u.generacion != null ? u.generacion.toString() : '')
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
].join('\t')).join('\n');
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');
return header + rows;
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),
},
},
},
relations: [
'genero',
'carreraUsuarios', // relación intermedia
'carreraUsuarios.carrera', // carrera asociada a través de la intermedia
],
select: {
num_cuenta: true,
nombre: true,
a_paterno: true,
a_materno: true,
rfc: true,
carreraUsuarios: true,
},
});
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<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
],
select: {
num_cuenta: true,
fecha_nacimiento: true,
},
});
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<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
],
select: {
num_cuenta: true,
nombre: true,
a_paterno: true,
a_materno: true,
fecha_nacimiento: true,
generacion: true,
carreraUsuarios: true,
},
});
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;
}