spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import Link from 'next/link';2import { Section } from '@/components/ui/section';3import { Badge, MatchBadge, StatusBadge } from '@/components/ui/badge';4import { EmptyState } from '@/components/ui/empty-state';5import { Pagination } from '@/components/ui/pagination';6import { JsonView } from '@/components/ui/json-view';7import { listUnresolved } from '@/lib/queries/admin';8import { resolveAction, rejectAction } from '@/lib/admin/actions';9import { fmtInt, fmtDateTime } from '@/lib/format';10import { str, int, withParams, type SP } from '@/lib/search-params';1112const PAGE_SIZE = 50;1314export default async function UnresolvedPage({ searchParams }: { searchParams: Promise<SP> }) {15 const sp = await searchParams;16 const entityKind = str(sp, 'kind');17 const status = str(sp, 'status', 'open');18 const page = int(sp, 'page', 1, 1, 10_000);19 const msg = str(sp, 'msg');20 const ok = str(sp, 'ok');21 const { rows, total, kinds } = await listUnresolved({ entityKind, status, page, pageSize: PAGE_SIZE });22 const href = (o: Record<string, string | number | null | undefined>) => `/admin/unresolved${withParams({ kind: entityKind, status }, o)}`;2324 return (25 <div className="space-y-6">26 <header className="pt-4">27 <h1 className="text-3xl">Unresolved labels</h1>28 <p className="mt-1 text-[13.5px] text-ink-2">Source labels that no identifier, alias or normalized string could map. Resolving adds an alias to the target entity, marks the label mapped and writes the audit log.</p>29 </header>30 {msg ? (31 <p role="status" className={`border-l-2 pl-3 text-[13px] ${ok === '1' ? 'border-ok text-ok' : 'border-danger text-danger'}`}>32 {msg}33 </p>34 ) : null}35 <div className="flex flex-wrap gap-4 text-[12.5px]">36 <div className="flex flex-wrap items-center gap-1.5">37 <span className="ci-kicker mr-1">Kind</span>38 <Link href={href({ kind: '', page: '' })} className={`border px-2 py-0.5 no-underline ${!entityKind ? 'border-accent bg-accent-soft' : 'border-rule'}`}>39 All40 </Link>41 {kinds.map((k) => (42 <Link key={k.k} href={href({ kind: k.k, page: '' })} className={`border px-2 py-0.5 no-underline ${entityKind === k.k ? 'border-accent bg-accent-soft' : 'border-rule'}`}>43 {k.k} <span className="ci-num text-ink-3">{fmtInt(k.n)}</span>44 </Link>45 ))}46 </div>47 <div className="flex flex-wrap items-center gap-1.5">48 <span className="ci-kicker mr-1">Status</span>49 {['open', 'mapped', 'rejected', 'ignored', ''].map((s) => (50 <Link key={s || 'all'} href={href({ status: s, page: '' })} className={`border px-2 py-0.5 no-underline ${status === s ? 'border-accent bg-accent-soft' : 'border-rule'}`}>51 {s || 'all'}52 </Link>53 ))}54 </div>55 </div>56 <Section id="queue" title={`${fmtInt(total)} labels`} kicker="Queue · sorted by occurrence count">57 {rows.length === 0 ? (58 <EmptyState compact title="Queue empty">No unresolved label matches these filters. Labels are enqueued by connectors when reconciliation fails.</EmptyState>59 ) : (60 <>61 <div className="ci-table-wrap">62 <table className="ci-table">63 <thead>64 <tr>65 <th className="num">Count</th>66 <th>Label</th>67 <th>Kind · source</th>68 <th>Suggestion</th>69 <th>Status</th>70 <th>Actions</th>71 </tr>72 </thead>73 <tbody>74 {rows.map((u) => (75 <tr key={u.id}>76 <td className="num font-medium">{fmtInt(u.count)}</td>77 <td className="min-w-[220px]">78 <span className="font-medium">{u.source_text}</span>79 <span className="ci-mono block text-[11px] text-ink-3">{u.normalized}</span>80 {Object.keys(u.context ?? {}).length ? (81 <details className="text-[11.5px]">82 <summary className="ci-link">context</summary>83 <JsonView data={u.context} />84 </details>85 ) : null}86 <span className="block text-[11px] text-ink-4">#{u.id} · {fmtDateTime(u.updated_at)}</span>87 </td>88 <td>89 <Badge>{u.entity_kind}</Badge> <span className="ci-mono text-[11.5px]">{u.source_slug}</span>90 </td>91 <td className="min-w-[200px] text-[12.5px]">92 {u.suggested_id ? (93 <>94 <span className="ci-mono">{u.suggested_id}</span> {u.suggested_name ? <span>— {u.suggested_name}</span> : null}95 <span className="mt-0.5 flex items-center gap-1">96 <MatchBadge matchType={u.suggested_match_type} />97 {u.suggested_score != null ? <span className="ci-num text-[11px] text-ink-3">score {u.suggested_score.toFixed(2)}</span> : null}98 </span>99 </>100 ) : (101 <span className="text-ink-4">no suggestion</span>102 )}103 </td>104 <td>105 <StatusBadge status={u.status} />106 {u.resolved_id ? <span className="ci-mono block text-[11px]">{u.resolved_id}</span> : null}107 </td>108 <td className="min-w-[280px]">109 {u.status === 'open' ? (110 <div className="space-y-1.5 text-[12.5px]">111 <form action={resolveAction} className="flex flex-wrap gap-1">112 <input type="hidden" name="id" value={u.id} />113 <input name="targetId" defaultValue={u.suggested_id ?? ''} placeholder="CI-CAN-00000000" className="ci-mono w-40 border border-rule-strong bg-white px-1.5 py-0.5 text-[12px]" aria-label="Target entity id" required />114 <input name="reason" placeholder="reason (optional)" className="w-36 border border-rule-strong bg-white px-1.5 py-0.5 text-[12px]" aria-label="Reason" />115 <button type="submit" className="border border-accent bg-accent-soft px-2 py-0.5 text-accent-2 hover:bg-accent hover:text-white">116 Resolve117 </button>118 </form>119 <form action={rejectAction} className="flex flex-wrap gap-1">120 <input type="hidden" name="id" value={u.id} />121 <select name="status" className="border border-rule-strong bg-white px-1 py-0.5 text-[12px]" aria-label="Rejection status">122 <option value="rejected">rejected (not a {u.entity_kind})</option>123 <option value="ignored">ignored</option>124 </select>125 <input name="reason" placeholder="reason (optional)" className="w-36 border border-rule-strong bg-white px-1.5 py-0.5 text-[12px]" aria-label="Reason" />126 <button type="submit" className="border border-rule px-2 py-0.5 text-ink-2 hover:border-danger hover:text-danger">127 Reject128 </button>129 </form>130 </div>131 ) : (132 <span className="text-[12px] text-ink-3">by {u.resolved_by ?? '—'}</span>133 )}134 </td>135 </tr>136 ))}137 </tbody>138 </table>139 </div>140 <Pagination page={page} pageSize={PAGE_SIZE} total={total} hrefFor={(p) => href({ page: p === 1 ? '' : p })} />141 </>142 )}143 </Section>144 </div>145 );146}147