Spaces:
Runtime error
Runtime error
| import React from 'react' | |
| const CAT_LABEL = { | |
| termination: 'Termination', | |
| auto_renewal: 'Auto-renewal', | |
| term: 'Term / Duration', | |
| liability_cap: 'Liability Cap', | |
| indemnification: 'Indemnification', | |
| governing_law: 'Governing Law', | |
| confidentiality: 'Confidentiality', | |
| payment_terms: 'Payment Terms', | |
| sla: 'Service Levels', | |
| data_protection: 'Data Protection', | |
| ip_ownership: 'IP Ownership', | |
| insurance: 'Insurance', | |
| audit_rights: 'Audit Rights', | |
| assignment: 'Assignment', | |
| exclusivity: 'Exclusivity', | |
| force_majeure: 'Force Majeure', | |
| warranty: 'Warranty', | |
| notice: 'Notice', | |
| deliverables: 'Deliverables', | |
| } | |
| const COLS = ['HIGH', 'MEDIUM', 'LOW'] | |
| function cellBg(level, count) { | |
| if (count === 0) return 'transparent' | |
| const alpha = Math.min(0.18 + count * 0.28, 0.92) | |
| if (level === 'HIGH') return `rgba(201,42,42,${alpha})` | |
| if (level === 'MEDIUM') return `rgba(217,117,11,${alpha})` | |
| return `rgba(91,100,120,${alpha})` | |
| } | |
| function cellColor(level, count) { | |
| if (count === 0) return '#ccc' | |
| if (count >= 2) return '#fff' | |
| if (level === 'HIGH') return '#c92a2a' | |
| if (level === 'MEDIUM') return '#d9750b' | |
| return '#5b6478' | |
| } | |
| export default function RiskHeatmap({ risks, onSelect }) { | |
| if (!risks.length) return <p className="muted pad">No risks to display.</p> | |
| // build grid: category -> { HIGH, MEDIUM, LOW } -> [risk, ...] | |
| const grid = {} | |
| risks.forEach((r) => { | |
| if (!grid[r.category]) grid[r.category] = { HIGH: [], MEDIUM: [], LOW: [] } | |
| grid[r.category][r.level].push(r) | |
| }) | |
| const cats = Object.keys(grid).sort((a, b) => { | |
| const maxSev = (cat) => Math.max(...risks.filter(r => r.category === cat).map(r => r.severity)) | |
| return maxSev(b) - maxSev(a) | |
| }) | |
| return ( | |
| <div className="heatmap-wrap"> | |
| <div className="heatmap-legend"> | |
| <span className="hm-dot hm-high" /> HIGH | |
| <span className="hm-dot hm-med" /> MEDIUM | |
| <span className="hm-dot hm-low" /> LOW | |
| <span className="hm-hint">— click a cell to see its risks</span> | |
| </div> | |
| <table className="heatmap"> | |
| <thead> | |
| <tr> | |
| <th className="hm-cat-hdr">Category</th> | |
| {COLS.map(c => ( | |
| <th key={c} className={`hm-col-hdr hm-col-${c.toLowerCase()}`}>{c}</th> | |
| ))} | |
| <th className="hm-col-hdr">Total</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {cats.map((cat) => { | |
| const row = grid[cat] | |
| const total = COLS.reduce((s, l) => s + row[l].length, 0) | |
| return ( | |
| <tr key={cat}> | |
| <td className="hm-cat">{CAT_LABEL[cat] || cat}</td> | |
| {COLS.map((level) => { | |
| const n = row[level].length | |
| const firstRisk = row[level][0] | |
| return ( | |
| <td | |
| key={level} | |
| className="hm-cell" | |
| style={{ | |
| background: cellBg(level, n), | |
| color: cellColor(level, n), | |
| cursor: n > 0 ? 'pointer' : 'default', | |
| }} | |
| title={n > 0 ? row[level].map(r => r.title).join('; ') : ''} | |
| onClick={() => n > 0 && firstRisk && onSelect(firstRisk.clause_id)} | |
| > | |
| {n > 0 ? n : <span className="hm-empty">·</span>} | |
| </td> | |
| ) | |
| })} | |
| <td className="hm-total">{total}</td> | |
| </tr> | |
| ) | |
| })} | |
| </tbody> | |
| </table> | |
| </div> | |
| ) | |
| } | |