diff --git a/src/components/cases/documents-tab.tsx b/src/components/cases/documents-tab.tsx index 544ec93..4f8c8ff 100644 --- a/src/components/cases/documents-tab.tsx +++ b/src/components/cases/documents-tab.tsx @@ -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 }) { Folder - - + diff --git a/src/routes/collections.index.tsx b/src/routes/collections.index.tsx index e87b9bc..d522b41 100644 --- a/src/routes/collections.index.tsx +++ b/src/routes/collections.index.tsx @@ -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("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 ( + + + + ); + }; + return ( @@ -161,17 +209,17 @@ function CollectionsIndexPage() { - Homeowner - HOA / Case - Stage - Open tasks - Opened - Balance + + + + + + - {filtered.map((r) => { + {sorted.map((r) => { const bal = balances[r.id] ?? 0; const tcount = openTaskCounts[r.id] ?? 0; return (