diff --git a/src/action-plans/action-plans.controller.ts b/src/action-plans/action-plans.controller.ts new file mode 100644 index 0000000..8e9bcb0 --- /dev/null +++ b/src/action-plans/action-plans.controller.ts @@ -0,0 +1,91 @@ +import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common'; +import { IsIn, IsOptional, IsString, IsUUID } from 'class-validator'; +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 { actionPlans } from '../db/schema'; + +class CreateActionPlanDto { + @IsString() title!: string; + @IsOptional() @IsString() description?: string; + @IsOptional() @IsIn(['draft','active','completed','cancelled']) status?: string; + @IsOptional() @IsIn(['low','medium','high','critical']) priority?: string; + @IsOptional() @IsString() owner?: string; + @IsOptional() @IsString() dueDate?: string; + @IsOptional() @IsUUID() decisionId?: string; + @IsOptional() @IsUUID() goalId?: string; + @IsOptional() @IsUUID() projectId?: string; +} + +class UpdateActionPlanDto { + @IsOptional() @IsString() title?: string; + @IsOptional() @IsString() description?: string; + @IsOptional() @IsIn(['draft','active','completed','cancelled']) status?: string; + @IsOptional() @IsIn(['low','medium','high','critical']) priority?: string; + @IsOptional() @IsString() owner?: string; + @IsOptional() @IsString() dueDate?: string; +} + +@Controller('action-plans') +export class ActionPlansController { + @Get() + async list( + @CurrentSession() session: AuthenticatedSession, + @Query('status') status?: string, + ) { + const tid = session.tenantId; + const where = status + ? and(eq(actionPlans.tenantId, tid), isNull(actionPlans.deletedAt), eq(actionPlans.status, status)) + : and(eq(actionPlans.tenantId, tid), isNull(actionPlans.deletedAt)); + return db.query.actionPlans.findMany({ where, orderBy: [desc(actionPlans.createdAt)], limit: 200 }); + } + + @Post() + async create(@CurrentSession() session: AuthenticatedSession, @Body() dto: CreateActionPlanDto) { + const [row] = await db.insert(actionPlans).values({ + tenantId: session.tenantId, + title: dto.title, + description: dto.description, + status: dto.status ?? 'draft', + priority: dto.priority ?? 'medium', + owner: dto.owner, + dueDate: dto.dueDate ?? null, + decisionId: dto.decisionId ?? null, + goalId: dto.goalId ?? null, + projectId: dto.projectId ?? null, + }).returning(); + return row; + } + + @Patch(':id') + async update( + @CurrentSession() session: AuthenticatedSession, + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateActionPlanDto, + ) { + const updates: Record = { updatedAt: new Date() }; + if (dto.title !== undefined) updates.title = dto.title; + if (dto.description !== undefined) updates.description = dto.description; + if (dto.status !== undefined) { + updates.status = dto.status; + if (dto.status === 'completed') updates.completedAt = new Date(); + } + if (dto.priority !== undefined) updates.priority = dto.priority; + if (dto.owner !== undefined) updates.owner = dto.owner; + if (dto.dueDate !== undefined) updates.dueDate = dto.dueDate; + const [row] = await db.update(actionPlans) + .set(updates) + .where(and(eq(actionPlans.id, id), eq(actionPlans.tenantId, session.tenantId))) + .returning(); + return row; + } + + @Delete(':id') + async remove(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) { + await db.update(actionPlans) + .set({ deletedAt: new Date() }) + .where(and(eq(actionPlans.id, id), eq(actionPlans.tenantId, session.tenantId))); + return { deleted: true }; + } +}