feat: updade workspace

This commit is contained in:
thanhnv
2026-07-11 15:56:31 +09:00
parent 4fc72332f5
commit 193a449829
120 changed files with 868 additions and 350 deletions
@@ -0,0 +1,101 @@
import { ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import type { JwtUser } from '../common/auth.types.js';
import { PrismaService } from '../prisma/prisma.service.js';
import type { CreateKeyResultDto } from './dto/create-key-result.dto.js';
import type { UpdateProgressDto } from './dto/update-progress.dto.js';
const keyResultInclude = {
objective: {
include: {
owner: { select: { id: true, name: true, username: true, email: true, role: true } },
},
},
} satisfies Prisma.KeyResultInclude;
export type KeyResultWithObjective = Prisma.KeyResultGetPayload<{ include: typeof keyResultInclude }>;
@Injectable()
export class KeyResultsService {
constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {}
async getById(id: number, user: JwtUser): Promise<KeyResultWithObjective> {
const keyResult = await this.prisma.keyResult.findUnique({ where: { id }, include: keyResultInclude });
if (keyResult === null) {
throw new NotFoundException('Key result not found');
}
this.assertCanRead(keyResult.objective.ownerId, user);
return keyResult;
}
async create(dto: CreateKeyResultDto, user: JwtUser): Promise<KeyResultWithObjective> {
const objective = await this.prisma.objective.findUnique({ where: { id: dto.objectiveId } });
if (objective === null) {
throw new NotFoundException('Objective not found');
}
this.assertCanWrite(objective.ownerId, user);
const keyResult = await this.prisma.keyResult.create({
data: {
objectiveId: dto.objectiveId,
title: dto.title,
progress: dto.progress,
startValue: dto.startValue,
targetValue: dto.targetValue,
deadline: new Date(dto.deadline),
},
include: keyResultInclude,
});
await this.recalculateObjectiveStatus(dto.objectiveId);
return keyResult;
}
async updateProgress(id: number, dto: UpdateProgressDto, user: JwtUser): Promise<KeyResultWithObjective> {
const existing = await this.prisma.keyResult.findUnique({ where: { id }, include: keyResultInclude });
if (existing === null) {
throw new NotFoundException('Key result not found');
}
this.assertCanWrite(existing.objective.ownerId, user);
const updated = await this.prisma.$transaction(async (tx) => {
const keyResult = await tx.keyResult.update({
where: { id },
data: { progress: dto.progress },
include: keyResultInclude,
});
await tx.progressUpdate.create({
data: {
keyResultId: id,
progress: dto.progress,
comment: dto.comment,
createdById: user.sub,
},
});
return keyResult;
});
await this.recalculateObjectiveStatus(existing.objectiveId);
return updated;
}
private assertCanRead(ownerId: number, user: JwtUser): void {
if (user.role === 'EMPLOYEE' && ownerId !== user.sub) {
throw new ForbiddenException('Key result belongs to another owner');
}
}
private assertCanWrite(ownerId: number, user: JwtUser): void {
if (user.role === 'EMPLOYEE' && ownerId !== user.sub) {
throw new ForbiddenException('Only the owner can update this key result');
}
}
private async recalculateObjectiveStatus(objectiveId: number): Promise<void> {
const keyResults = await this.prisma.keyResult.findMany({ where: { objectiveId } });
const average =
keyResults.length === 0
? 0
: Math.round(keyResults.reduce((total, keyResult) => total + keyResult.progress, 0) / keyResults.length);
const status =
average === 0 ? 'NOT_STARTED' : average >= 100 ? 'COMPLETED' : 'IN_PROGRESS';
await this.prisma.objective.update({ where: { id: objectiveId }, data: { status } });
}
}