diff --git a/src/tasks/tasks.controller.ts b/src/tasks/tasks.controller.ts index 9dfe4e0..c146cc9 100644 --- a/src/tasks/tasks.controller.ts +++ b/src/tasks/tasks.controller.ts @@ -1,43 +1,106 @@ -import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common'; +import { Body, Controller, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { IsNumber, IsOptional, IsString } from 'class-validator'; +import { and, asc, desc, eq, isNull, inArray, lt, gte } from 'drizzle-orm'; import { CurrentSession } from '../auth/session.decorator'; import type { AuthenticatedSession } from '../auth/tenant.guard'; -import { CreateTaskDto, TASK_STATUSES, UpdateTaskDto, type TaskStatusValue } from './dto'; -import { TasksService } from './tasks.service'; -import { BadRequestException } from '@nestjs/common'; +import { db } from '../db/client'; +import { tasks } from '../db/schema'; + +class CreateTaskDto { + @IsString() title: string; + @IsOptional() @IsNumber() priority?: number; + @IsOptional() @IsString() dueAt?: string; + @IsOptional() @IsString() workspaceId?: string; + @IsOptional() @IsString() sourceEntityType?: string; + @IsOptional() @IsString() sourceEntityId?: string; +} + +class UpdateTaskDto { + @IsOptional() @IsString() status?: string; + @IsOptional() @IsNumber() priority?: number; + @IsOptional() @IsString() dueAt?: string; + @IsOptional() @IsString() title?: string; +} + +const ACTIVE_STATUSES = ['open', 'in_progress', 'blocked'] as const; @Controller('tasks') export class TasksController { - constructor(private readonly tasksService: TasksService) {} - @Get() - list(@CurrentSession() session: AuthenticatedSession, @Query('status') status?: string) { - if (status && !TASK_STATUSES.includes(status as TaskStatusValue)) { - throw new BadRequestException(`status must be one of: ${TASK_STATUSES.join(', ')}`); - } - return this.tasksService.list(session, status as TaskStatusValue | undefined); + async list( + @CurrentSession() session: AuthenticatedSession, + @Query('status') status?: string, + @Query('limit') limitStr?: string, + ) { + const limit = Math.min(Math.max(1, parseInt(limitStr ?? '100', 10) || 100), 500); + const statuses = status + ? [status] + : [...ACTIVE_STATUSES]; + + return db.query.tasks.findMany({ + where: and( + eq(tasks.tenantId, session.tenantId), + isNull(tasks.deletedAt), + inArray(tasks.status, statuses as any[]), + ), + orderBy: [asc(tasks.priority), asc(tasks.dueAt), desc(tasks.createdAt)], + limit, + }); } - @Get(':id') - getById(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) { - return this.tasksService.getById(session, id); + @Get('overdue') + async overdue(@CurrentSession() session: AuthenticatedSession) { + const now = new Date(); + return db.query.tasks.findMany({ + where: and( + eq(tasks.tenantId, session.tenantId), + isNull(tasks.deletedAt), + inArray(tasks.status, [...ACTIVE_STATUSES] as any[]), + lt(tasks.dueAt, now), + ), + orderBy: [asc(tasks.dueAt)], + limit: 50, + }); } @Post() - create(@CurrentSession() session: AuthenticatedSession, @Body() dto: CreateTaskDto) { - return this.tasksService.create(session, dto); + async create( + @CurrentSession() session: AuthenticatedSession, + @Body() dto: CreateTaskDto, + ) { + const [created] = await db + .insert(tasks) + .values({ + tenantId: session.tenantId, + workspaceId: dto.workspaceId ?? null, + ownerUserId: session.userId, + title: dto.title, + priority: dto.priority ?? 3, + dueAt: dto.dueAt ? new Date(dto.dueAt) : null, + sourceEntityType: dto.sourceEntityType ?? null, + sourceEntityId: dto.sourceEntityId ? dto.sourceEntityId as any : null, + }) + .returning(); + return created; } @Patch(':id') - update( + async update( @CurrentSession() session: AuthenticatedSession, - @Param('id', ParseUUIDPipe) id: string, + @Param('id') id: string, @Body() dto: UpdateTaskDto, ) { - return this.tasksService.update(session, id, dto); - } - - @Delete(':id') - softDelete(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) { - return this.tasksService.softDelete(session, id); + const [updated] = await db + .update(tasks) + .set({ + ...(dto.status !== undefined ? { status: dto.status as any } : {}), + ...(dto.priority !== undefined ? { priority: dto.priority } : {}), + ...(dto.dueAt !== undefined ? { dueAt: dto.dueAt ? new Date(dto.dueAt) : null } : {}), + ...(dto.title !== undefined ? { title: dto.title } : {}), + updatedAt: new Date(), + }) + .where(and(eq(tasks.id, id), eq(tasks.tenantId, session.tenantId))) + .returning(); + return updated; } }