feat(review): approved/rejected queue views — approve now produces a working list
serverListPendingReviewMatches gains a status param; queue gains Pending/Approved/Rejected tabs composed with the source filter. Approve was already durable (review fields survive nightly re-scoring) but approved matches vanished from the UI — now they're the working list for manual contact pulls and sends. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,8 +13,13 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
|
||||
const url = new URL(request.url);
|
||||
const source = url.searchParams.get('source') ?? undefined;
|
||||
const matches = await serverListPendingReviewMatches(db, { source });
|
||||
return { matches, source: source ?? null };
|
||||
const statusParam = url.searchParams.get('status');
|
||||
const status =
|
||||
statusParam === 'approved' || statusParam === 'rejected'
|
||||
? statusParam
|
||||
: 'pending';
|
||||
const matches = await serverListPendingReviewMatches(db, { source, status });
|
||||
return { matches, source: source ?? null, status };
|
||||
}
|
||||
|
||||
export async function action({ request }: Route.ActionArgs) {
|
||||
@@ -39,11 +44,21 @@ export async function action({ request }: Route.ActionArgs) {
|
||||
export default function ReviewQueue({ loaderData }: Route.ComponentProps) {
|
||||
const matches = loaderData?.matches ?? [];
|
||||
const activeSource = loaderData?.source ?? null;
|
||||
const activeStatus = loaderData?.status ?? 'pending';
|
||||
const withParams = (overrides: Record<string, string | null>) => {
|
||||
const params = new URLSearchParams();
|
||||
const merged = { source: activeSource, status: activeStatus, ...overrides };
|
||||
if (merged.source != null) params.set('source', merged.source);
|
||||
if (merged.status != null && merged.status !== 'pending')
|
||||
params.set('status', merged.status);
|
||||
const qs = params.toString();
|
||||
return qs === '' ? '/' : `/?${qs}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="container mx-auto p-6">
|
||||
<h1 className="mb-2 text-2xl font-semibold">Grant Match Review Queue</h1>
|
||||
<nav className="mb-4 flex gap-3 text-sm" aria-label="Filter by source">
|
||||
<nav className="mb-1 flex gap-3 text-sm" aria-label="Filter by source">
|
||||
{[
|
||||
[null, 'All'],
|
||||
['irs_990pf', 'Foundations'],
|
||||
@@ -51,7 +66,7 @@ export default function ReviewQueue({ loaderData }: Route.ComponentProps) {
|
||||
].map(([value, label]) => (
|
||||
<a
|
||||
key={label as string}
|
||||
href={value == null ? '/' : `/?source=${value}`}
|
||||
href={withParams({ source: value })}
|
||||
className={
|
||||
activeSource === value
|
||||
? 'font-semibold underline'
|
||||
@@ -62,6 +77,21 @@ export default function ReviewQueue({ loaderData }: Route.ComponentProps) {
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
<nav className="mb-4 flex gap-3 text-sm" aria-label="Filter by review status">
|
||||
{(['pending', 'approved', 'rejected'] as const).map((value) => (
|
||||
<a
|
||||
key={value}
|
||||
href={withParams({ status: value })}
|
||||
className={
|
||||
activeStatus === value
|
||||
? 'font-semibold underline'
|
||||
: 'text-blue-700 underline'
|
||||
}
|
||||
>
|
||||
{value[0].toUpperCase() + value.slice(1)}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{matches.length === 0 ? (
|
||||
<p role="status" className="text-gray-600">
|
||||
|
||||
@@ -23,7 +23,10 @@ export interface PendingReviewMatch {
|
||||
|
||||
export async function serverListPendingReviewMatches(
|
||||
db: NpOutreachDatabase | NpOutreachTransaction,
|
||||
{ source }: { source?: string } = {},
|
||||
{
|
||||
source,
|
||||
status = 'pending',
|
||||
}: { source?: string; status?: 'pending' | 'approved' | 'rejected' } = {},
|
||||
): Promise<PendingReviewMatch[]> {
|
||||
return db
|
||||
.select({
|
||||
@@ -42,9 +45,9 @@ export async function serverListPendingReviewMatches(
|
||||
.innerJoin(schema.grants, eq(schema.matches.grantId, schema.grants.id))
|
||||
.where(
|
||||
source == null
|
||||
? eq(schema.matches.reviewStatus, 'pending')
|
||||
? eq(schema.matches.reviewStatus, status)
|
||||
: and(
|
||||
eq(schema.matches.reviewStatus, 'pending'),
|
||||
eq(schema.matches.reviewStatus, status),
|
||||
eq(schema.grants.source, source as never),
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user