Se agregaron los campos de los que se deben de mandar a origen de usuario
This commit is contained in:
+130
-29
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user