import { Injectable, NotFoundException } from "@nestjs/common"; import { Prisma, Project } from "@prisma/client"; import { PrismaService } from "../prisma/prisma.service"; import { ActivityService } from "../activity/activity.service"; import { ProjectStatus } from "@prisma/client"; import type { CreateProjectDto, UpdateProjectDto, QueryProjectsDto } from "./dto"; type ProjectWithRelations = Project & { creator: { id: string; name: string; email: string }; _count: { tasks: number; events: number }; }; type ProjectWithDetails = Project & { creator: { id: string; name: string; email: string }; tasks: { id: string; title: string; status: string; priority: string; dueDate: Date | null; }[]; events: { id: string; title: string; startTime: Date; endTime: Date | null; }[]; _count: { tasks: number; events: number }; }; /** * Service for managing projects */ @Injectable() export class ProjectsService { constructor( private readonly prisma: PrismaService, private readonly activityService: ActivityService ) {} /** * Create a new project */ async create( workspaceId: string, userId: string, createProjectDto: CreateProjectDto ): Promise { const data: Prisma.ProjectCreateInput = { ...(createProjectDto.domainId ? { domain: { connect: { id: createProjectDto.domainId } } } : {}), name: createProjectDto.name, description: createProjectDto.description ?? null, color: createProjectDto.color ?? null, startDate: createProjectDto.startDate ?? null, endDate: createProjectDto.endDate ?? null, workspace: { connect: { id: workspaceId } }, creator: { connect: { id: userId } }, status: createProjectDto.status ?? ProjectStatus.PLANNING, metadata: createProjectDto.metadata ? (createProjectDto.metadata as unknown as Prisma.InputJsonValue) : {}, }; const project = await this.prisma.project.create({ data, include: { creator: { select: { id: true, name: true, email: true }, }, _count: { select: { tasks: true, events: true }, }, }, }); // Log activity await this.activityService.logProjectCreated(workspaceId, userId, project.id, { name: project.name, }); return project; } /** * Get paginated projects with filters */ async findAll(query: QueryProjectsDto): Promise<{ data: ProjectWithRelations[]; meta: { total: number; page: number; limit: number; totalPages: number; }; }> { const page = query.page ?? 1; const limit = query.limit ?? 50; const skip = (page - 1) * limit; // Build where clause const where: Prisma.ProjectWhereInput = query.workspaceId ? { workspaceId: query.workspaceId, } : {}; if (query.status) { where.status = query.status; } if (query.startDateFrom || query.startDateTo) { where.startDate = {}; if (query.startDateFrom) { where.startDate.gte = query.startDateFrom; } if (query.startDateTo) { where.startDate.lte = query.startDateTo; } } // Execute queries in parallel const [data, total] = await Promise.all([ this.prisma.project.findMany({ where, include: { creator: { select: { id: true, name: true, email: true }, }, _count: { select: { tasks: true, events: true }, }, }, orderBy: { createdAt: "desc", }, skip, take: limit, }), this.prisma.project.count({ where }), ]); return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit), }, }; } /** * Get a single project by ID */ async findOne(id: string, workspaceId: string): Promise { const project = await this.prisma.project.findUnique({ where: { id, workspaceId, }, include: { creator: { select: { id: true, name: true, email: true }, }, tasks: { select: { id: true, title: true, status: true, priority: true, dueDate: true, }, orderBy: { sortOrder: "asc" }, }, events: { select: { id: true, title: true, startTime: true, endTime: true, }, orderBy: { startTime: "asc" }, }, _count: { select: { tasks: true, events: true }, }, }, }); if (!project) { throw new NotFoundException(`Project with ID ${id} not found`); } return project; } /** * Update a project */ async update( id: string, workspaceId: string, userId: string, updateProjectDto: UpdateProjectDto ): Promise { // Verify project exists const existingProject = await this.prisma.project.findUnique({ where: { id, workspaceId }, }); if (!existingProject) { throw new NotFoundException(`Project with ID ${id} not found`); } // Build update data, only including defined fields const updateData: Prisma.ProjectUpdateInput = {}; if (updateProjectDto.name !== undefined) updateData.name = updateProjectDto.name; if (updateProjectDto.description !== undefined) updateData.description = updateProjectDto.description; if (updateProjectDto.status !== undefined) updateData.status = updateProjectDto.status; if (updateProjectDto.startDate !== undefined) updateData.startDate = updateProjectDto.startDate; if (updateProjectDto.endDate !== undefined) updateData.endDate = updateProjectDto.endDate; if (updateProjectDto.color !== undefined) updateData.color = updateProjectDto.color; if (updateProjectDto.domainId !== undefined) updateData.domain = updateProjectDto.domainId ? { connect: { id: updateProjectDto.domainId } } : { disconnect: true }; if (updateProjectDto.domainId !== undefined) updateData.domain = updateProjectDto.domainId ? { connect: { id: updateProjectDto.domainId, }, } : { disconnect: true }; if (updateProjectDto.metadata !== undefined) { updateData.metadata = updateProjectDto.metadata as unknown as Prisma.InputJsonValue; } const project = await this.prisma.project.update({ where: { id, workspaceId, }, data: updateData, include: { creator: { select: { id: true, name: true, email: true }, }, _count: { select: { tasks: true, events: true }, }, }, }); // Log activity await this.activityService.logProjectUpdated(workspaceId, userId, id, { changes: updateProjectDto as Prisma.JsonValue, }); return project; } /** * Delete a project */ async remove(id: string, workspaceId: string, userId: string): Promise { // Verify project exists const project = await this.prisma.project.findUnique({ where: { id, workspaceId }, }); if (!project) { throw new NotFoundException(`Project with ID ${id} not found`); } await this.prisma.project.delete({ where: { id, workspaceId, }, }); // Log activity await this.activityService.logProjectDeleted(workspaceId, userId, id, { name: project.name, }); } }