feat: Admin Escrow Analytics Dashboard#21
feat: Admin Escrow Analytics Dashboard#21Michaelkingsdev wants to merge 1 commit intoTrustless-Work:mainfrom
Conversation
📝 WalkthroughWalkthroughThis PR introduces a complete Admin Analytics Dashboard system consisting of an admin page entry point, dashboard components with charting utilities, and specialized escrow analytics hooks with aggregation logic for computing metrics like total escrows, type distribution, and month-over-month growth. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant AdminPage as Admin Page
participant Hook as useAdminEscrowAnalytics
participant Indexer as Escrow Indexer
participant Agg as Aggregation Utils
participant Dashboard as Analytics Dashboard
participant Charts as Chart Components
User->>AdminPage: Enter engagementId & submit
AdminPage->>Hook: Call hook with engagementId
Hook->>Indexer: Fetch escrows by signer for engagement
Indexer-->>Hook: Return escrow list
Hook->>Indexer: Fetch detailed escrows by contract IDs
Indexer-->>Hook: Return detailed escrow data
Hook->>Agg: groupEscrowsByType(escrows)
Agg-->>Hook: Return type distribution
Hook->>Agg: groupEscrowsByDate(escrows)
Agg-->>Hook: Return date aggregation
Hook->>Agg: calculateMoMGrowth(escrows)
Agg-->>Hook: Return monthly growth %
Hook-->>AdminPage: Return aggregated analytics
AdminPage->>Dashboard: Render with analytics data
Dashboard->>Charts: Render charts with data
Charts-->>Dashboard: Display visualizations
Dashboard-->>User: Present dashboard UI
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (9)
src/components/tw-blocks/dashboard/dashboard-01/Dashboard.tsx (2)
76-85: Consider using domain-specific naming instead of template placeholders.The donut data transformation uses
browserandvisitorsas property names, which appear to be artifacts from a template. For clarity and consistency with other analytics components, consider using domain-specific names liketypeandcount.♻️ Suggested naming improvement
const donutData = React.useMemo( () => typeSlices.map((s) => ({ - browser: s.type === "single" ? "single" : "multi", - visitors: s.value, + type: s.type, + count: s.value, fill: s.type === "single" ? "var(--color-single)" : "var(--color-multi)", })), [typeSlices] );Then update the PieChart to use
dataKey="count"andnameKey="type".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/tw-blocks/dashboard/dashboard-01/Dashboard.tsx` around lines 76 - 85, The donutData mapping uses generic template names ("browser" and "visitors") instead of domain-specific names; change the object keys produced by donutData (computed from typeSlices) to use type and count (e.g., { type: ..., count: ..., fill: ... }) and then update the PieChart usage to reference dataKey="count" and nameKey="type" so the chart and data shape are consistent.
117-118: Currency is hardcoded to "USDC".Unlike
AdminEscrowAnalyticsDashboardwhich extracts the currency from the escrow data (data.raw[0]?.trustline?.symbol || "USDC"), this dashboard hardcodes "USDC". If multi-currency support is expected, consider deriving the currency from the data.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/tw-blocks/dashboard/dashboard-01/Dashboard.tsx` around lines 117 - 118, The JSX currently calls formatCurrency(totalAmount, "USDC") and hardcodes the currency; change it to derive the currency from the component's data (same approach as AdminEscrowAnalyticsDashboard) by passing something like data.raw?.[0]?.trustline?.symbol || "USDC" to formatCurrency instead of the literal "USDC". Update the expression inside the div (which uses isLoading, formatCurrency, and totalAmount) to use the derivedCurrency variable or inline expression so multi-currency values render correctly with a fallback to "USDC".src/components/tw-blocks/escrows/admin-analytics/useAdminEscrowAnalytics.ts (1)
36-37: Return type includesnullbut the function never returns it.The
queryFnreturn type isPromise<AdminAnalyticsData | null>, but the function either returns anAdminAnalyticsDataobject or throws an error—it never returnsnull. Consider simplifying the return type toPromise<AdminAnalyticsData>.🔧 Suggested type fix
- queryFn: async (): Promise<AdminAnalyticsData | null> => { + queryFn: async (): Promise<AdminAnalyticsData> => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/tw-blocks/escrows/admin-analytics/useAdminEscrowAnalytics.ts` around lines 36 - 37, The queryFn is typed as Promise<AdminAnalyticsData | null> but it always either throws or returns AdminAnalyticsData; update the function's return type to Promise<AdminAnalyticsData> and adjust any surrounding generics (e.g., the useQuery call) to use AdminAnalyticsData (remove the nullable union) so types correctly reflect that null is never returned; check the queryFn declaration and the useQuery<T> generic where AdminAnalyticsData | null is used and replace with AdminAnalyticsData.src/components/tw-blocks/escrows/admin-analytics/AdminEscrowAnalyticsDashboard.tsx (1)
228-233: Pie chart color assignment by array index may cause inconsistent coloring.The Cell colors are assigned based on array index (
index === 0 ? "white" : "hsl(var(--primary))"), but the order ofdata.byTypeentries depends on the order types are encountered in the reduce operation (ingroupEscrowsByType). If only multi-release escrows exist, they would get the "white" color intended for single-release.Consider assigning colors based on the
typevalue instead:🎨 Suggested fix for consistent coloring
{data.byType.map((_: unknown, index: number) => ( + {data.byType.map((entry: { type: string }, index: number) => ( <Cell key={`cell-${index}`} - fill={index === 0 ? "white" : "hsl(var(--primary))"} + fill={entry.type === "Single Release" ? "white" : "hsl(var(--primary))"} /> ))}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/tw-blocks/escrows/admin-analytics/AdminEscrowAnalyticsDashboard.tsx` around lines 228 - 233, The pie slice coloring relies on array index in the map (data.byType and the rendered Cell) which can miscolor types when grouping order changes; update the mapping to determine color from the escrow type value instead of index: inside the map over data.byType use the group's type property (from the output of groupEscrowsByType) to pick the correct color (e.g., map "singleRelease" -> "white", "multiRelease" -> "hsl(var(--primary))") so rendering of Cell uses that computed color, ensuring colors remain consistent regardless of array order.src/components/tw-blocks/dashboard/dashboard-01/useDashboard.ts (1)
61-105: Consider reusing aggregation utilities from the admin-analytics module.This hook duplicates aggregation logic that exists in
src/components/tw-blocks/escrows/admin-analytics/aggregation.ts(e.g.,calculateTotalAmount,calculateTotalBalance,groupEscrowsByDate). While the implementations differ slightly, consolidating could reduce maintenance burden.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/tw-blocks/dashboard/dashboard-01/useDashboard.ts` around lines 61 - 105, Replace the inline aggregation code in useDashboard (the computations for totalEscrows, totalAmount, totalBalance, amountsByDate and createdByDate) with calls to the shared functions from src/components/tw-blocks/escrows/admin-analytics/aggregation.ts (for example calculateTotalAmount, calculateTotalBalance and groupEscrowsByDate); import those utilities at the top of the file, use calculateTotalAmount(data) and calculateTotalBalance(data) for totalAmount/totalBalance, derive totalEscrows from data.length (or a provided utility if present), and use groupEscrowsByDate(data) (or its variants) to produce the amountsByDate and createdByDate shapes, leaving typeSlices as-is or moving its logic into a shared utility if available.src/components/tw-blocks/escrows/admin-analytics/aggregation.ts (3)
40-54: AssumescreatedAt._secondsalways exists.If an escrow has a malformed or missing
createdAtfield, accessing_secondswill returnundefined, causingnew Date(undefined * 1000)to produce an Invalid Date. Consider adding defensive handling.🛡️ Defensive handling for malformed dates
export const groupEscrowsByDate = (escrows: Escrow[]): ChartDataPoint[] => { const groups = escrows.reduce((acc, escrow) => { + if (!escrow.createdAt?._seconds) return acc; const date = new Date(escrow.createdAt._seconds * 1000); + if (isNaN(date.getTime())) return acc; const dateKey = format(date, "yyyy-MM-dd"); acc[dateKey] = (acc[dateKey] || 0) + 1; return acc; }, {} as Record<string, number>);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/tw-blocks/escrows/admin-analytics/aggregation.ts` around lines 40 - 54, The reducer in groupEscrowsByDate assumes escrow.createdAt._seconds exists; add defensive validation to handle missing/malformed createdAt by checking escrow.createdAt and escrow.createdAt._seconds (or other timestamp forms) before building the Date, and either skip the escrow or use a safe fallback (e.g., treat as current time or a sentinel) when the timestamp is absent; ensure you only call format(date, ...) for a valid Date and update the groups accumulation (acc / dateKey) accordingly so invalid dates don't produce "Invalid Date" keys.
31-34: Type mapping treats "unknown" types as "Multi Release".The current mapping only explicitly handles
"single-release"- all other types (including"unknown"or any unexpected values) are labeled as "Multi Release". This could be misleading in analytics if escrows have unexpected type values.🔧 Suggested fix for explicit type handling
return Object.entries(types).map(([type, count]) => ({ - type: type === "single-release" ? "Single Release" : "Multi Release", + type: type === "single-release" + ? "Single Release" + : type === "multi-release" + ? "Multi Release" + : "Unknown", count, }));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/tw-blocks/escrows/admin-analytics/aggregation.ts` around lines 31 - 34, The current mapping in aggregation.ts that transforms Object.entries(types).map(([type, count]) => ({ type: type === "single-release" ? "Single Release" : "Multi Release", count })) incorrectly treats any non-"single-release" value (including "unknown") as "Multi Release"; change the mapping to explicitly handle "single-release" and "multi-release" and map any other/unknown values to a neutral label like "Unknown" (or include the raw type) so analytics don't misclassify unexpected types—update the map callback accordingly to check for "single-release", "multi-release", then default to "Unknown".
74-78: Month iteration mutates Date object in place.The while loop mutates
currentviasetMonth(), then immediately wraps it innew Date()andstartOfMonth(). While this works, the mutation pattern is error-prone and could cause subtle bugs if the logic changes.♻️ Cleaner iteration using date-fns addMonths
+import { startOfMonth, format, addMonths } from "date-fns"; while (current <= end) { const monthKey = format(current, "yyyy-MM"); months[monthKey] = 0; - current = startOfMonth(new Date(current.setMonth(current.getMonth() + 1))); + current = addMonths(current, 1); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/tw-blocks/escrows/admin-analytics/aggregation.ts` around lines 74 - 78, The loop mutates the Date object via current.setMonth(...); change it to use date-fns addMonths to avoid in-place mutation: in the while loop that builds months (variables current, end, months, format, startOfMonth), set current = startOfMonth(addMonths(current, 1)) instead of new Date(current.setMonth(...)); import addMonths from date-fns and ensure current is treated immutably so each iteration advances by addMonths(current, 1).src/components/tw-blocks/dashboard/dashboard-01/chart.tsx (1)
19-23: Type assertion toanyfor CSS custom properties.The
(style as any)[varName]assertion is needed because TypeScript'sCSSPropertiesdoesn't include CSS custom properties by default. This is a known limitation. An alternative is to use a typed index signature.🔧 Optional: Type-safe CSS custom properties
export function ChartContainer({ config, className, children, }: ChartContainerProps) { - const style: React.CSSProperties = {}; + const style: React.CSSProperties & Record<`--color-${string}`, string> = {}; for (const [key, value] of Object.entries(config)) { const varName = `--color-${key}` as const; - if (value.color) (style as any)[varName] = value.color; + if (value.color) style[varName] = value.color; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/tw-blocks/dashboard/dashboard-01/chart.tsx` around lines 19 - 23, The code uses an unsafe (style as any)[varName] cast to assign CSS custom properties; instead declare style with a typed index signature so TypeScript knows custom properties are allowed (e.g. change the declaration of style to React.CSSProperties & Record<string, string> or a template-keyed type for `--color-*`) and then assign (style[varName] = value.color) without using any; apply this to the existing symbols `style`, `varName`, and the loop over `config`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/components/tw-blocks/dashboard/dashboard-01/chart.tsx`:
- Around line 19-23: The code uses an unsafe (style as any)[varName] cast to
assign CSS custom properties; instead declare style with a typed index signature
so TypeScript knows custom properties are allowed (e.g. change the declaration
of style to React.CSSProperties & Record<string, string> or a template-keyed
type for `--color-*`) and then assign (style[varName] = value.color) without
using any; apply this to the existing symbols `style`, `varName`, and the loop
over `config`.
In `@src/components/tw-blocks/dashboard/dashboard-01/Dashboard.tsx`:
- Around line 76-85: The donutData mapping uses generic template names
("browser" and "visitors") instead of domain-specific names; change the object
keys produced by donutData (computed from typeSlices) to use type and count
(e.g., { type: ..., count: ..., fill: ... }) and then update the PieChart usage
to reference dataKey="count" and nameKey="type" so the chart and data shape are
consistent.
- Around line 117-118: The JSX currently calls formatCurrency(totalAmount,
"USDC") and hardcodes the currency; change it to derive the currency from the
component's data (same approach as AdminEscrowAnalyticsDashboard) by passing
something like data.raw?.[0]?.trustline?.symbol || "USDC" to formatCurrency
instead of the literal "USDC". Update the expression inside the div (which uses
isLoading, formatCurrency, and totalAmount) to use the derivedCurrency variable
or inline expression so multi-currency values render correctly with a fallback
to "USDC".
In `@src/components/tw-blocks/dashboard/dashboard-01/useDashboard.ts`:
- Around line 61-105: Replace the inline aggregation code in useDashboard (the
computations for totalEscrows, totalAmount, totalBalance, amountsByDate and
createdByDate) with calls to the shared functions from
src/components/tw-blocks/escrows/admin-analytics/aggregation.ts (for example
calculateTotalAmount, calculateTotalBalance and groupEscrowsByDate); import
those utilities at the top of the file, use calculateTotalAmount(data) and
calculateTotalBalance(data) for totalAmount/totalBalance, derive totalEscrows
from data.length (or a provided utility if present), and use
groupEscrowsByDate(data) (or its variants) to produce the amountsByDate and
createdByDate shapes, leaving typeSlices as-is or moving its logic into a shared
utility if available.
In
`@src/components/tw-blocks/escrows/admin-analytics/AdminEscrowAnalyticsDashboard.tsx`:
- Around line 228-233: The pie slice coloring relies on array index in the map
(data.byType and the rendered Cell) which can miscolor types when grouping order
changes; update the mapping to determine color from the escrow type value
instead of index: inside the map over data.byType use the group's type property
(from the output of groupEscrowsByType) to pick the correct color (e.g., map
"singleRelease" -> "white", "multiRelease" -> "hsl(var(--primary))") so
rendering of Cell uses that computed color, ensuring colors remain consistent
regardless of array order.
In `@src/components/tw-blocks/escrows/admin-analytics/aggregation.ts`:
- Around line 40-54: The reducer in groupEscrowsByDate assumes
escrow.createdAt._seconds exists; add defensive validation to handle
missing/malformed createdAt by checking escrow.createdAt and
escrow.createdAt._seconds (or other timestamp forms) before building the Date,
and either skip the escrow or use a safe fallback (e.g., treat as current time
or a sentinel) when the timestamp is absent; ensure you only call format(date,
...) for a valid Date and update the groups accumulation (acc / dateKey)
accordingly so invalid dates don't produce "Invalid Date" keys.
- Around line 31-34: The current mapping in aggregation.ts that transforms
Object.entries(types).map(([type, count]) => ({ type: type === "single-release"
? "Single Release" : "Multi Release", count })) incorrectly treats any
non-"single-release" value (including "unknown") as "Multi Release"; change the
mapping to explicitly handle "single-release" and "multi-release" and map any
other/unknown values to a neutral label like "Unknown" (or include the raw type)
so analytics don't misclassify unexpected types—update the map callback
accordingly to check for "single-release", "multi-release", then default to
"Unknown".
- Around line 74-78: The loop mutates the Date object via current.setMonth(...);
change it to use date-fns addMonths to avoid in-place mutation: in the while
loop that builds months (variables current, end, months, format, startOfMonth),
set current = startOfMonth(addMonths(current, 1)) instead of new
Date(current.setMonth(...)); import addMonths from date-fns and ensure current
is treated immutably so each iteration advances by addMonths(current, 1).
In `@src/components/tw-blocks/escrows/admin-analytics/useAdminEscrowAnalytics.ts`:
- Around line 36-37: The queryFn is typed as Promise<AdminAnalyticsData | null>
but it always either throws or returns AdminAnalyticsData; update the function's
return type to Promise<AdminAnalyticsData> and adjust any surrounding generics
(e.g., the useQuery call) to use AdminAnalyticsData (remove the nullable union)
so types correctly reflect that null is never returned; check the queryFn
declaration and the useQuery<T> generic where AdminAnalyticsData | null is used
and replace with AdminAnalyticsData.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (8)
src/app/admin/page.tsxsrc/components/tw-blocks/dashboard/dashboard-01/Dashboard.tsxsrc/components/tw-blocks/dashboard/dashboard-01/chart.tsxsrc/components/tw-blocks/dashboard/dashboard-01/useDashboard.tssrc/components/tw-blocks/escrows/admin-analytics/AdminEscrowAnalyticsDashboard.tsxsrc/components/tw-blocks/escrows/admin-analytics/aggregation.tssrc/components/tw-blocks/escrows/admin-analytics/index.tssrc/components/tw-blocks/escrows/admin-analytics/useAdminEscrowAnalytics.ts
Closes #14
Admin Escrow Analytics Dashboard
This PR adds a new Admin Escrow Analytics Dashboard.
It helps admins see escrow activity, total volume, and growth over time.
The goal is to keep the system scalable, simple, and easy to maintain.
What Was Done
Better Data Loading
Inside
useAdminEscrowAnalytics, I used a two-step data loading process to avoid Firestore index limits:This:
Data Processing
Created
aggregation.tsto turn raw data into useful metrics like:The file only handles calculations. It is separate from the UI and easy to test.
State & Caching
@tanstack/react-queryto cache data and avoid repeated requests.engagementIdinlocalStorageso it stays after page refresh.Visuals
rechartsfor charts.Note
Since no route was provided in the issue, I created a temporary route at
/admin.This is just for review and can be changed or removed based on feedback.
Screenshot
Summary by CodeRabbit
Release Notes