diff --git a/src/evento_usuario/entities/evento_usuario.entity.ts b/src/evento_usuario/entities/evento_usuario.entity.ts index 2e5cfda..a9d4650 100644 --- a/src/evento_usuario/entities/evento_usuario.entity.ts +++ b/src/evento_usuario/entities/evento_usuario.entity.ts @@ -31,4 +31,7 @@ export class EventoUsuario { @Column({ type: 'varchar', length: 20 }) identificador: string; + + @Column({ type: 'int', nullable: true }) + contador: number | null; } diff --git a/src/evento_usuario/evento_usuario.service.ts b/src/evento_usuario/evento_usuario.service.ts index e5d7360..5de92b2 100644 --- a/src/evento_usuario/evento_usuario.service.ts +++ b/src/evento_usuario/evento_usuario.service.ts @@ -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; } diff --git a/src/participante_evento/participante_evento.controller.ts b/src/participante_evento/participante_evento.controller.ts index 8343ea9..2e795d5 100644 --- a/src/participante_evento/participante_evento.controller.ts +++ b/src/participante_evento/participante_evento.controller.ts @@ -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({ diff --git a/src/participante_evento/participante_evento.module.ts b/src/participante_evento/participante_evento.module.ts index d87fdb7..e19f39f 100644 --- a/src/participante_evento/participante_evento.module.ts +++ b/src/participante_evento/participante_evento.module.ts @@ -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, ], diff --git a/src/participante_evento/participante_evento.service.ts b/src/participante_evento/participante_evento.service.ts index 51837e9..2ff6396 100644 --- a/src/participante_evento/participante_evento.service.ts +++ b/src/participante_evento/participante_evento.service.ts @@ -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, @InjectRepository(Participante) private participanteRepository: Repository, + @InjectRepository(Cuestionario) + private cuestionarioRepository: Repository, + @InjectRepository(EventoUsuario) + private eventoUsuarioRepository: Repository, + @InjectRepository(RespuestaParticipanteAbierta) + private respuestaAbiertaRepository: Repository, 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, }, }; } diff --git a/src/qr/qr-token.service.ts b/src/qr/qr-token.service.ts index 0ab2268..6bca348 100644 --- a/src/qr/qr-token.service.ts +++ b/src/qr/qr-token.service.ts @@ -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, + private readonly asistenciaRepository: Repository, @InjectRepository(Cuestionario) - private readonly cuestionarioRepository: Repository, + private readonly cuestionarioRepository: Repository, @InjectRepository(Evento) - private readonly eventoREpo: Repository, + private readonly eventoREpo: Repository, + @InjectRepository(EventoUsuario) + private readonly eventoUsuarioRepo: Repository, + @InjectRepository(RespuestaParticipanteAbierta) + private readonly respuestaAbiertaRepo: Repository, ) {} /** @@ -46,12 +53,16 @@ export class QrTokenService { this.configService.get('QR_JWT_SECRET') || this.configService.get('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('QR_JWT_SECRET') || this.configService.get('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; } } diff --git a/src/qr/qr.module.ts b/src/qr/qr.module.ts index 658d954..fe4963f 100644 --- a/src/qr/qr.module.ts +++ b/src/qr/qr.module.ts @@ -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],