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 type { AuthenticatedSession } from '../auth/tenant.guard';
import { CreateGoalDto, GOAL_STATUSES, UpdateGoalDto, type GoalStatusValue } from './dto';
import { GoalsService } from './goals.service';
import { db } from '../db/client';
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')
export class GoalsController {
constructor(private readonly goalsService: GoalsService) {}
@Get()
list(@CurrentSession() session: AuthenticatedSession, @Query('status') status?: string) {
if (status && !GOAL_STATUSES.includes(status as GoalStatusValue)) {
throw new BadRequestException(`status must be one of: ${GOAL_STATUSES.join(', ')}`);
}
return this.goalsService.list(session, status as GoalStatusValue | undefined);
async list(@CurrentSession() session: AuthenticatedSession) {
return db.query.goals.findMany({
where: and(eq(goals.tenantId, session.tenantId), isNull(goals.deletedAt)),
orderBy: [desc(goals.createdAt)],
limit: 100,
});
}
@Get(':id')
getById(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) {
return this.goalsService.getById(session, id);
@Get('at-risk')
async atRisk(@CurrentSession() session: AuthenticatedSession) {
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()
create(@CurrentSession() session: AuthenticatedSession, @Body() dto: CreateGoalDto) {
return this.goalsService.create(session, dto);
async create(
@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')
update(
async update(
@CurrentSession() session: AuthenticatedSession,
@Param('id', ParseUUIDPipe) id: string,
@Param('id') id: string,
@Body() dto: UpdateGoalDto,
) {
return this.goalsService.update(session, id, dto);
}
@Delete(':id')
softDelete(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) {
return this.goalsService.softDelete(session, id);
const [updated] = await db
.update(goals)
.set({
...(dto.status !== undefined ? { status: dto.status as any } : {}),
...(dto.milestones !== undefined ? { milestones: dto.milestones } : {}),
...(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;
}
}