Skip to content

feat(dashboard): escrow monitor view #149

Description

@sotoJ24

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

  • Feature Request

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

  1. Merge fix(dashboard): create missing EscrowStatusBadge component #144 EscrowStatusBadge fix first
  2. Run pnpm run dev from repo root
  3. Navigate to http://localhost:3001/dashboard/escrow
  4. 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

  • /dashboard/escrow renders without build errors
  • Table: ID, Engagement ID, Wallet (truncated), Amount,
    Created, Status badge
  • EscrowStatusBadge renders correct color per status
  • Search input filters by engagement ID
  • Status dropdown filters the table
  • Pagination: 5 items per page, ← page → navigation
  • Empty, loading, and error states handled
  • Works in dark mode

Metadata

Metadata

Assignees

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions