Se agregaron los campos de los que se deben de mandar a origen de usuario

This commit is contained in:
evenegas
2025-06-18 07:33:21 -06:00
parent 8703a292e3
commit 4984c47029
12 changed files with 598 additions and 229 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"
}
+7 -6
View File
@@ -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<number>('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 { }
+7 -4
View File
@@ -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);
+36 -38
View File
@@ -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.' }),
);
}
}
+3 -3
View File
@@ -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 { }
+26 -14
View File
@@ -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<Usuario>,
@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<Usuario> {
const user = await this.userRepo.findOne({ where: { correo: email } });
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');
@@ -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
+2 -14
View File
@@ -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;
}
+1 -1
View File
@@ -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 };
}
}
+30 -36
View File
@@ -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' })
+130 -29
View File
@@ -3,7 +3,7 @@ import { Controller, Post, Get, UseGuards, Request, Res, UploadedFile, UseInterc
import { AuthGuard } from '@nestjs/passport';
import { ApiBearerAuth } from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express';
import { Response } from 'express';
import e, { Response } from 'express';
import { ExcelDocumentation } from './excel.documentation';
import { ExcelService } from './excel.service';
import { MovimientoService } from '../movimiento/movimiento.service';
@@ -15,7 +15,7 @@ export class ExcelController {
constructor(
private readonly excelService: ExcelService,
private readonly movimientoService: MovimientoService,
) {}
) { }
@Post('verify')
@UseInterceptors(FileInterceptor('file'))
@@ -24,7 +24,7 @@ export class ExcelController {
const userId: number = req.user.userId; // ahora sí existe
const errors = await this.excelService.validateFile(file.buffer);
await this.movimientoService.log(
userId,
'VERIFY',
errors.length ? 'FAILED' : 'SUCCESS',
errors.join('; ')
@@ -35,12 +35,20 @@ export class ExcelController {
@Post('load')
@UseInterceptors(FileInterceptor('file'))
@ExcelDocumentation.loadExcel()
async loadExcel(@UploadedFile() file: Express.Multer.File, @Request() req) {
const userId: number = req.user.userId;
async loadExcel(@Request() req, @UploadedFile() file: Express.Multer.File) {
const origen = req.user.origen; // ahora sí existe
if (origen != 'LOAD') {
await this.movimientoService.log(
'LOAD',
'FAILED',
'Origen no permitido para carga'
);
throw new Error('Origen no permitido para carga.');
}
try {
const result = await this.excelService.loadFile(file.buffer);
await this.movimientoService.log(
userId,
'LOAD',
'SUCCESS',
undefined,
@@ -49,7 +57,7 @@ export class ExcelController {
return result;
} catch (err) {
await this.movimientoService.log(
userId,
'LOAD',
'FAILED',
err.message
@@ -61,31 +69,124 @@ export class ExcelController {
@Get('download')
@ExcelDocumentation.downloadExcel()
async downloadData(@Request() req, @Res() res: Response) {
const userId: number = req.user.userId;
try {
const csv = await this.excelService.generateCsv();
const origen = req.user.origen; // ahora sí existe
console.log('Origen de descarga:', origen);
console.log('Usuario:', req.user);
if (origen == 'SOLICITA') {
try {
const csv = await this.excelService.generateCsvSolicita();
await this.movimientoService.log(
origen,
'SUCCESS',
undefined,
`size=${csv.length}`
);
return res
.status(HttpStatus.OK)
.header('Content-Type', 'text/tab-separated-values')
.header('Content-Disposition', 'attachment; filename="usuarios_solicita.tsv"')
.send(csv);
} catch (err) {
await this.movimientoService.log(
origen,
'FAILED',
'Origen no permitido para descarga'
);
return res
.status(HttpStatus.FORBIDDEN)
.json({ statusCode: 403, message: 'Origen no permitido para descarga.' });
}
} else if (origen == 'CORREO') {
try {
const csv = await this.excelService.generateCsvCorreo();
await this.movimientoService.log(
origen,
'SUCCESS',
undefined,
`size=${csv.length}`
);
return res
.status(HttpStatus.OK)
.header('Content-Type', 'text/tab-separated-values')
.header('Content-Disposition', 'attachment; filename="usuarios.tsv"')
.send(csv);
} catch (err) {
await this.movimientoService.log(
origen,
'FAILED',
err.message
);
return res
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.json({ statusCode: 500, message: 'Error generando descarga.' });
}
} else if (origen == 'AT') {
try {
const csv = await this.excelService.generateCsvAT();
await this.movimientoService.log(
origen,
'SUCCESS',
undefined,
`size=${csv.length}`
);
return res
.status(HttpStatus.OK)
.header('Content-Type', 'text/tab-separated-values')
.header('Content-Disposition', 'attachment; filename="usuarios_at.tsv"')
.send(csv);
} catch (err) {
await this.movimientoService.log(
origen,
'FAILED',
err.message
);
return res
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.json({ statusCode: 500, message: 'Error generando descarga.' });
}
} else if (origen == 'RED') {
try {
const csv = await this.excelService.generateCsvRed();
await this.movimientoService.log(
origen,
'SUCCESS',
undefined,
`size=${csv.length}`
);
return res
.status(HttpStatus.OK)
.header('Content-Type', 'text/tab-separated-values')
.header('Content-Disposition', 'attachment; filename="usuarios_red.tsv"')
.send(csv);
} catch (err) {
await this.movimientoService.log(
origen,
'FAILED',
err.message
);
return res
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.json({ statusCode: 500, message: 'Error generando descarga.' });
}
} else {
await this.movimientoService.log(
userId,
'DOWNLOAD',
'SUCCESS',
undefined,
`size=${csv.length}`
);
return res
.status(HttpStatus.OK)
.header('Content-Type', 'text/tab-separated-values')
.header('Content-Disposition', 'attachment; filename="usuarios.tsv"')
.send(csv);
} catch (err) {
await this.movimientoService.log(
userId,
'DOWNLOAD',
origen,
'FAILED',
err.message
'Origen no permitido para descarga'
);
return res
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.json({ statusCode: 500, message: 'Error generando descarga.' });
.status(HttpStatus.FORBIDDEN)
.json({ statusCode: 403, message: 'Origen no permitido para descarga.' });
}
}
}
}
+332 -76
View File
@@ -1,7 +1,7 @@
// src/excel/excel.service.ts
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
import { Workbook } from 'exceljs';
import { Repository } from 'typeorm';
import { In, Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Usuario } from '../entities/entities';
import { Genero } from '../entities/entities';
@@ -35,7 +35,7 @@ export class ExcelService {
@InjectRepository(Carrera)
private readonly carreraRepo: Repository<Carrera>,
// … inyecta otros repositorios si los usarás
) {}
) { }
/** Lee el buffer del Excel y devuelve un arreglo de filas tipadas */
private async parseFile(buffer: Buffer): Promise<UsuarioRow[]> {
@@ -92,54 +92,54 @@ export class ExcelService {
}
/** Valida duplicados, celdas vacías y patrones básicos */
validateRows(rows: UsuarioRow[]) {
const errors: string[] = [];
const seen = new Set<string>();
validateRows(rows: UsuarioRow[]) {
const errors: string[] = [];
const seen = new Set<string>();
rows.forEach((r, i) => {
const rowNum = i + 2; // por el encabezado
rows.forEach((r, i) => {
const rowNum = i + 2; // por el encabezado
// Campos que no pueden quedar vacíos
['cuenta','nombreCompleto','clave','fechnac'].forEach(field => {
const raw = (r as any)[field];
const str = raw != null ? raw.toString().trim() : '';
if (!str) {
errors.push(`Fila ${rowNum}: campo "${field}" está vacío.`);
// Campos que no pueden quedar vacíos
['cuenta', 'nombreCompleto'].forEach(field => {
const raw = (r as any)[field];
const str = raw != null ? raw.toString().trim() : '';
if (!str) {
errors.push(`Fila ${rowNum}: campo "${field}" está vacío.`);
}
});
// Duplicados de "cuenta"
const cuentaStr = r.cuenta != null ? r.cuenta.toString().trim() : '';
if (seen.has(cuentaStr)) {
errors.push(`Fila ${rowNum}: cuenta duplicada "${cuentaStr}".`);
} else {
seen.add(cuentaStr);
}
// Numérico en cuenta y generación
const genStr = r.gen != null ? r.gen.toString().trim() : '';
if (genStr && !/^[0-9]{4}$/.test(genStr)) {
errors.push(`Fila ${rowNum}: generación inválida "${genStr}".`);
}
// Fecha en formato YYYYMMDD
const fechaStr = r.fechnac != null ? r.fechnac.toString().trim() : '';
if (!/^[0-9]{8}$/.test(fechaStr)) {
errors.push(`Fila ${rowNum}: fecha de nacimiento inválida "${fechaStr}".`);
}
// RFC alfanumérico
const rfcStr = r.rfc != null ? r.rfc.toString().trim() : '';
if (rfcStr && !/^[A-Z0-9]+$/.test(rfcStr)) {
errors.push(`Fila ${rowNum}: RFC contiene caracteres no válidos.`);
}
});
// Duplicados de "cuenta"
const cuentaStr = r.cuenta != null ? r.cuenta.toString().trim() : '';
if (seen.has(cuentaStr)) {
errors.push(`Fila ${rowNum}: cuenta duplicada "${cuentaStr}".`);
} else {
seen.add(cuentaStr);
}
// Numérico en cuenta y generación
if (!/^[0-9]+$/.test(cuentaStr)) {
errors.push(`Fila ${rowNum}: cuenta debe ser numérica.`);
}
const genStr = r.gen != null ? r.gen.toString().trim() : '';
if (!/^[0-9]{4}$/.test(genStr)) {
errors.push(`Fila ${rowNum}: generación inválida "${genStr}".`);
}
// Fecha en formato YYYYMMDD
const fechaStr = r.fechnac != null ? r.fechnac.toString().trim() : '';
if (!/^[0-9]{8}$/.test(fechaStr)) {
errors.push(`Fila ${rowNum}: fecha de nacimiento inválida "${fechaStr}".`);
}
// RFC alfanumérico
const rfcStr = r.rfc != null ? r.rfc.toString().trim() : '';
if (rfcStr && !/^[A-Z0-9]+$/.test(rfcStr)) {
errors.push(`Fila ${rowNum}: RFC contiene caracteres no válidos.`);
}
});
return errors;
}
return errors;
}
/**
@@ -181,6 +181,9 @@ export class ExcelService {
await this.carreraRepo.save(carrera);
}
const generacion = /^[0-9]{4}$/.test(r.gen?.toString().trim())
? parseInt(r.gen.toString().trim(), 10)
: null;
// 3) Usuario
const usuario = this.usuarioRepo.create({
num_cuenta: r.cuenta,
@@ -189,8 +192,9 @@ export class ExcelService {
a_materno: r.apellidoma,
rfc: r.rfc,
fecha_nacimiento: r.fechnac,
generacion: parseInt(r.gen, 10),
generacion: generacion,
genero,
});
await this.usuarioRepo.save(usuario);
@@ -207,48 +211,300 @@ export class ExcelService {
// async generateCsv(): Promise<string> {
// const users = await this.usuarioRepo.find({
// relations: ['genero', 'carreraUsuarios'], // si quieres más info
// select: ['num_cuenta', 'nombre', 'a_paterno', 'a_materno', 'rfc', 'fecha_nacimiento', 'generacion'],
// });
// const header = [
// 'num_cuenta', 'nombre', 'a_paterno', 'a_materno',
// 'carrera', 'rfc', 'fecha_nacimiento', 'generacion'
// ].join('\t') + '\n';
// const rows = users.map(u => [
// u.num_cuenta,
// u.nombre,
// u.a_paterno,
// u.a_materno,
// u.rfc ?? '',
// u.fecha_nacimiento,
// (u.generacion != null ? u.generacion.toString() : '')
// ].join('\t')).join('\n');
// return header + rows;
// }
async generateCsvCorreo(): Promise<string> {
const tipos = [
'Diplomado',
'Extra Largo',
'Ampliación de Conocimiento',
'Servicio Social',
'Movilidad',
'Intercambio UNAM',
'Idiomas Sabatino',
'Idiomas R (UNAM)',
'Trabajadores',
'Profesor',
'Oyente',
'Posgrado',
'Reinscrito',
'Licenciatura',
];
const usuarios = await this.usuarioRepo.find({
where: {
usuarioTipos: {
tipoUsuario: {
tipo_usuario: In(tipos),
},
},
},
relations: [
'genero',
'carreraUsuarios', // relación intermedia
'carreraUsuarios.carrera', // carrera asociada a través de la intermedia
],
select: {
num_cuenta: true,
nombre: true,
a_paterno: true,
a_materno: true,
rfc: true,
fecha_nacimiento: true,
generacion: true,
carreraUsuarios: true,
async generateCsv(): Promise<string> {
const users = await this.usuarioRepo.find({
relations: ['genero', 'carreraUsuarios'], // si quieres más info
select: [ 'num_cuenta','nombre','a_paterno','a_materno','correo','rfc','fecha_nacimiento','generacion' ],
},
});
const header = [
'num_cuenta','nombre','a_paterno','a_materno',
'correo','rfc','fecha_nacimiento','generacion'
'num_cuenta', 'nombre', 'a_paterno', 'a_materno',
'carreras', 'rfc', 'fecha_nacimiento', 'generacion'
].join('\t') + '\n';
const rows = users.map(u => [
u.num_cuenta,
u.nombre,
u.a_paterno,
u.a_materno,
u.correo ?? '',
u.rfc ?? '',
u.fecha_nacimiento,
(u.generacion != null ? u.generacion.toString() : '')
const rows = usuarios.map(u => {
const carreras = (u.carreraUsuarios ?? [])
.map(cu => cu.carrera?.carrera ?? '') // obtenemos solo el nombre
.join(', '); // une todas las carreras en un solo string
].join('\t')).join('\n');
return [
u.num_cuenta,
u.nombre,
u.a_paterno,
u.a_materno,
carreras,
u.rfc ?? '',
u.fecha_nacimiento,
u.generacion?.toString() ?? ''
].join('\t');
}).join('\n');
return header + rows;
const tablaCompleta = header + rows;
return tablaCompleta;
}
async generateCsvSolicita(): Promise<string> {
const tipos = [
'Extra Largo',
'Ampliación de Conocimiento',
'Movilidad',
'Intercambio UNAM',
'Profesor',
'Posgrado',
'Reinscrito',
'Licenciatura',
];
const usuarios = await this.usuarioRepo.find({
where: {
usuarioTipos: {
tipoUsuario: {
tipo_usuario: In(tipos),
},
},
},
relations: [
'genero',
'carreraUsuarios', // relación intermedia
'carreraUsuarios.carrera', // carrera asociada a través de la intermedia
],
select: {
num_cuenta: true,
nombre: true,
a_paterno: true,
a_materno: true,
rfc: true,
carreraUsuarios: true,
},
});
const header = [
'num_cuenta', 'nombre', 'a_paterno', 'a_materno',
'carreras', 'rfc'
].join('\t') + '\n';
const rows = usuarios.map(u => {
const carreras = (u.carreraUsuarios ?? [])
.map(cu => cu.carrera?.carrera ?? '') // obtenemos solo el nombre
.join(', '); // une todas las carreras en un solo string
return [
u.num_cuenta,
u.nombre,
u.a_paterno,
u.a_materno,
carreras,
u.rfc ?? '',
].join('\t');
}).join('\n');
const tablaCompleta = header + rows;
return tablaCompleta;
}
async generateCsvRed(): Promise<string> {
const tipos = [
'Ampliación de Conocimiento',
'Servicio Social',
'Movilidad',
'Intercambio UNAM',
'Idiomas Sabatino',
'Idiomas R (UNAM)',
'Trabajadores',
'Profesor',
'Posgrado',
'Reinscrito',
'Licenciatura',
];
const usuarios = await this.usuarioRepo.find({
where: {
usuarioTipos: {
tipoUsuario: {
tipo_usuario: In(tipos),
},
},
},
relations: [
'genero',
'carreraUsuarios', // relación intermedia
'carreraUsuarios.carrera', // carrera asociada a través de la intermedia
],
select: {
num_cuenta: true,
fecha_nacimiento: true,
},
});
const header = [
'num_cuenta', 'fecha_nacimiento'
].join('\t') + '\n';
const rows = usuarios.map(u => {
return [
u.num_cuenta,
u.fecha_nacimiento,
].join('\t');
}).join('\n');
const tablaCompleta = header + rows;
return tablaCompleta;
}
async generateCsvAT(): Promise<string> {
const tipos = [
'Ampliación de Conocimiento',
'Servicio Social',
'Movilidad',
'Intercambio UNAM',
'Profesor',
'Posgrado',
'Reinscrito',
'Licenciatura',
];
const usuarios = await this.usuarioRepo.find({
where: {
usuarioTipos: {
tipoUsuario: {
tipo_usuario: In(tipos),
},
},
},
relations: [
'genero',
'carreraUsuarios', // relación intermedia
'carreraUsuarios.carrera', // carrera asociada a través de la intermedia
],
select: {
num_cuenta: true,
nombre: true,
a_paterno: true,
a_materno: true,
fecha_nacimiento: true,
generacion: true,
carreraUsuarios: true,
},
});
const header = [
'num_cuenta', 'nombre', 'a_paterno', 'a_materno',
'carreras', 'rfc', 'fecha_nacimiento', 'generacion'
].join('\t') + '\n';
const rows = usuarios.map(u => {
const carreras = (u.carreraUsuarios ?? [])
.map(cu => cu.carrera?.carrera ?? '') // obtenemos solo el nombre
.join(', '); // une todas las carreras en un solo string
return [
u.num_cuenta,
u.nombre,
u.a_paterno,
u.a_materno,
carreras,
u.fecha_nacimiento,
u.generacion?.toString() ?? ''
].join('\t');
}).join('\n');
const tablaCompleta = header + rows;
return tablaCompleta;
}
+5 -8
View File
@@ -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<Movimiento>,
@InjectRepository(Origen)
private readonly origenRepo: Repository<Origen>,
) {}
) { }
/** 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 },
});
}
}