Added BK/PP flags and badges

X-Lovable-Edit-ID: edt-8c019d5d-3093-4389-91d1-3c0b9eebe919
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-20 00:25:45 +00:00
co-authored by renee-png
7 changed files with 128 additions and 12 deletions
+7 -1
View File
@@ -77,6 +77,7 @@ import {
import { SortableLedgerRow } from "./ledger-row";
import { ApplyCollectionWorkflowDialog } from "@/components/collections/apply-collection-workflow-dialog";
import { MergeCollectionsDialog } from "./merge-collections-dialog";
import { StatusFlagBadges } from "@/components/collections/status-flag-badges";
const TXN_TYPES = [
{ value: "assessment", label: "Assessment" },
@@ -303,7 +304,12 @@ export function CaseCollectionsTab({
onClick={() => setActiveId(c.id)}
>
<TableCell className="font-medium">
{c.homeowner?.last_name}, {c.homeowner?.first_name}
<span className="inline-flex items-center gap-1.5">
<span>
{c.homeowner?.last_name}, {c.homeowner?.first_name}
</span>
<StatusFlagBadges bk={c.in_bankruptcy} pp={c.on_payment_plan} />
</span>
</TableCell>
<TableCell className="text-muted-foreground">
{c.homeowner?.unit_number || "—"}
@@ -0,0 +1,40 @@
import { Badge } from "@/components/ui/badge";
/**
* Compact BK / PP indicator badges used next to case/collection names.
* - BK = Bankruptcy
* - PP = Payment Plan
*/
export function StatusFlagBadges({
bk,
pp,
className,
}: {
bk?: boolean | null;
pp?: boolean | null;
className?: string;
}) {
if (!bk && !pp) return null;
return (
<span className={`inline-flex items-center gap-1 ${className ?? ""}`}>
{bk && (
<Badge
variant="outline"
className="text-[9px] px-1.5 py-0 h-4 border-rose-600 text-rose-700 dark:text-rose-300 dark:border-rose-400 font-semibold"
title="In bankruptcy"
>
BK
</Badge>
)}
{pp && (
<Badge
variant="outline"
className="text-[9px] px-1.5 py-0 h-4 border-blue-600 text-blue-700 dark:text-blue-300 dark:border-blue-400 font-semibold"
title="On payment plan"
>
PP
</Badge>
)}
</span>
);
}
+6
View File
@@ -880,9 +880,11 @@ export type Database = {
current_stage: string | null
homeowner_id: string | null
id: string
in_bankruptcy: boolean
interest_rate_override: number | null
name: string | null
notes: string | null
on_payment_plan: boolean
opened_at: string
status: string
updated_at: string
@@ -895,9 +897,11 @@ export type Database = {
current_stage?: string | null
homeowner_id?: string | null
id?: string
in_bankruptcy?: boolean
interest_rate_override?: number | null
name?: string | null
notes?: string | null
on_payment_plan?: boolean
opened_at?: string
status?: string
updated_at?: string
@@ -910,9 +914,11 @@ export type Database = {
current_stage?: string | null
homeowner_id?: string | null
id?: string
in_bankruptcy?: boolean
interest_rate_override?: number | null
name?: string | null
notes?: string | null
on_payment_plan?: boolean
opened_at?: string
status?: string
updated_at?: string
+27 -10
View File
@@ -16,6 +16,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { setArchived } from "@/lib/archive";
import { NewCaseDialog } from "@/components/cases/new-case-dialog";
import { BulkConvertToCollectionsDialog } from "@/components/cases/bulk-convert-to-collections-dialog";
import { StatusFlagBadges } from "@/components/collections/status-flag-badges";
import { formatDistanceToNow } from "date-fns";
import { toast } from "sonner";
@@ -61,7 +62,7 @@ function CasesList() {
// Aggregate latest activity from related child tables.
const [
tasksRes, timeRes, expensesRes, docsRes, callRes,
commentsRes, invoicesRes, eventsRes,
commentsRes, invoicesRes, eventsRes, collectionsRes,
] = await Promise.all([
supabase.from("tasks").select("case_id, updated_at").not("case_id", "is", null).limit(20000),
supabase.from("time_entries").select("case_id, updated_at").limit(20000),
@@ -71,6 +72,7 @@ function CasesList() {
supabase.from("comments").select("case_id, created_at").not("case_id", "is", null).limit(20000),
supabase.from("invoices").select("case_id, updated_at").not("case_id", "is", null).limit(20000),
supabase.from("events").select("case_id, updated_at").not("case_id", "is", null).limit(20000),
supabase.from("collections").select("case_id, in_bankruptcy, on_payment_plan").limit(20000),
]);
const latest: Record<string, string> = {};
@@ -91,11 +93,23 @@ function CasesList() {
ingest(invoicesRes.data, "updated_at");
ingest(eventsRes.data, "updated_at");
// Roll up BK / PP flags: case has the flag if ANY of its collections do.
const flagMap: Record<string, { bk: boolean; pp: boolean }> = {};
for (const r of collectionsRes.data ?? []) {
const cid = (r as any).case_id as string | null;
if (!cid) continue;
const cur = flagMap[cid] ?? { bk: false, pp: false };
if ((r as any).in_bankruptcy) cur.bk = true;
if ((r as any).on_payment_plan) cur.pp = true;
flagMap[cid] = cur;
}
// Fallback to the case's own updated_at if no child activity found.
const enriched = baseRows.map((c: any) => {
const childTs = latest[c.id];
const lastActivity = childTs && (!c.updated_at || childTs > c.updated_at) ? childTs : c.updated_at;
return { ...c, last_activity: lastActivity };
const flags = flagMap[c.id];
return { ...c, last_activity: lastActivity, in_bankruptcy: !!flags?.bk, on_payment_plan: !!flags?.pp };
});
setCases(enriched);
@@ -457,14 +471,17 @@ function CasesList() {
/>
</td>
<td className="px-4 py-3">
<Link
to="/cases/$caseId"
params={{ caseId: c.id }}
className="font-medium hover:text-primary"
onClick={(e) => e.stopPropagation()}
>
{c.title}
</Link>
<span className="inline-flex items-center gap-1.5">
<Link
to="/cases/$caseId"
params={{ caseId: c.id }}
className="font-medium hover:text-primary"
onClick={(e) => e.stopPropagation()}
>
{c.title}
</Link>
<StatusFlagBadges bk={c.in_bankruptcy} pp={c.on_payment_plan} />
</span>
<div className="text-xs text-muted-foreground">{c.case_number}</div>
</td>
<td className="px-4 py-3 text-muted-foreground">{c.client?.name ?? "—"}</td>
+42
View File
@@ -67,6 +67,7 @@ import { SearchableSelect } from "@/components/ui/searchable-select";
import { toast } from "sonner";
import { spawnNextCollectionWorkflowTask } from "@/lib/workflow-chain";
import { ApplyCollectionWorkflowDialog } from "@/components/collections/apply-collection-workflow-dialog";
import { StatusFlagBadges } from "@/components/collections/status-flag-badges";
export const Route = createFileRoute("/collections/$collectionId")({
component: CollectionDetailRoute,
@@ -356,8 +357,49 @@ function CollectionDetailRoute() {
Closed{collection.closed_at ? ` · ${formatDate(collection.closed_at)}` : ""}
</Badge>
)}
<StatusFlagBadges bk={collection.in_bankruptcy} pp={collection.on_payment_plan} />
</div>
<div className="flex items-center gap-2 flex-wrap">
<label className="flex items-center gap-1.5 text-xs text-muted-foreground cursor-pointer select-none">
<Checkbox
checked={!!collection.in_bankruptcy}
onCheckedChange={async (v) => {
const val = !!v;
setCollection((c: any) => c ? { ...c, in_bankruptcy: val } : c);
const { error } = await supabase
.from("collections")
.update({ in_bankruptcy: val })
.eq("id", collection.id);
if (error) {
toast.error("Could not update", { description: error.message });
setCollection((c: any) => c ? { ...c, in_bankruptcy: !val } : c);
} else {
toast.success(val ? "Marked in bankruptcy" : "Bankruptcy flag removed");
}
}}
/>
In bankruptcy
</label>
<label className="flex items-center gap-1.5 text-xs text-muted-foreground cursor-pointer select-none">
<Checkbox
checked={!!collection.on_payment_plan}
onCheckedChange={async (v) => {
const val = !!v;
setCollection((c: any) => c ? { ...c, on_payment_plan: val } : c);
const { error } = await supabase
.from("collections")
.update({ on_payment_plan: val })
.eq("id", collection.id);
if (error) {
toast.error("Could not update", { description: error.message });
setCollection((c: any) => c ? { ...c, on_payment_plan: !val } : c);
} else {
toast.success(val ? "Marked on payment plan" : "Payment plan flag removed");
}
}}
/>
On payment plan
</label>
<Button size="sm" variant="outline" onClick={() => setApplyHoOpen(true)}>
<UserPlus className="h-4 w-4 mr-1.5" />
{collection.homeowner ? "Change homeowner" : "Apply homeowner"}
+3 -1
View File
@@ -23,6 +23,7 @@ import {
} from "@/components/ui/table";
import { formatCurrency, formatDate } from "@/lib/format";
import { ChevronRight, Wallet, Search, ArrowUp, ArrowDown, ArrowUpDown } from "lucide-react";
import { StatusFlagBadges } from "@/components/collections/status-flag-badges";
export const Route = createFileRoute("/collections/")({
component: CollectionsIndexPage,
@@ -57,7 +58,7 @@ function CollectionsIndexPage() {
supabase
.from("collections")
.select(
"id, status, current_stage, opened_at, name, homeowner:homeowners(id, first_name, last_name, unit_number, opening_balance), case:cases(id, case_number, title, client:clients(id, name))",
"id, status, current_stage, opened_at, name, in_bankruptcy, on_payment_plan, homeowner:homeowners(id, first_name, last_name, unit_number, opening_balance), case:cases(id, case_number, title, client:clients(id, name))",
)
.order("created_at", { ascending: false }),
supabase
@@ -266,6 +267,7 @@ function CollectionsIndexPage() {
</span>
)}
</Link>
<StatusFlagBadges bk={r.in_bankruptcy} pp={r.on_payment_plan} className="ml-2" />
{r.status === "closed" && (
<Badge variant="outline" className="ml-2 text-[10px] border-emerald-600 text-emerald-700">
Closed
@@ -0,0 +1,3 @@
ALTER TABLE public.collections
ADD COLUMN IF NOT EXISTS in_bankruptcy boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS on_payment_plan boolean NOT NULL DEFAULT false;