import React, { useState, useMemo } from 'react'; import { Risk, Project } from '../../types'; import { getUserEmail } from '../../lib/formatters'; import { AlertTriangle, ShieldAlert, CheckCircle2, ChevronRight, X, Info } from 'lucide-react'; import { HelpTooltip } from './HelpTooltip'; interface RiskHeatmapMatrixProps { risks: Risk[]; projects?: Project[]; compact?: boolean; onSelectRisk?: (risk: Risk) => void; title?: string; subtitle?: string; } // Standard 5-point likelihood and impact levels const LIKELIHOOD_LEVELS = [ { val: 1, label: 'Rare', short: '1 - Rare' }, { val: 2, label: 'Unlikely', short: '2 - Unlikely' }, { val: 3, label: 'Possible', short: '3 - Possible' }, { val: 4, label: 'Likely', short: '4 - Likely' }, { val: 5, label: 'Almost Certain', short: '5 - Almost Certain' }, ]; const IMPACT_LEVELS = [ { val: 5, label: 'Severe', short: '5 - Severe' }, { val: 4, label: 'Critical', short: '4 - Critical' }, { val: 3, label: 'Major', short: '3 - Major' }, { val: 2, label: 'Moderate', short: '2 - Moderate' }, { val: 1, label: 'Minor', short: '1 - Minor' }, ]; export const getCellSeverity = (score: number) => { if (score >= 15) { return { name: 'Critical', bgClass: 'bg-rose-950/70 border-rose-600/70 text-rose-100 hover:bg-rose-900/90', badgeClass: 'bg-rose-500/20 text-rose-300 border-rose-500/40', textColor: 'text-rose-400', }; } if (score >= 10) { return { name: 'High', bgClass: 'bg-orange-950/60 border-orange-700/60 text-orange-200 hover:bg-orange-900/80', badgeClass: 'bg-orange-500/20 text-orange-300 border-orange-500/40', textColor: 'text-orange-400', }; } if (score >= 5) { return { name: 'Moderate', bgClass: 'bg-amber-950/50 border-amber-800/50 text-amber-300 hover:bg-amber-900/70', badgeClass: 'bg-amber-500/20 text-amber-300 border-amber-500/40', textColor: 'text-amber-400', }; } return { name: 'Low', bgClass: 'bg-emerald-950/40 border-emerald-800/40 text-emerald-300 hover:bg-emerald-900/60', badgeClass: 'bg-emerald-500/20 text-emerald-300 border-emerald-500/40', textColor: 'text-emerald-400', }; }; export const RiskHeatmapMatrix: React.FC = ({ risks, projects = [], compact = false, onSelectRisk, title, subtitle, }) => { const [matrixMode, setMatrixMode] = useState<'5x5' | '3x3'>('5x5'); const [selectedCell, setSelectedCell] = useState<{ impactLabel: string; likelihoodLabel: string; score: number; risks: Risk[]; } | null>(null); // Map project names for easy lookup const projectMap = useMemo(() => { const map: Record = {}; projects.forEach((p) => { map[String(p.id)] = p.name; }); return map; }, [projects]); // Pre-calculate risk counts by exact coordinates (1-5) const matrix5x5 = useMemo(() => { const grid: Record = {}; for (let imp = 1; imp <= 5; imp++) { for (let lik = 1; lik <= 5; lik++) { grid[`${imp}-${lik}`] = []; } } risks.forEach((r) => { const imp = Math.min(5, Math.max(1, Number(r.impact) || 1)); const lik = Math.min(5, Math.max(1, Number(r.likelihood) || 1)); grid[`${imp}-${lik}`].push(r); }); return grid; }, [risks]); // Pre-calculate 3x3 summary matrix const matrix3x3 = useMemo(() => { const buckets: Record = { 'high-low': [], 'high-med': [], 'high-high': [], 'med-low': [], 'med-med': [], 'med-high': [], 'low-low': [], 'low-med': [], 'low-high': [], }; risks.forEach((r) => { const imp = Number(r.impact) || 1; const lik = Number(r.likelihood) || 1; const impKey = imp >= 4 ? 'high' : imp === 3 ? 'med' : 'low'; const likKey = lik >= 4 ? 'high' : lik === 3 ? 'med' : 'low'; buckets[`${impKey}-${likKey}`].push(r); }); return buckets; }, [risks]); // Overall severity summary counters const summaryCounters = useMemo(() => { let low = 0; let moderate = 0; let high = 0; let critical = 0; risks.forEach((r) => { const score = (Number(r.impact) || 1) * (Number(r.likelihood) || 1); if (score >= 15) critical++; else if (score >= 10) high++; else if (score >= 5) moderate++; else low++; }); return { low, moderate, high, critical }; }, [risks]); const handleCellClick = (impactLabel: string, likelihoodLabel: string, score: number, cellRisks: Risk[]) => { if (cellRisks.length === 0) { if (selectedCell) setSelectedCell(null); return; } setSelectedCell({ impactLabel, likelihoodLabel, score, risks: cellRisks, }); }; return (
{/* Header & Controls */}
{title &&

{title}

}
{subtitle &&

{subtitle}

}
{/* Mode Switcher */}
{/* Summary Score Badges */}
Critical (15–25) {summaryCounters.critical}
High (10–14) {summaryCounters.high}
Moderate (5–9) {summaryCounters.moderate}
Low (1–4) {summaryCounters.low}
{/* 5x5 Matrix Mode */} {matrixMode === '5x5' && (
{/* Top axis label: Likelihood */}
Likelihood / Probability →
{/* Left Y-Axis Label: Impact */}
↑ Impact / Severity
{/* Grid with Row Headers */}
{/* Column Headers */}
Impact \ Lik.
{LIKELIHOOD_LEVELS.map((lik) => (
{lik.short}
))}
{/* Rows (Impact 5 down to 1) */}
{IMPACT_LEVELS.map((imp) => (
{/* Row Header */}
{imp.short}
{/* 5 Cells */} {LIKELIHOOD_LEVELS.map((lik) => { const cellRisks = matrix5x5[`${imp.val}-${lik.val}`] || []; const score = imp.val * lik.val; const sev = getCellSeverity(score); const hasRisks = cellRisks.length > 0; const isSelected = selectedCell?.impactLabel === imp.label && selectedCell?.likelihoodLabel === lik.label; return ( ); })}
))}
)} {/* 3x3 Summary Mode */} {matrixMode === '3x3' && (
{/* Top axis label: Likelihood */}
Likelihood / Probability →
{/* Left Y-Axis Label: Impact */}
↑ Impact / Severity
{/* 3x3 Table */}
{/* Column Headers */}
Impact \ Lik.
Low (1–2 Rare/Unlikely)
Medium (3 Possible)
High (4–5 Likely/Certain)
{/* Row 1: High Impact (4-5) */}
High (4–5 Major/Crit.)
{/* High Imp - Low Lik */} {(() => { const cRisks = matrix3x3['high-low']; return ( ); })()} {/* High Imp - Med Lik */} {(() => { const cRisks = matrix3x3['high-med']; return ( ); })()} {/* High Imp - High Lik */} {(() => { const cRisks = matrix3x3['high-high']; return ( ); })()}
{/* Row 2: Medium Impact (3) */}
Medium (3 Moderate)
{/* Med Imp - Low Lik */} {(() => { const cRisks = matrix3x3['med-low']; return ( ); })()} {/* Med Imp - Med Lik */} {(() => { const cRisks = matrix3x3['med-med']; return ( ); })()} {/* Med Imp - High Lik */} {(() => { const cRisks = matrix3x3['med-high']; return ( ); })()}
{/* Row 3: Low Impact (1-2) */}
Low (1–2 Minor)
{/* Low Imp - Low Lik */} {(() => { const cRisks = matrix3x3['low-low']; return ( ); })()} {/* Low Imp - Med Lik */} {(() => { const cRisks = matrix3x3['low-med']; return ( ); })()} {/* Low Imp - High Lik */} {(() => { const cRisks = matrix3x3['low-high']; return ( ); })()}
)} {/* Selected Cell Inspector (Drill-down) */} {selectedCell && selectedCell.risks.length > 0 && (

Risks in Box: Impact [{selectedCell.impactLabel}] × Likelihood [{selectedCell.likelihoodLabel}]

Residual Score: {selectedCell.score} / 25 • Total Items: {selectedCell.risks.length}

{selectedCell.risks.map((rk) => (
onSelectRisk && onSelectRisk(rk)} className={`pt-2 first:pt-0 ${onSelectRisk ? 'cursor-pointer hover:bg-slate-800/30 p-2 rounded-xl transition' : ''}`} >
{rk.name} Project #{rk.projectId} {projectMap[String(rk.projectId)] ? `(${projectMap[String(rk.projectId)]})` : ''}
{rk.description && (

{rk.description}

)}
Likelihood: {rk.likelihood} Impact: {rk.impact} Owner: {getUserEmail(rk.owner)}
{rk.mitigationAction && (
Mitigation: {rk.mitigationAction}
)}
))}
)}
); };