import { and, asc, eq, isNull, sql } from 'drizzle-orm'; import { db } from '$lib/server/db/client'; import { projects, subtasks, tasks, workPackages } from '$lib/server/db/schema/projects'; import { notify } from './notifications'; export type TaskStatus = 'todo' | 'doing' | 'done' | 'blocked'; async function assertWorkPackage(companyId: string, wpId: string): Promise { const [row] = await db .select({ id: workPackages.id }) .from(workPackages) .innerJoin(projects, eq(projects.id, workPackages.projectId)) .where( and( eq(workPackages.id, wpId), eq(projects.companyId, companyId), isNull(workPackages.deletedAt) ) ) .limit(1); if (!row) throw new Error('work package not found'); } export async function listTasksForWorkPackage(companyId: string, wpId: string) { await assertWorkPackage(companyId, wpId); return db .select() .from(tasks) .where(and(eq(tasks.workPackageId, wpId), isNull(tasks.deletedAt))) .orderBy(asc(tasks.order), asc(tasks.createdAt)); } export async function createTask(input: { companyId: string; createdBy: string; workPackageId: string; title: string; description?: string | null; assigneeId?: string | null; dueAt?: Date | null; }): Promise<{ id: string }> { await assertWorkPackage(input.companyId, input.workPackageId); const [{ next }] = await db .select({ next: sql`coalesce(max(${tasks.order}), -1) + 1` }) .from(tasks) .where(eq(tasks.workPackageId, input.workPackageId)); const [row] = await db .insert(tasks) .values({ workPackageId: input.workPackageId, title: input.title.trim(), description: input.description ?? null, assigneeId: input.assigneeId ?? null, dueAt: input.dueAt ?? null, order: next ?? 0, createdBy: input.createdBy }) .returning({ id: tasks.id }); return row; } export async function getTaskWithSubtasks(companyId: string, id: string) { const [t] = await db .select({ task: tasks, projectId: workPackages.projectId, wpName: workPackages.name }) .from(tasks) .innerJoin(workPackages, eq(workPackages.id, tasks.workPackageId)) .innerJoin(projects, eq(projects.id, workPackages.projectId)) .where(and(eq(tasks.id, id), eq(projects.companyId, companyId), isNull(tasks.deletedAt))) .limit(1); if (!t) return null; const subs = await db .select() .from(subtasks) .where(eq(subtasks.taskId, id)) .orderBy(asc(subtasks.order), asc(subtasks.createdAt)); return { task: t.task, projectId: t.projectId, workPackageName: t.wpName, subtasks: subs }; } export async function updateTask( companyId: string, id: string, patch: { title?: string; description?: string | null; status?: TaskStatus; assigneeId?: string | null; dueAt?: Date | null; } ): Promise { const t = await getTaskWithSubtasks(companyId, id); if (!t) throw new Error('task not found'); const update: Partial = {}; if (patch.title !== undefined) update.title = patch.title.trim(); if (patch.description !== undefined) update.description = patch.description; if (patch.assigneeId !== undefined) update.assigneeId = patch.assigneeId; if (patch.dueAt !== undefined) update.dueAt = patch.dueAt; if (patch.status !== undefined) { update.status = patch.status; if (patch.status === 'done' && !t.task.completedAt) update.completedAt = new Date(); if (patch.status !== 'done' && t.task.completedAt) update.completedAt = null; } await db.update(tasks).set(update).where(eq(tasks.id, id)); // Fire task_assigned when assignee actually changes to a real user. const newAssignee = patch.assigneeId ?? null; if ( patch.assigneeId !== undefined && newAssignee && newAssignee !== t.task.assigneeId ) { const title = patch.title ?? t.task.title; void notify({ companyId, userIds: [newAssignee], kind: 'task_assigned', title: `Task assigned: ${title}`, body: t.task.description?.slice(0, 500) ?? 'You were assigned this task.', link: `/projects/${t.projectId}/work/${t.task.workPackageId}/${id}` }); } } export async function softDeleteTask(companyId: string, id: string): Promise { const t = await getTaskWithSubtasks(companyId, id); if (!t) throw new Error('task not found'); await db .update(tasks) .set({ deletedAt: sql`now()` }) .where(eq(tasks.id, id)); } // --- subtasks --------------------------------------------------------------- export async function addSubtask(companyId: string, taskId: string, name: string): Promise { const t = await getTaskWithSubtasks(companyId, taskId); if (!t) throw new Error('task not found'); const order = t.subtasks.length; await db.insert(subtasks).values({ taskId, name: name.trim(), order }); } export async function toggleSubtask( companyId: string, taskId: string, subtaskId: string, done: boolean ): Promise { const t = await getTaskWithSubtasks(companyId, taskId); if (!t) throw new Error('task not found'); await db .update(subtasks) .set({ done }) .where(and(eq(subtasks.id, subtaskId), eq(subtasks.taskId, taskId))); } export async function removeSubtask( companyId: string, taskId: string, subtaskId: string ): Promise { const t = await getTaskWithSubtasks(companyId, taskId); if (!t) throw new Error('task not found'); await db .delete(subtasks) .where(and(eq(subtasks.id, subtaskId), eq(subtasks.taskId, taskId))); }