import { Injectable, NotFoundException } from "@nestjs/common"; import { Prisma } from "@prisma/client"; import { PrismaService } from "../prisma/prisma.service"; import { ActivityService } from "../activity/activity.service"; import { TaskStatus, TaskPriority } from "@prisma/client"; import type { CreateTaskDto, UpdateTaskDto, QueryTasksDto } from "./dto"; /** * Service for managing tasks */ @Injectable() export class TasksService { constructor( private readonly prisma: PrismaService, private readonly activityService: ActivityService ) {} /** * Create a new task */ async create(workspaceId: string, userId: string, createTaskDto: CreateTaskDto) { const assigneeConnection = createTaskDto.assigneeId ? { connect: { id: createTaskDto.assigneeId } } : undefined; const projectConnection = createTaskDto.projectId ? { connect: { id: createTaskDto.projectId } } : undefined; const parentConnection = createTaskDto.parentId ? { connect: { id: createTaskDto.parentId } } : undefined; const data: Prisma.TaskCreateInput = { title: createTaskDto.title, description: createTaskDto.description ?? null, dueDate: createTaskDto.dueDate ?? null, workspace: { connect: { id: workspaceId } }, creator: { connect: { id: userId } }, status: createTaskDto.status ?? TaskStatus.NOT_STARTED, priority: createTaskDto.priority ?? TaskPriority.MEDIUM, sortOrder: createTaskDto.sortOrder ?? 0, metadata: createTaskDto.metadata ? (createTaskDto.metadata as unknown as Prisma.InputJsonValue) : {}, ...(assigneeConnection && { assignee: assigneeConnection }), ...(projectConnection && { project: projectConnection }), ...(parentConnection && { parent: parentConnection }), }; // Set completedAt if status is COMPLETED if (data.status === TaskStatus.COMPLETED) { data.completedAt = new Date(); } const task = await this.prisma.task.create({ data, include: { assignee: { select: { id: true, name: true, email: true }, }, creator: { select: { id: true, name: true, email: true }, }, project: { select: { id: true, name: true, color: true }, }, }, }); // Log activity await this.activityService.logTaskCreated(workspaceId, userId, task.id, { title: task.title, }); return task; } /** * Get paginated tasks with filters */ async findAll(query: QueryTasksDto) { const page = query.page ?? 1; const limit = query.limit ?? 50; const skip = (page - 1) * limit; // Build where clause const where: Prisma.TaskWhereInput = query.workspaceId ? { workspaceId: query.workspaceId, } : {}; if (query.status) { where.status = Array.isArray(query.status) ? { in: query.status } : query.status; } if (query.priority) { where.priority = Array.isArray(query.priority) ? { in: query.priority } : query.priority; } if (query.assigneeId) { where.assigneeId = query.assigneeId; } if (query.projectId) { where.projectId = query.projectId; } if (query.parentId) { where.parentId = query.parentId; } if (query.dueDateFrom || query.dueDateTo) { where.dueDate = {}; if (query.dueDateFrom) { where.dueDate.gte = query.dueDateFrom; } if (query.dueDateTo) { where.dueDate.lte = query.dueDateTo; } } // Execute queries in parallel const [data, total] = await Promise.all([ this.prisma.task.findMany({ where, include: { assignee: { select: { id: true, name: true, email: true }, }, creator: { select: { id: true, name: true, email: true }, }, project: { select: { id: true, name: true, color: true }, }, }, orderBy: { createdAt: "desc", }, skip, take: limit, }), this.prisma.task.count({ where }), ]); return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit), }, }; } /** * Get a single task by ID */ async findOne(id: string, workspaceId: string) { const task = await this.prisma.task.findUnique({ where: { id, workspaceId, }, include: { assignee: { select: { id: true, name: true, email: true }, }, creator: { select: { id: true, name: true, email: true }, }, project: { select: { id: true, name: true, color: true }, }, subtasks: { include: { assignee: { select: { id: true, name: true, email: true }, }, }, }, }, }); if (!task) { throw new NotFoundException(`Task with ID ${id} not found`); } return task; } /** * Update a task */ async update(id: string, workspaceId: string, userId: string, updateTaskDto: UpdateTaskDto) { // Verify task exists const existingTask = await this.prisma.task.findUnique({ where: { id, workspaceId }, }); if (!existingTask) { throw new NotFoundException(`Task with ID ${id} not found`); } // Build update data - only include defined fields const data: Prisma.TaskUpdateInput = {}; if (updateTaskDto.title !== undefined) { data.title = updateTaskDto.title; } if (updateTaskDto.description !== undefined) { data.description = updateTaskDto.description; } if (updateTaskDto.status !== undefined) { data.status = updateTaskDto.status; } if (updateTaskDto.priority !== undefined) { data.priority = updateTaskDto.priority; } if (updateTaskDto.dueDate !== undefined) { data.dueDate = updateTaskDto.dueDate; } if (updateTaskDto.sortOrder !== undefined) { data.sortOrder = updateTaskDto.sortOrder; } if (updateTaskDto.metadata !== undefined) { data.metadata = updateTaskDto.metadata as unknown as Prisma.InputJsonValue; } if (updateTaskDto.assigneeId !== undefined && updateTaskDto.assigneeId !== null) { data.assignee = { connect: { id: updateTaskDto.assigneeId } }; } if (updateTaskDto.projectId !== undefined && updateTaskDto.projectId !== null) { data.project = { connect: { id: updateTaskDto.projectId } }; } if (updateTaskDto.parentId !== undefined && updateTaskDto.parentId !== null) { data.parent = { connect: { id: updateTaskDto.parentId } }; } // Handle completedAt based on status changes if (updateTaskDto.status) { if ( updateTaskDto.status === TaskStatus.COMPLETED && existingTask.status !== TaskStatus.COMPLETED ) { data.completedAt = new Date(); } else if ( updateTaskDto.status !== TaskStatus.COMPLETED && existingTask.status === TaskStatus.COMPLETED ) { data.completedAt = null; } } const task = await this.prisma.task.update({ where: { id, workspaceId, }, data, include: { assignee: { select: { id: true, name: true, email: true }, }, creator: { select: { id: true, name: true, email: true }, }, project: { select: { id: true, name: true, color: true }, }, }, }); // Log activities await this.activityService.logTaskUpdated(workspaceId, userId, id, { changes: updateTaskDto as Prisma.JsonValue, }); // Log completion if status changed to COMPLETED if ( updateTaskDto.status === TaskStatus.COMPLETED && existingTask.status !== TaskStatus.COMPLETED ) { await this.activityService.logTaskCompleted(workspaceId, userId, id); } // Log assignment if assigneeId changed if ( updateTaskDto.assigneeId !== undefined && updateTaskDto.assigneeId !== existingTask.assigneeId ) { await this.activityService.logTaskAssigned( workspaceId, userId, id, updateTaskDto.assigneeId ?? "" ); } return task; } /** * Delete a task */ async remove(id: string, workspaceId: string, userId: string) { // Verify task exists const task = await this.prisma.task.findUnique({ where: { id, workspaceId }, }); if (!task) { throw new NotFoundException(`Task with ID ${id} not found`); } await this.prisma.task.delete({ where: { id, workspaceId, }, }); // Log activity await this.activityService.logTaskDeleted(workspaceId, userId, id, { title: task.title, }); } }