Added sort and rename

X-Lovable-Edit-ID: edt-55916af3-06fe-482f-8f51-4b3702c7f905
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:11:59 +00:00
co-authored by renee-png
2 changed files with 113 additions and 11 deletions
+56 -2
View File
@@ -16,6 +16,7 @@ import {
Home,
Eye,
ExternalLink,
Pencil,
} from "lucide-react";
import { formatDate } from "@/lib/format";
import { toast } from "sonner";
@@ -254,6 +255,56 @@ export function CaseDocumentsTab({ caseId }: { caseId: string }) {
load();
};
const renameFolder = async (full: string) => {
const parts = full.split("/");
const currentName = parts[parts.length - 1];
const parent = parts.slice(0, -1).join("/");
const raw = window.prompt(`Rename folder "${currentName}" to:`, currentName);
if (raw === null) return;
const trimmed = raw.trim();
if (!trimmed || trimmed === currentName) return;
const safe = sanitizeName(trimmed);
const newFull = parent ? `${parent}/${safe}` : safe;
if (folders.includes(newFull)) {
toast.error("A folder with that name already exists here");
return;
}
// Update all document rows whose folder == full or starts with full + "/"
const affectedDocs = docs.filter(
(d) => (d.folder || "") === full || (d.folder || "").startsWith(`${full}/`),
);
for (const d of affectedDocs) {
const next = d.folder === full ? newFull : `${newFull}${d.folder.slice(full.length)}`;
const { error } = await supabase.from("documents").update({ folder: next }).eq("id", d.id);
if (error) { toast.error("Rename failed", { description: error.message }); return; }
}
// Update folder rows (this folder + all descendants)
const affectedFolders = folders.filter((f) => f === full || f.startsWith(`${full}/`));
for (const f of affectedFolders) {
const next = f === full ? newFull : `${newFull}${f.slice(full.length)}`;
// Try update; if conflict (rare), delete old row instead
const { error } = await supabase
.from("document_folders")
.update({ path: next })
.eq("case_id", caseId)
.eq("path", f);
if (error) {
await supabase.from("document_folders").delete().eq("case_id", caseId).eq("path", f);
await ensureFolder(next);
}
}
// If we are currently inside the renamed folder, update the breadcrumb path
if (path === full || path.startsWith(`${full}/`)) {
setPath(newFull + path.slice(full.length));
}
toast.success("Folder renamed");
load();
};
const crumbs = path ? path.split("/") : [];
return (
@@ -348,8 +399,11 @@ export function CaseDocumentsTab({ caseId }: { caseId: string }) {
</button>
</td>
<td colSpan={3} className="px-4 py-3 text-xs text-muted-foreground">Folder</td>
<td className="px-4 py-3 text-right">
<Button variant="ghost" size="icon" onClick={() => deleteFolder(f.full)}>
<td className="px-4 py-3 text-right whitespace-nowrap">
<Button variant="ghost" size="icon" onClick={() => renameFolder(f.full)} title="Rename folder">
<Pencil className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" onClick={() => deleteFolder(f.full)} title="Delete folder">
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</td>
+57 -9
View File
@@ -1,5 +1,5 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { ProtectedLayout } from "@/components/protected-layout";
import { PageContainer, PageHeader } from "@/components/app-shell";
import { supabase } from "@/integrations/supabase/client";
@@ -22,7 +22,7 @@ import {
TableRow,
} from "@/components/ui/table";
import { formatCurrency, formatDate } from "@/lib/format";
import { ChevronRight, Wallet, Search } from "lucide-react";
import { ChevronRight, Wallet, Search, ArrowUp, ArrowDown, ArrowUpDown } from "lucide-react";
export const Route = createFileRoute("/collections/")({
component: CollectionsIndexPage,
@@ -42,6 +42,13 @@ function CollectionsIndexPage() {
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [stageFilter, setStageFilter] = useState<string>("all");
const [sortKey, setSortKey] = useState<"homeowner" | "case" | "stage" | "tasks" | "opened" | "balance">("opened");
const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
const toggleSort = (key: typeof sortKey) => {
if (sortKey === key) setSortDir((d) => (d === "asc" ? "desc" : "asc"));
else { setSortKey(key); setSortDir("asc"); }
};
const load = async () => {
setLoading(true);
@@ -114,6 +121,47 @@ function CollectionsIndexPage() {
);
});
const sorted = useMemo(() => {
const getVal = (r: any): string | number => {
switch (sortKey) {
case "homeowner":
return `${r.homeowner?.last_name ?? ""} ${r.homeowner?.first_name ?? ""}`.toLowerCase();
case "case":
return (r.case?.client?.name ?? "").toLowerCase();
case "stage":
return (stages.find((s) => s.key === r.current_stage)?.label ?? "").toLowerCase();
case "tasks":
return openTaskCounts[r.id] ?? 0;
case "opened":
return r.opened_at ? new Date(r.opened_at).getTime() : 0;
case "balance":
return balances[r.id] ?? 0;
}
};
return [...filtered].sort((a, b) => {
const av = getVal(a);
const bv = getVal(b);
if (av < bv) return sortDir === "asc" ? -1 : 1;
if (av > bv) return sortDir === "asc" ? 1 : -1;
return 0;
});
}, [filtered, sortKey, sortDir, balances, openTaskCounts, stages]);
const SortHeader = ({ k, label, className }: { k: typeof sortKey; label: string; className?: string }) => {
const Icon = sortKey !== k ? ArrowUpDown : sortDir === "asc" ? ArrowUp : ArrowDown;
return (
<TableHead className={className}>
<button
onClick={() => toggleSort(k)}
className="inline-flex items-center gap-1 hover:text-foreground"
>
{label}
<Icon className={`h-3 w-3 ${sortKey === k ? "" : "opacity-50"}`} />
</button>
</TableHead>
);
};
return (
<ProtectedLayout>
<PageContainer>
@@ -161,17 +209,17 @@ function CollectionsIndexPage() {
<Table>
<TableHeader>
<TableRow>
<TableHead>Homeowner</TableHead>
<TableHead>HOA / Case</TableHead>
<TableHead>Stage</TableHead>
<TableHead className="text-center">Open tasks</TableHead>
<TableHead>Opened</TableHead>
<TableHead className="text-right">Balance</TableHead>
<SortHeader k="homeowner" label="Homeowner" />
<SortHeader k="case" label="HOA / Case" />
<SortHeader k="stage" label="Stage" />
<SortHeader k="tasks" label="Open tasks" className="text-center" />
<SortHeader k="opened" label="Opened" />
<SortHeader k="balance" label="Balance" className="text-right" />
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((r) => {
{sorted.map((r) => {
const bal = balances[r.id] ?? 0;
const tcount = openTaskCounts[r.id] ?? 0;
return (