← back to Homesonspec
apps/admin/src/app/review/page.tsx
89 lines
import { prisma } from "@homesonspec/database";
import { CreatedChip } from "@homesonspec/shared-ui";
import ReviewActions from "./ReviewActions";
export const dynamic = "force-dynamic";
/** Review queue: staged record + per-field evidence side-by-side, approve/reject. */
export default async function ReviewQueuePage() {
const items = await prisma.reviewItem.findMany({
where: { status: "OPEN" },
orderBy: { createdAt: "asc" },
include: {
stagedRecord: {
include: {
evidence: { orderBy: { field: "asc" } },
events: { where: { passed: false } },
source: { select: { name: true, key: true } },
},
},
},
});
return (
<div>
<h1 className="text-2xl font-bold">Review queue</h1>
<p className="mt-1 text-sm text-neutral-500">
Records validators flagged for human judgment. Approving publishes; rejecting keeps them out.
</p>
{items.length === 0 && (
<p className="mt-8 rounded-lg border border-neutral-200 bg-white p-6 text-center text-neutral-500" data-testid="review-empty">
Queue is clear.
</p>
)}
<div className="mt-4 space-y-4">
{items.map((item) => {
const staged = item.stagedRecord;
const payload = (staged?.payload ?? {}) as Record<string, { value: unknown; raw: string | null }>;
return (
<div key={item.id} className="rounded-xl border border-neutral-200 bg-white p-4 shadow-sm" data-testid="review-card">
<div className="flex flex-wrap items-center justify-between gap-2">
<div>
<span className="font-semibold">{staged?.entityType ?? "—"}</span>
<span className="ml-2 font-mono text-xs text-neutral-400">{staged?.canonicalKey.slice(0, 12)}…</span>
<span className="ml-2 text-xs text-neutral-500">{staged?.source.name}</span>
</div>
{/* Standing rule: every admin card shows created date + time. */}
<CreatedChip date={item.createdAt} />
</div>
<p className="mt-2 rounded bg-amber-50 p-2 text-sm text-amber-900">{item.reason}</p>
<div className="mt-3 grid gap-4 md:grid-cols-2">
<div>
<h3 className="text-xs font-semibold uppercase text-neutral-500">Extracted values</h3>
<table className="mt-1 w-full text-xs">
<tbody>
{Object.entries(payload).map(([field, envelope]) => (
<tr key={field} className="border-b border-neutral-100">
<td className="py-1 pr-2 font-medium">{field}</td>
<td className="py-1">{envelope.value === null ? <em className="text-neutral-400">null</em> : String(envelope.value)}</td>
</tr>
))}
</tbody>
</table>
</div>
<div>
<h3 className="text-xs font-semibold uppercase text-neutral-500">Source evidence</h3>
<table className="mt-1 w-full text-xs">
<tbody>
{(staged?.evidence ?? []).map((row) => (
<tr key={row.id} className="border-b border-neutral-100">
<td className="py-1 pr-2 font-medium">{row.field}</td>
<td className="py-1 text-neutral-600">{row.evidenceText ?? <em className="text-neutral-400">not stated</em>}</td>
<td className="py-1 pl-2 text-neutral-400">{(row.confidence * 100).toFixed(0)}%</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{staged && <ReviewActions reviewItemId={item.id} stagedRecordId={staged.id} />}
</div>
);
})}
</div>
</div>
);
}