import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { useEffect, useMemo, useState } from "react"; import { ProtectedLayout } from "@/components/protected-layout"; import { PageContainer } from "@/components/app-shell"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/lib/auth"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { CollectionDetail } from "@/components/cases/collections-tab"; import { PaymentPlansPanel } from "@/components/collections/payment-plans-panel"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ContactsLinkTab } from "@/components/contacts/contacts-link-tab"; import { CaseCallLogsTab } from "@/components/cases/call-logs-tab"; import { CaseDocumentsTab } from "@/components/cases/documents-tab"; import { CaseTimeTab } from "@/components/cases/time-tab"; import { CaseExpensesTab } from "@/components/cases/expenses-tab"; import { CaseStatusTab } from "@/components/cases/status-tab"; import { CaseInvoicesTab } from "@/components/cases/invoices-tab"; import { CaseLitigationTab } from "@/components/cases/litigation-tab"; import { CaseCustomFieldsTab } from "@/components/cases/custom-fields-tab"; import { formatDate } from "@/lib/format"; import { ArrowLeft, ArrowRight, CheckCircle2, Circle, ListChecks, Loader2, Plus, Trash2, Workflow, Scale, Activity, FileText, Contact, Clock, DollarSign, Receipt, Phone, Tag, } from "lucide-react"; import { toast } from "sonner"; export const Route = createFileRoute("/collections/$collectionId")({ component: CollectionDetailRoute, }); interface Stage { key: string; label: string; sort_order: number; } interface Profile { id: string; full_name: string; email: string; } interface Task { id: string; collection_id: string; stage_key: string | null; title: string; due_date: string | null; assignee_id: string | null; done: boolean; done_at: string | null; sort_order: number; } function CollectionDetailRoute() { const { collectionId } = Route.useParams(); const navigate = useNavigate(); const { user } = useAuth(); const [collection, setCollection] = useState(null); const [stages, setStages] = useState([]); const [profiles, setProfiles] = useState([]); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [escalating, setEscalating] = useState(false); const [addTaskOpen, setAddTaskOpen] = useState(false); const [reload, setReload] = useState(0); useEffect(() => { let active = true; (async () => { setLoading(true); const [{ data: col }, { data: stgs }, { data: profs }] = await Promise.all([ supabase .from("collections") .select( "*, homeowner:homeowners(*), case:cases(id, case_number, title, default_hourly_rate, client:clients(id, name, annual_interest_rate))", ) .eq("id", collectionId) .maybeSingle(), supabase.from("collection_workflow_stages").select("*").order("sort_order"), supabase.from("profiles").select("id, full_name, email"), ]); if (!active) return; setCollection(col); setStages((stgs ?? []) as Stage[]); setProfiles((profs ?? []) as Profile[]); const { data: tsks } = await supabase .from("collection_tasks") .select("*") .eq("collection_id", collectionId) .order("done") .order("sort_order") .order("created_at"); if (!active) return; setTasks((tsks ?? []) as Task[]); setLoading(false); })(); return () => { active = false; }; }, [collectionId, reload]); const refreshTasks = async () => { const { data } = await supabase .from("collection_tasks") .select("*") .eq("collection_id", collectionId) .order("done") .order("sort_order") .order("created_at"); setTasks((data ?? []) as Task[]); }; const currentStageIdx = useMemo( () => stages.findIndex((s) => s.key === collection?.current_stage), [stages, collection?.current_stage], ); const nextStage = currentStageIdx >= 0 ? stages[currentStageIdx + 1] : null; const isLastStage = currentStageIdx === stages.length - 1; const escalate = async () => { if (!collection || !nextStage) return; if ( !confirm( `Escalate to "${nextStage.label}"? This will populate the checklist tasks for that stage.`, ) ) return; setEscalating(true); // Update stage const { error: updErr } = await supabase .from("collections") .update({ current_stage: nextStage.key }) .eq("id", collection.id); if (updErr) { setEscalating(false); toast.error("Could not escalate", { description: updErr.message }); return; } // Pull templates for the new stage const { data: templates } = await supabase .from("collection_workflow_tasks") .select("*") .eq("stage_key", nextStage.key) .order("sort_order"); if (templates && templates.length) { const today = new Date(); const inserts = templates.map((t: any) => { const due = new Date(today); due.setDate(due.getDate() + (t.days_to_due ?? 7)); return { collection_id: collection.id, stage_key: nextStage.key, title: t.title, due_date: due.toISOString().slice(0, 10), sort_order: t.sort_order, created_by: user?.id, }; }); const { error: insErr } = await supabase .from("collection_tasks") .insert(inserts); if (insErr) { toast.error("Stage advanced but task creation failed", { description: insErr.message, }); } } setEscalating(false); toast.success(`Escalated to ${nextStage.label}`); setReload((r) => r + 1); }; const toggleDone = async (t: Task) => { const next = !t.done; const { error } = await supabase .from("collection_tasks") .update({ done: next, done_at: next ? new Date().toISOString() : null, }) .eq("id", t.id); if (error) toast.error("Could not update", { description: error.message }); else refreshTasks(); }; const deleteTask = async (id: string) => { if (!confirm("Delete this task?")) return; const { error } = await supabase .from("collection_tasks") .delete() .eq("id", id); if (error) toast.error("Could not delete", { description: error.message }); else refreshTasks(); }; const updateAssignee = async (id: string, assignee_id: string | null) => { const { error } = await supabase .from("collection_tasks") .update({ assignee_id }) .eq("id", id); if (error) toast.error("Could not update", { description: error.message }); else refreshTasks(); }; const updateDueDate = async (id: string, due_date: string) => { const { error } = await supabase .from("collection_tasks") .update({ due_date: due_date || null }) .eq("id", id); if (error) toast.error("Could not update", { description: error.message }); else refreshTasks(); }; if (loading) { return (
Loading…
); } if (!collection) { return (
Collection not found.{" "} Back
); } const annualRate = collection.case?.client?.annual_interest_rate ?? null; const balanceFromOpening = Number(collection.homeowner?.opening_balance ?? 0); const currentStageLabel = stages.find((s) => s.key === collection.current_stage)?.label ?? "—"; const openTasks = tasks.filter((t) => !t.done); const doneTasks = tasks.filter((t) => t.done); return ( {/* Tabs: Workflow (collections-specific) + same case tabs */} Workflow {collection.case && ( <> Litigation Status log Documents Contacts Time Expenses Invoices Calls Custom fields )} {/* Workflow header */}
Workflow stage
{stages.map((s, i) => { const isCurrent = s.key === collection.current_stage; const isPast = i < currentStageIdx; return (
{s.label} {i < stages.length - 1 && ( )}
); })}
{/* Tasks */}

Tasks {openTasks.length > 0 && ( {openTasks.length} open )}

{tasks.length === 0 ? (

No tasks yet. Escalate to populate the next stage's checklist, or add one manually.

) : (
{[...openTasks, ...doneTasks].map((t) => { const stageLabel = stages.find((s) => s.key === t.stage_key)?.label; const overdue = !t.done && t.due_date && new Date(t.due_date) < new Date(new Date().toDateString()); return (
{t.title}
{stageLabel && (
{stageLabel}
)}
updateDueDate(t.id, e.target.value)} />
); })}
)}
{/* Payment plans */} {/* Ledger */} navigate({ to: "/collections" })} onChange={() => setReload((r) => r + 1)} />
{collection.case && ( <> setReload((r) => r + 1)} /> {}} /> )}
); } function AddTaskDialog({ open, onOpenChange, collectionId, currentStageKey, profiles, userId, onSaved, }: { open: boolean; onOpenChange: (v: boolean) => void; collectionId: string; currentStageKey: string | null; profiles: Profile[]; userId?: string; onSaved: () => void; }) { const [title, setTitle] = useState(""); const [dueDate, setDueDate] = useState(() => { const d = new Date(); d.setDate(d.getDate() + 7); return d.toISOString().slice(0, 10); }); const [assignee, setAssignee] = useState("unassigned"); const [submitting, setSubmitting] = useState(false); useEffect(() => { if (open) { setTitle(""); const d = new Date(); d.setDate(d.getDate() + 7); setDueDate(d.toISOString().slice(0, 10)); setAssignee("unassigned"); } }, [open]); const submit = async () => { if (!title.trim()) return toast.error("Title required"); setSubmitting(true); const { error } = await supabase.from("collection_tasks").insert({ collection_id: collectionId, stage_key: currentStageKey, title: title.trim(), due_date: dueDate || null, assignee_id: assignee === "unassigned" ? null : assignee, created_by: userId, }); setSubmitting(false); if (error) { toast.error("Could not add", { description: error.message }); return; } toast.success("Task added"); onOpenChange(false); onSaved(); }; return ( Add task Manual task on this collection. Will be tagged with the current stage.
setTitle(e.target.value)} placeholder="e.g. Call homeowner re: payment plan" />
setDueDate(e.target.value)} />
); }