Add contador field to EventoUsuario and update related services for handling user data #40

Manually merged
CIDWA merged 1 commits from develop into master 2026-05-15 01:10:16 +00:00
7 changed files with 260 additions and 83 deletions
@@ -31,4 +31,7 @@ export class EventoUsuario {
@Column({ type: 'varchar', length: 20 })
identificador: string;
@Column({ type: 'int', nullable: true })
contador: number | null;
}
+69 -36
View File
@@ -44,59 +44,78 @@ export class EventoUsuarioService {
async bulkInsert(
id_evento: number,
alumnos: string[],
trabajadores: string[],
alumnos: { identificador: string; contador: number | null }[],
trabajadores: { identificador: string; contador: number | null }[],
): Promise<{ insertados: number; omitidos: number; errores: string[] }> {
console.log(id_evento, alumnos, trabajadores);
let insertados = 0;
let omitidos = 0;
const errores: string[] = [];
for (const cuenta of alumnos) {
for (const alumno of alumnos) {
try {
const result = await this.eventoUsuarioRepo
.createQueryBuilder()
.insert()
.into(EventoUsuario)
.values({
const existing = await this.eventoUsuarioRepo.findOne({
where: {
id_evento,
tipo_usuario: TipoUsuarioEnum.ALUMNO,
identificador: cuenta.toUpperCase(),
})
.orIgnore()
.execute();
if (result.raw?.affectedRows > 0) {
insertados++;
identificador: alumno.identificador.toUpperCase(),
},
});
if (existing) {
if (existing.contador !== alumno.contador) {
await this.eventoUsuarioRepo.update(existing.id_evento_usuario, {
contador: alumno.contador,
});
insertados++;
} else {
omitidos++;
}
} else {
omitidos++;
await this.eventoUsuarioRepo.insert({
id_evento,
tipo_usuario: TipoUsuarioEnum.ALUMNO,
identificador: alumno.identificador.toUpperCase(),
contador: alumno.contador,
});
insertados++;
}
} catch {
omitidos++;
errores.push(`Error al insertar cuenta ${cuenta}`);
errores.push(`Error al insertar cuenta ${alumno.identificador}`);
}
}
for (const rfc of trabajadores) {
for (const trabajador of trabajadores) {
try {
const result = await this.eventoUsuarioRepo
.createQueryBuilder()
.insert()
.into(EventoUsuario)
.values({
const existing = await this.eventoUsuarioRepo.findOne({
where: {
id_evento,
tipo_usuario: TipoUsuarioEnum.TRABAJADOR,
identificador: rfc.toUpperCase(),
})
.orIgnore()
.execute();
if (result.raw?.affectedRows > 0) {
insertados++;
identificador: trabajador.identificador.toUpperCase(),
},
});
if (existing) {
if (existing.contador !== trabajador.contador) {
await this.eventoUsuarioRepo.update(existing.id_evento_usuario, {
contador: trabajador.contador,
});
insertados++;
} else {
omitidos++;
}
} else {
omitidos++;
await this.eventoUsuarioRepo.insert({
id_evento,
tipo_usuario: TipoUsuarioEnum.TRABAJADOR,
identificador: trabajador.identificador.toUpperCase(),
contador: trabajador.contador,
});
insertados++;
}
} catch {
omitidos++;
errores.push(`Error al insertar RFC ${rfc}`);
errores.push(`Error al insertar RFC ${trabajador.identificador}`);
}
}
@@ -117,27 +136,41 @@ export class EventoUsuarioService {
const sheetName = workbook.SheetNames[0];
const data: any[] = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]);
const alumnosValidos: string[] = [];
const trabajadoresValidos: string[] = [];
const alumnosValidos: { identificador: string; contador: number | null }[] =
[];
const trabajadoresValidos: {
identificador: string;
contador: number | null;
}[] = [];
let omitidos = 0;
const errores: string[] = [];
for (const row of data) {
const cuentaRaw = row['cuenta'];
const rfcRaw = row['rfc'];
const contadorRaw = row['contador'];
const contador =
contadorRaw !== undefined &&
contadorRaw !== null &&
contadorRaw !== '' &&
!isNaN(Number(contadorRaw))
? Number(contadorRaw)
: null;
if (cuentaRaw !== undefined && cuentaRaw !== null && cuentaRaw !== '') {
const cuenta = String(cuentaRaw).trim();
const alumno = await this.alumnoRepo.findOne({
where: { id_ncuenta: parseInt(cuenta, 10) },
});
console.log('Alumno encontrado:', alumno);
if (!alumno) {
omitidos++;
errores.push(`Cuenta no encontrada en el sistema: ${cuenta}`);
continue;
}
alumnosValidos.push(alumno.id_ncuenta.toString());
alumnosValidos.push({
identificador: alumno.id_ncuenta.toString(),
contador,
});
continue;
}
@@ -151,7 +184,7 @@ export class EventoUsuarioService {
errores.push(`RFC no encontrado en el sistema: ${rfc}`);
continue;
}
trabajadoresValidos.push(trabajador.rfc);
trabajadoresValidos.push({ identificador: trabajador.rfc, contador });
continue;
}
@@ -261,11 +261,26 @@ export class ParticipanteEventoController {
@ApiOperation({
summary: 'Retornala info del token QR',
})
@ApiBody({
description: 'Token JWT generado por el código QR',
schema: {
type: 'object',
properties: {
token: { type: 'string' },
},
example: {
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
},
},
})
@ApiResponse({
status: 200,
description: 'Token válido, información decodificada retornada',
})
@ApiResponse({ status: 400, description: 'Token inválido o expirado' })
@Post('decode-token')
decodeToken(@Body() body: { token: string }) {
return this.participanteEventoService.validToken(
body.token,
);
return this.participanteEventoService.validToken(body.token);
}
@ApiOperation({
@@ -7,10 +7,13 @@ import { Evento } from 'src/evento/entities/evento.entity';
import { Participante } from 'src/participante/entities/participante.entity';
import { CuestionarioModule } from 'src/cuestionario/cuestionario.module';
import { QrModule } from 'src/qr/qr.module';
import { EventoUsuario } from 'src/evento_usuario/entities/evento_usuario.entity';
import { RespuestaParticipanteAbierta } from 'src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
@Module({
imports: [
TypeOrmModule.forFeature([ParticipanteEvento, Evento, Participante]),
TypeOrmModule.forFeature([ParticipanteEvento, Evento, Participante, Cuestionario, EventoUsuario, RespuestaParticipanteAbierta]),
CuestionarioModule,
QrModule,
],
@@ -8,6 +8,10 @@ import { Evento } from 'src/evento/entities/evento.entity';
import { Participante } from 'src/participante/entities/participante.entity';
import { CuestionarioService } from 'src/cuestionario/cuestionario.service';
import { QrTokenService } from 'src/qr/qr-token.service';
import { EventoUsuario } from 'src/evento_usuario/entities/evento_usuario.entity';
import { RespuestaParticipanteAbierta } from 'src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
import { TiposValidacion } from 'src/pregunta/entities/pregunta.entity';
import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
@Injectable()
export class ParticipanteEventoService {
@@ -17,6 +21,12 @@ export class ParticipanteEventoService {
@InjectRepository(Evento) private eventoRepository: Repository<Evento>,
@InjectRepository(Participante)
private participanteRepository: Repository<Participante>,
@InjectRepository(Cuestionario)
private cuestionarioRepository: Repository<Cuestionario>,
@InjectRepository(EventoUsuario)
private eventoUsuarioRepository: Repository<EventoUsuario>,
@InjectRepository(RespuestaParticipanteAbierta)
private respuestaAbiertaRepository: Repository<RespuestaParticipanteAbierta>,
private readonly cuestionarioService: CuestionarioService,
private readonly qrTokenService: QrTokenService,
) {}
@@ -178,7 +188,55 @@ export class ParticipanteEventoService {
participante_eventoFound.fecha_asistencia = new Date();
participante_eventoFound.asistio = true;
return this.participanteEventoRepository.save(participante_eventoFound);
const resultado =
await this.participanteEventoRepository.save(participante_eventoFound);
const cuestionario = await this.cuestionarioRepository.findOne({
where: { id_cuestionario },
});
let contador: number | null | undefined = undefined;
if (cuestionario?.id_evento) {
const tieneListaRestringida =
(await this.eventoUsuarioRepository.count({
where: { id_evento: cuestionario.id_evento },
})) > 0;
if (tieneListaRestringida) {
const respuesta =
(await this.respuestaAbiertaRepository.findOne({
where: {
cuestionarioRespondido: {
participante: { id_participante },
cuestionario: { id_cuestionario },
},
pregunta: { validacion: TiposValidacion.CUENTA_ALUMNO },
},
relations: ['pregunta', 'cuestionarioRespondido'],
})) ??
(await this.respuestaAbiertaRepository.findOne({
where: {
cuestionarioRespondido: {
participante: { id_participante },
cuestionario: { id_cuestionario },
},
pregunta: { validacion: TiposValidacion.RFC },
},
relations: ['pregunta', 'cuestionarioRespondido'],
}));
if (respuesta) {
const identificador = respuesta.respuesta.trim().toUpperCase();
const eventoUsuario = await this.eventoUsuarioRepository.findOne({
where: { id_evento: cuestionario.id_evento, identificador },
});
contador = eventoUsuario?.contador ?? null;
}
}
}
return { ...resultado, contador };
}
/**
@@ -199,7 +257,7 @@ export class ParticipanteEventoService {
}
}
const { id_participante, id_evento, id_cuestionario } = tokenData;
const { id_participante, id_evento, id_cuestionario, contador } = tokenData;
// Verificar que el participante existe
const participante = await this.participanteRepository.findOne({
@@ -239,6 +297,7 @@ export class ParticipanteEventoService {
participante: participante.correo,
fecha_asistencia_previa: participanteEvento.fecha_asistencia,
fecha_actual: new Date(),
contador,
},
};
}
@@ -250,7 +309,6 @@ export class ParticipanteEventoService {
const resultado =
await this.participanteEventoRepository.save(participanteEvento);
return {
success: true,
message: 'Asistencia registrada exitosamente',
@@ -259,6 +317,7 @@ export class ParticipanteEventoService {
fecha_asistencia: resultado.fecha_asistencia,
id_evento,
id_cuestionario,
contador,
},
};
}
+101 -39
View File
@@ -6,6 +6,9 @@ import { Repository } from 'typeorm';
import { Asistencia } from 'src/asistencia/entities/asistencia.entity';
import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
import { Evento } from 'src/evento/entities/evento.entity';
import { EventoUsuario } from 'src/evento_usuario/entities/evento_usuario.entity';
import { RespuestaParticipanteAbierta } from 'src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
import { TiposValidacion } from 'src/pregunta/entities/pregunta.entity';
@Injectable()
export class QrTokenService {
@@ -13,11 +16,15 @@ export class QrTokenService {
private jwtService: JwtService,
private configService: ConfigService,
@InjectRepository(Asistencia)
private readonly asistenciaRepository: Repository<Asistencia>,
private readonly asistenciaRepository: Repository<Asistencia>,
@InjectRepository(Cuestionario)
private readonly cuestionarioRepository: Repository<Cuestionario>,
private readonly cuestionarioRepository: Repository<Cuestionario>,
@InjectRepository(Evento)
private readonly eventoREpo: Repository<Evento>,
private readonly eventoREpo: Repository<Evento>,
@InjectRepository(EventoUsuario)
private readonly eventoUsuarioRepo: Repository<EventoUsuario>,
@InjectRepository(RespuestaParticipanteAbierta)
private readonly respuestaAbiertaRepo: Repository<RespuestaParticipanteAbierta>,
) {}
/**
@@ -46,12 +53,16 @@ export class QrTokenService {
this.configService.get<string>('QR_JWT_SECRET') ||
this.configService.get<string>('JWT_SECRET', 'tu_clave_secreta');
console.log("fecha de caducidad:",fecha_fin);
console.log('fecha de caducidad:', fecha_fin);
// Convert fecha_fin to seconds from now for expiresIn
const expiresInSeconds = Math.floor((fecha_fin.getTime() - Date.now()) / 1000);
console.log("expiresInSeconds:",expiresInSeconds);
console.log("Caducidad token: ", Math.floor((fecha_fin.getTime() - Date.now()) / 1000));
const expiresInSeconds = Math.floor(
(fecha_fin.getTime() - Date.now()) / 1000,
);
console.log('expiresInSeconds:', expiresInSeconds);
console.log(
'Caducidad token: ',
Math.floor((fecha_fin.getTime() - Date.now()) / 1000),
);
return this.jwtService.sign(payload, {
secret,
@@ -65,60 +76,111 @@ export class QrTokenService {
* @returns Datos decodificados del token o null si es inválido
*/
async validateQrToken(token: string): Promise<{
id_participante
: number;
id_participante: number;
id_evento: number;
id_cuestionario: number;
type: string;
iat: number;
exp?: number;
} | null >{
contador?: number | null;
} | null> {
try {
const secret =
this.configService.get<string>('QR_JWT_SECRET') ||
this.configService.get<string>('JWT_SECRET', 'tu_clave_secreta');
console.log(secret)
console.log(secret);
const decoded = this.jwtService.verify(token, { secret });
//console.log("fecha fin:", fecha_fin2.fecha_fin);
console.log("este es el token", decoded);
console.log('este es el token', decoded);
const fecha_fin2 = await this.eventoREpo.findOne({
where: { id_evento:decoded.id_evento },
const evento = await this.eventoREpo.findOne({
where: { id_evento: decoded.id_evento },
});
});
if (!evento) {
throw new UnauthorizedException('Evento no encontrado');
}
const fechaFinEvento = new Date(evento.fecha_fin);
const now = new Date();
if (fechaFinEvento.getTime() < now.getTime()) {
throw new Error('El evento ha finalizado');
}
if (!fecha_fin2) {
throw new UnauthorizedException('Evento no encontrado');
}
const fechaFinEvento = new Date(fecha_fin2.fecha_fin); // UTC
const now = new Date();
console.log(fechaFinEvento.getTime())
console.log(now.getTime)
if (fechaFinEvento.getTime() < now.getTime()) {
throw new Error('El evento ha finalizado');
}
if (decoded.type !== 'qr_asistencia') {
return null;
}
// Verificar que sea un token de tipo QR de asistencia
if (decoded.type !== 'qr_asistencia') {
return null;
}
return {
const result: {
id_participante: number;
id_evento: number;
id_cuestionario: number;
type: string;
iat: number;
contador?: number | null;
} = {
id_participante: decoded.id_participante,
id_evento: decoded.id_evento,
id_cuestionario: decoded.id_cuestionario,
type: decoded.type,
iat: decoded.iat,
};
console.log({ result });
const tieneListaRestringida =
(await this.eventoUsuarioRepo.count({
where: { id_evento: decoded.id_evento },
})) > 0;
if (tieneListaRestringida) {
const respuesta = await this.respuestaAbiertaRepo.findOne({
where: {
cuestionarioRespondido: {
participante: { id_participante: decoded.id_participante },
cuestionario: { id_cuestionario: decoded.id_cuestionario },
},
pregunta: {
validacion: TiposValidacion.CUENTA_ALUMNO,
},
},
relations: ['pregunta', 'cuestionarioRespondido'],
});
const respuestaRfc = respuesta
? null
: await this.respuestaAbiertaRepo.findOne({
where: {
cuestionarioRespondido: {
participante: { id_participante: decoded.id_participante },
cuestionario: { id_cuestionario: decoded.id_cuestionario },
},
pregunta: {
validacion: TiposValidacion.RFC,
},
},
relations: ['pregunta', 'cuestionarioRespondido'],
});
const identificadorRespuesta = respuesta ?? respuestaRfc;
if (identificadorRespuesta) {
const identificador = identificadorRespuesta.respuesta
.trim()
.toUpperCase();
const eventoUsuario = await this.eventoUsuarioRepo.findOne({
where: { id_evento: decoded.id_evento, identificador },
});
result.contador = eventoUsuario?.contador ?? null;
}
}
return result;
} catch (error) {
console.error('Error validando token QR:', error.message);
console.error('Error validando token QR:', error);
return null;
}
}
+3 -1
View File
@@ -9,9 +9,11 @@ import { ConfigModule } from '@nestjs/config';
import { Asistencia } from 'src/asistencia/entities/asistencia.entity';
import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
import { Evento } from 'src/evento/entities/evento.entity';
import { EventoUsuario } from 'src/evento_usuario/entities/evento_usuario.entity';
import { RespuestaParticipanteAbierta } from 'src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
@Module({
imports: [TypeOrmModule.forFeature([Qr, Asistencia, Cuestionario, Evento]), AuthModule, ConfigModule],
imports: [TypeOrmModule.forFeature([Qr, Asistencia, Cuestionario, Evento, EventoUsuario, RespuestaParticipanteAbierta]), AuthModule, ConfigModule],
controllers: [QrController],
providers: [QrService, QrTokenService],
exports: [QrTokenService],