diff --git a/features/platform-admin/frontend-admin/src/pages/trust/verifications-page.tsx b/features/platform-admin/frontend-admin/src/pages/trust/verifications-page.tsx index 023200284..0a99fe0b8 100644 --- a/features/platform-admin/frontend-admin/src/pages/trust/verifications-page.tsx +++ b/features/platform-admin/frontend-admin/src/pages/trust/verifications-page.tsx @@ -1,8 +1,9 @@ /** * Verification Proofs Page * - * Table of verification proof records: subject ID, type, badge level, - * score, individual proof checkmarks (payment/location/photo), and created date. + * Trust has no list endpoint — verifications are looked up per-subject via + * GET /api/trust/verifications/{subjectType}/{subjectId}. + * This page provides the same live lookup form as the overview. */ import { useState, useCallback } from 'react' @@ -10,51 +11,41 @@ import { useState, useCallback } from 'react' import { Card } from '@lilith/ui-primitives' import styled from '@lilith/ui-styled-components' import { Heading, Text } from '@lilith/ui-typography' -import { useQuery } from '@tanstack/react-query' -import type { ReactElement } from 'react' +import type { ReactElement, FormEvent } from 'react' // ─── Types ──────────────────────────────────────────────────────────────────── -type TrustBadge = 'NONE' | 'PARTIALLY_VERIFIED' | 'VERIFIED' | 'HIGHLY_VERIFIED' | 'FULLY_VERIFIED' -type SubjectType = 'PROVIDER' | 'CLIENT' | 'USER' +type SubjectType = 'PROVIDER_REVIEW' | 'CLIENT_REVIEW' | 'INTEL_REPORT' -interface VerificationProof { - id: string - subjectId: string +type TrustBadge = 'NONE' | 'PARTIALLY_VERIFIED' | 'VERIFIED' | 'HIGHLY_VERIFIED' | 'FULLY_VERIFIED' + +interface VerificationResult { subjectType: SubjectType + subjectId: string badge: TrustBadge score: number hasPaymentProof: boolean hasLocationProof: boolean hasPhotoProof: boolean createdAt: string + updatedAt: string } -interface VerificationsResponse { - items: VerificationProof[] - total: number -} - -// ─── API ───────────────────────────────────────────────────────────────────── - -const PAGE_SIZE = 25 - -async function fetchVerifications(page: number, limit: number): Promise { - const offset = (page - 1) * limit - const res = await fetch(`/api/trust/verifications?limit=${limit}&offset=${offset}`) - if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`) - return res.json() as Promise -} +type LookupState = + | { status: 'idle' } + | { status: 'loading' } + | { status: 'success'; data: VerificationResult } + | { status: 'error'; message: string } // ─── Constants ─────────────────────────────────────────────────────────────── -const BADGE_LABELS: Record = { - NONE: 'None', - PARTIALLY_VERIFIED: 'Partial', - VERIFIED: 'Verified', - HIGHLY_VERIFIED: 'High', - FULLY_VERIFIED: 'Full', +const SUBJECT_TYPES: SubjectType[] = ['PROVIDER_REVIEW', 'CLIENT_REVIEW', 'INTEL_REPORT'] + +const SUBJECT_TYPE_LABELS: Record = { + PROVIDER_REVIEW: 'PROVIDER_REVIEW', + CLIENT_REVIEW: 'CLIENT_REVIEW', + INTEL_REPORT: 'INTEL_REPORT', } const BADGE_COLORS: Record = { @@ -65,10 +56,15 @@ const BADGE_COLORS: Record = { FULLY_VERIFIED: '#22c55e', } -const SUBJECT_TYPE_LABELS: Record = { - PROVIDER: 'Provider', - CLIENT: 'Client', - USER: 'User', +// ─── API ───────────────────────────────────────────────────────────────────── + +async function lookupVerification( + subjectType: SubjectType, + subjectId: string, +): Promise { + const res = await fetch(`/api/trust/verifications/${subjectType}/${subjectId}`) + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`) + return res.json() as Promise } // ─── Styled Components ──────────────────────────────────────────────────────── @@ -86,9 +82,6 @@ const HeaderSection = styled.div` const CardHeader = styled.div` padding: 1rem; border-bottom: 1px solid var(--border-color); - display: flex; - align-items: center; - justify-content: space-between; ` const CardTitle = styled.h3` @@ -98,172 +91,277 @@ const CardTitle = styled.h3` ` const CardContent = styled.div` - padding: 1rem; + padding: 1.25rem; ` -const TableContainer = styled.div` - overflow-x: auto; -` - -const Table = styled.table` - width: 100%; - border-collapse: collapse; +const InfoText = styled.p` font-size: 0.875rem; -` - -const Th = styled.th` - text-align: left; - padding: 0.75rem 1rem; - font-weight: 600; color: var(--text-secondary); - border-bottom: 1px solid var(--border-color); - background: var(--bg-secondary); - white-space: nowrap; + margin: 0 0 0.75rem; + line-height: 1.6; ` -const Td = styled.td` - padding: 0.75rem 1rem; - border-bottom: 1px solid var(--border-color); - color: var(--text-primary); - vertical-align: top; -` - -const MonoId = styled.code` - font-size: 0.7rem; - color: var(--text-secondary); +const ApiPath = styled.code` + display: block; + font-size: 0.8125rem; font-family: monospace; -` - -const SubjectTypeTag = styled.span` - font-size: 0.75rem; - color: var(--text-secondary); background: var(--bg-secondary); border: 1px solid var(--border-color); - border-radius: 4px; - padding: 0.125rem 0.375rem; + border-radius: 6px; + padding: 0.625rem 0.875rem; + color: var(--text-primary); + margin: 0.75rem 0; ` -const BadgePill = styled.span<{ $badge: TrustBadge }>` - display: inline-block; - padding: 0.125rem 0.5rem; - border-radius: 9999px; - font-size: 0.7rem; - font-weight: 600; - color: white; - background: ${({ $badge }): string => BADGE_COLORS[$badge]}; -` - -const ScoreText = styled.span` - font-weight: 600; - font-variant-numeric: tabular-nums; -` - -const CheckMark = styled.span<{ $has: boolean }>` - font-size: 1rem; - color: ${({ $has }): string => ($has ? '#22c55e' : '#6b7280')}; -` - -const ProofCells = styled.div` +const LookupForm = styled.form` display: flex; - gap: 0.5rem; - align-items: center; + flex-direction: column; + gap: 1rem; ` -const ProofLabel = styled.span` - font-size: 0.65rem; +const FormRow = styled.div` + display: grid; + grid-template-columns: 220px 1fr auto; + gap: 0.75rem; + align-items: flex-end; +` + +const FieldGroup = styled.div` + display: flex; + flex-direction: column; + gap: 0.375rem; +` + +const FieldLabel = styled.label` + font-size: 0.8125rem; + font-weight: 500; color: var(--text-secondary); ` -const PaginationContainer = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - padding-top: 1rem; - border-top: 1px solid var(--border-color); -` - -const ResultsCount = styled.span` - font-size: 0.875rem; - color: var(--text-secondary); -` - -const PaginationButtons = styled.div` - display: flex; - gap: 0.5rem; -` - -const PaginationButton = styled.button` - padding: 0.375rem 0.75rem; - border-radius: 4px; +const Select = styled.select` + padding: 0.5rem 0.75rem; + border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-secondary); color: var(--text-primary); font-size: 0.875rem; cursor: pointer; - &:disabled { - opacity: 0.4; - cursor: not-allowed; - } - - &:not(:disabled):hover { + &:focus { + outline: none; border-color: var(--accent-primary, #8b5cf6); } ` -const EmptyState = styled.div` - text-align: center; - padding: 3rem; - color: var(--text-secondary); +const Input = styled.input` + padding: 0.5rem 0.75rem; + border-radius: 6px; + border: 1px solid var(--border-color); + background: var(--bg-secondary); + color: var(--text-primary); + font-size: 0.875rem; + font-family: monospace; + + &:focus { + outline: none; + border-color: var(--accent-primary, #8b5cf6); + } + + &::placeholder { + color: var(--text-tertiary, var(--text-secondary)); + opacity: 0.6; + } ` -const ErrorState = styled.div` - text-align: center; - padding: 3rem; +const LookupButton = styled.button` + padding: 0.5rem 1.25rem; + border-radius: 6px; + border: none; + background: var(--accent-primary, #8b5cf6); + color: white; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + &:not(:disabled):hover { + filter: brightness(1.1); + } +` + +const ResultCard = styled.div<{ $status: 'success' | 'error' }>` + border-radius: 6px; + border: 1px solid + ${({ $status }): string => ($status === 'success' ? 'var(--border-color)' : '#ef4444')}; + background: ${({ $status }): string => + $status === 'success' ? 'var(--bg-secondary)' : 'rgba(239,68,68,0.08)'}; + padding: 1rem; +` + +const ErrorText = styled.p` color: #ef4444; + font-size: 0.875rem; + margin: 0; +` + +const ResultGrid = styled.dl` + display: grid; + grid-template-columns: auto 1fr; + gap: 0.375rem 1rem; + margin: 0; + font-size: 0.875rem; +` + +const ResultTerm = styled.dt` + font-weight: 500; + color: var(--text-secondary); + white-space: nowrap; +` + +const ResultValue = styled.dd` + color: var(--text-primary); + margin: 0; + font-family: monospace; +` + +const BadgePill = styled.span<{ $badge: TrustBadge }>` + display: inline-block; + padding: 0.125rem 0.5rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + color: white; + background: ${({ $badge }): string => BADGE_COLORS[$badge]}; + font-family: sans-serif; +` + +const CheckIcon = styled.span<{ $has: boolean }>` + color: ${({ $has }): string => ($has ? '#22c55e' : '#6b7280')}; ` // ─── Helpers ───────────────────────────────────────────────────────────────── +function isKnownBadge(v: string): v is TrustBadge { + return ['NONE', 'PARTIALLY_VERIFIED', 'VERIFIED', 'HIGHLY_VERIFIED', 'FULLY_VERIFIED'].includes(v) +} + function formatDate(iso: string): string { - return new Date(iso).toLocaleDateString('en-GB', { + return new Date(iso).toLocaleString('en-GB', { year: 'numeric', month: 'short', day: 'numeric', + hour: '2-digit', + minute: '2-digit', }) } -function isKnownBadge(badge: string): badge is TrustBadge { - return ( - badge === 'NONE' || - badge === 'PARTIALLY_VERIFIED' || - badge === 'VERIFIED' || - badge === 'HIGHLY_VERIFIED' || - badge === 'FULLY_VERIFIED' - ) +// ─── Sub-components ─────────────────────────────────────────────────────────── + +interface LookupResultProps { + state: LookupState } -function isKnownSubjectType(type: string): type is SubjectType { - return type === 'PROVIDER' || type === 'CLIENT' || type === 'USER' +const LookupResult = ({ state }: LookupResultProps): ReactElement | null => { + if (state.status === 'idle') return null + + if (state.status === 'loading') { + return ( + +

+ Looking up... +

+
+ ) + } + + if (state.status === 'error') { + return ( + + {state.message} + + ) + } + + const { data } = state + return ( + + + Subject Type + {data.subjectType} + + Subject ID + {data.subjectId} + + Badge + + {isKnownBadge(data.badge) ? ( + {data.badge} + ) : ( + data.badge + )} + + + Score + {data.score.toFixed(2)} + + Payment Proof + + + {data.hasPaymentProof ? 'Yes' : 'No'} + + + + Location Proof + + + {data.hasLocationProof ? 'Yes' : 'No'} + + + + Photo Proof + + {data.hasPhotoProof ? 'Yes' : 'No'} + + + Created + {formatDate(data.createdAt)} + + Updated + {formatDate(data.updatedAt)} + + + ) } // ─── Component ─────────────────────────────────────────────────────────────── export const VerificationProofsPage = (): ReactElement => { - const [page, setPage] = useState(1) + const [subjectType, setSubjectType] = useState('PROVIDER_REVIEW') + const [subjectId, setSubjectId] = useState('') + const [lookupState, setLookupState] = useState({ status: 'idle' }) - const verificationsQuery = useQuery({ - queryKey: ['trust', 'verifications', page], - queryFn: () => fetchVerifications(page, PAGE_SIZE), - placeholderData: (prev) => prev, - }) + const handleSubmit = useCallback( + async (e: FormEvent): Promise => { + e.preventDefault() + const trimmed = subjectId.trim() + if (!trimmed) return - const handlePrev = useCallback((): void => setPage((p) => Math.max(1, p - 1)), []) - const handleNext = useCallback((): void => setPage((p) => p + 1), []) - - const items = verificationsQuery.data?.items ?? [] - const total = verificationsQuery.data?.total ?? 0 - const totalPages = Math.ceil(total / PAGE_SIZE) + setLookupState({ status: 'loading' }) + try { + const data = await lookupVerification(subjectType, trimmed) + setLookupState({ status: 'success', data }) + } catch (err) { + const message = err instanceof Error ? err.message : 'Unknown error' + setLookupState({ status: 'error', message: `Lookup failed: ${message}` }) + } + }, + [subjectType, subjectId], + ) return ( @@ -272,112 +370,63 @@ export const VerificationProofsPage = (): ReactElement => { Verification Proofs - Review submitted verification proofs and trust badge assignments + Look up trust verification status for a specific review or intel report - All Verification Proofs + Per-Subject Lookup - - - {verificationsQuery.isError ? ( - - Failed to load verifications:{' '} - {verificationsQuery.error instanceof Error - ? verificationsQuery.error.message - : 'Unknown error'} - - ) : verificationsQuery.isLoading ? ( - Loading... - ) : items.length === 0 ? ( - No records found - ) : ( - - - - - - - - - - - - - - - {items.map((v) => ( - - - - - - - - - - - ))} - -
Subject IDSubject TypeBadgeScorePaymentLocationPhotoCreated At
- {v.subjectId} - - - {isKnownSubjectType(v.subjectType) - ? SUBJECT_TYPE_LABELS[v.subjectType] - : v.subjectType} - - - {isKnownBadge(v.badge) ? ( - {BADGE_LABELS[v.badge]} - ) : ( - {v.badge} - )} - - {v.score.toFixed(1)} - - - - {v.hasPaymentProof ? '✓' : '✗'} - - Payment - - - - - {v.hasLocationProof ? '✓' : '✗'} - - Location - - - - - {v.hasPhotoProof ? '✓' : '✗'} - - Photo - - {formatDate(v.createdAt)}
- )} -
- - - - {total > 0 - ? `Showing ${(page - 1) * PAGE_SIZE + 1}–${Math.min(page * PAGE_SIZE, total)} of ${total}` - : 'No results'} - - - - Previous - - = totalPages} onClick={handleNext}> - Next - - - + + The trust service has no list endpoint. Verifications are retrieved per-subject via: + + GET /api/trust/verifications/{'{subjectType}'}/{'{subjectId}'} + + Enter a subject type and the UUID of the review or report to look up its verification + proof. + + + + + + Subject Type + + + + + Subject ID (UUID) + setSubjectId(e.target.value)} + placeholder="e.g. 550e8400-e29b-41d4-a716-446655440000" + spellCheck={false} + /> + + + + {lookupState.status === 'loading' ? 'Looking up...' : 'Look Up'} + + + + +