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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user