se cambio función

This commit is contained in:
Emilio-11
2026-02-06 13:49:36 -06:00
parent a288c37531
commit 2e7d01552d
5 changed files with 234 additions and 262 deletions
+1 -2
View File
@@ -1,4 +1,3 @@
services:
database:
image: mariadb:latest
@@ -12,7 +11,7 @@ services:
env_file:
- ../.env
ports:
- "${DB_PORT}:3306"
- '${DB_PORT}:3306'
volumes:
- mariadb_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
+26 -36
View File
@@ -37,11 +37,10 @@ export class ExcelController {
private readonly movimientoService: MovimientoService,
private readonly mailService: MailService,
private readonly usuarioService: UsuariosService, // Asegúrate de importar el servicio de correo
) { }
) {}
@Post('carga')
async cargaUsuario(@Request() req, @Body() usuario: CreateUsuarioDto) {
if (req.user.origen != 'LOAD' && req.user.origen != 'EXTERNO') {
throw new Error('Origen no permitido para carga');
}
@@ -110,7 +109,11 @@ export class ExcelController {
@Post('load')
@UseInterceptors(FileInterceptor('file'))
@ExcelDocumentation.loadExcel()
async loadExcel(@Request() req, @UploadedFile() file: Express.Multer.File, @Query('function') funcion?: number) {
async loadExcel(
@Request() req,
@UploadedFile() file: Express.Multer.File,
@Query('function') funcion?: number,
) {
const origen = req.user.origen; // ahora sí existe
if (origen != 'LOAD') {
@@ -123,19 +126,12 @@ export class ExcelController {
}
try {
let result;
if(funcion==2){
result = await this.excelService.loadFile2(
file.buffer,
req.user.email,
);
}else{
result = await this.excelService.loadFile(
file.buffer,
req.user.email,
);
if (funcion == 2) {
result = await this.excelService.loadFile2(file.buffer, req.user.email);
} else {
result = await this.excelService.loadFile(file.buffer, req.user.email);
}
//Actualizar el estado de los movimientos
if (!result) {
this.mailService.sendMail({
@@ -248,12 +244,10 @@ export class ExcelController {
'FAILED',
'Origen no permitido para descarga',
);
return res
.status(HttpStatus.FORBIDDEN)
.json({
statusCode: 403,
message: 'Origen no permitido para descarga.',
});
return res.status(HttpStatus.FORBIDDEN).json({
statusCode: 403,
message: 'Origen no permitido para descarga.',
});
}
} else if (origen == 'CORREO' || origen == 'LOAD') {
try {
@@ -334,17 +328,16 @@ export class ExcelController {
'FAILED',
'Origen no permitido para descarga',
);
return res
.status(HttpStatus.FORBIDDEN)
.json({
statusCode: 403,
message: 'Origen no permitido para descarga.',
});
return res.status(HttpStatus.FORBIDDEN).json({
statusCode: 403,
message: 'Origen no permitido para descarga.',
});
}
}
//Agregarlo en su propio controller
@Get('movimientos')
@ExcelDocumentation.getMovimientos()
async getMovimientos(
@Request() req,
@Query('page') page: number,
@@ -369,18 +362,15 @@ export class ExcelController {
return { message: 'Servicios activados correctamente' };
}
@Get('cargaIndividual')
async cargarIndividual(@Request() req) {
const origen = req.user.origen; // ahora sí existe
return this.movimientoService.findAllCargaIndv(origen);
}
@Get('serv_act')
async serv(){
return await this.excelService.buscaAct()
async serv() {
return await this.excelService.buscaAct();
}
@Post('mov/:id_movimiento')
async MOV(@Param('id_movimiento', ParseIntPipe) id_movimiento: number) {
return await this.movimientoService.findAllWithRelations(id_movimiento);
}
// @Delete()
// async borrar(){
// return await this.excelService.borrarUsers()
+81 -50
View File
@@ -1,16 +1,55 @@
import { applyDecorators } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiConsumes, ApiBody, ApiResponse } from '@nestjs/swagger';
import {
ApiTags,
ApiOperation,
ApiConsumes,
ApiBody,
ApiResponse,
ApiBearerAuth,
ApiQuery,
} from '@nestjs/swagger';
export class ExcelDocumentation {
/**
* Decorators Swagger para el endpoint POST /excel/verify
*/
static getMovimientos() {
return applyDecorators(
ApiBearerAuth(),
ApiOperation({
summary: 'Obtener movimientos',
description:
'Obtiene un listado paginado de movimientos según el origen del usuario autenticado.',
}),
ApiQuery({
name: 'page',
required: false,
type: Number,
example: 1,
description: 'Número de página',
}),
ApiQuery({
name: 'limit',
required: false,
type: Number,
example: 10,
description: 'Cantidad de registros por página',
}),
ApiResponse({
status: 200,
description: 'Listado de movimientos obtenido correctamente',
}),
ApiResponse({
status: 401,
description: 'No autorizado',
}),
);
}
static verifyExcel() {
return applyDecorators(
ApiTags('Excel'),
ApiOperation({
summary: 'Verificar archivo Excel',
description: 'Valida duplicados, filas en blanco y formato básico de los datos sin realizar carga en la base.'
description:
'Valida duplicados, filas en blanco y formato básico de los datos sin realizar carga en la base.',
}),
ApiConsumes('multipart/form-data'),
ApiBody({
@@ -20,10 +59,10 @@ export class ExcelDocumentation {
file: {
type: 'string',
format: 'binary',
description: 'Archivo Excel (.xlsx) con usuarios a validar'
}
}
}
description: 'Archivo Excel (.xlsx) con usuarios a validar',
},
},
},
}),
ApiResponse({
status: 200,
@@ -35,12 +74,18 @@ export class ExcelDocumentation {
errors: {
type: 'array',
items: { type: 'string' },
example: ['Fila 2: cuenta duplicada "42515101".', 'Fila 3: fecha de nacimiento inválida "1988051".']
}
}
}
example: [
'Fila 2: cuenta duplicada "42515101".',
'Fila 3: fecha de nacimiento inválida "1988051".',
],
},
},
},
}),
ApiResponse({
status: 400,
description: 'No se recibió archivo o formato inválido.',
}),
ApiResponse({ status: 400, description: 'No se recibió archivo o formato inválido.' })
);
}
@@ -52,7 +97,8 @@ export class ExcelDocumentation {
ApiTags('Excel'),
ApiOperation({
summary: 'Cargar archivo Excel',
description: 'Valida y persiste los datos en la base de datos. Retorna el número de registros insertados.'
description:
'Valida y persiste los datos en la base de datos. Retorna el número de registros insertados.',
}),
ApiConsumes('multipart/form-data'),
ApiBody({
@@ -62,10 +108,10 @@ export class ExcelDocumentation {
file: {
type: 'string',
format: 'binary',
description: 'Archivo Excel (.xlsx) con usuarios a cargar'
}
}
}
description: 'Archivo Excel (.xlsx) con usuarios a cargar',
},
},
},
}),
ApiResponse({
status: 201,
@@ -73,9 +119,9 @@ export class ExcelDocumentation {
schema: {
type: 'object',
properties: {
inserted: { type: 'number', example: 3 }
}
}
inserted: { type: 'number', example: 3 },
},
},
}),
ApiResponse({
status: 400,
@@ -86,52 +132,37 @@ export class ExcelDocumentation {
errors: {
type: 'array',
items: { type: 'string' },
example: ['Fila 5: rfc contiene caracteres inválidos.']
}
}
}
})
example: ['Fila 5: rfc contiene caracteres inválidos.'],
},
},
},
}),
);
}
static downloadExcel() {
return applyDecorators(
ApiTags('Excel'),
//ApiBearerAuth('bearer'),
ApiOperation({
summary: 'Descargar usuarios',
description: 'Exporta todos los usuarios en un archivo TSV (o CSV) y registra el movimiento.'
description:
'Exporta todos los usuarios en un archivo TSV (o CSV) y registra el movimiento.',
}),
ApiResponse({
status: 200,
description: 'TSV generado correctamente',
content: {
'text/tab-separated-values': {
schema: { type: 'string', example: 'num_cuenta\\tnombre\\t...\\n...' }
}
}
schema: {
type: 'string',
example: 'num_cuenta\\tnombre\\t...\\n...',
},
},
},
}),
ApiResponse({ status: 401, description: 'No autorizado' }),
ApiResponse({ status: 500, description: 'Error al generar descarga' }),
);
}
}
+89 -137
View File
@@ -34,7 +34,6 @@ export interface UsuarioRow {
@Injectable()
export class ExcelService {
private readonly logger = new Logger(ExcelService.name);
constructor(
@@ -59,7 +58,7 @@ export class ExcelService {
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[]> {
@@ -278,9 +277,8 @@ export class ExcelService {
// 🔹 3. Arreglos para inserciones masivas
const usuarios: Usuario[] = [];
const carrerasUsuario: CarreraUsuario[] = [];
const usuariosTipos: UsuarioTipoUsuario[] = [];
const serviciosActivos: ServActivos[] = [];
const usuariosConRow: { usuario: Usuario; row: any }[] = [];
let contador = 0;
@@ -345,50 +343,84 @@ export class ExcelService {
});
usuarios.push(usuario);
usuariosConRow.push({ usuario, row: r });
}
await queryRunner.manager.save([...mapGenero.values()]);
await queryRunner.manager.save([...mapCarreraClave.values()]);
await queryRunner.manager.save([...mapTipo.values()]);
await queryRunner.manager.save(usuarios);
const carrerasUsuarioFinal: CarreraUsuario[] = [];
for (const { usuario, row } of usuariosConRow) {
const carrera =
(row.clave && mapCarreraClave.get(row.clave)) ||
(row.nomCarr && mapCarreraNom.get(row.nomCarr));
if (carrera) {
const carreraUsuario = queryRunner.manager.create(CarreraUsuario, {
usuario,
carrera,
});
carrerasUsuario.push(carreraUsuario);
carrerasUsuarioFinal.push(
queryRunner.manager.create(CarreraUsuario, {
usuario,
carrera,
}),
);
}
}
usuariosTipos.push(
await queryRunner.manager.save(carrerasUsuarioFinal);
const usuariosTiposFinal: UsuarioTipoUsuario[] = [];
for (const { usuario, row } of usuariosConRow) {
const tipo = mapTipo.get(row.tipo.trim());
if (!tipo) continue;
usuariosTiposFinal.push(
queryRunner.manager.create(UsuarioTipoUsuario, {
usuario,
tipoUsuario: tipo,
}),
);
}
// Servicios activos
const servData: Partial<ServActivos> = {
await queryRunner.manager.save(usuariosTiposFinal);
const serviciosActivosFinal: ServActivos[] = [];
for (const { usuario, row } of usuariosConRow) {
const serv = queryRunner.manager.create(ServActivos, {
usuario,
Red: false,
AT: false,
Correo: false,
Prestamos: false,
RedStatus: 'Inactivo',
ATStatus: 'Inactivo',
CorreoStatus: 'Inactivo',
PrestamosStatus: 'Inactivo',
};
});
switch (r.tipo.trim()) {
switch (row.tipo.trim()) {
case 'Diplomado':
Object.assign(servData, { Correo: true });
Object.assign(serv, { Correo: true });
conteoTiposCorreo++;
break;
case 'Extra Largo':
Object.assign(servData, { Correo: true, Prestamos: true });
Object.assign(serv, { Correo: true, Prestamos: true });
conteoTiposCorreo++;
conteoTiposSolicita++;
break;
case 'Servicio Social':
Object.assign(servData, { Correo: true, AT: true, Red: true });
Object.assign(serv, { Correo: true, AT: true, Red: true });
conteoTiposCorreo++;
conteoTiposAt++;
conteoTiposRed++;
break;
case 'Idiomas R (UNAM)':
case 'Idiomas Sabatino':
Object.assign(servData, { Correo: true, Red: true });
Object.assign(serv, { Correo: true, Red: true });
conteoTiposCorreo++;
conteoTiposRed++;
break;
@@ -399,7 +431,7 @@ export class ExcelService {
case 'Ampliación de Conocimiento':
case 'Licenciatura':
case 'Profesor':
Object.assign(servData, {
Object.assign(serv, {
Correo: true,
AT: true,
Red: true,
@@ -411,79 +443,17 @@ export class ExcelService {
conteoTiposSolicita++;
break;
case 'Trabajadores':
Object.assign(servData, { Red: true });
Object.assign(serv, { Red: true });
conteoTiposRed++;
break;
}
serviciosActivos.push(
queryRunner.manager.create(ServActivos, servData),
);
serviciosActivosFinal.push(serv);
}
// 🔹 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);
await queryRunner.commitTransaction();
return {
inserted: rows.length - contador,
id_movimiento: movimiento.id_mov,
@@ -507,9 +477,6 @@ export class ExcelService {
const errs = status.errors;
const rows = status.rowsGood;
if (rows.length === 0) {
throw new BadRequestException(
'No se encontraron filas válidas para cargar. ' + errs,
@@ -524,7 +491,7 @@ export class ExcelService {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
let idsUsuarios: number[] =[]
let idsUsuarios: number[] = [];
try {
const movimiento = await this.movimientoService.logger(
queryRunner,
@@ -587,12 +554,13 @@ export class ExcelService {
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)
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;
}
@@ -651,7 +619,7 @@ export class ExcelService {
});
usuarios.push(usuario);
if (carrera) {
const carreraUsuario = queryRunner.manager.create(CarreraUsuario, {
usuario,
@@ -734,10 +702,8 @@ export class ExcelService {
// 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)
);
idsUsuarios = usuarios.map((u) => u.id_usuario);
idsUsuarios.push(...existentes.map((e) => e.id_usuario));
// 2️⃣ Reconstruir relaciones con usuarios ya guardados
// CarreraUsuario
@@ -793,11 +759,6 @@ export class ExcelService {
await queryRunner.manager.save(usuariosTiposFinal);
await queryRunner.manager.save(serviciosActivosFinal);
return {
inserted: rows.length - contador,
id_movimiento: movimiento.id_mov,
@@ -812,26 +773,22 @@ export class ExcelService {
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 (:...ids)', { ids: idsUsuarios })
.execute();
.createQueryBuilder()
.update()
.set({
Red: false,
Prestamos: false,
AT: false,
Correo: false,
RedStatus: 'Inactivo',
PrestamosStatus: 'Inactivo',
ATStatus: 'Inactivo',
CorreoStatus: 'Inactivo',
})
.where('id_usuario NOT IN (:...ids)', { ids: idsUsuarios })
.execute();
}
}
@@ -1210,7 +1167,6 @@ export class ExcelService {
'Carga de Usuarios',
`Se ha realizado una carga de usuarios en el sistema. Nuevos: ${data?.nuevos ?? 0}, Actualizados: ${data?.actualizados ?? 0}`,
);
}
async enviarInforme(
@@ -1238,18 +1194,14 @@ export class ExcelService {
}
}
async buscaAct() {
return await this.servActivosRepo.find({
where: [
{ AT: true },
{ Correo: true },
{ Prestamos: true },
{ Red: true },
],
});
}
async buscaAct() {
return await this.servActivosRepo.find({
where: [
{ AT: true },
{ Correo: true },
{ Prestamos: true },
{ Red: true },
],
});
}
}
+37 -37
View File
@@ -13,7 +13,7 @@ export class MovimientoService {
private readonly origenRepo: Repository<Origen>,
@InjectRepository(Usuario)
private readonly usuarioRepo: Repository<Usuario>,
) { }
) {}
/** Crea un registro de movimiento */
async log(
@@ -54,7 +54,7 @@ export class MovimientoService {
origenNombre: string,
status: string,
observaciones?: string,
reporte?: string
reporte?: string,
): Promise<Movimiento> {
const origenEntidad = await queryRunner.manager.findOne(Origen, {
where: { origen: origenNombre },
@@ -76,17 +76,21 @@ export class MovimientoService {
return await queryRunner.manager.save(nuevoMov);
}
/** Actualiza el status de un movimiento existente */
async updateStatus(id_movimiento: number, nuevoStatus: string, observaciones?: string): Promise<void> {
const movimiento = await this.movRepo.findOne({ where: { id_mov: id_movimiento } });
async updateStatus(
id_movimiento: number,
nuevoStatus: string,
observaciones?: string,
): Promise<void> {
const movimiento = await this.movRepo.findOne({
where: { id_mov: id_movimiento },
});
if (!movimiento) {
throw new Error(`Movimiento con ID ${id_movimiento} no encontrado`);
}
console.log('movimiento' + movimiento);
movimiento.status = nuevoStatus;
movimiento.observaciones = observaciones || movimiento.observaciones; // opcional: actualizar observaciones
movimiento.fecha_mov = new Date(); // opcional: actualizar fecha
@@ -98,8 +102,12 @@ export class MovimientoService {
origen: string,
page: number = 1,
limit: number = 10,
): Promise<{ move: any[]; button: boolean; total?: number; lastPage?: number }> {
): Promise<{
move: any[];
button: boolean;
total?: number;
lastPage?: number;
}> {
if (origen === 'LOAD') {
const [movimientos, total] = await this.movRepo.findAndCount({
where: {
@@ -136,8 +144,6 @@ export class MovimientoService {
SOLICITA: 'Prestamos',
} as const;
const field = statusFieldMap[origen as keyof typeof statusFieldMap];
const origenField = FieldMap[origen as keyof typeof FieldMap];
@@ -147,7 +153,6 @@ export class MovimientoService {
origen: { origen: 'LOAD' },
status: 'SUCCESS',
reporte: 'CARGA MASIVA DE USUARIOS',
},
});
@@ -156,23 +161,16 @@ export class MovimientoService {
origen: { origen: 'SISTEMA' },
status: 'SUCCESS',
reporte: 'CARGA MASIVA WEB SERVICE',
},
});
const mov2 = await this.movRepo.find({
where: {
reporte: 'CARGA INDIVIDUAL DE USUARIOS',
status: 'SUCCESS',
},
});
const todosLosMovimientos = [...movimientos, ...mov, ...mov2];
return { move: todosLosMovimientos, button: false };
}
@@ -186,7 +184,7 @@ export class MovimientoService {
servActivo: {
[origenField]: true,
[field]: 'Inactivo'
[field]: 'Inactivo',
},
},
},
@@ -199,20 +197,19 @@ export class MovimientoService {
usuario: {
servActivo: {
[origenField]: true,
[field]: 'Inactivo'
[field]: 'Inactivo',
},
},
},
});
const mov2 = await this.movRepo.find({
where: {
reporte: 'CARGA INDIVIDUAL DE USUARIOS',
status: 'SUCCESS',
usuario: {
servActivo: {
[origenField]: true,
[field]: 'Inactivo'
[field]: 'Inactivo',
},
},
},
@@ -224,7 +221,6 @@ export class MovimientoService {
}
async findAllCargaIndv(origen): Promise<Usuario[]> {
const statusFieldMap = {
AT: 'ATStatus',
RED: 'RedStatus',
@@ -239,10 +235,6 @@ export class MovimientoService {
SOLICITA: 'Prestamos',
} as const;
const field = statusFieldMap[origen as keyof typeof statusFieldMap];
const origenField = FieldMap[origen as keyof typeof FieldMap];
@@ -254,7 +246,6 @@ export class MovimientoService {
const usuarios = await this.usuarioRepo.find({
where: {
movimiento: {
reporte: 'CARGA INDIVIDUAL DE USUARIOS',
},
@@ -262,18 +253,27 @@ export class MovimientoService {
[field]: 'Inactivo',
[origenField]: true,
},
},
});
return usuarios;
}
async findAllWithRelations(id: number): Promise<Movimiento[]> {
return await this.movRepo.find({
where: { id_mov: id },
relations: [
'usuario',
'origen',
'usuario.servActivo',
'usuario.carreraUsuarios',
'usuario.carreraUsuarios.carrera',
'usuario.usuarioTipos',
'usuario.usuarioTipos.tipoUsuario',
],
order: {
fecha_mov: 'DESC',
},
});
}
}