'use client'; import React, { useState, useEffect } from 'react'; import { formatLocalDate } from '../utils/date'; import { FaGithub, FaGitlab, FaStackOverflow, FaLinkedin, FaGoogle, FaKaggle } from 'react-icons/fa'; import { SiCredly, SiFiverr } from 'react-icons/si'; import { DomainCSVRow } from '../types'; import { red, yellow, green, hexToRgba } from '../colors'; import RiskIcon from './icons/risk'; import CodeIcon from './icons/code'; import AiIcon from './icons/ai'; import { ProviderIcon } from '../utils/provider'; interface SanctionSource { issuingEntity: string; listName: string; matched?: boolean; sublists?: string[]; } interface DomainSource { country?: string; entityType?: string; entityName?: string; url: string; } const PAGE_SIZE = 25; interface BusinessRuleRow { provider: string; category?: string; label?: string; likert_label?: string; likert_value?: number; weight?: number; uid?: string; } interface AppendixTableProps { type: 'sanctions' | 'domains' | 'business_rules'; sources: (string | DomainCSVRow | SanctionSource | BusinessRuleRow)[]; searchedAt: string; developerName: string; // Used for business_rules: map genres (e.g., Technical/Risk) to category arrays genreMapping?: Record; headless?: boolean; } const SanctionsRow = ({ source, searchedAt, developerName, onToggle, expanded, colSpan, headless, }: { source: SanctionSource; searchedAt: string; developerName: string; onToggle: () => void; expanded: boolean; colSpan: number; headless?: boolean; }) => ( <>
{source.issuingEntity} {source.sublists && source.sublists.length > 0 && !headless && ( )}
{source.listName} {searchedAt} {source.matched ? ( Match ) : ( Not Found )} {source.matched ? (Exact or strong match for {developerName} was found on this list.) : (No exact match for {developerName} was found on this list.)} {(headless || expanded) && source.sublists && source.sublists.length > 0 && (
)} ); const DomainRow = ({ source, searchedAt, developerName }: { source: DomainSource, searchedAt: string, developerName: string }) => ( {source.country || 'N/A'} {source.entityType || 'N/A'} {source.entityName || source.url} {searchedAt} Not Found No profile matching {developerName} (based on name and email) was found at this domain. ); const AppendixTables: React.FC = ({ type, sources, searchedAt, developerName, genreMapping, headless }) => { const [visibleCount, setVisibleCount] = useState(PAGE_SIZE); const [expanded, setExpanded] = useState<{ [k: number]: boolean }>({}); const [sortBy, setSortBy] = useState<'pillar' | 'category' | 'label' | 'weight' | null>(null); const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc'); useEffect(() => { const flashIfRule = () => { const hash = typeof window !== 'undefined' ? window.location.hash : ''; if (!hash || !hash.startsWith('#rule-')) return; const id = hash.slice(1); const el = document.getElementById(id) as HTMLElement | null; if (!el) return; try { el.style.scrollMarginTop = '96px'; el.scrollIntoView({ block: 'start', behavior: 'smooth' }); } catch {} el.style.transition = 'background-color 300ms ease'; el.style.backgroundColor = 'rgba(2, 163, 137, 0.14)'; setTimeout(() => { el.style.backgroundColor = ''; }, 1200); }; flashIfRule(); window.addEventListener('hashchange', flashIfRule); return () => window.removeEventListener('hashchange', flashIfRule); }, []); const formattedDate = formatLocalDate(searchedAt, { year: 'numeric', month: 'short', day: 'numeric', // hour: 'numeric', minute: '2-digit', }); const headers = type === 'sanctions' ? ["Issuing Entity", "List Name", "List Searched On", "Value", "Findings"] : type === 'domains' ? ["Country", "Entity Type", "Entity/Domain", "Searched On", "Value", "Findings"] : ["Platform", "KYD Pillar", "Category", "Observation", "Weight Impact"]; const sortableBusinessHeaders: Record = { 'KYD Pillar': 'pillar', 'Category': 'category', 'Observation': 'label', 'Weight Impact': 'weight', }; const onHeaderClick = (header: string) => { if (type !== 'business_rules') return; const key = sortableBusinessHeaders[header as keyof typeof sortableBusinessHeaders]; if (!key) return; if (sortBy === key) { setSortDir(d => (d === 'asc' ? 'desc' : 'asc')); } else { setSortBy(key); setSortDir('asc'); } }; // Build quick lookup: category -> KYD Pillar (e.g., Technical or Risk) const categoryToPillar: Record = (() => { const map: Record = {}; try { const gm = genreMapping || {}; Object.keys(gm || {}).forEach((genre) => { const cats = (gm as Record)[genre] || []; cats.forEach((c) => { if (typeof c === 'string') map[c] = genre; }); }); } catch {} return map; })(); const parsedSources = type === 'sanctions' ? (sources).map((src) => { if (typeof src === 'string') { const parts = src.split(':'); return { issuingEntity: parts[0].trim(), listName: parts.slice(1).join(':').trim(), matched: false } as SanctionSource; } return src as SanctionSource; }) : type === 'domains' ? (sources as DomainCSVRow[]).map(s => ({ country: s.Country, entityType: s['Entity Type'], entityName: s['Entity Name'], url: s.URL, })) : (sources as BusinessRuleRow[]) .filter(s => !((s?.provider || '').toLowerCase().includes('linkedin'))) .filter(s => (s?.label || '') !== 'Sanctions & Criminal Exposure') .map(s => ({ provider: s.provider, // Keep original category label; pillar derived from genreMapping category: s.category || '', label: s.label || '', likert_label: s.likert_label || '', likert_value: s.likert_value || 0, weight: s.weight || 0, uid: s.uid || '', pillar: categoryToPillar[(s.category || '')] || '', })); const sortedParsedSources = (type === 'sanctions') ? (parsedSources as SanctionSource[]).slice().sort((a, b) => (b.matched === true ? 1 : 0) - (a.matched === true ? 1 : 0)) : type === 'business_rules' ? (() => { const orderForPillar = (p: string) => (p === 'Technical' ? 0 : p === 'Risk' ? 1 : p === 'AI' ? 2 : 3); const items = (parsedSources as (BusinessRuleRow & { pillar?: string })[]).slice(); if (!sortBy) { return items.sort((a, b) => { const ap = (a as any).pillar || ''; const bp = (b as any).pillar || ''; const cmpPillar = orderForPillar(ap) - orderForPillar(bp); if (cmpPillar !== 0) return cmpPillar; const ac = (a.category || '').localeCompare(b.category || ''); if (ac !== 0) return ac; return Number(b.weight || 0) - Number(a.weight || 0); }); } return items.sort((a, b) => { const dir = sortDir === 'asc' ? 1 : -1; if (sortBy === 'pillar') { return dir * (orderForPillar((a as any).pillar || '') - orderForPillar((b as any).pillar || '')); } if (sortBy === 'category') { return dir * ((a.category || '').localeCompare(b.category || '')); } if (sortBy === 'label') { return dir * ((a.label || '').localeCompare(b.label || '')); } if (sortBy === 'weight') { return dir * (Number(a.weight || 0) - Number(b.weight || 0)); } return 0; }); })() : parsedSources; const visibleParsedSources = (type === 'business_rules') ? sortedParsedSources : (headless ? sortedParsedSources : sortedParsedSources.slice(0, visibleCount)); const handleLoadMore = () => { setVisibleCount(currentCount => currentCount + PAGE_SIZE); }; if (!sources || sources.length === 0) { return null; } return (
{headers.map((_, idx) => ( ))} {headers.map(header => { const isSortable = type === 'business_rules' && Object.prototype.hasOwnProperty.call(sortableBusinessHeaders, header); const isActive = isSortable && sortBy === sortableBusinessHeaders[header as keyof typeof sortableBusinessHeaders]; const arrow = isActive ? (sortDir === 'asc' ? ' ▲' : ' ▼') : ''; return ( ); })} {visibleParsedSources.map((source, index) => { if (type === 'sanctions') { const src = source as SanctionSource; return ( setExpanded(prev => ({ ...prev, [index]: !prev[index] }))} expanded={!!expanded[index]} colSpan={headers.length} headless={headless} /> ); } if (type === 'domains') { return } const br = source as BusinessRuleRow & { pillar?: string }; const anchorId = br.uid ? `rule-${br.uid}` : undefined; return ( { const w = Number(br.weight); if (!Number.isFinite(w)) return undefined; const hex = w > 0 ? green : w < 0 ? red : yellow; return hexToRgba(hex, 0.06); })(), }} > ); })}
onHeaderClick(header)} className={'px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider'} style={{ color: 'var(--text-secondary)', cursor: isSortable ? 'pointer' : 'default', userSelect: isSortable ? 'none' : 'auto' }} aria-sort={isActive ? (sortDir === 'asc' ? 'ascending' : 'descending') : 'none'} > {header}{arrow}
{br.pillar || '—'} {br.category || '—'} {br.label || '—'} {Number.isFinite(Number(br.weight)) ? br.weight : '—'}
{type !== 'business_rules' && parsedSources.length > PAGE_SIZE && !headless && (

Showing {Math.min(visibleCount, parsedSources.length)} of {parsedSources.length} entries

{visibleCount < parsedSources.length && ( )}
)}
); }; export default AppendixTables; // deprecated SharedBadgeDisplay code, storing here for later {/*

Country-specific Entity Affiliations

*/}