← back to Dw Boardroom Governance
frontend/src/components/DecisionPanel.tsx
109 lines
import { useState } from 'react';
import { api } from '../api';
import { useApi } from '../hooks/useApi';
import type { Decision } from '../types';
const SCORE_COLORS: Record<string, string> = {
veto: '#ef4444', resolve: '#f59e0b', escalate: '#8b5cf6',
};
function getScoreLabel(score: number | null): { label: string; color: string } {
if (score === null) return { label: '—', color: '#7a7f96' };
if (score <= 2) return { label: 'VETO', color: SCORE_COLORS.veto };
if (score <= 6) return { label: 'RESOLVE', color: SCORE_COLORS.resolve };
return { label: 'ESCALATE', color: SCORE_COLORS.escalate };
}
export default function DecisionPanel() {
const { data: decisions, refresh } = useApi(() => api.getDecisions('pending'), []);
const [topic, setTopic] = useState('');
const [recommendation, setRecommendation] = useState('');
const [decisionType, setDecisionType] = useState('B');
const [creating, setCreating] = useState(false);
async function create() {
if (!topic.trim()) return;
setCreating(true);
try {
await api.createDecision({ topic, recommendation, decisionType, owner: 'operator' });
setTopic('');
setRecommendation('');
refresh();
} catch (err: any) {
alert(err.message);
}
setCreating(false);
}
async function resolve(id: string, action: 'approve' | 'reject' | 'defer') {
const fn = action === 'approve' ? api.approveDecision : action === 'reject' ? api.rejectDecision : api.deferDecision;
try {
await fn(id);
refresh();
} catch (err: any) {
console.error('[DecisionPanel] resolve failed:', err.message);
}
}
return (
<div style={styles.card}>
<h3 style={styles.cardTitle}>Pending Decisions</h3>
{/* Create form */}
<div style={styles.form}>
<input style={styles.input} placeholder="Topic" value={topic} onChange={e => setTopic(e.target.value)} />
<input style={styles.input} placeholder="Recommendation" value={recommendation} onChange={e => setRecommendation(e.target.value)} />
<select style={styles.select} value={decisionType} onChange={e => setDecisionType(e.target.value)}>
<option value="A">A</option><option value="B">B</option><option value="C">C</option>
</select>
<button style={styles.createBtn} onClick={create} disabled={creating}>+ Create</button>
</div>
{/* Decision list */}
<div style={styles.list}>
{(!decisions || decisions.length === 0) && <div style={styles.empty}>No pending decisions.</div>}
{decisions?.map((d: Decision) => {
const { label, color } = getScoreLabel(d.score);
return (
<div key={d.id} style={styles.decision}>
<div style={styles.decisionHeader}>
<span style={{ ...styles.scoreBadge, background: color + '22', color, borderColor: color }}>
{d.score ?? '?'} {label}
</span>
<span style={styles.typeBadge}>{d.decision_type}</span>
<span style={{ flex: 1, fontWeight: 600, fontSize: 13 }}>{d.topic}</span>
</div>
{d.recommendation && <div style={styles.rec}>{d.recommendation}</div>}
{d.reasoning && <div style={styles.reasoning}>{d.reasoning}</div>}
<div style={styles.actions}>
<button style={{ ...styles.actionBtn, background: '#22c55e33', color: '#22c55e' }} onClick={() => resolve(d.id, 'approve')}>Approve</button>
<button style={{ ...styles.actionBtn, background: '#ef444433', color: '#ef4444' }} onClick={() => resolve(d.id, 'reject')}>Reject</button>
<button style={{ ...styles.actionBtn, background: '#3b82f633', color: '#3b82f6' }} onClick={() => resolve(d.id, 'defer')}>Defer</button>
</div>
</div>
);
})}
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
card: { background: '#1a1a2e', borderRadius: 12, padding: 20, border: '1px solid #252540' },
cardTitle: { margin: '0 0 12px', fontSize: 15, fontWeight: 700 },
form: { display: 'flex', gap: 6, marginBottom: 16 },
input: { flex: 1, padding: '8px 10px', borderRadius: 6, border: '1px solid #252540', background: '#0a0a12', color: '#e8eaf0', fontSize: 12, fontFamily: 'inherit' },
select: { padding: '8px', borderRadius: 6, border: '1px solid #252540', background: '#0a0a12', color: '#e8eaf0', fontSize: 12, fontFamily: 'inherit' },
createBtn: { padding: '8px 14px', borderRadius: 6, border: 'none', background: '#8b5cf6', color: '#fff', fontWeight: 600, fontSize: 12, cursor: 'pointer', fontFamily: 'inherit' },
list: { display: 'flex', flexDirection: 'column', gap: 8 },
empty: { color: '#7a7f96', fontSize: 13, textAlign: 'center' as const, padding: 20 },
decision: { background: '#12121f', borderRadius: 8, padding: 12, border: '1px solid #252540' },
decisionHeader: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 6 },
scoreBadge: { padding: '2px 8px', borderRadius: 4, fontSize: 10, fontWeight: 700, border: '1px solid' },
typeBadge: { padding: '2px 6px', borderRadius: 4, fontSize: 10, fontWeight: 700, background: '#3b82f622', color: '#3b82f6' },
rec: { fontSize: 12, color: '#b0b3c0', marginBottom: 4, paddingLeft: 4 },
reasoning: { fontSize: 11, color: '#7a7f96', marginBottom: 8, fontStyle: 'italic', paddingLeft: 4 },
actions: { display: 'flex', gap: 6 },
actionBtn: { padding: '6px 14px', borderRadius: 6, border: 'none', fontWeight: 600, fontSize: 11, cursor: 'pointer', fontFamily: 'inherit' },
};