diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index d2af33a..2d0cc62 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -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 diff --git a/src/excel/excel.controller.ts b/src/excel/excel.controller.ts index c0957b0..f9c4967 100644 --- a/src/excel/excel.controller.ts +++ b/src/excel/excel.controller.ts @@ -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() diff --git a/src/excel/excel.documentation.ts b/src/excel/excel.documentation.ts index b93d5ca..30cc0e0 100644 --- a/src/excel/excel.documentation.ts +++ b/src/excel/excel.documentation.ts @@ -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' }), ); } - - - - - - - - - - - - - - } diff --git a/src/excel/excel.service.ts b/src/excel/excel.service.ts index 15c84e3..5da4ea0 100644 --- a/src/excel/excel.service.ts +++ b/src/excel/excel.service.ts @@ -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 { @@ -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 = { + 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 }, + ], + }); + } } diff --git a/src/movimiento/movimiento.service.ts b/src/movimiento/movimiento.service.ts index 8e555f5..2efcd98 100644 --- a/src/movimiento/movimiento.service.ts +++ b/src/movimiento/movimiento.service.ts @@ -13,7 +13,7 @@ export class MovimientoService { private readonly origenRepo: Repository, @InjectRepository(Usuario) private readonly usuarioRepo: Repository, - ) { } + ) {} /** 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 { 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 { - const movimiento = await this.movRepo.findOne({ where: { id_mov: id_movimiento } }); + async updateStatus( + id_movimiento: number, + nuevoStatus: string, + observaciones?: string, + ): Promise { + 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 { - 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 { + 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', + }, + }); + } }