Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 07:09:17 +00:00
co-authored by renee-png
parent 1a11c65a6f
commit 629ae95fc5
+195 -159
View File
@@ -298,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}