+
+
+ {data.error &&
Couldn't load projects: {data.error}
}
+
+
+
+
+
+
+
+
+
+ setQuery(e.target.value)} />
+ {query && }
+
+
+ {tab === "construction" && (
+
+
+ {QUICK_FILTERS.map((s) => (
+
+ ))}
+
+ )}
+
+
+ {data.live && data.loading && projects.length === 0 ? (
+
+ ) : filtered.length === 0 ? (
+
+
+
No {tab === "construction" ? "projects" : "leads"} match your search.
+
{ setQuery(""); setStatusFilter("all"); }}>Clear filters
+
+ ) : (
+
+
+ Lead
+ {live ? (
+ <>StatusOwnerValueDue date>
+ ) : tab === "construction" ? (
+ <>StatusStageJob typeAgentProgressHealth>
+ ) : (
+ <>Job typeStageAgentCreated>
+ )}
+
+
+
+ {filtered.map((p) => (
+
+
+
{p.name}
+ {p.address &&
{p.address}
}
+
+
+ {live ? (
+ <>
+
{STATUS_LABEL[p.status].toUpperCase()}
+
{p.agent}
+
{p.value != null ? money(p.value) : —}
+
{p.dueDate ? new Date(p.dueDate).toLocaleDateString(undefined, { day: "numeric", month: "short", year: "numeric" }) : —}
+ >
+ ) : tab === "construction" ? (
+ <>
+
{STATUS_LABEL[p.status].toUpperCase()}
+
{p.stage}
+
{p.jobType}
+
{p.agent}
+
+
+ {p.progress}%
+
+
{p.health}
+ >
+ ) : (
+ <>
+
{p.jobType}
+
{p.stage}
+
{p.agent}
+
{p.createdAgo}
+ >
+ )}
+
+
+
+
+
+ ))}
+
+ )}
+
+
+ Showing {filtered.length} of {list.length} {tab === "construction" ? "projects" : "leads"}
+ {tab === "construction" && {money(budgetTotal)} total}
+
+
+
setViewProject(null)} />
+
+ );
+}
+
+/* ---------------------------------------------------------- */
+/* Collected details modal — payment ledger for one project */
+/* ---------------------------------------------------------- */
+
+function CollectedDetailsModal({ project, onClose }: { project: UiProject | null; onClose: () => void }) {
+ const [query, setQuery] = useState("");
+ const records = project?.collections ?? [];
+ const total = useMemo(() => records.reduce((s, r) => s + r.amount, 0), [records]);
+ if (!project) return null;
+
+ const q = query.trim().toLowerCase();
+ const filtered = records.filter((r) =>
+ !q || r.name.toLowerCase().includes(q) || r.referenceId.toLowerCase().includes(q) || r.type.toLowerCase().includes(q));
+ const netTotal = filtered.reduce((s, r) => s + r.amount, 0);
+ const projectName = project.name;
+
+ function downloadCsv() {
+ const header = ["Name/Project", "Reference ID", "Date", "Type", "Status", "Amount"];
+ const rows = filtered.map((r) => [r.name, r.referenceId, r.date, r.type, COLLECTION_STATUS_LABEL[r.status], r.amount.toFixed(2)]);
+ const csv = [header, ...rows].map((row) => row.map((c) => `"${String(c).replace(/"/g, '""')}"`).join(",")).join("\n");
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `${projectName.replace(/\s+/g, "-").toLowerCase()}-collections.csv`;
+ a.click();
+ URL.revokeObjectURL(url);
+ }
+
+ return (
+ Total: {moneyExact(total)}>}
+ size="xl"
+ headerExtra={records.length > 0 && (
+
+ )}
+ >
+ {project.collections === null ? (
+ Collections aren't available yet for live projects — this needs a billing module in be-crm.
+ ) : records.length === 0 ? (
+ No payments collected yet for {project.name}.
+ ) : (
+ <>
+
+
+ setQuery(e.target.value)} />
+ {query && }
+
+
+
+
+ Name / ProjectReference IDDateTypeStatusAmount
+
+ {filtered.map((r) => (
+
+
+
{r.name}
+
{r.address}
+
+
{r.referenceId}
+
{r.date}
+
{r.type}
+
{COLLECTION_STATUS_LABEL[r.status].toUpperCase()}
+
{moneyExact(r.amount)}
+
+ ))}
+
+
+
+ Showing {filtered.length} record{filtered.length === 1 ? "" : "s"}
+ Net Total: {moneyExact(netTotal)}
+
+ >
+ )}
+
+ );
+}
diff --git a/src/components/dashboard/sidebar.tsx b/src/components/dashboard/sidebar.tsx
index e995cb0..a52ea7e 100644
--- a/src/components/dashboard/sidebar.tsx
+++ b/src/components/dashboard/sidebar.tsx
@@ -86,6 +86,7 @@ const NAV_PERMISSION: Record