import { and, asc, eq, max } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; import db from "../../database"; import { assetTable, columnTable, projectTable, taskTable, } from "../../database/schema"; import { publishEvent } from "../../events"; import getNextTaskNumber from "./get-next-task-number"; type DbOrTx = typeof db | Parameters[0]>[0]; function isSameProjectMove( sourceProjectId: string, destinationProjectId: string, ) { return sourceProjectId === destinationProjectId; } async function resolveDestinationStatus( destinationProjectId: string, currentStatus: string, requestedStatus?: string, ) { const destinationColumns = await db .select({ id: columnTable.id, slug: columnTable.slug, position: columnTable.position, }) .from(columnTable) .where(eq(columnTable.projectId, destinationProjectId)) .orderBy(asc(columnTable.position)); if (destinationColumns.length === 0) { throw new HTTPException(400, { message: "Destination project does not have a workflow", }); } const requestedColumn = requestedStatus ? destinationColumns.find((column) => column.slug === requestedStatus) : null; if (requestedStatus && !requestedColumn) { throw new HTTPException(400, { message: "Selected status is not valid for the destination project", }); } const matchingCurrentColumn = destinationColumns.find( (column) => column.slug === currentStatus, ); return requestedColumn ?? matchingCurrentColumn ?? destinationColumns[0]; } async function getNextTaskPosition( dbOrTx: DbOrTx, projectId: string, status: string, columnId: string, ) { const [maxPositionResult] = await dbOrTx .select({ maxPosition: max(taskTable.position) }) .from(taskTable) .where( and( eq(taskTable.projectId, projectId), eq(taskTable.status, status), eq(taskTable.columnId, columnId), ), ); return (maxPositionResult?.maxPosition ?? 0) + 1; } async function moveTask({ taskId, destinationProjectId, destinationStatus, userId, }: { taskId: string; destinationProjectId: string; destinationStatus?: string; userId: string; }) { const existingTask = await db.query.taskTable.findFirst({ where: eq(taskTable.id, taskId), }); if (!existingTask) { throw new HTTPException(404, { message: "Task not found", }); } if (isSameProjectMove(existingTask.projectId, destinationProjectId)) { throw new HTTPException(400, { message: "Task is already in that project", }); } const [sourceProject, destinationProject] = await Promise.all([ db.query.projectTable.findFirst({ where: eq(projectTable.id, existingTask.projectId), }), db.query.projectTable.findFirst({ where: eq(projectTable.id, destinationProjectId), }), ]); if (!sourceProject || !destinationProject) { throw new HTTPException(404, { message: "Project not found", }); } if (sourceProject.workspaceId !== destinationProject.workspaceId) { throw new HTTPException(400, { message: "Tasks can only be moved within the same workspace", }); } const resolvedColumn = await resolveDestinationStatus( destinationProjectId, existingTask.status, destinationStatus, ); const movedTask = await db.transaction(async (tx) => { const [nextTaskNumber, nextPosition] = await Promise.all([ getNextTaskNumber(destinationProjectId, tx), getNextTaskPosition( tx, destinationProjectId, resolvedColumn.slug, resolvedColumn.id, ), ]); const [updatedTask] = await tx .update(taskTable) .set({ projectId: destinationProjectId, status: resolvedColumn.slug, columnId: resolvedColumn.id, number: nextTaskNumber + 1, position: nextPosition, }) .where(eq(taskTable.id, taskId)) .returning(); if (!updatedTask) { throw new HTTPException(500, { message: "Failed to move task", }); } await tx .update(assetTable) .set({ projectId: destinationProjectId }) .where(eq(assetTable.taskId, taskId)); return updatedTask; }); await publishEvent("task.moved", { taskId, type: "task", userId, content: `Moved task from ${sourceProject.name} to ${destinationProject.name}`, eventData: { fromProjectId: sourceProject.id, fromProjectName: sourceProject.name, toProjectId: destinationProject.id, toProjectName: destinationProject.name, oldStatus: existingTask.status, newStatus: resolvedColumn.slug, }, }); return { task: movedTask, sourceProjectId: sourceProject.id, destinationProjectId: destinationProject.id, }; } export default moveTask;