From 9f40a1b9b8bd9f4798695d5eed24bdb783b37ad2 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sat, 1 Aug 2026 21:20:53 +0000 Subject: [PATCH] feat(CC-063): add ProjectsController (CRUD projects with status/priority/tags) --- src/projects/projects.controller.ts | 66 +++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/projects/projects.controller.ts diff --git a/src/projects/projects.controller.ts b/src/projects/projects.controller.ts new file mode 100644 index 0000000..87d1b27 --- /dev/null +++ b/src/projects/projects.controller.ts @@ -0,0 +1,66 @@ +import { Body, Controller, Get, IsArray, IsOptional, IsString, NotFoundException, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common'; +import { and, desc, eq, isNull } from 'drizzle-orm'; +import { CurrentSession } from '../auth/session.decorator'; +import type { AuthenticatedSession } from '../auth/tenant.guard'; +import { db } from '../db/client'; +import { projects } from '../db/schema'; + +class CreateProjectDto { + @IsString() name!: string; + @IsOptional() @IsString() description?: string; + @IsOptional() @IsString() organizationId?: string; + @IsOptional() @IsString() status?: string; + @IsOptional() @IsString() priority?: string; + @IsOptional() @IsString() startDate?: string; + @IsOptional() @IsString() dueDate?: string; + @IsOptional() @IsString() currency?: string; + @IsOptional() @IsArray() tags?: string[]; +} + +class UpdateProjectDto { + @IsOptional() @IsString() name?: string; + @IsOptional() @IsString() description?: string; + @IsOptional() @IsString() status?: string; + @IsOptional() @IsString() priority?: string; + @IsOptional() @IsString() startDate?: string; + @IsOptional() @IsString() dueDate?: string; + @IsOptional() @IsArray() tags?: string[]; +} + +@Controller('projects') +export class ProjectsController { + @Get() + async list(@CurrentSession() session: AuthenticatedSession, @Query('status') status?: string) { + const where = status + ? and(eq(projects.tenantId, session.tenantId), isNull(projects.deletedAt), eq(projects.status, status)) + : and(eq(projects.tenantId, session.tenantId), isNull(projects.deletedAt)); + return db.query.projects.findMany({ where, orderBy: desc(projects.createdAt), limit: 200 }); + } + + @Post() + async create(@CurrentSession() session: AuthenticatedSession, @Body() dto: CreateProjectDto) { + const [project] = await db.insert(projects).values({ + ...dto, + tenantId: session.tenantId, + tags: dto.tags ?? [], + }).returning(); + return project; + } + + @Get(':id') + async getOne(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) { + const project = await db.query.projects.findFirst({ + where: and(eq(projects.id, id), eq(projects.tenantId, session.tenantId), isNull(projects.deletedAt)), + }); + if (!project) throw new NotFoundException('Project not found'); + return project; + } + + @Patch(':id') + async update(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateProjectDto) { + const [updated] = await db.update(projects).set({ ...dto, updatedAt: new Date() }) + .where(and(eq(projects.id, id), eq(projects.tenantId, session.tenantId))).returning(); + if (!updated) throw new NotFoundException('Project not found'); + return updated; + } +}