Issue Summary
The /dashboard/escrow route compiles after #144 is merged but
renders a blank page with no content. This issue builds the full
escrow monitor view — a paginated filterable table of all escrows
for the authenticated user, modeled after the "Interested People"
table design reference.
Type of Issue
Current Behavior
After EscrowStatusBadge fix (#144 ) is merged,
/dashboard/escrow compiles but shows a blank page.
No table, no filters, no data.
Expected Behavior
┌─────────────────────────────────────────────────────────┐
│ Escrow Monitor Showing X of Y │
├─────────────────────────────────────────────────────────┤
│ [Search engagement ID...] [Status ▾] │
├────┬──────────────┬──────────┬────────────┬────────────┤
│ ID │ Engagement │ Wallet │ Created │ Status │
├────┼──────────────┼──────────┼────────────┼────────────┤
│ 1 │ ST-1-171... │ XR6...2D │ 01/06/2026 │ ● Active │
│ 2 │ ST-1-171... │ XR6...2D │ 01/06/2026 │ ● Funded │
├────┴──────────────┴──────────┴────────────┴────────────┤
│ ← Page 1 → per page: 5 │
└─────────────────────────────────────────────────────────┘
Reproduction Steps
Merge fix(dashboard): create missing EscrowStatusBadge component #144 EscrowStatusBadge fix first
Run pnpm run dev from repo root
Navigate to http://localhost:3001/dashboard/escrow
Observe blank page with no content
Environment Details
Project Version: dApp-SafeTrust main
Runtime: Node.js 20+
Package Manager: pnpm (always run from repo root)
Port: 3001
Supporting Information
Depends On
Files to Create
apps/frontend/src/components/dashboard/
├── EscrowMonitorTable.tsx
└── EscrowMonitorFilters.tsx
apps/frontend/src/app/dashboard/escrow/
└── page.tsx
EscrowMonitorFilters.tsx
"use client" ;
import { Input } from "@/components/ui/input" ;
import {
Select , SelectContent , SelectItem ,
SelectTrigger , SelectValue ,
} from "@/components/ui/select" ;
interface EscrowMonitorFiltersProps {
search : string ;
onSearchChange : ( v : string ) => void ;
status : string ;
onStatusChange : ( v : string ) => void ;
}
export function EscrowMonitorFilters ( {
search, onSearchChange, status, onStatusChange,
} : EscrowMonitorFiltersProps ) {
return (
< div className = "flex items-center gap-3" >
< Input
placeholder = "Search by engagement ID..."
value = { search }
onChange = { ( e ) => onSearchChange ( e . target . value ) }
className = "max-w-xs"
/>
< Select value = { status } onValueChange = { onStatusChange } >
< SelectTrigger className = "w-[160px]" >
< SelectValue placeholder = "Status" />
</ SelectTrigger >
< SelectContent >
< SelectItem value = "all" > All statuses</ SelectItem >
< SelectItem value = "pending_signature" > Pending</ SelectItem >
< SelectItem value = "funded" > Funded</ SelectItem >
< SelectItem value = "active" > Active</ SelectItem >
< SelectItem value = "completed" > Completed</ SelectItem >
< SelectItem value = "disputed" > Disputed</ SelectItem >
< SelectItem value = "resolved" > Resolved</ SelectItem >
< SelectItem value = "cancelled" > Cancelled</ SelectItem >
</ SelectContent >
</ Select >
</ div >
) ;
}
EscrowMonitorTable.tsx
"use client" ;
import { EscrowStatusBadge } from "./EscrowStatusBadge" ;
import {
Table , TableBody , TableCell ,
TableHead , TableHeader , TableRow ,
} from "@/components/ui/table" ;
interface Escrow {
id : string ;
engagement_id : string ;
status : string ;
amount : number ;
created_at : string ;
tenant_id : string ;
}
export function EscrowMonitorTable ( {
escrows,
offset,
} : {
escrows : Escrow [ ] ;
offset : number ;
} ) {
return (
< Table >
< TableHeader >
< TableRow >
< TableHead > ID No.</ TableHead >
< TableHead > Engagement ID</ TableHead >
< TableHead > Wallet</ TableHead >
< TableHead > Amount</ TableHead >
< TableHead > Created</ TableHead >
< TableHead > Status</ TableHead >
</ TableRow >
</ TableHeader >
< TableBody >
{ escrows . map ( ( escrow , index ) => (
< TableRow key = { escrow . id } >
< TableCell > { offset + index + 1 } </ TableCell >
< TableCell className = "font-mono text-xs" >
{ escrow . engagement_id }
</ TableCell >
< TableCell className = "font-mono text-xs" >
{ escrow . tenant_id . slice ( 0 , 6 ) } ...
{ escrow . tenant_id . slice ( - 4 ) }
</ TableCell >
< TableCell >
${ escrow . amount ?. toLocaleString ( ) ?? "—" }
</ TableCell >
< TableCell >
{ new Date ( escrow . created_at ) . toLocaleDateString ( ) }
</ TableCell >
< TableCell >
< EscrowStatusBadge status = { escrow . status as any } />
</ TableCell >
</ TableRow >
) ) }
</ TableBody >
</ Table >
) ;
}
page.tsx
"use client" ;
import { useState } from "react" ;
import { useQuery , gql } from "@apollo/client" ;
import { EscrowMonitorTable } from
"@/components/dashboard/EscrowMonitorTable" ;
import { EscrowMonitorFilters } from
"@/components/dashboard/EscrowMonitorFilters" ;
import { useGlobalAuthenticationStore } from "@/core/store/data" ;
const ITEMS_PER_PAGE = 5 ;
const GET_USER_ESCROWS = gql `
query GetUserEscrows(
$userId: String!
$limit: Int!
$offset: Int!
) {
escrows(
where: { tenant_id: { _eq: $userId } }
limit: $limit
offset: $offset
order_by: { created_at: desc }
) {
id engagement_id status
amount created_at tenant_id
}
escrows_aggregate(
where: { tenant_id: { _eq: $userId } }
) {
aggregate { count }
}
}
` ;
export default function EscrowMonitorPage ( ) {
const [ page , setPage ] = useState ( 0 ) ;
const [ search , setSearch ] = useState ( "" ) ;
const [ statusFilter , setStatus ] = useState ( "all" ) ;
const userId = useGlobalAuthenticationStore (
( state ) => state . address ?? ""
) ;
const { data, loading, error } = useQuery ( GET_USER_ESCROWS , {
variables : {
userId,
limit : ITEMS_PER_PAGE ,
offset : page * ITEMS_PER_PAGE ,
} ,
skip : ! userId ,
} ) ;
const escrows = data ?. escrows ?? [ ] ;
const total = data ?. escrows_aggregate ?. aggregate ?. count ?? 0 ;
return (
< div className = "p-6 space-y-4" >
< div className = "flex items-center justify-between" >
< h1 className = "text-xl font-semibold" > Escrow Monitor</ h1 >
< p className = "text-sm text-muted-foreground" >
Showing { escrows . length } of { total }
</ p >
</ div >
< EscrowMonitorFilters
search = { search } onSearchChange = { setSearch }
status = { statusFilter } onStatusChange = { setStatus }
/>
{ loading && (
< p className = "text-sm text-muted-foreground" > Loading...</ p >
) }
{ error && (
< p className = "text-sm text-red-500" >
Failed to load escrows
</ p >
) }
{ ! loading && ! error && escrows . length === 0 && (
< p className = "text-sm text-muted-foreground" >
No escrows found
</ p >
) }
{ escrows . length > 0 && (
< EscrowMonitorTable
escrows = { escrows }
offset = { page * ITEMS_PER_PAGE }
/>
) }
< div className = "flex items-center justify-between pt-2" >
< button
disabled = { page === 0 }
onClick = { ( ) => setPage ( ( p ) => p - 1 ) }
className = "text-sm disabled:opacity-40"
> ←</ button >
< span className = "text-sm text-muted-foreground" >
Page { page + 1 }
</ span >
< button
disabled = { ( page + 1 ) * ITEMS_PER_PAGE >= total }
onClick = { ( ) => setPage ( ( p ) => p + 1 ) }
className = "text-sm disabled:opacity-40"
> →</ button >
</ div >
</ div >
) ;
}
Future Issues (maintainer wires these)
feat → real-time subscription via Hasura WebSocket
feat → escrow detail drawer on row click
Acceptance Criteria
Issue Summary
The
/dashboard/escrowroute compiles after #144 is merged butrenders a blank page with no content. This issue builds the full
escrow monitor view — a paginated filterable table of all escrows
for the authenticated user, modeled after the "Interested People"
table design reference.
Type of Issue
Current Behavior
After
EscrowStatusBadgefix (#144) is merged,/dashboard/escrowcompiles but shows a blank page.No table, no filters, no data.
Expected Behavior
Reproduction Steps
EscrowStatusBadgefix firstpnpm run devfrom repo roothttp://localhost:3001/dashboard/escrowEnvironment Details
Supporting Information
Depends On
Files to Create
EscrowMonitorFilters.tsxEscrowMonitorTable.tsxpage.tsxFuture Issues (maintainer wires these)
Acceptance Criteria
/dashboard/escrowrenders without build errorsCreated, Status badge
EscrowStatusBadgerenders correct color per status