feat(cc-053): GoalsController — CRUD pentru obiective OKR

This commit is contained in:
admin-valentin 2026-08-01 10:43:33 +00:00
parent 871dac9f0a
commit 04f142e7ea

View file

@ -1,42 +1,84 @@
import { BadRequestException, Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common'; import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
import { IsOptional, IsString } from 'class-validator';
import { and, desc, eq, isNull, or, inArray } from 'drizzle-orm';
import { CurrentSession } from '../auth/session.decorator'; import { CurrentSession } from '../auth/session.decorator';
import type { AuthenticatedSession } from '../auth/tenant.guard'; import type { AuthenticatedSession } from '../auth/tenant.guard';
import { CreateGoalDto, GOAL_STATUSES, UpdateGoalDto, type GoalStatusValue } from './dto'; import { db } from '../db/client';
import { GoalsService } from './goals.service'; import { goals } from '../db/schema';
class CreateGoalDto {
@IsString() horizon: string;
@IsString() metric: string;
@IsString() target: string;
@IsOptional() @IsString() workspaceId?: string;
}
class UpdateGoalDto {
@IsOptional() @IsString() status?: string;
@IsOptional() milestones?: unknown[];
@IsOptional() @IsString() target?: string;
}
@Controller('goals') @Controller('goals')
export class GoalsController { export class GoalsController {
constructor(private readonly goalsService: GoalsService) {}
@Get() @Get()
list(@CurrentSession() session: AuthenticatedSession, @Query('status') status?: string) { async list(@CurrentSession() session: AuthenticatedSession) {
if (status && !GOAL_STATUSES.includes(status as GoalStatusValue)) { return db.query.goals.findMany({
throw new BadRequestException(`status must be one of: ${GOAL_STATUSES.join(', ')}`); where: and(eq(goals.tenantId, session.tenantId), isNull(goals.deletedAt)),
} orderBy: [desc(goals.createdAt)],
return this.goalsService.list(session, status as GoalStatusValue | undefined); limit: 100,
});
} }
@Get(':id') @Get('at-risk')
getById(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) { async atRisk(@CurrentSession() session: AuthenticatedSession) {
return this.goalsService.getById(session, id); return db.query.goals.findMany({
where: and(
eq(goals.tenantId, session.tenantId),
isNull(goals.deletedAt),
inArray(goals.status, ['at_risk', 'not_started']),
),
orderBy: [desc(goals.createdAt)],
limit: 20,
});
} }
@Post() @Post()
create(@CurrentSession() session: AuthenticatedSession, @Body() dto: CreateGoalDto) { async create(
return this.goalsService.create(session, dto); @CurrentSession() session: AuthenticatedSession,
@Body() dto: CreateGoalDto,
) {
const [created] = await db
.insert(goals)
.values({
tenantId: session.tenantId,
workspaceId: dto.workspaceId ?? null,
ownerUserId: session.userId,
horizon: dto.horizon,
metric: dto.metric,
target: dto.target,
})
.returning();
return created;
} }
@Patch(':id') @Patch(':id')
update( async update(
@CurrentSession() session: AuthenticatedSession, @CurrentSession() session: AuthenticatedSession,
@Param('id', ParseUUIDPipe) id: string, @Param('id') id: string,
@Body() dto: UpdateGoalDto, @Body() dto: UpdateGoalDto,
) { ) {
return this.goalsService.update(session, id, dto); const [updated] = await db
} .update(goals)
.set({
@Delete(':id') ...(dto.status !== undefined ? { status: dto.status as any } : {}),
softDelete(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) { ...(dto.milestones !== undefined ? { milestones: dto.milestones } : {}),
return this.goalsService.softDelete(session, id); ...(dto.target !== undefined ? { target: dto.target } : {}),
updatedAt: new Date(),
version: db.$count(goals, eq(goals.id, id)),
})
.where(and(eq(goals.id, id), eq(goals.tenantId, session.tenantId)))
.returning();
return updated;
} }
} }