import { Controller, Post, Body, Get, Param, UseGuards, Request, Req, Sse, MessageEvent, Query, Delete, Put, ForbiddenException, Logger, } from '@nestjs/common'; import { map } from 'rxjs/operators'; import { AssessmentService } from './assessment.service'; import { ExportService } from './services/export.service'; import { AuditLogService } from './services/audit-log.service'; import { CombinedAuthGuard } from '../auth/combined-auth.guard'; import { Public } from '../auth/public.decorator'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; @ApiTags('Assessment') @Controller('assessment') @UseGuards(CombinedAuthGuard) export class AssessmentController { private readonly logger = new Logger(AssessmentController.name); constructor( private readonly assessmentService: AssessmentService, private readonly exportService: ExportService, private readonly auditLog: AuditLogService, ) {} @Post('start') @ApiOperation({ summary: 'Start a new assessment session' }) async startSession( @Request() req: any, @Body() body: { knowledgeBaseId?: string; language?: string; templateId?: string }, ) { const { id: userId, tenantId } = req.user; this.logger.log( `startSession: user=${userId}, tenant=${tenantId}, templateId=${body.templateId}, kbId=${body.knowledgeBaseId}`, ); const session = await this.assessmentService.startSession( userId, body.knowledgeBaseId, tenantId, body.language, body.templateId, ); this.auditLog.log({ userId, tenantId, action: 'session.start', resourceType: 'assessment_session', resourceId: session.id }); return session; } @Post(':id/answer') @ApiOperation({ summary: 'Submit an answer to the current question' }) async submitAnswer( @Request() req: any, @Param('id') sessionId: string, @Body() body: { answer: string; language?: string }, ) { const { id: userId, tenantId } = req.user; this.logger.log( `submitAnswer: user=${userId}, session=${sessionId}, answerLen=${body.answer?.length}`, ); const result = await this.assessmentService.submitAnswer( sessionId, userId, body.answer, body.language, ); this.auditLog.log({ userId, tenantId, action: 'session.answer', resourceType: 'assessment_session', resourceId: sessionId, details: { answerLength: body.answer?.length } }); return result; } @Sse(':id/start-stream') @ApiOperation({ summary: 'Stream initial session generation' }) startSessionStream(@Request() req: any, @Param('id') sessionId: string) { const { id: userId } = req.user; this.logger.log( `startSessionStream: user=${userId}, session=${sessionId}`, ); return this.assessmentService .startSessionStream(sessionId, userId) .pipe(map((data) => ({ data }) as MessageEvent)); } @Sse(':id/answer-stream') @ApiOperation({ summary: 'Stream answer evaluation and next question generation', }) submitAnswerStream( @Request() req: any, @Param('id') sessionId: string, @Query('answer') answer: string, @Query('language') language?: string, ) { const { id: userId } = req.user; this.logger.log( `submitAnswerStream: user=${userId}, session=${sessionId}, answerLen=${answer?.length}, lang=${language}`, ); return this.assessmentService .submitAnswerStream(sessionId, userId, answer, language) .pipe(map((data) => ({ data }) as MessageEvent)); } @Get(':id/state') @ApiOperation({ summary: 'Get the current state of an assessment session' }) async getSessionState(@Request() req: any, @Param('id') sessionId: string) { const { id: userId } = req.user; this.logger.log( `getSessionState: user=${userId}, session=${sessionId}`, ); return this.assessmentService.getSessionState(sessionId, userId); } @Get(':id/review') @ApiOperation({ summary: 'Get review data for a completed assessment (shows correct answers)' }) async getReview(@Request() req: any, @Param('id') sessionId: string) { const { id: userId } = req.user; this.logger.log(`getReview: user=${userId}, session=${sessionId}`); return this.assessmentService.getSessionReview(sessionId, userId); } @Delete(':id') @ApiOperation({ summary: 'Delete an assessment session' }) async deleteSession(@Request() req: any, @Param('id') sessionId: string) { const user = req.user; this.logger.log( `deleteSession: user=${user.id}, role=${user.role}, session=${sessionId}`, ); await this.assessmentService.deleteSession(sessionId, user); this.auditLog.log({ userId: user.id, tenantId: user.tenantId, action: 'session.delete', resourceType: 'assessment_session', resourceId: sessionId }); return { success: true }; } @Get(':id/certificate') @ApiOperation({ summary: 'Get certificate for completed assessment' }) async getCertificate( @Request() req: any, @Param('id') sessionId: string, ) { const { id: userId, tenantId } = req.user; this.logger.log( `getCertificate: user=${userId}, session=${sessionId}`, ); return this.assessmentService.generateCertificate(sessionId, userId, tenantId); } @Public() @Get('certificate/verify/:certificateId') @ApiOperation({ summary: 'Verify certificate by ID (public)' }) async verifyCertificate( @Param('certificateId') certificateId: string, ) { return this.assessmentService.verifyCertificate(certificateId); } @Public() @Get('certificate/public/:sessionId') @ApiOperation({ summary: 'Get public certificate info for verification' }) async getPublicCertificate( @Param('sessionId') sessionId: string, ) { return this.assessmentService.getPublicCertificateInfo(sessionId); } @Get('history') @ApiOperation({ summary: 'Get current user assessment history (keep latest 3)' }) async getHistory( @Request() req: any, ) { const { id: userId } = req.user; return this.assessmentService.getUserHistory(userId); } @Get('stats') @ApiOperation({ summary: 'Get assessment statistics for admin' }) async getStats( @Request() req: any, @Query('startDate') startDate?: string, @Query('endDate') endDate?: string, @Query('templateId') templateId?: string, @Query('knowledgeGroupId') knowledgeGroupId?: string, ) { const { id: userId, tenantId, role } = req.user; this.logger.log( `getStats: user=${userId}, role=${role}, tenant=${tenantId}`, ); return this.assessmentService.getStats( userId, tenantId, role, startDate, endDate, templateId, knowledgeGroupId, ); } @Get('stats/radar') @ApiOperation({ summary: 'Get radar chart data for dimension scores' }) async getRadarStats( @Request() req: any, @Query('templateId') templateId?: string, ) { const { id: userId, tenantId, role } = req.user; return this.assessmentService.getRadarStats( userId, tenantId, role, templateId, ); } @Get('stats/trend') @ApiOperation({ summary: 'Get trend data for scores over time' }) async getTrendStats( @Request() req: any, @Query('startDate') startDate?: string, @Query('endDate') endDate?: string, ) { const { id: userId, tenantId, role } = req.user; return this.assessmentService.getTrendStats( userId, tenantId, role, startDate, endDate, ); } @Post('batch-delete') @ApiOperation({ summary: 'Batch delete assessment sessions (admin only)' }) async batchDelete(@Request() req: any, @Body() body: { ids: string[] }) { const user = req.user; const isAdmin = user.role?.toLowerCase() === 'super_admin' || user.role?.toLowerCase() === 'admin'; if (!isAdmin) { throw new ForbiddenException('Only admin can batch delete'); } const count = await this.assessmentService.batchDeleteSessions(body.ids, user); this.auditLog.log({ userId: user.id, tenantId: user.tenantId, action: 'session.batch_delete', resourceType: 'assessment_session', details: { count, ids: body.ids } }); return { deleted: count }; } @Post('batch-export') @ApiOperation({ summary: 'Batch export assessments as JSON array' }) async batchExport(@Request() req: any, @Body() body: { ids: string[] }) { const { id: userId } = req.user; return this.assessmentService.batchExportSessions(body.ids, userId); } @Put(':id/review') @ApiOperation({ summary: 'Review assessment - adjust final score' }) async review( @Param('id') sessionId: string, @Body() body: { newScore: number; comment?: string }, @Req() req: any, ) { const { id: userId, tenantId } = req.user; const result = await this.assessmentService.reviewAssessment( sessionId, body.newScore, body.comment, userId, tenantId, ); this.auditLog.log({ userId, tenantId, action: 'session.review', resourceType: 'assessment_session', resourceId: sessionId, details: { newScore: body.newScore, comment: body.comment } }); return result; } @Get(':id/time-check') @ApiOperation({ summary: 'Check assessment time limits' }) async checkTimeLimits(@Param('id') sessionId: string) { return this.assessmentService.checkTimeLimits(sessionId); } @Post(':id/next-question') @ApiOperation({ summary: 'Start timing for next question' }) async nextQuestion(@Param('id') sessionId: string) { await this.assessmentService.updateQuestionStartTime(sessionId); return { success: true }; } @Post(':id/force-end') @ApiOperation({ summary: 'Force end assessment (admin only)' }) async forceEnd( @Param('id') sessionId: string, @Request() req: any, ) { const { id: userId, tenantId, role } = req.user; const isAdmin = role?.toLowerCase() === 'super_admin' || role?.toLowerCase() === 'admin'; if (!isAdmin) { throw new ForbiddenException('Only admin can force end assessment'); } const result = await this.assessmentService.forceEndAssessment(sessionId); this.auditLog.log({ userId, tenantId, action: 'session.force_end', resourceType: 'assessment_session', resourceId: sessionId }); return result; } @Get(':id/export/excel') @ApiOperation({ summary: 'Export assessment to Excel' }) async exportExcel(@Param('id') sessionId: string) { const buffer = await this.exportService.exportToExcel(sessionId); return { filename: `assessment-${sessionId}.xlsx`, buffer: buffer.toString('base64'), }; } @Get(':id/export/pdf') @ApiOperation({ summary: 'Export assessment to HTML report' }) async exportPdf(@Param('id') sessionId: string) { const buffer = await this.exportService.exportToPdf(sessionId); return { filename: `assessment-${sessionId}.html`, buffer: buffer.toString('base64'), }; } }