feature: Tasks tab added for lab organizations. tasks now can be assigned and their status can be updated by the assignee.

This commit is contained in:
2026-06-28 22:56:56 +03:30
parent feb0b26ad0
commit 2a48946a51
29 changed files with 851 additions and 43 deletions

View File

@@ -50,7 +50,7 @@ export class CasesController {
}
@Patch(':id/tasks/:taskId')
@ApiOperation({ summary: 'Update task assignee or status' })
@ApiOperation({ summary: 'Update task assignee or priority' })
updateTask(
@Param('id') id: string,
@Param('taskId') taskId: string,

View File

@@ -200,14 +200,19 @@ export class CasesService {
}
if (dto.assigneeUserId !== undefined && dto.assigneeUserId !== null) {
await this.ensureLabMember(dto.assigneeUserId, labOrganizationId);
await this.ensureAssignableMember(dto.assigneeUserId, labOrganizationId);
}
const updated = await this.prisma.labCaseTask.update({
where: { id: taskId },
data: {
...(dto.assigneeUserId !== undefined ? { assigneeUserId: dto.assigneeUserId } : {}),
...(dto.status !== undefined ? { status: dto.status } : {}),
...(dto.assigneeUserId !== undefined
? {
assigneeUserId: dto.assigneeUserId,
assignedAt: dto.assigneeUserId === null ? null : new Date(),
}
: {}),
...(dto.priority !== undefined ? { priority: dto.priority } : {}),
},
include: {
assignee: { select: { id: true, name: true, email: true } },
@@ -222,18 +227,27 @@ export class CasesService {
const memberships = await this.prisma.membership.findMany({
where: { organizationId: labOrganizationId, isActive: true },
include: { user: { select: { id: true, name: true, email: true } } },
include: {
user: { select: { id: true, name: true, email: true } },
permissions: { include: { permission: true } },
},
orderBy: [{ isOwner: 'desc' }, { createdAt: 'asc' }],
});
return {
success: true,
data: memberships.map((m) => ({
userId: m.user.id,
name: m.user.name,
email: m.user.email,
isOwner: m.isOwner,
})),
data: memberships
.filter((m) => {
if (m.isOwner) return true;
const names = m.permissions.map((p) => p.permission.name);
return names.includes('TAB_TASKS_READ') || names.includes('TAB_TASKS_EDIT');
})
.map((m) => ({
userId: m.user.id,
name: m.user.name,
email: m.user.email,
isOwner: m.isOwner,
})),
};
}
@@ -377,7 +391,10 @@ export class CasesService {
stepOrder: number;
stepLabel: string;
status: LabTaskStatus;
priority: number;
assigneeUserId: string | null;
assignedAt: Date | null;
createdAt: Date;
assignee: { id: string; name: string; email: string } | null;
}>,
) {
@@ -411,7 +428,10 @@ export class CasesService {
stepOrder: number;
stepLabel: string;
status: LabTaskStatus;
priority: number;
assigneeUserId: string | null;
assignedAt: Date | null;
createdAt: Date;
assignee: { id: string; name: string; email: string } | null;
}) {
return {
@@ -421,6 +441,9 @@ export class CasesService {
stepOrder: task.stepOrder,
stepLabel: task.stepLabel,
status: task.status,
priority: task.priority,
assignedAt: task.assignedAt?.toISOString() ?? null,
createdAt: task.createdAt.toISOString(),
assigneeUserId: task.assigneeUserId,
assignee: task.assignee
? { id: task.assignee.id, name: task.assignee.name, email: task.assignee.email }
@@ -428,14 +451,20 @@ export class CasesService {
};
}
private async ensureLabMember(userId: string, labOrganizationId: string) {
private async ensureAssignableMember(userId: string, labOrganizationId: string) {
const membership = await this.prisma.membership.findFirst({
where: { userId, organizationId: labOrganizationId, isActive: true },
select: { id: true },
include: { permissions: { include: { permission: true } } },
});
if (!membership) {
throw new BadRequestException('Assignee must be an active member of this lab');
}
if (membership.isOwner) return;
const names = membership.permissions.map((p) => p.permission.name);
if (names.includes('TAB_TASKS_READ') || names.includes('TAB_TASKS_EDIT')) {
return;
}
throw new BadRequestException('Assignee must have access to the Tasks tab');
}
private async assertCanReadCases(userId: string, organizationId: string) {

View File

@@ -1,6 +1,5 @@
import { Transform } from 'class-transformer';
import { IsDateString, IsEnum, IsInt, IsOptional, IsString, IsUUID, Max, Min, ValidateIf } from 'class-validator';
import { LabTaskStatus } from '@prisma/client';
import { IsDateString, IsInt, IsOptional, IsString, IsUUID, Max, Min, ValidateIf } from 'class-validator';
export class UpdateLabCaseTaskDto {
@IsOptional()
@@ -9,8 +8,11 @@ export class UpdateLabCaseTaskDto {
assigneeUserId?: string | null;
@IsOptional()
@IsEnum(LabTaskStatus)
status?: LabTaskStatus;
@Transform(({ value }) => Number(value))
@IsInt()
@Min(1)
@Max(5)
priority?: number;
}
export class ListLabCasesDto {
@@ -46,4 +48,4 @@ export class ListLabCasesDto {
@Min(1)
@Max(100)
limit = 20;
}
}