From 4984c47029d1fbb8fa381756fafa7c23bf031e4d Mon Sep 17 00:00:00 2001 From: evenegas Date: Wed, 18 Jun 2025 07:33:21 -0600 Subject: [PATCH] Se agregaron los campos de los que se deben de mandar a origen de usuario --- .devcontainer/devcontainer.json | 19 ++ src/app.module.ts | 13 +- src/auth/auth.controller.ts | 11 +- src/auth/auth.documentation.ts | 74 +++-- src/auth/auth.module.ts | 6 +- src/auth/auth.service.ts | 40 ++- src/auth/dto/register.dto.ts | 16 +- src/auth/jwt.strategy.ts | 2 +- src/entities/entities.ts | 66 ++--- src/excel/excel.controller.ts | 159 +++++++++-- src/excel/excel.service.ts | 408 ++++++++++++++++++++++----- src/movimiento/movimiento.service.ts | 13 +- 12 files changed, 598 insertions(+), 229 deletions(-) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..af4a840 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -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" +} \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 83172c0..ba8aadb 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,7 +12,7 @@ import { MovimientoModule } from './movimiento/movimiento.module'; @Module({ - imports: [ + imports: [ ConfigModule.forRoot({ isGlobal: true }), TypeOrmModule.forRootAsync({ imports: [ConfigModule], @@ -20,7 +20,7 @@ import { MovimientoModule } from './movimiento/movimiento.module'; useFactory: async (config: ConfigService) => { const dbConfig = { type: 'mysql' as const, - host: config.get('DB_HOST', 'localhost'), + host: config.get('DB_HOST', 'database'), port: config.get('DB_PORT', 3306), username: config.get('DB_USER', 'root'), // no prints en claro en prod: @@ -40,18 +40,19 @@ import { MovimientoModule } from './movimiento/movimiento.module'; logger: 'advanced-console', retryAttempts: 5, retryDelay: 2000, + dropSchema: true, // solo para desarrollo }; }, }), TypeOrmModule.forFeature(Object.values(entities)), - ExcelModule, - AuthModule, - MovimientoModule + ExcelModule, + AuthModule, + MovimientoModule ], controllers: [AppController], providers: [AppService], }) -export class AppModule {} +export class AppModule { } diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index f092d3f..a49b9de 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -8,19 +8,22 @@ import { ApiBearerAuth } from '@nestjs/swagger'; @Controller() export class AuthController { - constructor(private readonly authService: AuthService) {} + 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')) + @UseGuards(AuthGuard('jwt')) @ApiBearerAuth('bearer') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt')) @Get('profile') @AuthDocumentation.bearerAuth() getProfile(@Request() req) { @@ -34,7 +37,7 @@ export class AuthController { - @Post('register') + @Post('register') @AuthDocumentation.register() // ver siguiente sección async register(@Body() dto: RegisterDto) { return this.authService.register(dto); diff --git a/src/auth/auth.documentation.ts b/src/auth/auth.documentation.ts index b3d5b90..afe30cd 100644 --- a/src/auth/auth.documentation.ts +++ b/src/auth/auth.documentation.ts @@ -1,5 +1,6 @@ import { applyDecorators } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBody, ApiConsumes, ApiResponse } from '@nestjs/swagger'; +import { Origen } from 'src/entities/entities'; export class AuthDocumentation { /** @@ -63,44 +64,41 @@ export class AuthDocumentation { 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' }, - nombre: { type: 'string', example: 'Miguel' }, - a_paterno: { type: 'string', example: 'Cuadra' }, - a_materno: { type: 'string', example: 'Chavez' }, - rfc: { type: 'string', example: 'CAHM000343DG2' }, - }, - required: ['email','password','nombre','a_paterno','a_materno'] - } - }), - ApiResponse({ - status: 201, - description: 'Usuario creado correctamente', - schema: { - type: 'object', - properties: { - id_usuario: { type: 'number', example: 1 }, - correo: { type: 'string', example: 'nuevo@dominio.com' }, - nombre: { type: 'string', example: 'Miguel' }, - a_paterno: { type: 'string', example: 'Cuadra' }, - a_materno: { type: 'string', example: 'Chavez' }, - rfc: { type: 'string', example: 'CAHM000343DG2' }, + 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: 'redes', description: 'Origen del usuario' } + + } } - } - }), - ApiResponse({ status: 409, description: 'El correo ya está registrado.' }), - ); -} + }), + 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: 'redes' } + + + } + } + }), + ApiResponse({ status: 409, description: 'El correo ya está registrado.' }), + ); + } } diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index d0b3a47..2b406e0 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -6,12 +6,12 @@ import { ConfigService, ConfigModule } from '@nestjs/config'; import { AuthService } from './auth.service'; import { AuthController } from './auth.controller'; import { JwtStrategy } from './jwt.strategy'; -import { Usuario } from '../entities/entities'; +import { Origen, UsuariosDelSistema } from '../entities/entities'; import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [ - TypeOrmModule.forFeature([Usuario]), + TypeOrmModule.forFeature([UsuariosDelSistema, Origen]), PassportModule, JwtModule.registerAsync({ imports: [ConfigModule], @@ -25,4 +25,4 @@ import { TypeOrmModule } from '@nestjs/typeorm'; providers: [AuthService, JwtStrategy], controllers: [AuthController], }) -export class AuthModule {} +export class AuthModule { } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index b49a1ce..0bacb35 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,23 +1,26 @@ // src/auth/auth.service.ts import { ConflictException, Injectable, UnauthorizedException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import { Repository } from 'typeorm'; +import { Or, Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; -import { Usuario } from '../entities/entities'; +import { Origen, UsuariosDelSistema } from '../entities/entities'; import * as bcrypt from 'bcrypt'; import { RegisterDto } from './dto/register.dto'; @Injectable() export class AuthService { constructor( - @InjectRepository(Usuario) - private readonly userRepo: Repository, + @InjectRepository(UsuariosDelSistema) + private readonly userRepo: Repository, + @InjectRepository(Origen) + private readonly origenRepo: Repository, + private readonly jwtService: JwtService, - ) {} + ) { } // 1) Valida email/password - async validateUser(email: string, pass: string): Promise { - const user = await this.userRepo.findOne({ where: { correo: email } }); + async validateUser(email: string, pass: string): Promise { + 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'); @@ -25,8 +28,12 @@ export class AuthService { } // 2) Genera el JWT - async login(user: Usuario) { - const payload = { sub: user.id_usuario, email: user.correo }; + 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), }; @@ -45,15 +52,20 @@ export class AuthService { // 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, - nombre: dto.nombre, - a_paterno: dto.a_paterno, - a_materno: dto.a_materno, - rfc: dto.rfc ?? null, - // otros campos necesarios (fecha_nacimiento, generacion…) o valores por defecto + origen: origen, + }); // 4) Guardar en BD diff --git a/src/auth/dto/register.dto.ts b/src/auth/dto/register.dto.ts index 3c32c2b..d8bded3 100644 --- a/src/auth/dto/register.dto.ts +++ b/src/auth/dto/register.dto.ts @@ -1,4 +1,4 @@ -import { IsEmail, IsString, MinLength, MaxLength, IsOptional } from 'class-validator'; +import { IsEmail, IsString, MinLength, MaxLength, IsOptional, IsNumber } from 'class-validator'; export class RegisterDto { @IsEmail() @@ -8,20 +8,8 @@ export class RegisterDto { @MinLength(6) password: string; - @IsString() - @MaxLength(60) - nombre: string; @IsString() @MaxLength(60) - a_paterno: string; - - @IsString() - @MaxLength(60) - a_materno: string; - - @IsOptional() - @IsString() - @MaxLength(15) - rfc?: string; + origen: string; } diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts index 77658ce..ddf6e23 100644 --- a/src/auth/jwt.strategy.ts +++ b/src/auth/jwt.strategy.ts @@ -22,6 +22,6 @@ export class JwtStrategy extends PassportStrategy(Strategy) { async validate(payload: any) { // Devuelve lo que estará disponible en Request.user - return { userId: payload.sub, email: payload.email }; + return { userId: payload.sub, email: payload.email, origen: payload.tipo }; } } diff --git a/src/entities/entities.ts b/src/entities/entities.ts index a6f1d06..0c71e52 100644 --- a/src/entities/entities.ts +++ b/src/entities/entities.ts @@ -6,7 +6,7 @@ export class Origen { id_origen: number; @Column({ type: 'varchar', length: 30 }) - fuente: string; + origen: string; @OneToMany(() => Movimiento, movimiento => movimiento.origen) movimientos: Movimiento[]; @@ -17,7 +17,7 @@ export class Origen { @Entity({ name: 'genero' }) export class Genero { - @PrimaryGeneratedColumn({ name: 'id_genero', type: 'int', }) + @PrimaryGeneratedColumn({ name: 'id_genero', type: 'int', }) id_genero: number; @Column({ type: 'varchar', length: 20, nullable: true }) @@ -47,14 +47,14 @@ export class Usuario { @PrimaryGeneratedColumn({ name: 'id_usuario', type: 'int' }) id_usuario: number; - @Column({ + @Column({ type: 'varchar', length: 9, unique: true, - nullable: true, + nullable: true, default: null, }) - num_cuenta: string; + num_cuenta: string; @Column({ type: 'varchar', length: 60 }) nombre: string; @@ -65,36 +65,24 @@ export class Usuario { @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 }) + @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, }) + @Column({ type: 'int', nullable: true, }) generacion: number | null; - @ManyToOne(() => Genero, genero => genero.usuarios, { nullable: true }) -@JoinColumn({ name: 'id_genero' }) -genero: Genero | null; - @Column({ - type: 'varchar', - length: 100, // ajusta longitud según tu necesidad - unique: true, - nullable: true, - default: null, -}) -correo: string | null; + @ManyToOne(() => Genero, genero => genero.usuarios, { nullable: true }) + @JoinColumn({ name: 'id_genero' }) + genero: Genero | null; - @Column({ type: 'varchar', length: 60 }) - contraseña: string; - - @OneToMany(() => Movimiento, mov => mov.usuario) - movimientos: Movimiento[]; @OneToMany(() => CarreraUsuario, cu => cu.usuario) carreraUsuarios: CarreraUsuario[]; @@ -105,8 +93,17 @@ correo: string | null; @Entity({ name: 'usuariosDelSistema' }) export class UsuariosDelSistema { - @PrimaryColumn({ type: 'varchar', length: 60 }) - usuario: string; + @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; @@ -189,16 +186,13 @@ export class Movimiento { @Column({ type: 'bit' }) flag: boolean; -@Column({ + @Column({ type: 'varchar', length: 200, nullable: true, // permite NULL }) reporte?: string; - @ManyToOne(() => Usuario, usuario => usuario.movimientos) - @JoinColumn({ name: 'id_usuario' }) - usuario: Usuario; @ManyToOne(() => Origen, origen => origen.movimientos) @JoinColumn({ name: 'id_origen' }) diff --git a/src/excel/excel.controller.ts b/src/excel/excel.controller.ts index 152593e..bf6eaad 100644 --- a/src/excel/excel.controller.ts +++ b/src/excel/excel.controller.ts @@ -3,7 +3,7 @@ import { Controller, Post, Get, UseGuards, Request, Res, UploadedFile, UseInterc import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth } from '@nestjs/swagger'; import { FileInterceptor } from '@nestjs/platform-express'; -import { Response } from 'express'; +import e, { Response } from 'express'; import { ExcelDocumentation } from './excel.documentation'; import { ExcelService } from './excel.service'; import { MovimientoService } from '../movimiento/movimiento.service'; @@ -15,7 +15,7 @@ export class ExcelController { constructor( private readonly excelService: ExcelService, private readonly movimientoService: MovimientoService, - ) {} + ) { } @Post('verify') @UseInterceptors(FileInterceptor('file')) @@ -24,7 +24,7 @@ export class ExcelController { const userId: number = req.user.userId; // ahora sí existe const errors = await this.excelService.validateFile(file.buffer); await this.movimientoService.log( - userId, + 'VERIFY', errors.length ? 'FAILED' : 'SUCCESS', errors.join('; ') @@ -35,12 +35,20 @@ export class ExcelController { @Post('load') @UseInterceptors(FileInterceptor('file')) @ExcelDocumentation.loadExcel() - async loadExcel(@UploadedFile() file: Express.Multer.File, @Request() req) { - const userId: number = req.user.userId; + async loadExcel(@Request() req, @UploadedFile() file: Express.Multer.File) { + const origen = req.user.origen; // ahora sí existe + if (origen != 'LOAD') { + await this.movimientoService.log( + 'LOAD', + 'FAILED', + 'Origen no permitido para carga' + ); + throw new Error('Origen no permitido para carga.'); + } try { const result = await this.excelService.loadFile(file.buffer); await this.movimientoService.log( - userId, + 'LOAD', 'SUCCESS', undefined, @@ -49,7 +57,7 @@ export class ExcelController { return result; } catch (err) { await this.movimientoService.log( - userId, + 'LOAD', 'FAILED', err.message @@ -61,31 +69,124 @@ export class ExcelController { @Get('download') @ExcelDocumentation.downloadExcel() async downloadData(@Request() req, @Res() res: Response) { - const userId: number = req.user.userId; - try { - const csv = await this.excelService.generateCsv(); + const origen = req.user.origen; // ahora sí existe + console.log('Origen de descarga:', origen); + console.log('Usuario:', req.user); + + if (origen == 'SOLICITA') { + try { + const csv = await this.excelService.generateCsvSolicita(); + await this.movimientoService.log( + + origen, + 'SUCCESS', + undefined, + `size=${csv.length}` + ); + return res + .status(HttpStatus.OK) + .header('Content-Type', 'text/tab-separated-values') + .header('Content-Disposition', 'attachment; filename="usuarios_solicita.tsv"') + .send(csv); + } catch (err) { + await this.movimientoService.log( + origen, + 'FAILED', + 'Origen no permitido para descarga' + ); + return res + .status(HttpStatus.FORBIDDEN) + .json({ statusCode: 403, message: 'Origen no permitido para descarga.' }); + } + } else if (origen == 'CORREO') { + try { + const csv = await this.excelService.generateCsvCorreo(); + await this.movimientoService.log( + + origen, + 'SUCCESS', + undefined, + `size=${csv.length}` + ); + return res + .status(HttpStatus.OK) + .header('Content-Type', 'text/tab-separated-values') + .header('Content-Disposition', 'attachment; filename="usuarios.tsv"') + .send(csv); + } catch (err) { + await this.movimientoService.log( + + origen, + 'FAILED', + err.message + ); + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ statusCode: 500, message: 'Error generando descarga.' }); + } + } else if (origen == 'AT') { + try { + const csv = await this.excelService.generateCsvAT(); + await this.movimientoService.log( + + origen, + 'SUCCESS', + undefined, + `size=${csv.length}` + ); + return res + .status(HttpStatus.OK) + .header('Content-Type', 'text/tab-separated-values') + .header('Content-Disposition', 'attachment; filename="usuarios_at.tsv"') + .send(csv); + } catch (err) { + await this.movimientoService.log( + + origen, + 'FAILED', + err.message + ); + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ statusCode: 500, message: 'Error generando descarga.' }); + } + } else if (origen == 'RED') { + try { + const csv = await this.excelService.generateCsvRed(); + await this.movimientoService.log( + + origen, + 'SUCCESS', + undefined, + `size=${csv.length}` + ); + return res + .status(HttpStatus.OK) + .header('Content-Type', 'text/tab-separated-values') + .header('Content-Disposition', 'attachment; filename="usuarios_red.tsv"') + .send(csv); + } catch (err) { + await this.movimientoService.log( + + origen, + 'FAILED', + err.message + ); + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ statusCode: 500, message: 'Error generando descarga.' }); + } + } else { await this.movimientoService.log( - userId, - 'DOWNLOAD', - 'SUCCESS', - undefined, - `size=${csv.length}` - ); - return res - .status(HttpStatus.OK) - .header('Content-Type', 'text/tab-separated-values') - .header('Content-Disposition', 'attachment; filename="usuarios.tsv"') - .send(csv); - } catch (err) { - await this.movimientoService.log( - userId, - 'DOWNLOAD', + + origen, 'FAILED', - err.message + 'Origen no permitido para descarga' ); return res - .status(HttpStatus.INTERNAL_SERVER_ERROR) - .json({ statusCode: 500, message: 'Error generando descarga.' }); + .status(HttpStatus.FORBIDDEN) + .json({ statusCode: 403, message: 'Origen no permitido para descarga.' }); } + } -} +} \ No newline at end of file diff --git a/src/excel/excel.service.ts b/src/excel/excel.service.ts index cde45a9..56f1574 100644 --- a/src/excel/excel.service.ts +++ b/src/excel/excel.service.ts @@ -1,7 +1,7 @@ // src/excel/excel.service.ts import { Injectable, BadRequestException, Logger } from '@nestjs/common'; import { Workbook } from 'exceljs'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { Usuario } from '../entities/entities'; import { Genero } from '../entities/entities'; @@ -35,7 +35,7 @@ export class ExcelService { @InjectRepository(Carrera) private readonly carreraRepo: Repository, // … 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 { @@ -92,54 +92,54 @@ export class ExcelService { } /** Valida duplicados, celdas vacías y patrones básicos */ - validateRows(rows: UsuarioRow[]) { - const errors: string[] = []; - const seen = new Set(); + validateRows(rows: UsuarioRow[]) { + const errors: string[] = []; + const seen = new Set(); - rows.forEach((r, i) => { - const rowNum = i + 2; // por el encabezado + rows.forEach((r, i) => { + const rowNum = i + 2; // por el encabezado - // Campos que no pueden quedar vacíos - ['cuenta','nombreCompleto','clave','fechnac'].forEach(field => { - const raw = (r as any)[field]; - const str = raw != null ? raw.toString().trim() : ''; - if (!str) { - errors.push(`Fila ${rowNum}: campo "${field}" está vacío.`); + // Campos que no pueden quedar vacíos + ['cuenta', 'nombreCompleto'].forEach(field => { + const raw = (r as any)[field]; + const str = raw != null ? raw.toString().trim() : ''; + if (!str) { + errors.push(`Fila ${rowNum}: campo "${field}" está vacío.`); + } + }); + + // Duplicados de "cuenta" + const cuentaStr = r.cuenta != null ? r.cuenta.toString().trim() : ''; + if (seen.has(cuentaStr)) { + errors.push(`Fila ${rowNum}: cuenta duplicada "${cuentaStr}".`); + } else { + seen.add(cuentaStr); + } + + // Numérico en cuenta y generación + + + const genStr = r.gen != null ? r.gen.toString().trim() : ''; + if (genStr && !/^[0-9]{4}$/.test(genStr)) { + errors.push(`Fila ${rowNum}: generación inválida "${genStr}".`); + } + + + // Fecha en formato YYYYMMDD + const fechaStr = r.fechnac != null ? r.fechnac.toString().trim() : ''; + if (!/^[0-9]{8}$/.test(fechaStr)) { + errors.push(`Fila ${rowNum}: fecha de nacimiento inválida "${fechaStr}".`); + } + + // RFC alfanumérico + const rfcStr = r.rfc != null ? r.rfc.toString().trim() : ''; + if (rfcStr && !/^[A-Z0-9]+$/.test(rfcStr)) { + errors.push(`Fila ${rowNum}: RFC contiene caracteres no válidos.`); } }); - // Duplicados de "cuenta" - const cuentaStr = r.cuenta != null ? r.cuenta.toString().trim() : ''; - if (seen.has(cuentaStr)) { - errors.push(`Fila ${rowNum}: cuenta duplicada "${cuentaStr}".`); - } else { - seen.add(cuentaStr); - } - - // Numérico en cuenta y generación - if (!/^[0-9]+$/.test(cuentaStr)) { - errors.push(`Fila ${rowNum}: cuenta debe ser numérica.`); - } - const genStr = r.gen != null ? r.gen.toString().trim() : ''; - if (!/^[0-9]{4}$/.test(genStr)) { - errors.push(`Fila ${rowNum}: generación inválida "${genStr}".`); - } - - // Fecha en formato YYYYMMDD - const fechaStr = r.fechnac != null ? r.fechnac.toString().trim() : ''; - if (!/^[0-9]{8}$/.test(fechaStr)) { - errors.push(`Fila ${rowNum}: fecha de nacimiento inválida "${fechaStr}".`); - } - - // RFC alfanumérico - const rfcStr = r.rfc != null ? r.rfc.toString().trim() : ''; - if (rfcStr && !/^[A-Z0-9]+$/.test(rfcStr)) { - errors.push(`Fila ${rowNum}: RFC contiene caracteres no válidos.`); - } - }); - - return errors; -} + return errors; + } /** @@ -181,6 +181,9 @@ export class ExcelService { await this.carreraRepo.save(carrera); } + const generacion = /^[0-9]{4}$/.test(r.gen?.toString().trim()) + ? parseInt(r.gen.toString().trim(), 10) + : null; // 3) Usuario const usuario = this.usuarioRepo.create({ num_cuenta: r.cuenta, @@ -189,8 +192,9 @@ export class ExcelService { a_materno: r.apellidoma, rfc: r.rfc, fecha_nacimiento: r.fechnac, - generacion: parseInt(r.gen, 10), + generacion: generacion, genero, + }); await this.usuarioRepo.save(usuario); @@ -207,48 +211,300 @@ export class ExcelService { + // async generateCsv(): Promise { + // 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 { + const tipos = [ + 'Diplomado', + 'Extra Largo', + 'Ampliación de Conocimiento', + 'Servicio Social', + 'Movilidad', + 'Intercambio UNAM', + 'Idiomas Sabatino', + 'Idiomas R (UNAM)', + 'Trabajadores', + 'Profesor', + 'Oyente', + 'Posgrado', + 'Reinscrito', + 'Licenciatura', + ]; + const usuarios = await this.usuarioRepo.find({ + where: { + usuarioTipos: { + tipoUsuario: { + tipo_usuario: In(tipos), + }, + }, + }, + relations: [ + 'genero', + 'carreraUsuarios', // relación intermedia + 'carreraUsuarios.carrera', // carrera asociada a través de la intermedia + ], + select: { + num_cuenta: true, + nombre: true, + a_paterno: true, + a_materno: true, + rfc: true, + fecha_nacimiento: true, + generacion: true, + carreraUsuarios: true, - - - - - - - - - - - - - -async generateCsv(): Promise { - const users = await this.usuarioRepo.find({ - relations: ['genero', 'carreraUsuarios'], // si quieres más info - select: [ 'num_cuenta','nombre','a_paterno','a_materno','correo','rfc','fecha_nacimiento','generacion' ], + }, }); + + + const header = [ - 'num_cuenta','nombre','a_paterno','a_materno', - 'correo','rfc','fecha_nacimiento','generacion' + 'num_cuenta', 'nombre', 'a_paterno', 'a_materno', + 'carreras', 'rfc', 'fecha_nacimiento', 'generacion' ].join('\t') + '\n'; - const rows = users.map(u => [ - u.num_cuenta, - u.nombre, - u.a_paterno, - u.a_materno, - u.correo ?? '', - u.rfc ?? '', - u.fecha_nacimiento, - (u.generacion != null ? u.generacion.toString() : '') + const rows = usuarios.map(u => { + const carreras = (u.carreraUsuarios ?? []) + .map(cu => cu.carrera?.carrera ?? '') // obtenemos solo el nombre + .join(', '); // une todas las carreras en un solo string - ].join('\t')).join('\n'); + return [ + u.num_cuenta, + u.nombre, + u.a_paterno, + u.a_materno, + carreras, + u.rfc ?? '', + u.fecha_nacimiento, + u.generacion?.toString() ?? '' + ].join('\t'); + }).join('\n'); - return header + rows; + const tablaCompleta = header + rows; + + return tablaCompleta; + } + + async generateCsvSolicita(): Promise { + + + + const tipos = [ + + 'Extra Largo', + 'Ampliación de Conocimiento', + 'Movilidad', + 'Intercambio UNAM', + 'Profesor', + 'Posgrado', + 'Reinscrito', + 'Licenciatura', + ]; + + const usuarios = await this.usuarioRepo.find({ + where: { + usuarioTipos: { + tipoUsuario: { + tipo_usuario: In(tipos), + }, + }, + }, + relations: [ + 'genero', + 'carreraUsuarios', // relación intermedia + 'carreraUsuarios.carrera', // carrera asociada a través de la intermedia + ], + select: { + num_cuenta: true, + nombre: true, + a_paterno: true, + a_materno: true, + rfc: true, + carreraUsuarios: true, + + }, + }); + + + + + const header = [ + 'num_cuenta', 'nombre', 'a_paterno', 'a_materno', + 'carreras', 'rfc' + ].join('\t') + '\n'; + + const rows = usuarios.map(u => { + const carreras = (u.carreraUsuarios ?? []) + .map(cu => cu.carrera?.carrera ?? '') // obtenemos solo el nombre + .join(', '); // une todas las carreras en un solo string + + return [ + u.num_cuenta, + u.nombre, + u.a_paterno, + u.a_materno, + carreras, + u.rfc ?? '', + ].join('\t'); + }).join('\n'); + + const tablaCompleta = header + rows; + + return tablaCompleta; + } + + async generateCsvRed(): Promise { + + + + const tipos = [ + 'Ampliación de Conocimiento', + 'Servicio Social', + 'Movilidad', + 'Intercambio UNAM', + 'Idiomas Sabatino', + 'Idiomas R (UNAM)', + 'Trabajadores', + 'Profesor', + 'Posgrado', + 'Reinscrito', + 'Licenciatura', + ]; + + const usuarios = await this.usuarioRepo.find({ + where: { + usuarioTipos: { + tipoUsuario: { + tipo_usuario: In(tipos), + }, + }, + }, + relations: [ + 'genero', + 'carreraUsuarios', // relación intermedia + 'carreraUsuarios.carrera', // carrera asociada a través de la intermedia + ], + select: { + num_cuenta: true, + fecha_nacimiento: true, + }, + }); + + + + + const header = [ + 'num_cuenta', 'fecha_nacimiento' + ].join('\t') + '\n'; + + const rows = usuarios.map(u => { + return [ + u.num_cuenta, + u.fecha_nacimiento, + ].join('\t'); + }).join('\n'); + + const tablaCompleta = header + rows; + + return tablaCompleta; + } + + async generateCsvAT(): Promise { + + + + const tipos = [ + 'Ampliación de Conocimiento', + 'Servicio Social', + 'Movilidad', + 'Intercambio UNAM', + 'Profesor', + 'Posgrado', + 'Reinscrito', + 'Licenciatura', + ]; + + const usuarios = await this.usuarioRepo.find({ + where: { + usuarioTipos: { + tipoUsuario: { + tipo_usuario: In(tipos), + }, + }, + }, + relations: [ + 'genero', + 'carreraUsuarios', // relación intermedia + 'carreraUsuarios.carrera', // carrera asociada a través de la intermedia + ], + select: { + num_cuenta: true, + nombre: true, + a_paterno: true, + a_materno: true, + fecha_nacimiento: true, + generacion: true, + carreraUsuarios: true, + + }, + }); + + + + + const header = [ + 'num_cuenta', 'nombre', 'a_paterno', 'a_materno', + 'carreras', 'rfc', 'fecha_nacimiento', 'generacion' + ].join('\t') + '\n'; + + const rows = usuarios.map(u => { + const carreras = (u.carreraUsuarios ?? []) + .map(cu => cu.carrera?.carrera ?? '') // obtenemos solo el nombre + .join(', '); // une todas las carreras en un solo string + + return [ + u.num_cuenta, + u.nombre, + u.a_paterno, + u.a_materno, + carreras, + u.fecha_nacimiento, + u.generacion?.toString() ?? '' + ].join('\t'); + }).join('\n'); + + const tablaCompleta = header + rows; + + return tablaCompleta; } diff --git a/src/movimiento/movimiento.service.ts b/src/movimiento/movimiento.service.ts index a257777..45168f6 100644 --- a/src/movimiento/movimiento.service.ts +++ b/src/movimiento/movimiento.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Movimiento } from '../entities/entities'; -import { Usuario } from '../entities/entities'; import { Origen } from '../entities/entities'; @Injectable() @@ -12,20 +11,19 @@ export class MovimientoService { private readonly movRepo: Repository, @InjectRepository(Origen) private readonly origenRepo: Repository, - ) {} + ) { } /** Crea un registro de movimiento */ async log( - usuarioId: number, - fuente: string, + origen: string, status: string, observaciones?: string, reporte?: string, flag = false, ) { // 1) Obtén el origen o falla si no existe - const origen = await this.origenRepo.findOne({ where: { fuente } }); - if (!origen) throw new Error(`Origen desconocido: ${fuente}`); + const origin = await this.origenRepo.findOne({ where: { origen } }); + if (!origin) throw new Error(`Origen desconocido: ${origen}`); // 2) Inserta directamente usando los campos de FK await this.movRepo.insert({ @@ -34,8 +32,7 @@ export class MovimientoService { observaciones, // aquí usas el valor que vino como parámetro reporte, // idem flag, - usuario: { id_usuario: usuarioId }, // relación en lugar de id_usuario - origen: { id_origen: origen.id_origen }, + origen: { id_origen: origin.id_origen }, }); } }