Merge branch 'Emilio'

This commit is contained in:
evenegas
2025-07-04 11:30:55 -06:00
36 changed files with 4943 additions and 95 deletions
+19
View File
@@ -0,0 +1,19 @@
{
"name": "carga_api",
"dockerComposeFile": [
"../infra/docker-compose.yml"
],
"service": "api",
"workspaceFolder": "/app",
"runServices": [
"database"
],
"settings": {
"terminal.integrated.defaultProfile.linux": "bash"
},
"appPort": [
3051
],
"shutdownAction": "stopCompose",
"remoteUser": "node"
}
+18
View File
@@ -0,0 +1,18 @@
API_PORT = 4000
JWT_SECRET = "your_jwt_secret"
JWT_EXPIRES_IN = "1h"
DB_HOST = localhost
DB_PORT = 32769
DB_USER = root
DB_PASS = root
DB_NAME = carga_test
DB_SYNC = true
//api
API_URL = //URL web service
//Gmail configuration, llaves de acceso
EMAIL_USER =
CLIENT_ID =
CLIENT_SECRET =
REFRESH_TOKEN =
+10
View File
@@ -0,0 +1,10 @@
API_PORT=3051
DB_HOST=database
DB_PORT=3306
DB_USER=root
DB_PASS=root
DB_NAME=carga_test
DB_SYNC=true
DB_LOGGING=true
+30
View File
@@ -0,0 +1,30 @@
# Usa una imagen base liviana de Node.js
FROM node:22.10.0-alpine
# Crea un usuario no root para mayor seguridad
RUN adduser -D appuser
# Establece directorio de trabajo
WORKDIR /app
# Instala herramientas para compilar dependencias nativas
RUN apk add --no-cache python3 make g++
# Copia e instala dependencias
COPY package*.json ./
RUN npm install
# Copia el resto del proyecto
COPY . .
# Da permisos a todo el proyecto a appuser
RUN chown -R appuser:appuser /app
# Cambia a ese usuario
USER appuser
# Expone el puerto
EXPOSE 3051
# Comando para desarrollo
CMD ["npm", "run", "start:dev"]
+17
View File
@@ -0,0 +1,17 @@
version: '3.9'
services:
database:
image: mariadb:latest
container_name: carga_db
restart: always
env_file:
- ../.env
ports:
- "${DB_PORT}:3306"
volumes:
- mariadb_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
volumes:
mariadb_data:
+7
View File
@@ -0,0 +1,7 @@
-- init_insert_data.sql
USE carga_test;
INSERT IGNORE INTO origen (fuente) VALUES
('redes'),
('AT'),
('escolares');
+1688 -88
View File
File diff suppressed because it is too large Load Diff
+23 -3
View File
@@ -21,10 +21,27 @@
},
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.3",
"@nestjs/schedule": "^6.0.0",
"@nestjs/swagger": "^11.2.0",
"@nestjs/typeorm": "^11.0.0",
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"dotenv": "^16.5.0",
"exceljs": "^4.4.0",
"multer": "^2.0.1",
"mysql2": "^3.14.1",
"nodemailer": "^7.0.3",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"typeorm": "^0.3.24"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
@@ -34,9 +51,12 @@
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/bcrypt": "^5.0.2",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.7",
"@types/multer": "^1.4.13",
"@types/node": "^22.15.31",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
+60 -3
View File
@@ -1,10 +1,67 @@
import { Module } from '@nestjs/common';
import { Logger, Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import * as entities from './entities/entities';
import { ExcelModule } from './excel/excel.module';
import { AuthModule } from './auth/auth.module';
import { MovimientoModule } from './movimiento/movimiento.module';
import { UsuariosModule } from './usuarios/usuarios.module';
import { MailService } from './mail/mail.service';
import { MailModule } from './mail/mail.module';
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [],
imports: [
ScheduleModule.forRoot(),
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (config: ConfigService) => {
const dbConfig = {
type: 'mysql' as const,
host: config.get('DB_HOST', 'database'),
port: config.get<number>('DB_PORT', 3306),
username: config.get('DB_USER', 'root'),
// no prints en claro en prod:
password: config.get('DB_PASS', ''),
database: config.get('DB_NAME', 'test'),
dropSchema: false, // solo para desarrollo
};
Logger.log('[DB CONFIG] ' + JSON.stringify({
...dbConfig,
password: '***'
}), 'TypeORM');
return {
...dbConfig,
entities: Object.values(entities),
synchronize: config.get<boolean>('DB_SYNC', true),
logging: ['error', 'warn', 'info', 'query', 'schema'],
logger: 'advanced-console',
retryAttempts: 5,
retryDelay: 2000,
};
},
}),
TypeOrmModule.forFeature(Object.values(entities)),
ExcelModule,
AuthModule,
MovimientoModule,
UsuariosModule,
MailModule
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
export class AppModule { }
+48
View File
@@ -0,0 +1,48 @@
import { AuthDocumentation } from './auth.documentation';
import { Controller, Post, Body, UseGuards, Request, Get } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { AuthGuard } from '@nestjs/passport';
import { RegisterDto } from './dto/register.dto';
import { ApiBearerAuth } from '@nestjs/swagger';
@Controller()
export class AuthController {
constructor(private readonly authService: AuthService) { }
@Post('login')
@AuthDocumentation.login()
async login(@Body() dto: LoginDto) {
const user = await this.authService.validateUser(dto.email, dto.password);
if (!user) {
throw new Error('Invalid credentials');
}
return this.authService.login(user);
}
@UseGuards(AuthGuard('jwt'))
@ApiBearerAuth('bearer')
@UseGuards(AuthGuard('jwt'))
@Get('profile')
@AuthDocumentation.bearerAuth()
getProfile(@Request() req) {
// req.user viene de JwtStrategy.validate()
return {
userId: req.user.userId,
email: req.user.email,
};
}
@Post('register')
@AuthDocumentation.register() // ver siguiente sección
async register(@Body() dto: RegisterDto) {
return this.authService.register(dto);
}
}
+104
View File
@@ -0,0 +1,104 @@
import { applyDecorators } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBody, ApiConsumes, ApiResponse } from '@nestjs/swagger';
import { Origen } from 'src/entities/entities';
export class AuthDocumentation {
/**
* Decorators Swagger para el endpoint POST /login
*/
static login() {
return applyDecorators(
ApiTags('Auth'),
ApiOperation({
summary: 'Iniciar sesión',
description: 'Autentica a un usuario con correo y contraseña, retorna un JWT en caso de éxito.'
}),
ApiConsumes('application/json'),
ApiBody({
schema: {
type: 'object',
properties: {
email: { type: 'string', format: 'email', example: 'usuario@dominio.com' },
password: { type: 'string', minLength: 6, example: 'miPassword123' }
},
required: ['email', 'password']
}
}),
ApiResponse({
status: 200,
description: 'Autenticación exitosa. Retorna token JWT.',
schema: {
type: 'object',
properties: {
access_token: { type: 'string', example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' }
}
}
}),
ApiResponse({
status: 401,
description: 'Credenciales inválidas.',
schema: {
type: 'object',
properties: {
statusCode: { type: 'integer', example: 401 },
message: { type: 'string', example: 'Credenciales inválidas' },
error: { type: 'string', example: 'Unauthorized' }
}
}
})
);
}
/**
* Decorators para proteger rutas con JWT
*/
static bearerAuth() {
return applyDecorators(
ApiOperation({ summary: 'Requiere autenticación JWT' }),
ApiResponse({ status: 401, description: 'Token no provisto o inválido.' }),
ApiResponse({ status: 403, description: 'Token válido pero sin permisos.' })
);
}
static register() {
return applyDecorators(
ApiTags('Auth'),
ApiOperation({
summary: 'Registrar usuario',
description: 'Crea un nuevo usuario con correo y contraseña.'
}),
ApiConsumes('application/json'),
ApiBody({
schema: {
type: 'object',
properties: {
email: { type: 'string', format: 'email', example: 'nuevo@dominio.com' },
password: { type: 'string', example: 'Password123' },
origen: { type: 'string', example: 'RED', description: 'Origen del usuario' }
}
}
}),
ApiResponse({
status: 201,
description: 'Usuario creado correctamente',
schema: {
type: 'object',
properties: {
id_usuario: { type: 'number', example: 1 },
correo: { type: 'string', example: 'nuevo@dominio.com' },
origen: { type: 'string', example: 'RED' }
}
}
}),
ApiResponse({ status: 409, description: 'El correo ya está registrado.' }),
);
}
}
+28
View File
@@ -0,0 +1,28 @@
// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigService, ConfigModule } from '@nestjs/config';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';
import { Origen, UsuariosDelSistema } from '../entities/entities';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forFeature([UsuariosDelSistema, Origen]),
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (cs: ConfigService) => ({
secret: cs.get<string>('JWT_SECRET'),
signOptions: { expiresIn: cs.get<string>('JWT_EXPIRES_IN') },
}),
}),
],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
})
export class AuthModule { }
+78
View File
@@ -0,0 +1,78 @@
// src/auth/auth.service.ts
import { ConflictException, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Or, Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Origen, UsuariosDelSistema } from '../entities/entities';
import * as bcrypt from 'bcrypt';
import { RegisterDto } from './dto/register.dto';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(UsuariosDelSistema)
private readonly userRepo: Repository<UsuariosDelSistema>,
@InjectRepository(Origen)
private readonly origenRepo: Repository<Origen>,
private readonly jwtService: JwtService,
) { }
// 1) Valida email/password
async validateUser(email: string, pass: string): Promise<UsuariosDelSistema> {
const user = await this.userRepo.findOne({ where: { correo: email }, relations: ['origen'] });
if (!user) throw new UnauthorizedException('Credenciales inválidas');
const match = await bcrypt.compare(pass, user.contraseña);
if (!match) throw new UnauthorizedException('Credenciales inválidas');
return user;
}
// 2) Genera el JWT
async login(user: UsuariosDelSistema) {
if (!user)
throw new UnauthorizedException('Usuario no encontrado');
if (!user.origen)
throw new UnauthorizedException('Usuario no encontrado, tiene origen vacío');
const payload = { sub: user.id_usuario, email: user.correo, tipo: user.origen.origen };
return {
access_token: this.jwtService.sign(payload),
};
}
async register(dto: RegisterDto) {
// 1) Verificar duplicado
const exists = await this.userRepo.findOne({ where: { correo: dto.email } });
if (exists) {
throw new ConflictException('El correo ya está registrado');
}
// 2) Hashear contraseña
const hash = await bcrypt.hash(dto.password, 10);
let origen = await this.origenRepo.findOne({ where: { origen: dto.origen } });
if (!origen) {
origen = this.origenRepo.create({ origen: dto.origen });
origen = await this.origenRepo.save(origen);
}
// 3) Crear entidad Usuario
const user = this.userRepo.create({
correo: dto.email,
contraseña: hash,
origen: origen,
});
// 4) Guardar en BD
const saved = await this.userRepo.save(user);
// 5) Devolver sin contraseña
const { contraseña, ...result } = saved;
return result;
}
}
+11
View File
@@ -0,0 +1,11 @@
// src/auth/dto/login.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';
export class LoginDto {
@IsEmail()
email: string;
@IsString()
@MinLength(6)
password: string;
}
+15
View File
@@ -0,0 +1,15 @@
import { IsEmail, IsString, MinLength, MaxLength, IsOptional, IsNumber } from 'class-validator';
export class RegisterDto {
@IsEmail()
email: string;
@IsString()
@MinLength(6)
password: string;
@IsString()
@MaxLength(60)
origen: string;
}
+27
View File
@@ -0,0 +1,27 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt, StrategyOptions } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
const secret = configService.get<string>('JWT_SECRET');
if (!secret) {
throw new Error('JWT_SECRET is not defined');
}
const opts: StrategyOptions = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: secret,
};
super(opts);
}
async validate(payload: any) {
// Devuelve lo que estará disponible en Request.user
return { userId: payload.sub, email: payload.email, origen: payload.tipo };
}
}
+225
View File
@@ -0,0 +1,225 @@
import { ServActivos } from 'src/usuarios/entities/servActivos.entitie';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn, PrimaryColumn, OneToOne } from 'typeorm';
@Entity({ name: 'origen' })
export class Origen {
@PrimaryGeneratedColumn({ name: 'id_origen', type: 'int' })
id_origen: number;
@Column({ type: 'varchar', length: 30 })
origen: string;
@OneToMany(() => Movimiento, movimiento => movimiento.origen)
movimientos: Movimiento[];
@OneToMany(() => UsuariosDelSistema, uds => uds.origen)
usuariosDelSistema: UsuariosDelSistema[];
}
@Entity({ name: 'genero' })
export class Genero {
@PrimaryGeneratedColumn({ name: 'id_genero', type: 'int', })
id_genero: number;
@Column({ type: 'varchar', length: 20, nullable: true })
genero: string;
@OneToMany(() => Usuario, usuario => usuario.genero)
usuarios: Usuario[];
}
@Entity({ name: 'carrera' })
export class Carrera {
@PrimaryGeneratedColumn({ name: 'id_carrera', type: 'int' })
id_carrera: number;
@Column({ type: 'varchar', length: 100 })
carrera: string;
@Column({ type: 'varchar', length: 6 })
clave: string;
@OneToMany(() => CarreraUsuario, cu => cu.carrera)
carreraUsuarios: CarreraUsuario[];
}
@Entity({ name: 'movimiento' })
export class Movimiento {
@PrimaryGeneratedColumn({ name: 'id_mov', type: 'int' })
id_mov: number;
@Column({ name: 'fecha_mov', type: 'datetime' })
fecha_mov: Date;
@Column({ type: 'varchar', length: 100 })
status: string;
@Column({
type: 'varchar',
length: 200,
nullable: true, // permite NULL
})
observaciones?: string;
@Column({ type: 'bit' })
flag: boolean;
@Column({
type: 'varchar',
length: 200,
nullable: true, // permite NULL
})
reporte?: string;
@OneToMany(() => Usuario, usuario => usuario.movimiento)
usuario: Usuario[];
@ManyToOne(() => Origen, origen => origen.movimientos)
@JoinColumn({ name: 'id_origen' })
origen: Origen;
}
@Entity({ name: 'usuario' })
export class Usuario {
@PrimaryGeneratedColumn({ name: 'id_usuario', type: 'int' })
id_usuario: number;
@Column({
type: 'varchar',
unique: true,
nullable: true,
default: null,
})
num_cuenta: string;
@Column({ type: 'varchar', length: 60 })
nombre: string;
@Column({ name: 'a_paterno', type: 'varchar', length: 60 })
a_paterno: string;
@Column({ name: 'a_materno', type: 'varchar', length: 60 })
a_materno: string;
@Column({
type: 'varchar',
length: 15,
nullable: true,
default: null,
})
rfc: string | null;
@Column({ name: 'fecha_nacimiento', type: 'varchar', nullable: true, length: 8 })
fecha_nacimiento: string | null;
@Column({ type: 'int', nullable: true, })
generacion: number | null;
@ManyToOne(() => Genero, genero => genero.usuarios, { nullable: true })
@JoinColumn({ name: 'id_genero' })
genero: Genero | null;
@OneToMany(() => CarreraUsuario, cu => cu.usuario)
carreraUsuarios: CarreraUsuario[];
@OneToMany(() => UsuarioTipoUsuario, utu => utu.usuario)
usuarioTipos: UsuarioTipoUsuario[];
@ManyToOne(() => Movimiento, movimiento => movimiento.usuario)
@JoinColumn({ name: 'id_movimiento' })
movimiento: Movimiento;
@OneToOne(() => ServActivos, (servActivo) => servActivo.usuario,)
servActivo: ServActivos;
}
@Entity({ name: 'carrera_usuario' })
export class CarreraUsuario {
@PrimaryGeneratedColumn({ name: 'id_carrera_usuario', type: 'int' })
id_carrera_usuario: number;
@ManyToOne(() => Carrera, carrera => carrera.carreraUsuarios)
@JoinColumn({ name: 'id_carrera' })
carrera: Carrera;
@ManyToOne(() => Usuario, usuario => usuario.carreraUsuarios)
@JoinColumn({ name: 'id_usuario' })
usuario: Usuario;
}
@Entity({ name: 'usuariosDelSistema' })
export class UsuariosDelSistema {
@PrimaryGeneratedColumn({ name: 'id_usuario', type: 'int' })
id_usuario: string;
@Column({
type: 'varchar',
length: 100, // ajusta longitud según tu necesidad
unique: true,
nullable: true,
default: null,
})
correo: string | null;
@Column({ type: 'varchar', length: 60 })
contraseña: string;
@ManyToOne(() => Origen, origen => origen.usuariosDelSistema)
@JoinColumn({ name: 'id_origen' })
origen: Origen;
}
@Entity({ name: 'equipo' })
export class Equipo {
@PrimaryGeneratedColumn({ name: 'id_equipo', type: 'int' })
id_equipo: number;
@Column({ type: 'varchar', length: 10 })
equipo: string;
@Column({ name: 'numero_serie', type: 'varchar', length: 30 })
numero_serie: string;
}
@Entity({ name: 'tipo_usuario' })
export class TipoUsuario {
@PrimaryGeneratedColumn({ name: 'id_tipo_usuario', type: 'int' })
id_tipo_usuario: number;
@Column({ name: 'tipo_usuario', type: 'varchar', length: 20 })
tipo_usuario: string;
@OneToMany(() => UsuarioTipoUsuario, utu => utu.tipoUsuario)
usuariosTipos: UsuarioTipoUsuario[];
}
@Entity({ name: 'usuario_tipo_usuario' })
export class UsuarioTipoUsuario {
@PrimaryGeneratedColumn({ name: 'id_user_tipo_usuario', type: 'int' })
id_user_tipo_usuario: number;
@ManyToOne(() => TipoUsuario, tipo => tipo.usuariosTipos)
@JoinColumn({ name: 'id_tipo_usuario' })
tipoUsuario: TipoUsuario;
@ManyToOne(() => Usuario, usuario => usuario.usuarioTipos)
@JoinColumn({ name: 'id_usuario' })
usuario: Usuario;
}
export { ServActivos };
+307
View File
@@ -0,0 +1,307 @@
// src/excel/excel.controller.ts
import { Controller, Post, Get, UseGuards, Request, Res, UploadedFile, UseInterceptors, HttpStatus, Param, ParseIntPipe, Body, BadRequestException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiBearerAuth } from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express';
import { Response } from 'express';
import { ExcelDocumentation } from './excel.documentation';
import { ExcelService } from './excel.service';
import { MovimientoService } from '../movimiento/movimiento.service';
import { MailService } from 'src/mail/mail.service';
import { CreateUsuarioDto } from 'src/usuarios/dto/create-usuario.dto';
import { Origen } from 'src/entities/entities';
import { UsuariosService } from 'src/usuarios/usuarios.service';
@UseGuards(AuthGuard('jwt'))
@ApiBearerAuth('bearer')
@Controller('excel')
export class ExcelController {
constructor(
private readonly excelService: ExcelService,
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) {
console.log('Usuario recibido:', usuario);
if (req.user.origen != 'LOAD') {
throw new BadRequestException('Origen no permitido para carga')
}
const alta = await this.usuarioService.cargaIndividual(usuario, req.user.origen)
console.log('Alta de usuario:', alta);
await this.movimientoService.updateStatus(alta.movId, 'SUCCESS')
this.mailService.sendMail({
to: req.user.email, // Asegúrate de que el usuario tenga un email
subject: 'Carga de datos exitosa',
text: "La carga de datos se ha realizado con éxito.",
html: `<p>La carga de datos se ha realizado con éxito. ` + alta,
});
await this.excelService.enviarCargaMasiva();
}
@Post('verify')
@UseInterceptors(FileInterceptor('file'))
@ExcelDocumentation.verifyExcel()
async verifyExcel(@UploadedFile() file: Express.Multer.File, @Request() req) {
const userId: number = req.user.userId; // ahora sí existe
const status = await this.excelService.validateFile(file.buffer);
const errors = status.errors;
await this.movimientoService.log(
'VERIFY',
errors.length ? 'FAILED' : 'SUCCESS',
errors.join('; ')
);
return { valid: errors.length === 0, errors };
}
@Post('load')
@UseInterceptors(FileInterceptor('file'))
@ExcelDocumentation.loadExcel()
async loadExcel(@Request() req, @UploadedFile() file: Express.Multer.File) {
const origen = req.user.origen; // ahora sí existe
if (origen != 'LOAD') {
await this.movimientoService.log(
origen,
'FAILED',
'Origen no permitido para carga'
);
throw new Error('Origen no permitido para carga.');
}
try {
const result = await this.excelService.loadFile(file.buffer);
//Actualizar el estado de los movimientos
if (!result) {
this.mailService.sendMail({
to: req.user.email, // Asegúrate de que el usuario tenga un email
subject: 'Carga de datos Fallida',
text: "No se pudieron subir datos.",
html: '<p>No se pudieron subir datos.</p>',
});
throw new Error('No se pudo procesar el archivo.');
}
if (result.inserted == 0) {
this.mailService.sendMail({
to: req.user.email, // Asegúrate de que el usuario tenga un email
subject: 'Carga de datos Fallida',
text: "No se pudieron subir datos.",
html: `<p> Se han insertado ${result.inserted} registros.</p>
<p>Errores encontrados:</p>
<ul>${result.errors.map(error => `<li>${error}</li>`).join('')}</ul>`,
});
return result
}
console.log(result.id_movimiento)
await this.movimientoService.updateStatus(
result.id_movimiento,
'SUCCESS',
`inserted=${result.inserted}`
);
this.mailService.sendMail({
to: req.user.email, // Asegúrate de que el usuario tenga un email
subject: 'Carga de datos exitosa',
text: "La carga de datos se ha realizado con éxito.",
html: `<p>La carga de datos se ha realizado con éxito. Se han insertado ${result.inserted} registros.</p>
<p>Errores encontrados:</p>
<ul>${result.errors.map(error => `<li>${error}</li>`).join('')}</ul>`,
});
await this.excelService.enviarCargaMasiva();
return result
} catch (err) {
await this.movimientoService.log(
'LOAD',
'FAILED',
err.message
);
throw err;
}
}
@Get('download')
@ExcelDocumentation.downloadExcel()
async downloadData(@Request() req, @Res() res: Response) {
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',
'descarga de csv',
`size=${csv.length}`
);
return res
.status(HttpStatus.OK)
.header('Content-Type', 'text/csv')
.header('Content-Disposition', 'attachment; filename="usuarios_solicita.csv"')
.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' || origen == 'LOAD') {
try {
const csv = await this.excelService.generateCsvCorreo();
await this.movimientoService.log(
origen,
'SUCCESS',
'descarga de csv',
`size=${csv.length}`
);
let filename
if (origen == 'CORREO') {
filename = 'usuarios_correo.csv';
}
else {
filename = 'usuarios.csv';
}
return res
.status(HttpStatus.OK)
.header('Content-Type', 'text/csv')
.header('Content-Disposition', `attachment; filename="${filename}"`)
.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',
'descarga de csv',
`size=${csv.length}`
);
return res
.status(HttpStatus.OK)
.header('Content-Type', 'text/csv')
.header('Content-Disposition', 'attachment; filename="usuarios_AT.csv"')
.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',
'descarga de csv',
`size=${csv.length}`
);
return res
.status(HttpStatus.OK)
.header('Content-Type', 'text/csv')
.header('Content-Disposition', 'attachment; filename="usuarios_red.csv"')
.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(
origen,
'FAILED',
'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')
async getMovimientos(@Request() req) {
const movimientos = await this.movimientoService.findAll(req.user.origen);
return movimientos;
}
@Post('activar-servicios/:id_movimiento')
async activarServicios(
@Param('id_movimiento', ParseIntPipe) id_movimiento: number,
@Request() req,
): Promise<{ message: string }> {
const origen = req.user.origen;
if (!origen) {
throw new Error('No se encontró origen en el JWT');
}
await this.excelService.activarServicios(id_movimiento, origen);
return { message: 'Servicios activados correctamente' };
}
@Get('cargaIndividual')
async cargarIndividual(
@Request() req) {
const origen = req.user.origen; // ahora sí existe
return this.movimientoService.findAllCargaIndv(origen);
}
}
+137
View File
@@ -0,0 +1,137 @@
import { applyDecorators } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiConsumes, ApiBody, ApiResponse } from '@nestjs/swagger';
export class ExcelDocumentation {
/**
* Decorators Swagger para el endpoint POST /excel/verify
*/
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.'
}),
ApiConsumes('multipart/form-data'),
ApiBody({
schema: {
type: 'object',
properties: {
file: {
type: 'string',
format: 'binary',
description: 'Archivo Excel (.xlsx) con usuarios a validar'
}
}
}
}),
ApiResponse({
status: 200,
description: 'Resultado de la validación',
schema: {
type: 'object',
properties: {
valid: { type: 'boolean', example: false },
errors: {
type: 'array',
items: { type: 'string' },
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.' })
);
}
/**
* Decorators Swagger para el endpoint POST /excel/load
*/
static loadExcel() {
return applyDecorators(
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.'
}),
ApiConsumes('multipart/form-data'),
ApiBody({
schema: {
type: 'object',
properties: {
file: {
type: 'string',
format: 'binary',
description: 'Archivo Excel (.xlsx) con usuarios a cargar'
}
}
}
}),
ApiResponse({
status: 201,
description: 'Usuarios insertados correctamente',
schema: {
type: 'object',
properties: {
inserted: { type: 'number', example: 3 }
}
}
}),
ApiResponse({
status: 400,
description: 'Errores de validación o carga',
schema: {
type: 'object',
properties: {
errors: {
type: 'array',
items: { type: 'string' },
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.'
}),
ApiResponse({
status: 200,
description: 'TSV generado correctamente',
content: {
'text/tab-separated-values': {
schema: { type: 'string', example: 'num_cuenta\\tnombre\\t...\\n...' }
}
}
}),
ApiResponse({ status: 401, description: 'No autorizado' }),
ApiResponse({ status: 500, description: 'Error al generar descarga' }),
);
}
}
+22
View File
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { ExcelService } from './excel.service';
import { ExcelController } from './excel.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Usuario, Genero, Carrera, CarreraUsuario, UsuarioTipoUsuario, TipoUsuario, UsuariosDelSistema } from '../entities/entities';
import { MovimientoModule } from 'src/movimiento/movimiento.module';
import { ServActivos } from 'src/usuarios/entities/servActivos.entitie';
import { MailModule } from 'src/mail/mail.module';
import { UsuariosService } from 'src/usuarios/usuarios.service';
@Module({
imports: [
TypeOrmModule.forFeature([Usuario, Genero, Carrera, CarreraUsuario, UsuarioTipoUsuario, TipoUsuario, ServActivos, UsuariosDelSistema]),
MovimientoModule,
MailModule,
],
providers: [ExcelService, UsuariosService],
controllers: [ExcelController],
})
export class ExcelModule { }
+755
View File
@@ -0,0 +1,755 @@
// src/excel/excel.service.ts
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
import { Workbook } from 'exceljs';
import { DataSource, In, Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { CarreraUsuario, TipoUsuario, Usuario, UsuariosDelSistema, UsuarioTipoUsuario } from '../entities/entities';
import { Genero } from '../entities/entities';
import { Carrera } from '../entities/entities';
import { MovimientoService } from 'src/movimiento/movimiento.service';
import { ServActivos } from 'src/usuarios/entities/servActivos.entitie';
import { MailService } from 'src/mail/mail.service';
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;
}
@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>,
@InjectRepository(CarreraUsuario)
private readonly carreraUsuarioRepo: Repository<CarreraUsuario>,
@InjectRepository(UsuarioTipoUsuario)
private readonly usuarioTipoUsuarioRepo: Repository<UsuarioTipoUsuario>,
@InjectRepository(TipoUsuario)
private readonly tipoUsuarioRepo: Repository<TipoUsuario>,
@InjectRepository(ServActivos)
private readonly servActivosRepo: Repository<ServActivos>,
@InjectRepository(UsuariosDelSistema)
private readonly usuariosDelSistemaRepo: Repository<UsuariosDelSistema>,
private readonly movimientoService: MovimientoService,
private readonly mailService: MailService,
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[]> {
const wb = new Workbook();
await wb.xlsx.load(buffer);
const sheet = wb.worksheets[0];
const rows: UsuarioRow[] = [];
// Asume que la primera fila es encabezados
sheet.eachRow((row, idx) => {
if (idx === 1) return; // salto encabezados
// 1) Asegurarnos de que row.values no sea null/undefined
if (!row.values) {
this.logger.warn(`Fila ${idx + 1}: row.values vacío, se omite.`);
return;
}
// 2) row.values[0] es null, así que slice(1) sí existe
const [
cuenta,
nombreCompleto,
clave,
nomCarr,
gen,
fechnac,
apellidopa,
apellidoma,
nombres,
sexo,
tipo,
correo,
rfc,
] = (row.values as any[]).slice(1);
rows.push({
cuenta,
nombreCompleto,
clave,
nomCarr,
gen,
fechnac,
apellidopa,
apellidoma,
nombres,
sexo,
tipo,
correo,
rfc,
});
});
return rows;
}
/** Valida duplicados, celdas vacías y patrones básicos */
validateRows(rows: UsuarioRow[]) {
const errors: string[] = [];
const seen = new Set<string>();
const rowsGood: UsuarioRow[] = [];
let flagError: boolean = false;
rows.forEach((r, i) => {
const rowNum = i + 2; // por el encabezado
// Campos que no pueden quedar vacíos
['cuenta', 'tipo'].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.`);
flagError = true;
}
});
// El número de cuenta debe ser de nueve dígitos para los alumnos
if (r.cuenta && !/^\d{9}$/.test(r.cuenta.toString().trim()) && r.tipo.trim() === 'Licenciatura') {
errors.push(`Fila ${rowNum}: cuenta "${r.cuenta}" debe tener 9 dígitos.`);
flagError = true;
}
// Duplicados de "cuenta"
const cuentaStr = r.cuenta != null ? r.cuenta.toString().trim() : '';
if (seen.has(cuentaStr)) {
errors.push(`Fila ${rowNum}: cuenta duplicada "${cuentaStr}".`);
flagError = true;
} else {
seen.add(cuentaStr);
}
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}".`);
flagError = true;
}
// 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}".`);
flagError = true;
}
// 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.`);
flagError = true;
}
if (!flagError) {
rowsGood.push(r)
} else {
flagError = false; // reset para la siguiente fila
}
});
console.log(rowsGood);
return { errors, rowsGood };
}
/**
* Solo valida: retorna lista de errores (vacío = ok)
*/
async validateFile(buffer: Buffer) {
const rows = await this.parseFile(buffer);
const status = this.validateRows(rows);
return status;
}
/**
* Valida y luego persiste usuarios y relaciones básicas
*/
async loadFile(buffer: Buffer) {
const file = await this.parseFile(buffer);
const status = this.validateRows(file);
const errs = status.errors;
const rows = status.rowsGood;
if (rows.length === 0) {
throw new BadRequestException('No se encontraron filas válidas para cargar. ' + errs);
}
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const movimiento = await this.movimientoService.logger(
queryRunner,
'LOAD',
'LOADING',
undefined,
'CARGA MASIVA DE USUARIOS'
);
let contador = 0;
for (const r of rows) {
const whereCond: Array<{ num_cuenta?: string; rfc?: string }> = [{ num_cuenta: r.cuenta }];
if (r.rfc) {
whereCond.push({ rfc: r.rfc });
}
const userExists = await queryRunner.manager.findOne(Usuario, {
where: whereCond,
});
if (userExists) {
errs.push(`Cuenta ${r.cuenta} ya existe, se omite.`);
contador++;
continue;
}
if (!r.tipo || !r.nombres || !r.apellidopa || !r.apellidoma) {
errs.push(`Fila con cuenta ${r.cuenta} tiene campos faltantes.`);
contador++;
continue;
}
let genero = await queryRunner.manager.findOne(Genero, { where: { genero: r.sexo } });
if (!genero) {
genero = queryRunner.manager.create(Genero, { genero: r.sexo });
await queryRunner.manager.save(genero);
}
let carrera
if (r.clave || r.nomCarr) {
carrera = await queryRunner.manager.findOne(Carrera, { where: { clave: r.clave } });
if (!carrera) {
carrera = queryRunner.manager.create(Carrera, {
clave: r.clave,
carrera: r.nomCarr.slice(0, 100),
});
await queryRunner.manager.save(carrera);
}
} else {
carrera = null;
}
let tipo = await queryRunner.manager.findOne(TipoUsuario, {
where: { tipo_usuario: r.tipo.trim() },
});
if (!tipo) {
tipo = queryRunner.manager.create(TipoUsuario, { tipo_usuario: r.tipo.trim() });
await queryRunner.manager.save(tipo);
}
const generacion = /^[0-9]{4}$/.test(r.gen?.toString().trim())
? parseInt(r.gen.toString().trim(), 10)
: null;
const usuario = queryRunner.manager.create(Usuario, {
num_cuenta: r.cuenta,
nombre: r.nombres.trim().split(' ')[0],
a_paterno: r.apellidopa,
a_materno: r.apellidoma,
rfc: r.rfc,
fecha_nacimiento: r.fechnac,
generacion,
genero,
movimiento,
});
const user = await queryRunner.manager.save(usuario);
if (carrera) {
const carreraUsuario = queryRunner.manager.create(CarreraUsuario, {
usuario: user,
carrera,
});
await queryRunner.manager.save(carreraUsuario);
}
const usuarioTipo = queryRunner.manager.create(UsuarioTipoUsuario, {
usuario: user,
tipoUsuario: tipo,
});
await queryRunner.manager.save(usuarioTipo);
const servData = {
usuario: user,
RedStatus: 'Inactivo',
ATStatus: 'Inactivo',
CorreoStatus: 'Inactivo',
PrestamosStatus: 'Inactivo',
};
switch (r.tipo.trim()) {
case 'Diplomado':
Object.assign(servData, { Correo: true });
break;
case 'Extra Largo':
Object.assign(servData, { Correo: true, Prestamos: true });
break;
case 'Servicio Social':
Object.assign(servData, { Correo: true, AT: true, Red: true });
break;
case 'Idiomas R (UNAM)':
case 'Idiomas Sabatino':
Object.assign(servData, { Correo: true, Red: true });
break;
case 'Reinscrito':
case 'Posgrado':
case 'Intercambio UNAM':
case 'Movilidad':
case 'Ampliación de Conocimiento':
case 'Licenciatura':
case 'Profesor':
Object.assign(servData, { Correo: true, AT: true, Red: true, Prestamos: true });
break;
case 'Trabajadores':
Object.assign(servData, { Red: true });
break;
}
const serv = queryRunner.manager.create(ServActivos, servData);
await queryRunner.manager.save(serv);
}
await queryRunner.commitTransaction();
return { inserted: rows.length - contador, id_movimiento: movimiento.id_mov, errors: errs };
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
// 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),
},
},
servActivo: {
CorreoStatus: 'Inactivo',
Correo: true,
},
},
relations: [
'genero',
'carreraUsuarios',
'carreraUsuarios.carrera',
'usuarioTipos',
'usuarioTipos.tipoUsuario',
],
});
const header = [
'num_cuenta', 'nombre', 'a_paterno', 'a_materno',
'carreras', 'rfc', 'fecha_nacimiento', 'generacion', 'tipos_usuario'
].join(',') + '\n';
const rows = usuarios.map(u => {
const carreras = (u.carreraUsuarios ?? [])
.map(cu => cu.carrera?.carrera ?? '')
.join(' | ');
const tiposUsuario = (u.usuarioTipos ?? [])
.map(utu => utu.tipoUsuario?.tipo_usuario ?? '')
.join(' | ');
return [
u.num_cuenta ?? '',
u.nombre ?? '',
u.a_paterno ?? '',
u.a_materno ?? '',
carreras,
u.rfc ?? '',
u.fecha_nacimiento ?? '',
u.generacion?.toString() ?? '',
tiposUsuario
].join(',');
}).join('\n');
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),
},
},
servActivo: {
PrestamosStatus: 'Inactivo', // solo los que no tienen servicio activo
Prestamos: true, // solo los que tienen el servicio de préstamos activo
}
},
relations: [
'genero',
'carreraUsuarios', // relación intermedia
'carreraUsuarios.carrera', // carrera asociada a través de la intermedia
'usuarioTipos',
'usuarioTipos.tipoUsuario',
],
});
const header = [
'num_cuenta', 'nombre', 'a_paterno', 'a_materno',
'carreras', 'rfc', 'tipos_usuario'
].join(',') + '\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
const tiposUsuario = (u.usuarioTipos ?? [])
.map(utu => utu.tipoUsuario?.tipo_usuario ?? '')
.join(' | '); // une todos los tipos de usuario en un solo string
return [
u.num_cuenta ?? '',
u.nombre ?? '',
u.a_paterno ?? '',
u.a_materno ?? '',
carreras ?? '',
u.rfc ?? '',
tiposUsuario
].join(',');
}).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),
},
},
servActivo: {
RedStatus: 'Inactivo',
Red: true, // solo los que tienen el servicio de red activo
}
},
relations: [
'genero',
'carreraUsuarios', // relación intermedia
'carreraUsuarios.carrera', // carrera asociada a través de la intermedia
],
});
const header = [
'num_cuenta', 'fecha_nacimiento'
].join(',') + '\n';
const rows = usuarios.map(u => {
return [
u.num_cuenta,
u.fecha_nacimiento,
].join(',');
}).join('\n');
const tablaCompleta = header + rows;
console.log(tablaCompleta);
console.log('Usuarios encontrados:', usuarios);
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),
},
},
servActivo: {
ATStatus: 'Inactivo',
AT: true
},
},
relations: [
'genero',
'carreraUsuarios', // relación intermedia
'carreraUsuarios.carrera', // carrera asociada a través de la intermedia
'usuarioTipos',
'usuarioTipos.tipoUsuario',
],
});
const header = [
'num_cuenta', 'nombre', 'a_paterno', 'a_materno',
'carreras', 'fecha_nacimiento', 'generacion', 'tipos_usuario'
].join(',') + '\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
const tiposUsuario = (u.usuarioTipos ?? [])
.map(utu => utu.tipoUsuario?.tipo_usuario ?? '')
.join(' | '); // une todos los tipos de usuario en un solo string
return [
u.num_cuenta ?? '',
u.nombre ?? '',
u.a_paterno ?? '',
u.a_materno ?? '',
carreras ?? '',
u.fecha_nacimiento ?? '',
u.generacion?.toString() ?? '',
tiposUsuario
].join(',');
}).join('\n');
const tablaCompleta = header + rows;
return tablaCompleta;
}
//Funcion que se le debe hacer su propio módulo
async activarServicios(id_movimiento: number, origen: string): Promise<void> {
if (!['AT', 'SOLICITA', 'CORREO', 'RED'].includes(origen)) {
throw new BadRequestException(`Origen ${origen} no válido`);
}
const usuarios = await this.usuarioRepo.find({
where: { movimiento: { id_mov: id_movimiento } },
});
if (!usuarios || usuarios.length === 0) {
throw new BadRequestException('No se encontró el usuario asociado al movimiento');
}
for (const usuario of usuarios) {
const servActivos = await this.servActivosRepo.findOne({
where: { usuario: { id_usuario: usuario.id_usuario } },
});
if (!servActivos) {
throw new BadRequestException(
`No se encontraron servicios activos para el usuario con ID ${usuario.id_usuario}`,
);
}
const timestamp = 'Activo ' + new Date().toISOString();
if (origen === 'AT' && servActivos.AT) {
servActivos.ATStatus = timestamp;
} else if (origen === 'RED' && servActivos.Red) {
servActivos.RedStatus = timestamp;
} else if (origen === 'CORREO' && servActivos.Correo) {
servActivos.CorreoStatus = timestamp;
} else if (origen === 'SOLICITA' && servActivos.Prestamos) {
servActivos.PrestamosStatus = timestamp;
} else {
continue;
}
await this.servActivosRepo.save(servActivos);
console.log(`Servicio ${servActivos}`);
}
}
async enviarCargaMasiva(): Promise<void> {
const usuarios_red = await this.usuariosDelSistemaRepo.findOne({ where: { origen: { origen: 'RED' } } });
const usuarios_at = await this.usuariosDelSistemaRepo.findOne({ where: { origen: { origen: 'AT' } } });
const usuarios_correo = await this.usuariosDelSistemaRepo.findOne({ where: { origen: { origen: 'CORREO' } } });
const usuarios_solicita = await this.usuariosDelSistemaRepo.findOne({ where: { origen: { origen: 'SOLICITA' } } });
if (!usuarios_red || !usuarios_at || !usuarios_correo || !usuarios_solicita) {
throw new BadRequestException('No se encontraron usuarios para enviar correo');
}
// Aquí puedes enviar el correo usando MailService
await this.mailService.sendMail({
to: usuarios_red.correo ?? '',
subject: 'Carga de Usuarios',
text: 'Se ha realizado una carga de usuarios en el sistema.',
html: '',
});
await this.mailService.sendMail({
to: usuarios_at.correo ?? '',
subject: 'Carga de Usuarios',
text: 'Se ha realizado una carga de usuarios en el sistema.',
html: '',
});
await this.mailService.sendMail({
to: usuarios_correo.correo ?? '',
subject: 'Carga de Usuarios',
text: 'Se ha realizado una carga de usuarios en el sistema.',
html: '',
});
await this.mailService.sendMail({
to: usuarios_solicita.correo ?? '',
subject: 'Carga de Usuarios',
text: 'Se ha realizado una carga de usuarios en el sistema.',
html: '',
});
console.log(`Correo enviados`);
}
}
+54
View File
@@ -0,0 +1,54 @@
import {
Catch,
ArgumentsHost,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
import { Response } from 'express';
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
let status = 500; // Valor por defecto para errores no controlados
let message = 'Internal server error';
if (exception instanceof HttpException) {
status = exception.getStatus();
const responseBody = exception.getResponse();
// Evitar el problema de circular structure usando JSON-safe conversion
message =
typeof responseBody === 'object'
? this.safeStringify(responseBody)
: (responseBody as string);
} else {
// Puedes mapear el error a un código HTTP y mensaje adecuados
status = 400;
message = (exception as any).sqlMessage || exception.message;
console.error('Unhandled Exception:', exception); // Log de errores no controlados
}
response.status(status).json({
statusCode: status,
message,
timestamp: new Date().toISOString(),
path: ctx.getRequest().url,
});
}
// ✅ Método para manejar objetos circulares usando WeakSet
private safeStringify(obj: any): string {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) return '[Circular]';
seen.add(value);
}
return value;
});
}
}
+25
View File
@@ -0,0 +1,25 @@
import { IsString, IsEmail, IsDate, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';
export class SendCorreoDto {
@IsEmail()
to: string;
@IsString()
@IsOptional()
subject: string;
@IsString()
@IsOptional()
text: string;
@IsString()
@IsOptional()
html: string;
}
+31
View File
@@ -0,0 +1,31 @@
// src/excel/excel.controller.ts
import { Controller, Post, Get, UseGuards, Request, Res, UploadedFile, UseInterceptors, HttpStatus, Param, ParseIntPipe, Body } from '@nestjs/common';
import { SendCorreoDto } from './dto/send-email.dto';
import { MailService } from './mail.service';
@Controller('mail')
export class MailController {
constructor(
private readonly mailService: MailService
) { }
@Post('send')
async sendMail(@Body() body: SendCorreoDto) {
const result =
await this.mailService.sendMail(body)
.then((value) => {
console.log("Se mandaron los correos", value);
})
.catch((err) => {
console.log(err)
})
// sistem
return result;
}
}
+12
View File
@@ -0,0 +1,12 @@
// src/movimiento/movimiento.module.ts
import { Module } from '@nestjs/common';
import { MailService } from './mail.service';
import { MailController } from './mail.controller';
@Module({
imports: [],
controllers: [MailController],
providers: [MailService],
exports: [MailService],
})
export class MailModule { }
+76
View File
@@ -0,0 +1,76 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import * as nodemailer from 'nodemailer';
import { SendCorreoDto } from 'src/mail/dto/send-email.dto';
@Injectable()
export class MailService {
async sendMail(sendEmail: SendCorreoDto) {
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
type: 'OAuth2',
user: process.env.EMAIL_USER, // tu correo de Gmail
clientId: process.env.CLIENT_ID, // tu Client ID de OAuth2
clientSecret: process.env.CLIENT_SECRET, // tu Client Secret de OAuth2
refreshToken: process.env.REFRESH_TOKEN, // tu Refresh Token de OAuth2
//accessToken: sistem.accessToken, // opcional
},
pool: true,
maxConnections: 1,
maxMessages: 100,
rateDelta: 2000,
rateLimit: 1
});
const mailOptions = {
from: process.env.EMAIL_USER, // tu correo de Gmail
to: sendEmail.to,
subject: sendEmail.subject,
text: sendEmail.text,
html: sendEmail.html,
};
console.log(mailOptions);
let resMail = await transporter.sendMail(mailOptions);
if (!resMail) throw new Error("")
const statusTexto = resMail.accepted.length > 0 ? "Enviado" : "Fallido";
if (!resMail) {
throw new Error("fallo")
}
return (statusTexto);
}
}
+57 -1
View File
@@ -1,8 +1,64 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { GlobalExceptionFilter } from './helpers/exception.filter';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
app.enableCors({ exposedHeaders: ['Content-Disposition'] });
app.useGlobalPipes(
new ValidationPipe({
transform: true,
}),
);
app.useGlobalFilters(new GlobalExceptionFilter());
const config = new DocumentBuilder()
.setTitle('Documentacion de la api de carga masiva de CEDETEC')
.setDescription(
'El objetivo es mejorar la gestion interna y la integridad de los datos',
)
.setVersion('1.0')
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
name: 'Authorization',
in: 'header',
},
'bearer', // nombre del security definition
)
.build();
// .addServer(process.env.SWAGGER_SERVER_URL) // descomentar parael servidor de acatlan
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('documentation', app, document);
const port = process.env.API_PORT || 4000;
await app.listen(port, '0.0.0.0');
console.log(
`[main] Aplicación iniciada en el puerto ${port} a las ${new Date().toISOString()}`,
);
console.log(
`\n\x1b[36m=========================================================\x1b[0m`,
);
console.log(`\x1b[36m SISTEMA DE CARGA MASIVA - API\x1b[0m`);
console.log(
`\x1b[36m=========================================================\x1b[0m`,
);
console.log(`\x1b[32m✅ API corriendo en:\x1b[0m http://localhost:${port}`);
console.log(
`\x1b[32m✅ Documentación disponible en:\x1b[0m http://localhost:${port}/documentation`,
);
console.log(
`\x1b[36m=========================================================\x1b[0m\n`,
);
}
bootstrap();
+12
View File
@@ -0,0 +1,12 @@
// src/movimiento/movimiento.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MovimientoService } from './movimiento.service';
import { Movimiento, Origen, Usuario } from '../entities/entities';
@Module({
imports: [TypeOrmModule.forFeature([Movimiento, Origen, Usuario])],
providers: [MovimientoService],
exports: [MovimientoService],
})
export class MovimientoModule { }
+254
View File
@@ -0,0 +1,254 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { QueryRunner, Repository } from 'typeorm';
import { Movimiento, Usuario } from '../entities/entities';
import { Origen } from '../entities/entities';
@Injectable()
export class MovimientoService {
constructor(
@InjectRepository(Movimiento)
private readonly movRepo: Repository<Movimiento>,
@InjectRepository(Origen)
private readonly origenRepo: Repository<Origen>,
@InjectRepository(Usuario)
private readonly usuarioRepo: Repository<Usuario>,
) { }
/** Crea un registro de movimiento */
async log(
origen: string,
status: string,
observaciones?: string,
reporte?: string,
flag = false,
): Promise<Movimiento> {
// 1) Obtén el origen o falla si no existe
const origin = await this.origenRepo.findOne({ where: { origen } });
let origenRepo = origin;
if (!origin) {
const newOrigin = this.origenRepo.create({ origen });
origenRepo = await this.origenRepo.save(newOrigin);
}
if (!origenRepo) {
throw new Error(`No se pudo obtener o crear el origen: ${origen}`);
}
// 2) Crea el objeto movimiento
const movimiento = this.movRepo.create({
fecha_mov: new Date(),
status,
observaciones,
reporte,
flag,
origen: { id_origen: origenRepo.id_origen },
});
// 3) Guarda y devuelve el ID
const saved = await this.movRepo.save(movimiento);
return saved;
}
async logger(
queryRunner: QueryRunner,
origenNombre: string,
status: string,
observaciones?: string,
reporte?: string
): Promise<Movimiento> {
const origenEntidad = await queryRunner.manager.findOne(Origen, {
where: { origen: origenNombre },
});
if (!origenEntidad) {
throw new Error(`Origen no encontrado: ${origenNombre}`);
}
const nuevoMov = queryRunner.manager.create(Movimiento, {
fecha_mov: new Date(),
status,
observaciones,
reporte,
origen: origenEntidad,
flag: false,
});
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 } });
if (!movimiento) {
throw new Error(`Movimiento con ID ${id_movimiento} no encontrado`);
}
movimiento.status = nuevoStatus;
movimiento.observaciones = observaciones || movimiento.observaciones; // opcional: actualizar observaciones
movimiento.fecha_mov = new Date(); // opcional: actualizar fecha
await this.movRepo.save(movimiento);
}
async findAll(origen: string): Promise<{ move: any[]; button: boolean }> {
const statusFieldMap = {
AT: 'ATStatus',
RED: 'RedStatus',
CORREO: 'CorreoStatus',
SOLICITA: 'PrestamosStatus',
} as const;
const FieldMap = {
AT: 'AT',
RED: 'Red',
CORREO: 'Correo',
SOLICITA: 'Prestamos',
} as const;
const field = statusFieldMap[origen as keyof typeof statusFieldMap];
const origenField = FieldMap[origen as keyof typeof FieldMap];
if (!field) {
const movimientos = await this.movRepo.find({
where: {
origen: { origen: 'LOAD' },
status: 'SUCCESS',
reporte: 'CARGA MASIVA DE USUARIOS',
},
});
const mov = await this.movRepo.find({
where: {
origen: { origen: 'SISTEMA' },
status: 'SUCCESS',
reporte: 'CARGA MASIVA WEB SERVICE',
},
});
const mov2 = await this.movRepo.find({
where: {
reporte: 'CARGA INDIVIDUAL DE USUARIOS',
status: 'SUCCESS',
},
});
console.log('movimientos:', mov2)
const todosLosMovimientos = [...movimientos, ...mov, ...mov2];
console.log(todosLosMovimientos)
return { move: todosLosMovimientos, button: false };
}
const movimientos = await this.movRepo.find({
where: {
origen: { origen: 'LOAD' },
reporte: 'CARGA MASIVA DE USUARIOS',
status: 'SUCCESS',
usuario: {
servActivo: {
[origenField]: true,
[field]: 'Inactivo'
},
},
},
});
const mov = await this.movRepo.find({
where: {
origen: { origen: 'SISTEMA' },
reporte: 'CARGA MASIVA WEB SERVICE',
status: 'SUCCESS',
usuario: {
servActivo: {
[origenField]: true,
[field]: 'Inactivo'
},
},
},
});
const mov2 = await this.movRepo.find({
where: {
reporte: 'CARGA INDIVIDUAL DE USUARIOS',
status: 'SUCCESS',
usuario: {
servActivo: {
[origenField]: true,
[field]: 'Inactivo'
},
},
},
relations: ['usuario'],
});
const todosLosMovimientos = [...movimientos, ...mov, ...mov2];
return { move: todosLosMovimientos, button: true };
}
async findAllCargaIndv(origen): Promise<Usuario[]> {
const statusFieldMap = {
AT: 'ATStatus',
RED: 'RedStatus',
CORREO: 'CorreoStatus',
SOLICITA: 'PrestamosStatus',
} as const;
const FieldMap = {
AT: 'AT',
RED: 'Red',
CORREO: 'Correo',
SOLICITA: 'Prestamos',
} as const;
const field = statusFieldMap[origen as keyof typeof statusFieldMap];
const origenField = FieldMap[origen as keyof typeof FieldMap];
if (!field) {
throw new Error(`Origen desconocido: ${origen}`);
}
const usuarios = await this.usuarioRepo.find({
where: {
movimiento: {
reporte: 'CARGA INDIVIDUAL DE USUARIOS',
},
servActivo: {
[field]: 'Inactivo',
[origenField]: true,
},
},
});
return usuarios;
}
}
+46
View File
@@ -0,0 +1,46 @@
import { IsNumber, IsOptional, IsString, Length } from "class-validator";
export class CreateUsuarioDto {
@IsOptional()
@IsString()
num_cuenta?: string;
@IsOptional()
@IsString()
carrera?: string;
@IsString()
tipo_usuario: string;
@IsOptional()
@IsString()
clave_carrera?: string;
@IsString()
nombre: string;
@IsString()
a_paterno: string;
@IsString()
a_materno: string;
@IsOptional()
@IsString()
@Length(10, 13)
rfc?: string;
@IsOptional()
@IsString()
@Length(8, 8)
fecha_nacimiento?: string;
@IsOptional()
@IsNumber()
generacion?: number;
@IsOptional()
@IsString()
genero?: string;
}
+4
View File
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateUsuarioDto } from './create-usuario.dto';
export class UpdateUsuarioDto extends PartialType(CreateUsuarioDto) {}
@@ -0,0 +1,39 @@
import { Usuario } from "src/entities/entities";
import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from "typeorm";
@Entity({ name: 'servActivos' })
export class ServActivos {
@PrimaryGeneratedColumn({ name: 'id_servActivos' })
id_servActivos: number;
@OneToOne(() => Usuario, (usuario) => usuario.servActivo, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_usuario' })
usuario: Usuario;
@Column({ type: 'boolean', default: false })
Red: boolean;
@Column({ type: 'boolean', default: false })
Prestamos: boolean;
@Column({ type: 'boolean', default: false })
AT: boolean;
@Column({ type: 'boolean', default: false })
Correo: boolean;
@Column({ type: 'varchar', length: 40, nullable: true })
RedStatus: string;
@Column({ type: 'varchar', length: 40, nullable: true })
PrestamosStatus: string;
@Column({ type: 'varchar', length: 40, nullable: true })
ATStatus: string;
@Column({ type: 'varchar', length: 40, nullable: true })
CorreoStatus: string;
}
+48
View File
@@ -0,0 +1,48 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common';
import { UsuariosService } from './usuarios.service';
import { CreateUsuarioDto } from './dto/create-usuario.dto';
import { UpdateUsuarioDto } from './dto/update-usuario.dto';
import { AuthGuard } from '@nestjs/passport';
import { ApiBearerAuth } from '@nestjs/swagger';
@Controller('usuarios')
export class UsuariosController {
constructor(private readonly usuariosService: UsuariosService) { }
@Post('users')
finsdAll() {
return this.usuariosService.enviarSolicitud();
}
@Get(':noCuenta')
findOne(@Param('noCuenta') noCuenta: string) {
return this.usuariosService.findOneStatus(noCuenta);
}
// @UseGuards(AuthGuard('jwt'))
// @ApiBearerAuth('bearer')
// @Post()
// create(@Body() noCuenta: string) {
// return this.usuariosService.altaIndividual(createUsuarioDto);
// }
// @Get()
// findAll() {
// return this.usuariosService.findAll();
// }
// @Patch(':id')
// update(@Param('id') id: string, @Body() updateUsuarioDto: UpdateUsuarioDto) {
// return this.usuariosService.update(+id, updateUsuarioDto);
// }
// @Delete(':id')
// remove(@Param('id') id: string) {
// return this.usuariosService.remove(+id);
// }
}
+21
View File
@@ -0,0 +1,21 @@
import { Module } from '@nestjs/common';
import { UsuariosService } from './usuarios.service';
import { UsuariosController } from './usuarios.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Carrera, CarreraUsuario, Genero, TipoUsuario, Usuario, UsuariosDelSistema, UsuarioTipoUsuario } from 'src/entities/entities';
import { MovimientoModule } from 'src/movimiento/movimiento.module';
import { ServActivos } from './entities/servActivos.entitie';
import { MailService } from 'src/mail/mail.service';
import { ExcelService } from 'src/excel/excel.service';
@Module({
imports: [
TypeOrmModule.forFeature([Usuario, Genero, Carrera, CarreraUsuario, ServActivos, TipoUsuario, UsuarioTipoUsuario, UsuariosDelSistema]),
MovimientoModule
],
controllers: [UsuariosController],
providers: [UsuariosService, MailService, ExcelService],
exports: [TypeOrmModule.forFeature([ServActivos])],
})
export class UsuariosModule { }
+635
View File
@@ -0,0 +1,635 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { CreateUsuarioDto } from './dto/create-usuario.dto';
import { UpdateUsuarioDto } from './dto/update-usuario.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Carrera, CarreraUsuario, Genero, TipoUsuario, Usuario, UsuarioTipoUsuario } from 'src/entities/entities';
import { Repository } from 'typeorm';
import { ServActivos } from './entities/servActivos.entitie';
import { MovimientoService } from 'src/movimiento/movimiento.service';
import { MailService } from 'src/mail/mail.service';
import { error } from 'console';
import { Cron } from '@nestjs/schedule';
import { ExcelService } from 'src/excel/excel.service';
@Injectable()
export class UsuariosService {
private readonly logger = new Logger(UsuariosService.name);
constructor(
@InjectRepository(Usuario)
private readonly usuarioRepository: Repository<Usuario>,
@InjectRepository(Carrera)
private readonly carreraRepository: Repository<Carrera>,
@InjectRepository(ServActivos)
private readonly servActivosRepository: Repository<ServActivos>,
@InjectRepository(TipoUsuario)
private readonly tipoUsuarioRepository: Repository<TipoUsuario>,
@InjectRepository(Genero)
private readonly generoRepository: Repository<Genero>,
@InjectRepository(CarreraUsuario)
private readonly carreraUsuario: Repository<CarreraUsuario>,
@InjectRepository(UsuarioTipoUsuario)
private readonly usuarioTipoUsuario: Repository<UsuarioTipoUsuario>,
private readonly movimientoService: MovimientoService,
private readonly mailService: MailService,
private readonly excelService: ExcelService, // Asegúrate de importar el servicio de Excel
) { }
altaIndividual(createUsuarioDto: CreateUsuarioDto) {
return 'This action adds a new usuario';
}
async findOneStatus(noCuenta: string) {
const usuario = await this.usuarioRepository.findOne({
where: { num_cuenta: noCuenta },
});
if (!usuario) {
throw new NotFoundException('Usuario no encontrado');
}
const servActivo = await this.servActivosRepository.findOne({
where: { usuario: { id_usuario: usuario.id_usuario } },
});
if (!servActivo) {
throw new NotFoundException('Servicios no encontrados para el usuario');
}
const resultado: {
servicio: string;
status: string;
fecha_activacion: string | null;
}[] = [];
const mapServicio = (nombre: string, estado: string | undefined) => {
if (!estado) return;
if (estado.startsWith('Activo')) {
const fechaCompleta = estado.substring(7); // "2025-06-25T14:26:21.427Z"
const fechaSolo = fechaCompleta.substring(0, 10); // "2025-06-25"
resultado.push({
servicio: nombre,
status: 'Activo',
fecha_activacion: fechaSolo,
});
} else {
resultado.push({
servicio: nombre,
status: 'Inactivo',
fecha_activacion: null,
});
}
};
await mapServicio('Red', servActivo.RedStatus);
await mapServicio('Correo', servActivo.CorreoStatus);
await mapServicio('Préstamo de equipo PC Puma', servActivo.PrestamosStatus);
await mapServicio('Préstamo de equipo en CEDETEC', servActivo.ATStatus);
if (resultado.length === 0) {
return [{
servicio: 'Ninguno',
status: 'No hay servicios activos para este usuario',
fecha_activacion: null,
}];
}
return { resultado, usuario: usuario.nombre };
}
update(id: number, updateUsuarioDto: UpdateUsuarioDto) {
return `This action updates a #${id} usuario`;
}
@Cron('0 0 0 10,25 * *')
async tareaMensual() {
this.logger.log('Ejecutando consulta mensual de profesores');
await this.enviarSolicitud(); // tu función de importación
}
async enviarSolicitud() {
const mov = await this.movimientoService.log(
'SISTEMA',
'LOADING',
'Enviando solicitud de consulta masiva de profesores al WebService',
'CARGA MASIVA WEB SERVICE'
);
interface DatosEntrada {
Nombre: string;
ApellidoPaterno: string;
ApellidoMaterno: string;
NumeroTrabajador: string;
RFC: string;
}
interface DatosRespuesta {
numeroTrabajador: string;
nombre: string;
apellidoPaterno: string;
apellidoMaterno: string;
nombreCarrera: string;
fechaNacimiento: string;
rfc: string;
genero: string;
correoPersonal: string;
nombramiento: string;
unidadResponsable: string;
curp: string;
}
const apiUrl = process.env.API_URL;
if (!apiUrl) {
console.error("API_URL no está definida en las variables de entorno");
return;
}
const entrada: DatosEntrada = {
Nombre: "",
ApellidoPaterno: "",
ApellidoMaterno: "",
NumeroTrabajador: "",
RFC: ""
};
try {
const response = await fetch(apiUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(entrada)
});
if (response.status === 400) {
const errorText = await response.text();
console.error("Error 400:", errorText);
return;
} else if (response.status === 204) {
console.log("No se encontraron datos conforme a los criterios de búsqueda");
return;
}
if (!response.ok) throw new Error("Error en la petición: " + response.statusText);
const json: DatosRespuesta[] = await response.json();
let nuevos = 0;
let actualizados = 0;
let existe = await this.tipoUsuarioRepository.findOne({ where: { tipo_usuario: 'Profesor' } });
if (!existe) {
const tipoUsuario = this.tipoUsuarioRepository.create({ tipo_usuario: 'Profesor' });
existe = await this.tipoUsuarioRepository.save(tipoUsuario);
}
for (const prof of json) {
try {
if (!prof.rfc || !prof.nombre) {
console.warn('Profesor con datos incompletos detectado:', prof);
continue;
}
const existente = await this.usuarioRepository.findOne({
where: { rfc: prof.rfc },
relations: ['genero']
});
if (!existente) {
let genero: Genero | null = null;
if (prof.genero === 'M' || prof.genero === 'F') {
const generoTexto = prof.genero === 'M' ? 'Masculino' : 'Femenino';
genero = await this.generoRepository.findOne({ where: { genero: generoTexto } });
if (!genero) {
genero = await this.generoRepository.create({ genero: generoTexto });
await this.generoRepository.save(genero);
}
}
let carrera = await this.carreraRepository.findOne({ where: { carrera: prof.nombreCarrera.trim() } })
if (!carrera) {
const nuevaCarrera = await this.carreraRepository.create({ carrera: prof.nombreCarrera.trim(), clave: '' });
carrera = await this.carreraRepository.save(nuevaCarrera);
}
const nuevo = await this.usuarioRepository.create({
num_cuenta: prof.numeroTrabajador == '' ? undefined : prof.numeroTrabajador,
nombre: prof.nombre,
a_paterno: prof.apellidoPaterno,
a_materno: prof.apellidoMaterno,
rfc: prof.rfc,
fecha_nacimiento: prof.fechaNacimiento,
genero,
movimiento: mov
});
const savedUser = await this.usuarioRepository.save(nuevo);
const servActivo = await this.servActivosRepository.create({
ATStatus: 'Inactivo',
RedStatus: 'Inactivo',
PrestamosStatus: 'Inactivo',
CorreoStatus: 'Inactivo',
usuario: savedUser,
AT: prof.numeroTrabajador !== '',
Red: prof.numeroTrabajador !== '',
Prestamos: prof.numeroTrabajador !== '',
Correo: true
});
await this.servActivosRepository.save(servActivo);
const userCarrera = await this.carreraUsuario.create({ carrera, usuario: savedUser });
const usuarioTipo = await this.usuarioTipoUsuario.create({ tipoUsuario: existe, usuario: savedUser });
await this.carreraUsuario.save(userCarrera);
await this.usuarioTipoUsuario.save(usuarioTipo);
nuevos++;
} else {
let cambios = false;
if (existente.nombre !== prof.nombre) {
existente.nombre = prof.nombre;
const servActivo = await this.servActivosRepository.findOne({ where: { usuario: { id_usuario: existente.id_usuario } } });
if (servActivo) {
servActivo.Red = true;
servActivo.AT = true;
servActivo.Prestamos = true;
servActivo.ATStatus = 'Inactivo';
servActivo.PrestamosStatus = 'Inactivo';
servActivo.RedStatus = 'Inactivo';
await this.servActivosRepository.save(servActivo);
} else {
console.warn('No se encontró el servicio activo para el usuario:', existente);
}
cambios = true;
}
if (existente.a_paterno !== prof.apellidoPaterno) {
existente.a_paterno = prof.apellidoPaterno;
const servActivo = await this.servActivosRepository.findOne({ where: { usuario: { id_usuario: existente.id_usuario } } });
if (servActivo) {
servActivo.Red = true;
servActivo.AT = true;
servActivo.Prestamos = true;
servActivo.ATStatus = 'Inactivo';
servActivo.PrestamosStatus = 'Inactivo';
servActivo.RedStatus = 'Inactivo';
await this.servActivosRepository.save(servActivo);
} else {
console.warn('No se encontró el servicio activo para el usuario:', existente);
}
cambios = true;
}
if (existente.a_materno !== prof.apellidoMaterno) {
existente.a_materno = prof.apellidoMaterno;
const servActivo = await this.servActivosRepository.findOne({ where: { usuario: { id_usuario: existente.id_usuario } } });
if (servActivo) {
servActivo.Red = true;
servActivo.AT = true;
servActivo.Prestamos = true;
servActivo.ATStatus = 'Inactivo';
servActivo.PrestamosStatus = 'Inactivo';
servActivo.RedStatus = 'Inactivo';
await this.servActivosRepository.save(servActivo);
} else {
console.warn('No se encontró el servicio activo para el usuario:', existente);
}
cambios = true;
}
if (existente.rfc !== prof.rfc) {
existente.rfc = prof.rfc;
const servActivo = await this.servActivosRepository.findOne({ where: { usuario: { id_usuario: existente.id_usuario } } });
if (servActivo) {
servActivo.Red = true;
servActivo.Prestamos = true;
servActivo.PrestamosStatus = 'Inactivo';
servActivo.RedStatus = 'Inactivo';
await this.servActivosRepository.save(servActivo);
} else {
console.warn('No se encontró el servicio activo para el usuario:', existente);
}
cambios = true;
}
if (existente.fecha_nacimiento !== prof.fechaNacimiento) {
existente.fecha_nacimiento = prof.fechaNacimiento;
const servActivo = await this.servActivosRepository.findOne({ where: { usuario: { id_usuario: existente.id_usuario } } });
if (servActivo) {
servActivo.Prestamos = true;
servActivo.Correo = true;
servActivo.PrestamosStatus = 'Inactivo';
servActivo.CorreoStatus = 'Inactivo';
await this.servActivosRepository.save(servActivo);
} else {
console.warn('No se encontró el servicio activo para el usuario:', existente);
}
cambios = true;
}
if (prof.numeroTrabajador && existente.num_cuenta !== prof.numeroTrabajador) {
existente.num_cuenta = prof.numeroTrabajador;
const servActivo = await this.servActivosRepository.findOne({ where: { usuario: { id_usuario: existente.id_usuario } } });
if (servActivo) {
servActivo.Red = true;
servActivo.AT = true;
servActivo.Correo = true;
servActivo.Prestamos = true;
servActivo.CorreoStatus = 'Inactivo';
servActivo.ATStatus = 'Inactivo';
servActivo.PrestamosStatus = 'Inactivo';
servActivo.RedStatus = 'Inactivo';
await this.servActivosRepository.save(servActivo);
} else {
console.warn('No se encontró el servicio activo para el usuario:', existente);
}
cambios = true;
}
if (cambios) {
existente.movimiento = mov;
await this.usuarioRepository.save(existente);
actualizados++;
}
}
console.log(`Progreso -> Nuevos: ${nuevos}, Actualizados: ${actualizados}`);
} catch (innerError) {
console.error('Error procesando profesor:', prof, innerError);
}
}
this.movimientoService.updateStatus(mov.id_mov, 'SUCCESS', 'Carga masiva de web Service completa: ' + `Nuevos: ${nuevos}, Actualizados: ${actualizados}`)
if (!process.env.EMAIL_USER) {
throw new Error()
}
this.mailService.sendMail({
to: process.env.EMAIL_USER,
subject: 'Carga de datos exitosa',
text: 'Carga masiva de web Service completa: ' + `Nuevos: ${nuevos}, Actualizados: ${actualizados}`,
html: '<p></p>'
})
this.excelService.enviarCargaMasiva();
return `Importación finalizada. Nuevos: ${nuevos}, Actualizados: ${actualizados}`;
} catch (error) {
this.logger.error('Error al consultar profesores desde el WebService', error);
throw error;
}
}
async cargaIndividual(usuario: CreateUsuarioDto, origen: string) {
const queryRunner = this.usuarioRepository.manager.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const existente = await queryRunner.manager.findOne(Usuario, {
where: { num_cuenta: usuario.num_cuenta },
relations: ['genero'],
});
if (existente) {
throw new Error('Ya existe este usuario');
}
const mov = await this.movimientoService.logger(queryRunner, origen, 'LOADING', undefined, 'CARGA INDIVIDUAL DE USUARIOS');
let genero: Genero | null = null;
if (usuario.genero === 'M' || usuario.genero === 'F') {
const generoTexto = usuario.genero === 'M' ? 'Masculino' : 'Femenino';
genero = await queryRunner.manager.findOne(Genero, { where: { genero: generoTexto } });
if (!genero) {
genero = queryRunner.manager.create(Genero, { genero: generoTexto });
await queryRunner.manager.save(genero);
}
}
let carrera = await queryRunner.manager.findOne(Carrera, { where: { carrera: usuario.carrera } });
if (!carrera) {
const nuevaCarrera = queryRunner.manager.create(Carrera, { carrera: usuario.carrera, clave: usuario.clave_carrera });
carrera = await queryRunner.manager.save(nuevaCarrera);
}
const nuevo = queryRunner.manager.create(Usuario, {
num_cuenta: usuario.num_cuenta,
nombre: usuario.nombre,
a_paterno: usuario.a_paterno,
a_materno: usuario.a_materno,
rfc: usuario.rfc ?? null,
fecha_nacimiento: usuario.fecha_nacimiento,
generacion: usuario.generacion ?? null,
genero,
movimiento: mov,
});
const savedUser = await queryRunner.manager.save(nuevo);
const userCarrera = queryRunner.manager.create(CarreraUsuario, { carrera, usuario: savedUser });
const FieldMap = [
'Diplomado', 'Extra Largo', 'Servicio Social', 'Idiomas R (UNAM)', 'Idiomas Sabatino',
'Reinscrito', 'Posgrado', 'Intercambio UNAM', 'Movilidad', 'Ampliación de Conocimiento',
'Licenciatura', 'Profesor', 'Trabajadores',
];
if (!FieldMap.includes(usuario.tipo_usuario)) {
throw new Error(`Tipo de usuario no permitido: ${usuario.tipo_usuario}`);
}
// Verifica si el tipo de usuario ya existe
let tipoUsuario = await queryRunner.manager.findOne(TipoUsuario, {
where: { tipo_usuario: usuario.tipo_usuario },
});
// Si no existe, lo crea y guarda
if (!tipoUsuario) {
tipoUsuario = queryRunner.manager.create(TipoUsuario, {
tipo_usuario: usuario.tipo_usuario,
});
tipoUsuario = await queryRunner.manager.save(tipoUsuario);
}
// Luego crea la relación con el usuario ya guardado
const usuarioTipo = queryRunner.manager.create(UsuarioTipoUsuario, {
tipoUsuario,
usuario: savedUser,
});
await queryRunner.manager.save(usuarioTipo);
switch (usuario.tipo_usuario.trim()) {
case 'Diplomado':
const servActivosDiplomado = this.servActivosRepository.create({
Correo: true,
usuario: savedUser,
RedStatus: 'Inactivo',
ATStatus: 'Inactivo',
CorreoStatus: 'Inactivo',
PrestamosStatus: 'Inactivo',
});
await queryRunner.manager.save(servActivosDiplomado);
break;
case 'Extra Largo':
const servActivosExtraLargo = this.servActivosRepository.create({
Prestamos: true,
Correo: true,
usuario: savedUser,
RedStatus: 'Inactivo',
ATStatus: 'Inactivo',
CorreoStatus: 'Inactivo',
PrestamosStatus: 'Inactivo',
});
await queryRunner.manager.save(servActivosExtraLargo);
break;
case 'Servicio Social':
const servActivosServicio = this.servActivosRepository.create({
Red: true,
AT: true,
Correo: true,
usuario: savedUser,
RedStatus: 'Inactivo',
ATStatus: 'Inactivo',
CorreoStatus: 'Inactivo',
PrestamosStatus: 'Inactivo',
});
await queryRunner.manager.save(servActivosServicio);
break;
case 'Idiomas R (UNAM)':
case 'Idiomas Sabatino':
const servActivosIdiomas = this.servActivosRepository.create({
Red: true,
Correo: true,
usuario: savedUser,
RedStatus: 'Inactivo',
ATStatus: 'Inactivo',
CorreoStatus: 'Inactivo',
PrestamosStatus: 'Inactivo',
});
await queryRunner.manager.save(servActivosIdiomas);
break;
case 'Reinscrito':
case 'Posgrado':
case 'Intercambio UNAM':
case 'Movilidad':
case 'Ampliación de Conocimiento':
case 'Licenciatura':
case 'Profesor':
const servActivos = this.servActivosRepository.create({
Red: true,
AT: true,
Correo: true,
Prestamos: true,
usuario: savedUser,
RedStatus: 'Inactivo',
ATStatus: 'Inactivo',
CorreoStatus: 'Inactivo',
PrestamosStatus: 'Inactivo',
});
await queryRunner.manager.save(servActivos);
break;
case 'Trabajadores':
const servActivosTrabajadores = this.servActivosRepository.create({
Red: true,
usuario: savedUser,
RedStatus: 'Inactivo',
});
await queryRunner.manager.save(servActivosTrabajadores);
break;
}
console.log(savedUser)
await queryRunner.commitTransaction();
return { saved: savedUser, movId: mov.id_mov };
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
}