68371922ca
P0-1 Backend: dimensions column on template entity + validation P0-1 Frontend: dimensions edit UI in TemplateManager P0-2: routeAfterGrading unit tests (10 cases), service spec fix + certificate tests, jest-e2e.json P1-1: proper PDF generation with embedded CJK font via pdf-lib low-level API
283 lines
8.3 KiB
TypeScript
283 lines
8.3 KiB
TypeScript
import {
|
|
Controller,
|
|
Post,
|
|
Body,
|
|
Get,
|
|
Param,
|
|
UseGuards,
|
|
Request,
|
|
Req,
|
|
Sse,
|
|
MessageEvent,
|
|
Query,
|
|
Delete,
|
|
Put,
|
|
ForbiddenException,
|
|
} from '@nestjs/common';
|
|
import { map } from 'rxjs/operators';
|
|
import { AssessmentService } from './assessment.service';
|
|
import { ExportService } from './services/export.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 {
|
|
constructor(
|
|
private readonly assessmentService: AssessmentService,
|
|
private readonly exportService: ExportService,
|
|
) {}
|
|
|
|
@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;
|
|
console.log(
|
|
`[AssessmentController] startSession: user=${userId}, tenant=${tenantId}, templateId=${body.templateId}, kbId=${body.knowledgeBaseId}`,
|
|
);
|
|
return this.assessmentService.startSession(
|
|
userId,
|
|
body.knowledgeBaseId,
|
|
tenantId,
|
|
body.language,
|
|
body.templateId,
|
|
);
|
|
}
|
|
|
|
@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 } = req.user;
|
|
console.log(
|
|
`[AssessmentController] >>> submitAnswer CALLED: user=${userId}, session=${sessionId}, answerLen=${body.answer?.length}`,
|
|
);
|
|
return this.assessmentService.submitAnswer(
|
|
sessionId,
|
|
userId,
|
|
body.answer,
|
|
body.language,
|
|
);
|
|
}
|
|
|
|
@Sse(':id/start-stream')
|
|
@ApiOperation({ summary: 'Stream initial session generation' })
|
|
startSessionStream(@Request() req: any, @Param('id') sessionId: string) {
|
|
const { id: userId } = req.user;
|
|
console.log(
|
|
`[AssessmentController] 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;
|
|
console.log(
|
|
`[AssessmentController] >>> submitAnswerStream CALLED: 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;
|
|
console.log(
|
|
`[AssessmentController] getSessionState: user=${userId}, session=${sessionId}`,
|
|
);
|
|
return this.assessmentService.getSessionState(sessionId, userId);
|
|
}
|
|
|
|
@Delete(':id')
|
|
@ApiOperation({ summary: 'Delete an assessment session' })
|
|
async deleteSession(@Request() req: any, @Param('id') sessionId: string) {
|
|
const user = req.user;
|
|
console.log(
|
|
`[AssessmentController] deleteSession: user=${user.id}, role=${user.role}, session=${sessionId}`,
|
|
);
|
|
return this.assessmentService.deleteSession(sessionId, user);
|
|
}
|
|
|
|
@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;
|
|
console.log(
|
|
`[AssessmentController] 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;
|
|
console.log(
|
|
`[AssessmentController] 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,
|
|
);
|
|
}
|
|
|
|
@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;
|
|
return this.assessmentService.reviewAssessment(
|
|
sessionId,
|
|
body.newScore,
|
|
body.comment,
|
|
userId,
|
|
tenantId,
|
|
);
|
|
}
|
|
|
|
@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 { role } = req.user;
|
|
const isAdmin = role === 'super_admin' || role === 'admin';
|
|
if (!isAdmin) {
|
|
throw new ForbiddenException('Only admin can force end assessment');
|
|
}
|
|
return this.assessmentService.forceEndAssessment(sessionId);
|
|
}
|
|
|
|
@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 PDF' })
|
|
async exportPdf(@Param('id') sessionId: string) {
|
|
const buffer = await this.exportService.exportToPdf(sessionId);
|
|
return {
|
|
filename: `assessment-${sessionId}.pdf`,
|
|
buffer: buffer.toString('base64'),
|
|
};
|
|
}
|
|
}
|