/ dashboard-v2 / frontend / src / pages / Review.jsx
Review.jsx
  1  import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
  2  import { fetchReview } from '../api';
  3  import MetricCard from '../components/MetricCard';
  4  import DataTable from '../components/DataTable';
  5  
  6  const REVIEW_COLS = [
  7    { accessorKey: 'domain', header: 'Domain' },
  8    { accessorKey: 'task_type', header: 'Task' },
  9    {
 10      accessorKey: 'priority',
 11      header: 'Priority',
 12      cell: i => {
 13        const v = i.getValue();
 14        const color =
 15          v === 'high' ? 'text-red-400' : v === 'medium' ? 'text-amber-400' : 'text-slate-400';
 16        return <span className={color}>{v}</span>;
 17      },
 18    },
 19    {
 20      accessorKey: 'created_at',
 21      header: 'Created',
 22      cell: i => i.getValue()?.slice(0, 16).replace('T', ' ') ?? 'β€”',
 23    },
 24  ];
 25  
 26  const OUTREACH_COLS = [
 27    { accessorKey: 'domain', header: 'Domain' },
 28    {
 29      accessorKey: 'contact_method',
 30      header: 'Channel',
 31      cell: i => {
 32        const icons = { email: 'βœ‰οΈ', sms: 'πŸ“±', form: 'πŸ“‹', x: '𝕏', linkedin: 'πŸ’Ό' };
 33        return (
 34          <span>
 35            {icons[i.getValue()] ?? ''} {i.getValue()}
 36          </span>
 37        );
 38      },
 39    },
 40    { accessorKey: 'contact_uri', header: 'Contact' },
 41    {
 42      accessorKey: 'grade',
 43      header: 'Grade',
 44      cell: i => {
 45        const g = i.getValue();
 46        const color = !g
 47          ? 'text-slate-400'
 48          : g.startsWith('A')
 49            ? 'text-emerald-400'
 50            : g.startsWith('B')
 51              ? 'text-sky-400'
 52              : g.startsWith('C')
 53                ? 'text-amber-400'
 54                : 'text-red-400';
 55        return <span className={`font-bold ${color}`}>{g ?? 'β€”'}</span>;
 56      },
 57    },
 58    { accessorKey: 'score', header: 'Score', cell: i => i.getValue()?.toFixed(0) ?? 'β€”' },
 59  ];
 60  
 61  const FAILING_COLS = [
 62    { accessorKey: 'domain', header: 'Domain' },
 63    { accessorKey: 'status', header: 'Stage' },
 64    {
 65      accessorKey: 'error_message',
 66      header: 'Error',
 67      cell: i => (
 68        <span className="text-red-400 text-xs truncate max-w-xs block" title={i.getValue() ?? ''}>
 69          {i.getValue() ?? 'β€”'}
 70        </span>
 71      ),
 72    },
 73    {
 74      accessorKey: 'recapture_at',
 75      header: 'Retry At',
 76      cell: i => i.getValue()?.slice(0, 16).replace('T', ' ') ?? 'β€”',
 77    },
 78  ];
 79  
 80  export default function Review() {
 81    const qc = useQueryClient();
 82    const { data, isLoading, error } = useQuery({ queryKey: ['review'], queryFn: fetchReview });
 83  
 84    if (isLoading) return <div className="text-slate-400 p-8">Loading…</div>;
 85    if (error) return <div className="text-red-400 p-8">Error: {error.message}</div>;
 86  
 87    const d = data ?? {};
 88    const qs = d.queue_stats ?? {};
 89  
 90    return (
 91      <div className="space-y-6">
 92        {/* Queue metrics */}
 93        <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
 94          <MetricCard
 95            label="Pending Reviews"
 96            value={qs.pending_reviews ?? 0}
 97            variant={(qs.pending_reviews ?? 0) > 10 ? 'warning' : 'default'}
 98          />
 99          <MetricCard
100            label="Pending Outreaches"
101            value={qs.pending_outreaches ?? 0}
102            variant={(qs.pending_outreaches ?? 0) > 50 ? 'warning' : 'default'}
103          />
104          <MetricCard
105            label="Failing Sites"
106            value={qs.failing_sites ?? 0}
107            variant={(qs.failing_sites ?? 0) > 0 ? 'error' : 'success'}
108          />
109          <MetricCard label="Unread Replies" value={qs.unread_conversations ?? 0} variant="warning" />
110        </div>
111  
112        {/* Human review queue */}
113        {(d.pending_reviews ?? []).length > 0 && (
114          <div className="bg-slate-800 rounded-xl p-4 border border-slate-700">
115            <h3 className="text-sm font-medium text-slate-400 mb-3">
116              Human Review Queue ({(d.pending_reviews ?? []).length})
117            </h3>
118            <DataTable columns={REVIEW_COLS} data={d.pending_reviews ?? []} maxRows={20} />
119          </div>
120        )}
121  
122        {/* Pending outreaches */}
123        {(d.pending_outreaches ?? []).length > 0 && (
124          <div className="bg-slate-800 rounded-xl p-4 border border-slate-700">
125            <div className="flex items-center justify-between mb-3">
126              <h3 className="text-sm font-medium text-slate-400">
127                Pending Outreaches ({(d.pending_outreaches ?? []).length})
128              </h3>
129              <div className="flex gap-2 text-xs text-slate-500">
130                <span>
131                  Use CLI: <code className="bg-slate-900 px-1 rounded">npm run outreach:export</code>
132                </span>
133              </div>
134            </div>
135            <DataTable columns={OUTREACH_COLS} data={d.pending_outreaches ?? []} maxRows={30} />
136          </div>
137        )}
138  
139        {/* Failing sites */}
140        {(d.failing_sites ?? []).length > 0 && (
141          <div className="bg-slate-800 rounded-xl p-4 border border-slate-700">
142            <h3 className="text-sm font-medium text-slate-400 mb-3">
143              Failing Sites ({(d.failing_sites ?? []).length})
144            </h3>
145            <DataTable columns={FAILING_COLS} data={d.failing_sites ?? []} maxRows={20} />
146          </div>
147        )}
148  
149        {/* Empty state */}
150        {(d.pending_reviews ?? []).length === 0 &&
151          (d.pending_outreaches ?? []).length === 0 &&
152          (d.failing_sites ?? []).length === 0 && (
153            <div className="bg-slate-800 rounded-xl p-12 border border-slate-700 text-center">
154              <p className="text-emerald-400 text-lg font-medium">All clear!</p>
155              <p className="text-slate-500 text-sm mt-1">No items require manual review.</p>
156            </div>
157          )}
158      </div>
159    );
160  }