Files
aurak/server/src/assessment/services/template.service.ts
T
Developer 68371922ca P0-1/P0-2/P1-1: dimensions form + E2E tests + PDF export
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
2026-05-19 08:42:03 +08:00

115 lines
3.3 KiB
TypeScript

import {
Injectable,
NotFoundException,
ForbiddenException,
BadRequestException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AssessmentTemplate } from '../entities/assessment-template.entity';
import { CreateTemplateDto } from '../dto/create-template.dto';
import { UpdateTemplateDto } from '../dto/update-template.dto';
import { TenantService } from '../../tenant/tenant.service';
@Injectable()
export class TemplateService {
constructor(
@InjectRepository(AssessmentTemplate)
private readonly templateRepository: Repository<AssessmentTemplate>,
private readonly tenantService: TenantService,
) {}
private validateRequiredFields(data: {
linkedGroupIds?: string[];
dimensions?: Array<{ name: string; label?: string; weight?: number }>;
}) {
if (data.linkedGroupIds !== undefined) {
if (!Array.isArray(data.linkedGroupIds) || data.linkedGroupIds.length === 0) {
throw new BadRequestException('At least one knowledge group must be linked');
}
}
if (data.dimensions !== undefined) {
if (!Array.isArray(data.dimensions) || data.dimensions.length === 0) {
throw new BadRequestException('At least one dimension must be defined');
}
for (const d of data.dimensions) {
if (!d.name) {
throw new BadRequestException('Each dimension must have a name');
}
}
}
}
async create(
createDto: CreateTemplateDto,
userId: string,
tenantId: string,
): Promise<AssessmentTemplate> {
this.validateRequiredFields(createDto);
const { ...data } = createDto;
const template = this.templateRepository.create({
...data,
createdBy: userId,
tenantId,
});
return this.templateRepository.save(template);
}
async findAll(tenantId: string): Promise<AssessmentTemplate[]> {
return this.templateRepository.find({
where: { tenantId, isActive: true },
relations: ['knowledgeGroup'],
order: { createdAt: 'DESC' },
});
}
async findOne(
id: string,
userId: string,
tenantId: string,
): Promise<AssessmentTemplate> {
const template = await this.templateRepository.findOne({
where: { id },
relations: ['knowledgeGroup'],
});
if (!template) {
throw new NotFoundException(`Template with ID "${id}" not found`);
}
// Check permission using TenantService
const hasAccess = await this.tenantService.canAccessTenant(
userId,
template.tenantId,
tenantId,
);
if (!hasAccess) {
throw new ForbiddenException(
`You do not have permission to access this template`,
);
}
return template;
}
async update(
id: string,
updateDto: UpdateTemplateDto,
userId: string,
tenantId: string,
): Promise<AssessmentTemplate> {
const template = await this.findOne(id, userId, tenantId);
const merged = { ...template, ...updateDto };
this.validateRequiredFields(merged);
Object.assign(template, updateDto);
return this.templateRepository.save(template);
}
async remove(id: string, userId: string, tenantId: string): Promise<void> {
const template = await this.findOne(id, userId, tenantId);
// Soft delete by setting isActive to false
template.isActive = false;
await this.templateRepository.save(template);
}
}