← 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' },
};