import { Injectable, NotFoundException } from "@nestjs/common"; import { Prisma, Idea } from "@prisma/client"; import { PrismaService } from "../prisma/prisma.service"; import { ActivityService } from "../activity/activity.service"; import { IdeaStatus } from "@prisma/client"; import type { CreateIdeaDto, CaptureIdeaDto, UpdateIdeaDto, QueryIdeasDto } from "./dto"; type IdeaWithRelations = Idea & { creator: { id: string; name: string; email: string }; domain: { id: string; name: string; color: string | null } | null; project: { id: string; name: string; color: string | null } | null; }; type IdeaCaptured = Idea & { creator: { id: string; name: string; email: string }; }; /** * Service for managing ideas */ @Injectable() export class IdeasService { constructor( private readonly prisma: PrismaService, private readonly activityService: ActivityService ) {} /** * Create a new idea */ async create( workspaceId: string, userId: string, createIdeaDto: CreateIdeaDto ): Promise { const domainConnection = createIdeaDto.domainId ? { connect: { id: createIdeaDto.domainId } } : undefined; const projectConnection = createIdeaDto.projectId ? { connect: { id: createIdeaDto.projectId } } : undefined; const data: Prisma.IdeaCreateInput = { title: createIdeaDto.title ?? null, content: createIdeaDto.content, category: createIdeaDto.category ?? null, workspace: { connect: { id: workspaceId } }, creator: { connect: { id: userId } }, status: createIdeaDto.status ?? IdeaStatus.CAPTURED, priority: createIdeaDto.priority ?? "MEDIUM", tags: createIdeaDto.tags ?? [], metadata: createIdeaDto.metadata ? (createIdeaDto.metadata as unknown as Prisma.InputJsonValue) : {}, ...(domainConnection && { domain: domainConnection }), ...(projectConnection && { project: projectConnection }), }; const idea = await this.prisma.idea.create({ data, include: { creator: { select: { id: true, name: true, email: true }, }, domain: { select: { id: true, name: true, color: true }, }, project: { select: { id: true, name: true, color: true }, }, }, }); // Log activity await this.activityService.logIdeaCreated(workspaceId, userId, idea.id, { title: idea.title ?? "Untitled", }); return idea; } /** * Quick capture - create an idea with minimal fields * Optimized for rapid idea capture from the front-end */ async capture( workspaceId: string, userId: string, captureIdeaDto: CaptureIdeaDto ): Promise { const data: Prisma.IdeaCreateInput = { workspace: { connect: { id: workspaceId } }, creator: { connect: { id: userId } }, content: captureIdeaDto.content, title: captureIdeaDto.title ?? null, status: IdeaStatus.CAPTURED, priority: "MEDIUM", tags: [], metadata: {}, }; const idea = await this.prisma.idea.create({ data, include: { creator: { select: { id: true, name: true, email: true }, }, }, }); // Log activity await this.activityService.logIdeaCreated(workspaceId, userId, idea.id, { quickCapture: true, title: idea.title ?? "Untitled", }); return idea; } /** * Get paginated ideas with filters */ async findAll(query: QueryIdeasDto): Promise<{ data: IdeaWithRelations[]; 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.IdeaWhereInput = query.workspaceId ? { workspaceId: query.workspaceId, } : {}; if (query.status) { where.status = query.status; } if (query.domainId) { where.domainId = query.domainId; } if (query.projectId) { where.projectId = query.projectId; } if (query.category) { where.category = query.category; } // Add search filter if provided if (query.search) { where.OR = [ { title: { contains: query.search, mode: "insensitive" } }, { content: { contains: query.search, mode: "insensitive" } }, ]; } // Execute queries in parallel const [data, total] = await Promise.all([ this.prisma.idea.findMany({ where, include: { creator: { select: { id: true, name: true, email: true }, }, domain: { select: { id: true, name: true, color: true }, }, project: { select: { id: true, name: true, color: true }, }, }, orderBy: { createdAt: "desc", }, skip, take: limit, }), this.prisma.idea.count({ where }), ]); return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit), }, }; } /** * Get a single idea by ID */ async findOne(id: string, workspaceId: string): Promise { const idea = await this.prisma.idea.findUnique({ where: { id, workspaceId, }, include: { creator: { select: { id: true, name: true, email: true }, }, domain: { select: { id: true, name: true, color: true }, }, project: { select: { id: true, name: true, color: true }, }, }, }); if (!idea) { throw new NotFoundException(`Idea with ID ${id} not found`); } return idea; } /** * Update an idea */ async update( id: string, workspaceId: string, userId: string, updateIdeaDto: UpdateIdeaDto ): Promise { // Verify idea exists const existingIdea = await this.prisma.idea.findUnique({ where: { id, workspaceId }, }); if (!existingIdea) { throw new NotFoundException(`Idea with ID ${id} not found`); } // Build update data, only including defined fields (excluding domainId and projectId) const updateData: Prisma.IdeaUpdateInput = {}; if (updateIdeaDto.title !== undefined) updateData.title = updateIdeaDto.title; if (updateIdeaDto.content !== undefined) updateData.content = updateIdeaDto.content; if (updateIdeaDto.status !== undefined) updateData.status = updateIdeaDto.status; if (updateIdeaDto.priority !== undefined) updateData.priority = updateIdeaDto.priority; if (updateIdeaDto.category !== undefined) updateData.category = updateIdeaDto.category; if (updateIdeaDto.tags !== undefined) updateData.tags = updateIdeaDto.tags; if (updateIdeaDto.metadata !== undefined) { updateData.metadata = updateIdeaDto.metadata as unknown as Prisma.InputJsonValue; } // Handle domain and project relations separately if (updateIdeaDto.domainId !== undefined) { updateData.domain = { connect: { id: updateIdeaDto.domainId } }; } if (updateIdeaDto.projectId !== undefined) { updateData.project = { connect: { id: updateIdeaDto.projectId } }; } const idea = await this.prisma.idea.update({ where: { id, workspaceId, }, data: updateData, include: { creator: { select: { id: true, name: true, email: true }, }, domain: { select: { id: true, name: true, color: true }, }, project: { select: { id: true, name: true, color: true }, }, }, }); // Log activity await this.activityService.logIdeaUpdated(workspaceId, userId, id, { changes: updateIdeaDto as Prisma.JsonValue, }); return idea; } /** * Delete an idea */ async remove(id: string, workspaceId: string, userId: string): Promise { // Verify idea exists const idea = await this.prisma.idea.findUnique({ where: { id, workspaceId }, }); if (!idea) { throw new NotFoundException(`Idea with ID ${id} not found`); } await this.prisma.idea.delete({ where: { id, workspaceId, }, }); // Log activity await this.activityService.logIdeaDeleted(workspaceId, userId, id, { title: idea.title ?? "Untitled", }); } }