("all");
+ const { walletAddress } = useWalletContext();
+ const financialData = useFinancialDashboard(activeTab);
+
+ const amountByDateChartData = React.useMemo(
+ () => financialData.amountsByDate.map((d) => ({ month: d.date, desktop: d.amount })),
+ [financialData.amountsByDate]
+ );
+
+ const escrowTypeDonutData = React.useMemo(
+ () =>
+ financialData.typeSlices.map((s) => ({
+ browser: s.type === "single" ? "single" : "multi",
+ visitors: s.value,
+ fill:
+ s.type === "single" ? "var(--color-single)" : "var(--color-multi)",
+ })),
+ [financialData.typeSlices]
+ );
+
+ const createdByDateChartData = React.useMemo(
+ () =>
+ financialData.createdByDate.map((d) => ({ month: d.date, desktop: d.count })),
+ [financialData.createdByDate]
+ );
+
+ if (!walletAddress) {
+ return (
+
+
+ Financial Dashboard
+
+
+
+
+
+
+ Connect your wallet
+
+ Connect your wallet to see your receiver dashboard and escrow
+ totals.
+
+
+
+
+ );
+ }
+
+ if (financialData.isLoading && financialData.totalEscrows === 0) {
+ return (
+
+
+ Financial Dashboard
+
+
setActiveTab(v as FinancialDashboardTab)}>
+
+ All
+ Single Release
+ Multi Release
+
+
+
+
+ );
+ }
+
+ if (financialData.isError) {
+ return (
+
+
+ Financial Dashboard
+
+
+
+ Error loading escrows
+
+ Something went wrong. You can try again.
+
+ financialData.refetch()} className="mt-4">
+ Retry
+
+
+
+
+ );
+ }
+
+ if (!financialData.isLoading && financialData.totalEscrows === 0) {
+ return (
+
+
+ Financial Dashboard
+
+ setActiveTab(v as FinancialDashboardTab)}>
+
+ All
+ Single Release
+ Multi Release
+
+
+
+
+
+
+
+ No escrows as receiver
+
+ You have no escrows where you are the receiver. Totals will appear
+ here once you have incoming escrow activity.
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ Financial Dashboard
+
+
+
setActiveTab(v as FinancialDashboardTab)}>
+
+ All
+ Single Release
+ Multi Release
+
+
+
+ {/* KPI Cards: Total Amount, Total Released, Total Balance (spec), plus Escrows count */}
+
+
+
+ Total Amount
+
+
+
+
+ {financialData.isLoading ? "-" : formatCurrency(financialData.totalAmount, "USDC")}
+
+ Sum of all escrow amounts (SR + MR)
+
+
+
+
+ Total Released
+
+
+
+
+ {financialData.isLoading ? "-" : formatCurrency(financialData.totalReleased, "USDC")}
+
+ Released to you (SR + MR milestones)
+
+
+
+
+ Total Balance
+
+
+
+
+ {financialData.isLoading ? "-" : formatCurrency(financialData.totalBalance, "USDC")}
+
+ Pending (Total Amount − Released)
+
+
+
+
+ Escrows
+
+
+
+
+ {financialData.isLoading ? "-" : financialData.totalEscrows}
+
+ Total number of escrows
+
+
+
+
+
+
+ {/* Charts */}
+
+
+
+ Escrow Amounts
+ Amounts by date
+
+
+
+ {amountByDateChartData.length > 0 ? (
+
+
+
+ new Date(String(value)).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ })
+ }
+ />
+ } />
+
+
+ ) : (
+
+
+
+ No data
+ No Data Available
+
+
+ )}
+
+
+
+
+
+
+ Escrow Types
+ Escrow types
+
+
+
+ {escrowTypeDonutData.some((d) => Number(d.visitors) > 0) ? (
+
+ } />
+
+ {escrowTypeDonutData.map((slice, idx) => (
+ |
+ ))}
+
+
+ ) : (
+
+
+
+ No data
+ No Data Available
+
+
+ )}
+
+
+
+
+ Single
+
+
+
+ Multi
+
+
+
+
+
+
+
+ Escrow Created
+ Created escrows by date
+
+
+
+ {createdByDateChartData.length > 0 ? (
+
+
+
+ new Date(String(value)).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ })
+ }
+ />
+ Math.max(max, 1)]}
+ />
+ } />
+
+
+ ) : (
+
+
+
+ No data
+ No Data Available
+
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/src/components/tw-blocks/financial-dashboard/FinancialSummaryChart.tsx b/src/components/tw-blocks/financial-dashboard/FinancialSummaryChart.tsx
new file mode 100644
index 0000000..ba6a731
--- /dev/null
+++ b/src/components/tw-blocks/financial-dashboard/FinancialSummaryChart.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+
+
+import React from "react";
+import { Tooltip as RechartsTooltip } from "recharts";
+
+export type ChartConfig = Record;
+
+interface ChartContainerProps {
+ config: ChartConfig;
+ className?: string;
+ children: React.ReactNode;
+}
+
+export function ChartContainer({
+ config,
+ className,
+ children,
+}: ChartContainerProps) {
+ const style: React.CSSProperties = {};
+ for (const [key, value] of Object.entries(config)) {
+ const varName = `--color-${key}` as const;
+ if (value.color) (style as Record)[varName] = value.color;
+ }
+ return (
+
+ {children}
+
+ );
+}
+
+type RechartsPayloadItem = {
+ value?: number | string;
+ dataKey?: string;
+ color?: string;
+ name?: string;
+};
+
+type RechartsTooltipContentProps = {
+ active?: boolean;
+ label?: string | number;
+ payload?: RechartsPayloadItem[];
+};
+
+export type ChartTooltipContentProps = {
+ hideLabel?: boolean;
+ indicator?: "line" | "dot";
+} & RechartsTooltipContentProps;
+
+export function ChartTooltip(
+ props: React.ComponentProps
+) {
+ return ;
+}
+
+export function ChartTooltipContent({
+ active,
+ label,
+ payload,
+ hideLabel,
+}: ChartTooltipContentProps) {
+ if (!active || !payload || payload.length === 0) return null;
+ return (
+
+ {!hideLabel ?
{label}
: null}
+
+ {payload.map((item, idx) => (
+
+
+
+ {item.name ?? String(item.dataKey)}
+
+
+ {item.value as React.ReactNode}
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/tw-blocks/financial-dashboard/useFinancialDashboard.ts b/src/components/tw-blocks/financial-dashboard/useFinancialDashboard.ts
new file mode 100644
index 0000000..2397009
--- /dev/null
+++ b/src/components/tw-blocks/financial-dashboard/useFinancialDashboard.ts
@@ -0,0 +1,167 @@
+"use client";
+
+import React from "react";
+import { useWalletContext } from "../wallet-kit/WalletProvider";
+import { useEscrowsByRoleQuery } from "../tanstack/useEscrowsByRoleQuery";
+import type { GetEscrowsFromIndexerResponse as Escrow } from "@trustless-work/escrow/types";
+
+export type FinancialDashboardTab = "all" | "single" | "multi";
+
+type AmountsByDatePoint = { date: string; amount: number };
+type CreatedByDatePoint = { date: string; count: number };
+type DonutSlice = { type: "single" | "multi"; value: number; fill: string };
+
+function filterByTab(data: Escrow[], tab: FinancialDashboardTab): Escrow[] {
+ if (tab === "single") return data.filter((e) => e.type === "single-release");
+ if (tab === "multi") return data.filter((e) => e.type === "multi-release");
+ return data;
+}
+
+function getCreatedDateKey(createdAt: Escrow["createdAt"]): string {
+ const seconds = (createdAt as unknown as { _seconds?: number })?._seconds;
+ const d = seconds ? new Date(seconds * 1000) : new Date();
+ return d.toISOString().slice(0, 10);
+}
+
+function getSingleReleaseAmount(escrow: Escrow): number {
+ const raw = (escrow as unknown as { amount?: number | string }).amount;
+ const n = Number(raw ?? 0);
+ return Number.isFinite(n) ? n : 0;
+}
+
+function getMultiReleaseAmount(escrow: Escrow): number {
+ const milestones = (
+ escrow as unknown as {
+ milestones?: Array<{ amount?: number | string }>;
+ }
+ ).milestones;
+ if (!Array.isArray(milestones)) return 0;
+ return milestones.reduce((acc: number, m) => {
+ const n = Number(m?.amount ?? 0);
+ return acc + (Number.isFinite(n) ? n : 0);
+ }, 0);
+}
+
+function getEscrowAmount(escrow: Escrow): number {
+ if (escrow.type === "single-release") return getSingleReleaseAmount(escrow);
+ if (escrow.type === "multi-release") return getMultiReleaseAmount(escrow);
+ return 0;
+}
+
+function getReleasedAmount(escrow: Escrow): number {
+ if (escrow.type === "single-release") {
+ const amount = getSingleReleaseAmount(escrow);
+ return escrow.flags?.released ? amount : 0;
+ }
+ if (escrow.type === "multi-release") {
+ const milestones = (
+ escrow as unknown as {
+ milestones?: Array<{ amount?: number | string; status?: string }>;
+ }
+ ).milestones;
+ if (!Array.isArray(milestones)) return 0;
+ return milestones.reduce((acc: number, m) => {
+ if (m?.status === "released") {
+ const n = Number(m?.amount ?? 0);
+ return acc + (Number.isFinite(n) ? n : 0);
+ }
+ return acc;
+ }, 0);
+ }
+ return 0;
+}
+
+export function useFinancialDashboard(tab: FinancialDashboardTab) {
+ const { walletAddress } = useWalletContext();
+
+ const {
+ data = [],
+ isLoading,
+ isFetching,
+ isError,
+ refetch,
+ } = useEscrowsByRoleQuery({
+ role: "receiver",
+ roleAddress: walletAddress ?? "",
+ orderDirection: "desc",
+ enabled: !!walletAddress,
+ });
+
+ const filtered = React.useMemo(
+ () => filterByTab(data, tab),
+ [data, tab]
+ );
+
+ const totalEscrows = React.useMemo(
+ () => filtered.length,
+ [filtered.length]
+ );
+
+ const totalAmount = React.useMemo(() => {
+ return filtered.reduce((acc: number, e) => acc + getEscrowAmount(e), 0);
+ }, [filtered]);
+
+ const totalReleased = React.useMemo(() => {
+ return filtered.reduce(
+ (acc: number, e) => acc + getReleasedAmount(e),
+ 0
+ );
+ }, [filtered]);
+
+ const totalBalance = React.useMemo(
+ () => totalAmount - totalReleased,
+ [totalAmount, totalReleased]
+ );
+
+ const typeSlices = React.useMemo(() => {
+ let single = 0;
+ let multi = 0;
+ for (const e of filtered) {
+ if (e.type === "single-release") single += 1;
+ else if (e.type === "multi-release") multi += 1;
+ }
+ return [
+ { type: "single", value: single, fill: "var(--color-single)" },
+ { type: "multi", value: multi, fill: "var(--color-multi)" },
+ ];
+ }, [filtered]);
+
+ const amountsByDate = React.useMemo(() => {
+ const map = new Map();
+ for (const e of filtered) {
+ const key = getCreatedDateKey(e.createdAt);
+ const current = map.get(key) ?? 0;
+ map.set(key, current + getEscrowAmount(e));
+ }
+ return Array.from(map.entries())
+ .map(([date, amount]) => ({ date, amount }))
+ .sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
+ }, [filtered]);
+
+ const createdByDate = React.useMemo(() => {
+ const map = new Map();
+ for (const e of filtered) {
+ const key = getCreatedDateKey(e.createdAt);
+ map.set(key, (map.get(key) ?? 0) + 1);
+ }
+ return Array.from(map.entries())
+ .map(([date, count]) => ({ date, count }))
+ .sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
+ }, [filtered]);
+
+ return {
+ isLoading,
+ isFetching,
+ isError,
+ refetch,
+ totalEscrows,
+ totalAmount,
+ totalReleased,
+ totalBalance,
+ typeSlices,
+ amountsByDate,
+ createdByDate,
+ } as const;
+}
+
+export type UseFinancialDashboardReturn = ReturnType;