// Risk Rating + Periodic Review views. // // RiskRatingsView — current customer risk ratings, distribution, factor breakdown, // full customer table sortable/filterable by tier. // PeriodicReviewView— review cadence & migration: KPIs, migration matrix, // "deserves attention" list, recent reviews log. const { useState: useRRState, useMemo: useRRMemo } = React; // ── Shared bits ───────────────────────────────────────────────────────────── function RatingPill({ rating, size = "md" }) { // map rating → color tokens const map = { severe: { bg: "var(--risk-high-bg)", fg: "var(--risk-high)", label: "Severe" }, high: { bg: "oklch(95% 0.05 35)", fg: "oklch(56% 0.20 35)",label: "High" }, med: { bg: "var(--risk-med-bg)", fg: "var(--risk-med)", label: "Medium" }, low: { bg: "var(--risk-low-bg)", fg: "var(--risk-low)", label: "Low" }, }; // Dark-mode override for the "high" non-token color so it lifts properly const isDark = document.documentElement.dataset.theme === "dark"; if (isDark && rating === "high") { map.high = { bg: "oklch(28% 0.08 35)", fg: "oklch(76% 0.18 35)", label: "High" }; } const m = map[rating]; const cls = size === "sm" ? "" : ""; return ( {m.label} ); } function ScoreBadge({ score }) { const c = score >= 80 ? "var(--risk-high)" : score >= 60 ? "oklch(56% 0.20 35)" : score >= 35 ? "var(--risk-med)" : "var(--risk-low)"; const isDark = document.documentElement.dataset.theme === "dark"; const cLifted = isDark && score >= 60 && score < 80 ? "oklch(76% 0.18 35)" : c; return ( {score} ); } function MiniTrend({ data, height = 22, width = 70 }) { const min = Math.min(...data), max = Math.max(...data); const range = max - min || 1; const last = data[data.length - 1]; const prev = data[0]; const dir = last > prev ? "up" : last < prev ? "down" : "flat"; const c = dir === "up" ? "var(--risk-high)" : dir === "down" ? "var(--risk-low)" : "var(--fg-3)"; const pts = data.map((v, i) => { const x = (i / (data.length - 1)) * (width - 2) + 1; const y = height - 2 - ((v - min) / range) * (height - 4); return `${x.toFixed(1)},${y.toFixed(1)}`; }); return ( ); } function StackedDistBar() { // Single horizontal bar split by tier const order = ["low", "med", "high", "severe"]; const total = order.reduce((s, k) => s + RATING_DIST[k].count, 0); return (
{order.map(k => { const w = (RATING_DIST[k].count / total) * 100; return (
); })}
{order.slice().reverse().map(k => (
{k === "med" ? "Medium" : k}
{RATING_DIST[k].count.toLocaleString()}
{RATING_DIST[k].pct}%
))}
); } function FactorChart() { return (
{FACTOR_AVG.map(f => (
{f.label}
= 60 ? "var(--risk-high)" : f.value >= 35 ? "var(--risk-med)" : "var(--risk-low)", borderRadius: 3, }} />
{f.value}
))}
); } function RatingTrendChart() { // Stacked area chart, 12 months × 4 tiers const W = 720, H = 180, P = { l: 36, r: 8, t: 8, b: 22 }; const innerW = W - P.l - P.r; const innerH = H - P.t - P.b; const data = RATING_TREND; const total = (m) => m.low + m.med + m.high + m.severe; const maxTotal = Math.max(...data.map(total)); // Y for each tier stack (cumulative from low at bottom up to severe at top) const colors = { low: "var(--risk-low)", med: "var(--risk-med)", high: "oklch(56% 0.20 35)", severe: "var(--risk-high)", }; const isDark = document.documentElement.dataset.theme === "dark"; if (isDark) colors.high = "oklch(76% 0.18 35)"; const buildArea = (key, prevKey) => { const upper = data.map((d, i) => { const x = P.l + (i / (data.length - 1)) * innerW; const y = P.t + innerH - (cum(d, key) / maxTotal) * innerH; return [x, y]; }); const lower = data.slice().reverse().map((d, i) => { const realI = data.length - 1 - i; const x = P.l + (realI / (data.length - 1)) * innerW; const y = P.t + innerH - (cum(d, prevKey) / maxTotal) * innerH; return [x, y]; }); return upper.concat(lower) .map((p, i) => (i === 0 ? "M" : "L") + p[0].toFixed(1) + " " + p[1].toFixed(1)) .join(" ") + " Z"; }; const cum = (d, key) => { if (key === "low") return d.low; if (key === "med") return d.low + d.med; if (key === "high") return d.low + d.med + d.high; if (key === "severe") return d.low + d.med + d.high + d.severe; return 0; }; // Y axis grid lines const yTicks = [0, Math.round(maxTotal / 2), maxTotal]; return ( {/* Y grid */} {yTicks.map((t, i) => { const y = P.t + innerH - (t / maxTotal) * innerH; return ( {t === 0 ? "0" : t >= 1000 ? (t/1000).toFixed(1) + "k" : t} ); })} {/* Stacks: low at bottom up */} "L" + (P.l + (i / (data.length - 1)) * innerW) + " " + (P.t + innerH - (d.low / maxTotal) * innerH)).join(" ") + " L" + (W - P.r) + " " + (P.t + innerH) + " Z"} fill={colors.low} opacity="0.85" /> {/* X labels */} {data.map((d, i) => ( {d.m} ))} {/* Legend */} {["severe","high","med","low"].map((k, i) => ( {k === "med" ? "Medium" : k[0].toUpperCase() + k.slice(1)} ))} ); } // ── Risk Ratings view ─────────────────────────────────────────────────────── function mapRiskProfileToCustomer(p) { const score = Math.round(Number(p.risk_score || 0)); let rating = "low"; if (score >= 80) rating = "severe"; else if (score >= 60) rating = "high"; else if (score >= 35) rating = "med"; return { id: p.entity_id, name: p.entity_id, type: p.entity_type === "business" ? "business" : "person", country: "—", segment: "Live · " + p.entity_type, onboarded: "—", rating, score, prior: "low", priorScore: Math.max(0, score - 10), lastReview: p.last_transaction_at ? p.last_transaction_at.slice(0, 10) : "—", nextReview: "—", overdue: false, factors: { geo: 30, type: 40, product: 50, behavior: score }, trend: [Math.max(0, score - 20), Math.max(0, score - 15), Math.max(0, score - 10), Math.max(0, score - 5), score], flagged: score >= 60, reviewer: "System", reasons: p.risk_factors || ["Live risk profile"], _raw: p, }; } function RiskRatingsView({ onOpenCustomer, data }) { const liveProfiles = (data?.riskProfiles || []).map(mapRiskProfileToCustomer); const customers = liveProfiles.length ? liveProfiles : CUSTOMERS; const [tier, setTier] = useRRState("all"); const [sortBy, setSortBy] = useRRState("score"); const counts = useRRMemo(() => ({ all: customers.length, severe: customers.filter(c => c.rating === "severe").length, high: customers.filter(c => c.rating === "high").length, med: customers.filter(c => c.rating === "med").length, low: customers.filter(c => c.rating === "low").length, flagged:customers.filter(c => c.flagged).length, }), [customers]); const filtered = useRRMemo(() => { let r = customers; if (tier === "severe") r = r.filter(c => c.rating === "severe"); else if (tier === "high") r = r.filter(c => c.rating === "high"); else if (tier === "med") r = r.filter(c => c.rating === "med"); else if (tier === "low") r = r.filter(c => c.rating === "low"); else if (tier === "flagged") r = r.filter(c => c.flagged); r = [...r].sort((a, b) => sortBy === "score" ? b.score - a.score : sortBy === "delta" ? (b.score - b.priorScore) - (a.score - a.priorScore) : sortBy === "name" ? a.name.localeCompare(b.name) : a.lastReview < b.lastReview ? 1 : -1); return r; }, [tier, sortBy, customers]); return ( <>

Population distribution

1,288 rated customers

Avg factor contribution

population mean
{[ { k: "severe", l: "Severe", v: RR_KPIS.severe, c: "var(--risk-high)" }, { k: "newHigh", l: "New high · 30d", v: RR_KPIS.newHigh, c: "oklch(56% 0.20 35)" }, { k: "rerated", l: "Re-rated · MTD", v: RR_KPIS.reratedMTD, c: "var(--fg)" }, { k: "pending", l: "Pending refresh", v: RR_KPIS.pendingRefresh, c: "var(--risk-med)" }, ].map(({ k, l, v, c }) => (
{l}
{v.value}
{v.delta} this month
))}

Customer ratings

{filtered.map(c => { const delta = c.score - c.priorScore; const deltaUp = delta > 0; const tierChanged = c.rating !== c.prior; return ( onOpenCustomer?.(c)}> ); })}
Customer ID Name · Segment Tenant Rating Score Δ vs prior Factor profile Trend · 8mo Last review Next review
{c.id}
{c.country}
{c.type === "business" ? : }
{c.name}
{c.segment}
{tierChanged && (
from {c.prior === "med" ? "med" : c.prior}
)}
/100
{deltaUp && } {delta < 0 && } {delta > 0 ? "+" : ""}{delta} {c.lastReview} {c.overdue && } {c.nextReview} {c.overdue && (
overdue
)}
); } function FactorRow({ factors }) { const keys = [ { k: "geo", label: "Geo" }, { k: "type", label: "Type" }, { k: "product", label: "Prod" }, { k: "behavior", label: "Beh" }, ]; return (
{keys.map(({ k, label }) => { const v = factors[k]; const c = v >= 80 ? "var(--risk-high)" : v >= 60 ? "oklch(56% 0.20 35)" : v >= 35 ? "var(--risk-med)" : "var(--risk-low)"; return (
{label}
); })}
); } // ── Periodic Review view ──────────────────────────────────────────────────── const TIER_IDX = { low: 0, med: 1, high: 2, severe: 3 }; function ChangeArrow({ prior, next, scoreDelta }) { const dx = TIER_IDX[next] - TIER_IDX[prior]; const upgraded = dx > 0; // upgraded = worse (higher tier) const downgraded = dx < 0; const c = upgraded ? "var(--risk-high)" : downgraded ? "var(--risk-low)" : "var(--fg-3)"; return ( {scoreDelta != null && ( {scoreDelta > 0 ? "+" : ""}{scoreDelta} )} ); } function MigrationMatrix() { const rows = MIGRATION.rows; const cells = MIGRATION.cells; const max = Math.max(...cells.flat()); const isDark = document.documentElement.dataset.theme === "dark"; // colour intensity: // cell on diagonal → neutral grey // above diagonal (upgraded to worse) → red intensity // below diagonal (downgraded to better) → green intensity const cellBg = (r, c, v) => { if (v === 0) return "var(--bg-2)"; const t = v / max; if (isDark) { if (r === c) return `oklch(${18 + t * 12}% 0.008 260)`; if (c > r) return `oklch(${22 + t * 30}% ${0.04 + t * 0.16} 22)`; return `oklch(${22 + t * 20}% ${0.04 + t * 0.10} 155)`; } if (r === c) return `oklch(${94 - t * 18}% 0.005 260)`; if (c > r) return `oklch(${94 - t * 36}% ${0.03 + t * 0.14} 22)`; return `oklch(${94 - t * 24}% ${0.02 + t * 0.10} 155)`; }; const cellFg = (r, c, v) => { if (v === 0) return "var(--fg-4)"; if (isDark) { // Dark mode — text always light enough to read on the tinted bg if (v / max < 0.15) return "var(--fg-2)"; return "var(--fg)"; } if (c > r && v / max > 0.4) return "#fff"; if (r === c && v / max > 0.6) return "#fff"; return "var(--fg)"; }; const tierLabel = (t) => t === "med" ? "Medium" : t[0].toUpperCase() + t.slice(1); return (
{rows.map(r => (
{tierLabel(r)}
))} {rows.map((r, ri) => (
{tierLabel(r)}
{rows.map((c, ci) => { const v = cells[ri][ci]; const attention = ci > ri && (ci - ri) >= 2; // big jump upwards return (
0 ? "1px solid var(--risk-high)" : "1px solid transparent", }}>
{v}
{ci === ri && v > 0 && (
no change
)}
); })}
))}
Rows: prior rating Columns: new rating · last 30 days
upgraded · worse downgraded · better needs attention
); } function AttentionList() { // Filter REVIEWS where tier change is big OR is into high/severe const items = REVIEWS.filter(r => { const dx = TIER_IDX[r.new] - TIER_IDX[r.prior]; return dx >= 1 && (r.new === "severe" || dx >= 2 || (r.new === "high" && r.prior !== "high")); }); return (
{items.map((r, i) => { const dx = TIER_IDX[r.new] - TIER_IDX[r.prior]; const big = dx >= 2 || r.new === "severe"; return (
{r.customer} {big && Attention }
{r.id} · {r.reason}
{r.completed} by {r.reviewer}
); })}
); } function PeriodicReviewView() { const [tab, setTab] = useRRState("recent"); return ( <>
{/* KPI strip */}
{[ { ...PR_KPIS.dueMonth, color: "var(--accent)" }, { ...PR_KPIS.overdue, color: "var(--risk-high)" }, { ...PR_KPIS.completed, color: "var(--risk-low)" }, { ...PR_KPIS.ratingChanges, color: "var(--fg)" }, { ...PR_KPIS.escalations, color: "var(--risk-high)" }, { ...PR_KPIS.avgCycle, color: "var(--fg)" }, ].map((k, i) => (
{k.label}
{k.value}
{k.delta} vs last month
))}

Rating migration · last 30 days

456 transitions

Deserves attention

6 customers

Population over 12 months · by rating tier

stacked count

Recent reviews

{REVIEWS.length} reviews · last 5 days
{REVIEWS.map(r => ( ))}
Review Customer Tenant Rating change Reason Reviewer Completed
{r.id} {r.customer} {r.reason} {r.reviewer.split(" ").map(x => x[0]).join("").slice(0,2)} {r.reviewer.split(" ")[0]} {r.completed}
); } Object.assign(window, { RiskRatingsView, PeriodicReviewView });