176 lines
4.9 KiB
TypeScript
176 lines
4.9 KiB
TypeScript
import { Router, Request, Response } from 'express';
|
|
import { asyncHandler } from '../utils/asyncHandler';
|
|
import { db } from '../db';
|
|
import { subtasks, tasks } from '../db/schema';
|
|
import { eq, and, asc, desc, isNull } from 'drizzle-orm';
|
|
import { authMiddleware } from '../utils/auth';
|
|
import { AppError } from '../middleware/errorHandler';
|
|
import { subtaskCreateSchema, subtaskUpdateSchema } from '../utils/validation';
|
|
|
|
const router = Router();
|
|
|
|
router.use(authMiddleware);
|
|
|
|
// Helper to build nested subtask tree
|
|
const buildSubtaskTree = (allSubtasks: any[], parentId: string | null = null): any[] => {
|
|
return allSubtasks
|
|
.filter((s) => s.parentSubtaskId === parentId)
|
|
.sort((a, b) => a.order - b.order)
|
|
.map((s) => ({
|
|
...s,
|
|
subtasks: buildSubtaskTree(allSubtasks, s.id),
|
|
}));
|
|
};
|
|
|
|
router.get('/task/:taskId', asyncHandler(async (req: Request, res: Response) => {
|
|
const userId = req.user!.userId;
|
|
|
|
// Verify task exists and belongs to user
|
|
const task = await db
|
|
.select()
|
|
.from(tasks)
|
|
.where(and(eq(tasks.id, req.params.taskId), eq(tasks.userId, userId)))
|
|
.limit(1);
|
|
|
|
if (task.length === 0) {
|
|
throw new AppError('NOT_FOUND', 'Task not found', 404);
|
|
}
|
|
|
|
const taskSubtasks = await db
|
|
.select()
|
|
.from(subtasks)
|
|
.where(and(eq(subtasks.taskId, req.params.taskId), eq(subtasks.userId, userId)))
|
|
.orderBy(asc(subtasks.order));
|
|
|
|
const nestedSubtasks = buildSubtaskTree(taskSubtasks);
|
|
|
|
res.json({ subtasks: nestedSubtasks });
|
|
}));
|
|
|
|
router.post('/task/:taskId', asyncHandler(async (req: Request, res: Response) => {
|
|
const data = subtaskCreateSchema.parse(req.body);
|
|
const userId = req.user!.userId;
|
|
|
|
const task = await db
|
|
.select()
|
|
.from(tasks)
|
|
.where(and(eq(tasks.id, req.params.taskId), eq(tasks.userId, userId)))
|
|
.limit(1);
|
|
|
|
if (task.length === 0) {
|
|
throw new AppError('NOT_FOUND', 'Task not found', 404);
|
|
}
|
|
|
|
const now = Date.now();
|
|
const parentSubtaskId = data.parentSubtaskId || null;
|
|
|
|
const maxOrder = await db
|
|
.select({ order: subtasks.order })
|
|
.from(subtasks)
|
|
.where(and(
|
|
eq(subtasks.taskId, req.params.taskId),
|
|
eq(subtasks.userId, userId),
|
|
parentSubtaskId ? eq(subtasks.parentSubtaskId, parentSubtaskId) : isNull(subtasks.parentSubtaskId)
|
|
))
|
|
.orderBy(desc(subtasks.order))
|
|
.limit(1);
|
|
|
|
const subtaskId = `sub_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
|
|
const newSubtask = {
|
|
id: subtaskId,
|
|
userId,
|
|
taskId: req.params.taskId,
|
|
parentSubtaskId,
|
|
categoryId: data.categoryId ?? null,
|
|
tags: data.tags ?? '',
|
|
title: data.title,
|
|
description: data.description ?? '',
|
|
priority: data.priority ?? 'none',
|
|
completed: false,
|
|
dueDate: data.dueDate ?? 0,
|
|
dueTime: data.dueTime ?? '',
|
|
endTime: data.endTime ?? '',
|
|
allDay: data.allDay ?? false,
|
|
repeat: data.repeat ?? 'none',
|
|
repeatInterval: data.repeatInterval ?? 1,
|
|
repeatDays: data.repeatDays ?? '',
|
|
seriesId: data.seriesId ?? '',
|
|
reminder: data.reminder ?? 'none',
|
|
reminders: data.reminders ?? '',
|
|
assigneeId: data.assigneeId ?? null,
|
|
order: data.order ?? (maxOrder[0]?.order ?? -1) + 1,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
|
|
await db.insert(subtasks).values(newSubtask);
|
|
|
|
// Update task updatedAt
|
|
await db
|
|
.update(tasks)
|
|
.set({ updatedAt: now })
|
|
.where(and(eq(tasks.id, req.params.taskId), eq(tasks.userId, userId)));
|
|
|
|
res.status(201).json(newSubtask);
|
|
}));
|
|
|
|
router.patch('/:id', asyncHandler(async (req: Request, res: Response) => {
|
|
const data = subtaskUpdateSchema.parse(req.body);
|
|
const userId = req.user!.userId;
|
|
|
|
const existing: any[] = await db
|
|
.select()
|
|
.from(subtasks)
|
|
.where(and(eq(subtasks.id, req.params.id), eq(subtasks.userId, userId)))
|
|
.limit(1);
|
|
|
|
if (existing.length === 0) {
|
|
throw new AppError('NOT_FOUND', 'Subtask not found', 404);
|
|
}
|
|
|
|
const now = Date.now();
|
|
const updated = await db
|
|
.update(subtasks)
|
|
.set({ ...data, updatedAt: now })
|
|
.where(and(eq(subtasks.id, req.params.id), eq(subtasks.userId, userId)))
|
|
.returning();
|
|
|
|
// Update task updatedAt
|
|
await db
|
|
.update(tasks)
|
|
.set({ updatedAt: now })
|
|
.where(and(eq(tasks.id, existing[0].taskId), eq(tasks.userId, userId)));
|
|
|
|
res.json(updated[0]);
|
|
}));
|
|
|
|
router.delete('/:id', asyncHandler(async (req: Request, res: Response) => {
|
|
const userId = req.user!.userId;
|
|
|
|
const existing = await db
|
|
.select()
|
|
.from(subtasks)
|
|
.where(and(eq(subtasks.id, req.params.id), eq(subtasks.userId, userId)))
|
|
.limit(1);
|
|
|
|
if (existing.length === 0) {
|
|
throw new AppError('NOT_FOUND', 'Subtask not found', 404);
|
|
}
|
|
|
|
const taskId = existing[0].taskId;
|
|
|
|
await db
|
|
.delete(subtasks)
|
|
.where(and(eq(subtasks.id, req.params.id), eq(subtasks.userId, userId)));
|
|
|
|
// Update task updatedAt
|
|
await db
|
|
.update(tasks)
|
|
.set({ updatedAt: Date.now() })
|
|
.where(and(eq(tasks.id, taskId), eq(tasks.userId, userId)));
|
|
|
|
res.status(204).send();
|
|
}));
|
|
|
|
export default router; |