diff --git a/src/components/cases/tasks-tab.tsx b/src/components/cases/tasks-tab.tsx index b01488a..024aa9c 100644 --- a/src/components/cases/tasks-tab.tsx +++ b/src/components/cases/tasks-tab.tsx @@ -11,6 +11,7 @@ import { TaskDetailPanel } from "@/components/tasks/task-detail-panel"; import { NewTaskDialog } from "@/components/tasks/new-task-dialog"; import { ApplyWorkflowDialog } from "@/components/tasks/apply-workflow-dialog"; import { cn } from "@/lib/utils"; +import { spawnNextWorkflowTask } from "@/lib/workflow-chain"; interface TaskRow { id: string; @@ -95,6 +96,10 @@ export function CaseTasksTab({ caseId }: { caseId: string }) { completed_at: checked ? (new Date().toISOString() as any) : null, }) .eq("id", t.id); + if (checked) { + // Spawn the next task in the workflow chain (if any) + await spawnNextWorkflowTask(t.id); + } }; const today = startOfDay(new Date()); diff --git a/src/components/collections/apply-collection-workflow-dialog.tsx b/src/components/collections/apply-collection-workflow-dialog.tsx new file mode 100644 index 0000000..e725b46 --- /dev/null +++ b/src/components/collections/apply-collection-workflow-dialog.tsx @@ -0,0 +1,149 @@ +import { useState, useEffect } from "react"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "@/lib/auth"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Calendar } from "@/components/ui/calendar"; +import { CalendarIcon, Loader2 } from "lucide-react"; +import { format, addDays } from "date-fns"; +import { toast } from "sonner"; + +interface Template { + id: string; + name: string; +} + +export function ApplyCollectionWorkflowDialog({ + open, + onOpenChange, + collectionId, + onApplied, +}: { + open: boolean; + onOpenChange: (v: boolean) => void; + collectionId: string; + onApplied?: () => void; +}) { + const { user } = useAuth(); + const [templates, setTemplates] = useState([]); + const [templateId, setTemplateId] = useState(null); + const [startDate, setStartDate] = useState(new Date()); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + if (!open) return; + supabase + .from("workflow_templates") + .select("id, name") + .order("name") + .then(({ data }) => setTemplates((data ?? []) as Template[])); + setTemplateId(null); + setStartDate(new Date()); + }, [open]); + + const apply = async () => { + if (!user || !templateId) return; + setSubmitting(true); + const { data: tpl } = await supabase + .from("workflow_template_tasks") + .select("*") + .eq("template_id", templateId) + .order("sort_order"); + const items = tpl ?? []; + if (!items.length) { + setSubmitting(false); + toast.error("Template has no tasks."); + return; + } + const first = items[0]; + const { error } = await supabase.from("collection_tasks").insert({ + collection_id: collectionId, + title: first.title, + due_date: format(addDays(startDate, first.days_from_start), "yyyy-MM-dd"), + sort_order: 0, + assignee_id: first.default_assignee_id ?? null, + workflow_template_id: templateId, + template_task_id: first.id, + created_by: user.id, + }); + setSubmitting(false); + if (error) { + toast.error(error.message || "Failed to apply"); + return; + } + toast.success( + `Workflow started — ${items.length} step${items.length === 1 ? "" : "s"} will run sequentially.`, + ); + onApplied?.(); + onOpenChange(false); + }; + + return ( + + + + Apply workflow to this collection + +
+
+ + +
+
+ + + + + + + d && setStartDate(d)} /> + + +
+

+ Only the first task is created now. Each subsequent task is auto-created when the + previous one is marked complete, with a due date X days after completion. +

+
+ + + + +
+
+ ); +} diff --git a/src/components/tasks/apply-workflow-dialog.tsx b/src/components/tasks/apply-workflow-dialog.tsx index 5f39b3a..e55754a 100644 --- a/src/components/tasks/apply-workflow-dialog.tsx +++ b/src/components/tasks/apply-workflow-dialog.tsx @@ -71,54 +71,59 @@ export function ApplyWorkflowDialog({ toast.error("Template has no tasks."); return; } - const inserts = items.map((it: any, idx: number) => ({ - title: it.title, - description: it.description, - priority: it.priority, - due_date: format(addDays(startDate, it.days_from_start), "yyyy-MM-dd"), - case_id: caseId, - workflow_template_id: templateId, - created_by: user.id, - sort_order: idx, - })); - const { data: created, error } = await supabase.from("tasks").insert(inserts).select("id"); + // Sequential mode: only spawn the FIRST task. Subsequent tasks are + // auto-spawned when the previous one is marked complete. + const first = items[0]; + const { data: created, error } = await supabase + .from("tasks") + .insert({ + title: first.title, + description: first.description, + priority: first.priority, + due_date: format(addDays(startDate, first.days_from_start), "yyyy-MM-dd"), + case_id: caseId, + workflow_template_id: templateId, + template_task_id: first.id, + created_by: user.id, + sort_order: 0, + }) + .select("id") + .single(); if (error || !created) { setSubmitting(false); toast.error(error?.message || "Failed to apply"); return; } - // Insert default assignees + history - const assigneeRows: any[] = []; - const historyRows: any[] = []; - const notifs: any[] = []; - created.forEach((row, idx) => { - const def = items[idx].default_assignee_id; - if (def) { - assigneeRows.push({ task_id: row.id, user_id: def, assigned_by: user.id }); - if (def !== user.id) { - notifs.push({ - user_id: def, + if (first.default_assignee_id) { + await supabase.from("task_assignees").insert({ + task_id: created.id, + user_id: first.default_assignee_id, + assigned_by: user.id, + }); + if (first.default_assignee_id !== user.id) { + await createNotifications([ + { + user_id: first.default_assignee_id, kind: "task_assigned", - title: `Assigned: ${items[idx].title}`, - link: `/tasks?task=${row.id}`, - task_id: row.id, + title: `Assigned: ${first.title}`, + link: `/tasks?task=${created.id}`, + task_id: created.id, case_id: caseId, created_by: user.id, - }); - } + }, + ]); } - historyRows.push({ - task_id: row.id, - actor_id: user.id, - event: "created_from_workflow", - detail: { template_id: templateId }, - }); + } + await supabase.from("task_history").insert({ + task_id: created.id, + actor_id: user.id, + event: "created_from_workflow", + detail: { template_id: templateId, sequential: true, total_steps: items.length }, }); - if (assigneeRows.length) await supabase.from("task_assignees").insert(assigneeRows); - if (historyRows.length) await supabase.from("task_history").insert(historyRows); - if (notifs.length) await createNotifications(notifs); setSubmitting(false); - toast.success(`Created ${created.length} tasks`); + toast.success( + `Workflow started — ${items.length} step${items.length === 1 ? "" : "s"} will run sequentially.`, + ); onApplied?.(); onOpenChange(false); }; @@ -144,7 +149,7 @@ export function ApplyWorkflowDialog({
- + +
+ + +
{tasks.length === 0 ? (

@@ -517,6 +532,13 @@ function CollectionDetailRoute() { onSaved={refreshTasks} /> + + {collection.case?.client?.id && (

Workflow Templates

- Reusable task lists. Apply one to a case to spawn the tasks with computed due dates. + Sequential checklists. The first task is created when the workflow is applied; each next task spawns automatically when the previous one is marked complete, with its due date set X days after completion.