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:
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user