From 2aafeb6d6be37b79916a037aff3f9f969ceca12e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 23:42:51 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/lib/workflow-chain.ts | 151 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/lib/workflow-chain.ts diff --git a/src/lib/workflow-chain.ts b/src/lib/workflow-chain.ts new file mode 100644 index 0000000..660dedd --- /dev/null +++ b/src/lib/workflow-chain.ts @@ -0,0 +1,151 @@ +import { supabase } from "@/integrations/supabase/client"; +import { addDays, format } from "date-fns"; + +/** + * After a task is marked complete, spawn the next task from its workflow + * template (if any). Sequential mode: only one task at a time per chain. + * + * Returns the new task id, or null if nothing was spawned. + */ +export async function spawnNextWorkflowTask(taskId: string): Promise { + // Pull the just-completed task's workflow context + const { data: task } = await supabase + .from("tasks") + .select("id, case_id, workflow_template_id, template_task_id, created_by, completed_at") + .eq("id", taskId) + .maybeSingle(); + + if (!task?.workflow_template_id || !task.template_task_id) return null; + + // Find current template task to know its sort_order + const { data: currentTpl } = await supabase + .from("workflow_template_tasks") + .select("id, sort_order") + .eq("id", task.template_task_id) + .maybeSingle(); + if (!currentTpl) return null; + + // Find the next template task (smallest sort_order greater than current) + const { data: nextTpl } = await supabase + .from("workflow_template_tasks") + .select("*") + .eq("template_id", task.workflow_template_id) + .gt("sort_order", currentTpl.sort_order) + .order("sort_order", { ascending: true }) + .limit(1) + .maybeSingle(); + + if (!nextTpl) return null; + + // Don't double-spawn: skip if there's already a task in this chain for that template_task_id + const { data: existing } = await supabase + .from("tasks") + .select("id") + .eq("workflow_template_id", task.workflow_template_id) + .eq("template_task_id", nextTpl.id) + .eq("case_id", task.case_id ?? "") + .maybeSingle(); + if (existing) return existing.id; + + const baseDate = task.completed_at ? new Date(task.completed_at) : new Date(); + const dueDate = format(addDays(baseDate, nextTpl.days_from_start ?? 0), "yyyy-MM-dd"); + + const { data: created, error } = await supabase + .from("tasks") + .insert({ + title: nextTpl.title, + description: nextTpl.description, + priority: nextTpl.priority, + due_date: dueDate, + case_id: task.case_id, + workflow_template_id: task.workflow_template_id, + template_task_id: nextTpl.id, + created_by: task.created_by, + sort_order: nextTpl.sort_order, + }) + .select("id") + .single(); + + if (error || !created) return null; + + if (nextTpl.default_assignee_id) { + await supabase.from("task_assignees").insert({ + task_id: created.id, + user_id: nextTpl.default_assignee_id, + assigned_by: task.created_by, + }); + } + await supabase.from("task_history").insert({ + task_id: created.id, + actor_id: task.created_by, + event: "spawned_from_workflow", + detail: { template_id: task.workflow_template_id, after_task: taskId }, + }); + + return created.id; +} + +/** + * Same idea for collection_tasks (collections workflow chain). + */ +export async function spawnNextCollectionWorkflowTask( + collectionTaskId: string, +): Promise { + const { data: ct } = await supabase + .from("collection_tasks") + .select( + "id, collection_id, workflow_template_id, template_task_id, created_by, done_at, assignee_id", + ) + .eq("id", collectionTaskId) + .maybeSingle(); + + if (!ct?.workflow_template_id || !ct.template_task_id) return null; + + const { data: currentTpl } = await supabase + .from("workflow_template_tasks") + .select("id, sort_order") + .eq("id", ct.template_task_id) + .maybeSingle(); + if (!currentTpl) return null; + + const { data: nextTpl } = await supabase + .from("workflow_template_tasks") + .select("*") + .eq("template_id", ct.workflow_template_id) + .gt("sort_order", currentTpl.sort_order) + .order("sort_order", { ascending: true }) + .limit(1) + .maybeSingle(); + + if (!nextTpl) return null; + + const { data: existing } = await supabase + .from("collection_tasks") + .select("id") + .eq("collection_id", ct.collection_id) + .eq("workflow_template_id", ct.workflow_template_id) + .eq("template_task_id", nextTpl.id) + .maybeSingle(); + if (existing) return existing.id; + + const baseDate = ct.done_at ? new Date(ct.done_at) : new Date(); + const dueDate = format(addDays(baseDate, nextTpl.days_from_start ?? 0), "yyyy-MM-dd"); + + const { data: created, error } = await supabase + .from("collection_tasks") + .insert({ + collection_id: ct.collection_id, + title: nextTpl.title, + due_date: dueDate, + sort_order: nextTpl.sort_order, + assignee_id: nextTpl.default_assignee_id ?? null, + workflow_template_id: ct.workflow_template_id, + template_task_id: nextTpl.id, + created_by: ct.created_by, + }) + .select("id") + .single(); + + if (error || !created) return null; + return created.id; +}