Added case tabs to collections
X-Lovable-Edit-ID: edt-874d97b5-717a-4f62-a634-03903e4e7421 Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
@@ -27,6 +27,16 @@ import {
|
||||
} 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,
|
||||
@@ -37,6 +47,16 @@ import {
|
||||
Loader2,
|
||||
Plus,
|
||||
Trash2,
|
||||
Workflow,
|
||||
Scale,
|
||||
Activity,
|
||||
FileText,
|
||||
Contact,
|
||||
Clock,
|
||||
DollarSign,
|
||||
Receipt,
|
||||
Phone,
|
||||
Tag,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -278,168 +298,204 @@ function CollectionDetailRoute() {
|
||||
<ArrowLeft className="h-4 w-4 mr-1" /> All collections
|
||||
</Button>
|
||||
|
||||
{/* Workflow header */}
|
||||
<Card className="border-border/60 mb-4">
|
||||
<CardContent className="p-4 flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-wider text-muted-foreground mb-1">
|
||||
Workflow stage
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{stages.map((s, i) => {
|
||||
const isCurrent = s.key === collection.current_stage;
|
||||
const isPast = i < currentStageIdx;
|
||||
return (
|
||||
<div key={s.key} className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant={isCurrent ? "default" : "outline"}
|
||||
className={
|
||||
isCurrent
|
||||
? ""
|
||||
: isPast
|
||||
? "opacity-60 line-through"
|
||||
: "opacity-50"
|
||||
}
|
||||
>
|
||||
{s.label}
|
||||
</Badge>
|
||||
{i < stages.length - 1 && (
|
||||
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={escalate}
|
||||
disabled={escalating || isLastStage || !nextStage}
|
||||
size="lg"
|
||||
>
|
||||
{escalating ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<ArrowRight className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{isLastStage
|
||||
? "Final stage"
|
||||
: `Escalate → ${nextStage?.label ?? ""}`}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Tasks */}
|
||||
<Card className="border-border/60 mb-6">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-serif text-lg flex items-center gap-2">
|
||||
<ListChecks className="h-4 w-4 text-muted-foreground" />
|
||||
Tasks
|
||||
{openTasks.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-1">
|
||||
{openTasks.length} open
|
||||
</Badge>
|
||||
)}
|
||||
</h3>
|
||||
<Button size="sm" variant="outline" onClick={() => setAddTaskOpen(true)}>
|
||||
<Plus className="h-3.5 w-3.5 mr-1" /> Add task
|
||||
</Button>
|
||||
</div>
|
||||
{tasks.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground italic py-4">
|
||||
No tasks yet. Escalate to populate the next stage's checklist, or
|
||||
add one manually.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{[...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 (
|
||||
<div
|
||||
key={t.id}
|
||||
className={`flex items-center gap-3 px-2 py-2 rounded-md hover:bg-muted/40 ${
|
||||
t.done ? "opacity-60" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => toggleDone(t)}
|
||||
className="shrink-0"
|
||||
aria-label={t.done ? "Mark not done" : "Mark done"}
|
||||
>
|
||||
{t.done ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-emerald-600" />
|
||||
) : (
|
||||
<Circle className="h-5 w-5 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={`text-sm ${t.done ? "line-through" : ""}`}>
|
||||
{t.title}
|
||||
</div>
|
||||
{stageLabel && (
|
||||
<div className="text-[10px] uppercase tracking-wider text-muted-foreground">
|
||||
{stageLabel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
type="date"
|
||||
className={`h-8 w-[150px] text-xs ${
|
||||
overdue ? "border-destructive text-destructive" : ""
|
||||
}`}
|
||||
value={t.due_date ?? ""}
|
||||
onChange={(e) => updateDueDate(t.id, e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
value={t.assignee_id ?? "unassigned"}
|
||||
onValueChange={(v) =>
|
||||
updateAssignee(t.id, v === "unassigned" ? null : v)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[180px] text-xs">
|
||||
<SelectValue placeholder="Unassigned" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="unassigned">Unassigned</SelectItem>
|
||||
{profiles.map((p) => (
|
||||
<SelectItem key={p.id} value={p.id}>
|
||||
{p.full_name || p.email}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => deleteTask(t.id)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* Tabs: Workflow (collections-specific) + same case tabs */}
|
||||
<Tabs defaultValue="workflow">
|
||||
<TabsList className="mb-4 flex-wrap h-auto">
|
||||
<TabsTrigger value="workflow"><Workflow className="h-3.5 w-3.5 mr-1.5" />Workflow</TabsTrigger>
|
||||
{collection.case && (
|
||||
<>
|
||||
<TabsTrigger value="litigation"><Scale className="h-3.5 w-3.5 mr-1.5" />Litigation</TabsTrigger>
|
||||
<TabsTrigger value="status"><Activity className="h-3.5 w-3.5 mr-1.5" />Status log</TabsTrigger>
|
||||
<TabsTrigger value="documents"><FileText className="h-3.5 w-3.5 mr-1.5" />Documents</TabsTrigger>
|
||||
<TabsTrigger value="contacts"><Contact className="h-3.5 w-3.5 mr-1.5" />Contacts</TabsTrigger>
|
||||
<TabsTrigger value="time"><Clock className="h-3.5 w-3.5 mr-1.5" />Time</TabsTrigger>
|
||||
<TabsTrigger value="expenses"><DollarSign className="h-3.5 w-3.5 mr-1.5" />Expenses</TabsTrigger>
|
||||
<TabsTrigger value="invoices"><Receipt className="h-3.5 w-3.5 mr-1.5" />Invoices</TabsTrigger>
|
||||
<TabsTrigger value="calls"><Phone className="h-3.5 w-3.5 mr-1.5" />Calls</TabsTrigger>
|
||||
<TabsTrigger value="custom"><Tag className="h-3.5 w-3.5 mr-1.5" />Custom fields</TabsTrigger>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsList>
|
||||
|
||||
{/* Payment plans */}
|
||||
<PaymentPlansPanel collectionId={collection.id} />
|
||||
<TabsContent value="workflow">
|
||||
{/* Workflow header */}
|
||||
<Card className="border-border/60 mb-4">
|
||||
<CardContent className="p-4 flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-wider text-muted-foreground mb-1">
|
||||
Workflow stage
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{stages.map((s, i) => {
|
||||
const isCurrent = s.key === collection.current_stage;
|
||||
const isPast = i < currentStageIdx;
|
||||
return (
|
||||
<div key={s.key} className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant={isCurrent ? "default" : "outline"}
|
||||
className={
|
||||
isCurrent
|
||||
? ""
|
||||
: isPast
|
||||
? "opacity-60 line-through"
|
||||
: "opacity-50"
|
||||
}
|
||||
>
|
||||
{s.label}
|
||||
</Badge>
|
||||
{i < stages.length - 1 && (
|
||||
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={escalate}
|
||||
disabled={escalating || isLastStage || !nextStage}
|
||||
size="lg"
|
||||
>
|
||||
{escalating ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<ArrowRight className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{isLastStage
|
||||
? "Final stage"
|
||||
: `Escalate → ${nextStage?.label ?? ""}`}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Ledger */}
|
||||
<CollectionDetail
|
||||
collection={collection}
|
||||
annualRate={annualRate}
|
||||
currentBalance={balanceFromOpening /* recalculated by detail */}
|
||||
onBack={() => navigate({ to: "/collections" })}
|
||||
onChange={() => setReload((r) => r + 1)}
|
||||
/>
|
||||
{/* Tasks */}
|
||||
<Card className="border-border/60 mb-6">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-serif text-lg flex items-center gap-2">
|
||||
<ListChecks className="h-4 w-4 text-muted-foreground" />
|
||||
Tasks
|
||||
{openTasks.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-1">
|
||||
{openTasks.length} open
|
||||
</Badge>
|
||||
)}
|
||||
</h3>
|
||||
<Button size="sm" variant="outline" onClick={() => setAddTaskOpen(true)}>
|
||||
<Plus className="h-3.5 w-3.5 mr-1" /> Add task
|
||||
</Button>
|
||||
</div>
|
||||
{tasks.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground italic py-4">
|
||||
No tasks yet. Escalate to populate the next stage's checklist, or
|
||||
add one manually.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{[...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 (
|
||||
<div
|
||||
key={t.id}
|
||||
className={`flex items-center gap-3 px-2 py-2 rounded-md hover:bg-muted/40 ${
|
||||
t.done ? "opacity-60" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => toggleDone(t)}
|
||||
className="shrink-0"
|
||||
aria-label={t.done ? "Mark not done" : "Mark done"}
|
||||
>
|
||||
{t.done ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-emerald-600" />
|
||||
) : (
|
||||
<Circle className="h-5 w-5 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={`text-sm ${t.done ? "line-through" : ""}`}>
|
||||
{t.title}
|
||||
</div>
|
||||
{stageLabel && (
|
||||
<div className="text-[10px] uppercase tracking-wider text-muted-foreground">
|
||||
{stageLabel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
type="date"
|
||||
className={`h-8 w-[150px] text-xs ${
|
||||
overdue ? "border-destructive text-destructive" : ""
|
||||
}`}
|
||||
value={t.due_date ?? ""}
|
||||
onChange={(e) => updateDueDate(t.id, e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
value={t.assignee_id ?? "unassigned"}
|
||||
onValueChange={(v) =>
|
||||
updateAssignee(t.id, v === "unassigned" ? null : v)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[180px] text-xs">
|
||||
<SelectValue placeholder="Unassigned" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="unassigned">Unassigned</SelectItem>
|
||||
{profiles.map((p) => (
|
||||
<SelectItem key={p.id} value={p.id}>
|
||||
{p.full_name || p.email}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => deleteTask(t.id)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Payment plans */}
|
||||
<PaymentPlansPanel collectionId={collection.id} />
|
||||
|
||||
{/* Ledger */}
|
||||
<CollectionDetail
|
||||
collection={collection}
|
||||
annualRate={annualRate}
|
||||
currentBalance={balanceFromOpening /* recalculated by detail */}
|
||||
onBack={() => navigate({ to: "/collections" })}
|
||||
onChange={() => setReload((r) => r + 1)}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{collection.case && (
|
||||
<>
|
||||
<TabsContent value="litigation"><CaseLitigationTab caseRecord={collection.case} canManage={true} onSaved={() => setReload((r) => r + 1)} /></TabsContent>
|
||||
<TabsContent value="status"><CaseStatusTab caseId={collection.case.id} caseLabel={`${collection.case.case_number} — ${collection.case.title}`} /></TabsContent>
|
||||
<TabsContent value="documents"><CaseDocumentsTab caseId={collection.case.id} /></TabsContent>
|
||||
<TabsContent value="contacts"><ContactsLinkTab parentId={collection.case.id} parentTable="case_contacts" /></TabsContent>
|
||||
<TabsContent value="time"><CaseTimeTab caseRecord={collection.case} onInvoice={() => {}} /></TabsContent>
|
||||
<TabsContent value="expenses"><CaseExpensesTab caseId={collection.case.id} /></TabsContent>
|
||||
<TabsContent value="invoices"><CaseInvoicesTab caseRecord={collection.case} /></TabsContent>
|
||||
<TabsContent value="calls"><CaseCallLogsTab caseId={collection.case.id} /></TabsContent>
|
||||
<TabsContent value="custom"><CaseCustomFieldsTab caseId={collection.case.id} /></TabsContent>
|
||||
</>
|
||||
)}
|
||||
</Tabs>
|
||||
|
||||
<AddTaskDialog
|
||||
open={addTaskOpen}
|
||||
|
||||
Reference in New Issue
Block a user