← back to Dw Boardroom Governance

frontend/src/components/AgendaBuilder.tsx

227 lines

import { useState } from 'react';
import { api } from '../api';
import type { DecisionType } from '../types';

interface AgendaItem {
  driver: string;
  topic: string;
  recommendation: string;
  decisionType: DecisionType;
  decisionRequired: boolean;
}

interface TopicSuggestion {
  topic: string;
  recommendation: string;
  decisionType: DecisionType;
  source: string;
}

// Smart topic suggestions based on live system state
async function fetchSuggestions(): Promise<TopicSuggestion[]> {
  const suggestions: TopicSuggestion[] = [];

  try {
    // Pull pending decisions that need discussion
    const decisions = await api.getDecisions('pending');
    if (decisions && decisions.length > 0) {
      decisions.slice(0, 3).forEach(d => {
        suggestions.push({
          topic: d.title || d.description?.slice(0, 60) || 'Pending Decision',
          recommendation: d.description || 'Review and decide',
          decisionType: (d.decision_type as DecisionType) || 'B',
          source: 'pending-decision',
        });
      });
    }
  } catch {}

  try {
    // Pull active escalations
    const escalations = await api.getActiveEscalations();
    if (escalations && escalations.length > 0) {
      escalations.slice(0, 2).forEach(e => {
        suggestions.push({
          topic: `Escalation: ${e.title || e.description?.slice(0, 50) || 'Review needed'}`,
          recommendation: 'Requires immediate executive attention',
          decisionType: 'C',
          source: 'escalation',
        });
      });
    }
  } catch {}

  try {
    // Pull initiative status updates
    const initiatives = await api.getInitiatives();
    if (initiatives && initiatives.length > 0) {
      const active = initiatives.filter(i => i.status === 'active' || i.status === 'planning');
      active.slice(0, 2).forEach(i => {
        suggestions.push({
          topic: `Initiative Update: ${i.title || i.name || 'Unnamed'}`,
          recommendation: `Status: ${i.status} — review progress`,
          decisionType: 'B',
          source: 'initiative',
        });
      });
    }
  } catch {}

  // Always add some standing agenda items if we have room
  const standing: TopicSuggestion[] = [
    { topic: 'Agent Performance Review', recommendation: 'Review uptime, task completion, and efficiency metrics', decisionType: 'B', source: 'standing' },
    { topic: 'Budget & Spend Analysis', recommendation: 'Review API costs, infrastructure spend, ROI', decisionType: 'C', source: 'standing' },
    { topic: 'Product Pipeline Status', recommendation: 'New SKUs, catalog imports, vendor updates', decisionType: 'B', source: 'standing' },
    { topic: 'Customer Issues & Zendesk', recommendation: 'Open tickets, response times, escalations', decisionType: 'B', source: 'standing' },
    { topic: 'Security & Compliance Check', recommendation: 'Review access logs, settlement compliance, data protection', decisionType: 'C', source: 'standing' },
    { topic: 'Vendor Relationship Updates', recommendation: 'New contracts, pricing changes, catalog access', decisionType: 'B', source: 'standing' },
    { topic: 'Marketing & SEO Performance', recommendation: 'Traffic trends, conversion rates, campaign results', decisionType: 'A', source: 'standing' },
    { topic: 'Infrastructure & Uptime Report', recommendation: 'Server health, PM2 processes, error rates', decisionType: 'A', source: 'standing' },
  ];

  // Add standing items to fill up to 8 total suggestions
  const remaining = 8 - suggestions.length;
  if (remaining > 0) {
    // Shuffle standing items for variety
    const shuffled = standing.sort(() => Math.random() - 0.5);
    suggestions.push(...shuffled.slice(0, remaining));
  }

  return suggestions;
}

const SOURCE_COLORS: Record<string, { bg: string; label: string; color: string }> = {
  'pending-decision': { bg: '#f59e0b22', label: 'PENDING', color: '#f59e0b' },
  'escalation': { bg: '#ef444422', label: 'ESCALATION', color: '#ef4444' },
  'initiative': { bg: '#8b5cf622', label: 'INITIATIVE', color: '#8b5cf6' },
  'standing': { bg: '#22c55e22', label: 'SUGGESTED', color: '#22c55e' },
};

export default function AgendaBuilder({ onRun, disabled }: { onRun: (items: AgendaItem[]) => void; disabled: boolean }) {
  const [items, setItems] = useState<AgendaItem[]>([]);
  const [topic, setTopic] = useState('');
  const [recommendation, setRecommendation] = useState('');
  const [decisionType, setDecisionType] = useState<DecisionType>('B');
  const [suggestions, setSuggestions] = useState<TopicSuggestion[]>([]);
  const [showSuggestions, setShowSuggestions] = useState(false);
  const [loadingSuggestions, setLoadingSuggestions] = useState(false);

  function addItem() {
    if (!topic.trim()) return;
    if (items.length >= 5) { alert('Max 5 agenda items (President rule).'); return; }
    setItems([...items, { driver: 'operator', topic, recommendation, decisionType, decisionRequired: true }]);
    setTopic('');
    setRecommendation('');
  }

  function addSuggestion(s: TopicSuggestion) {
    if (items.length >= 5) { alert('Max 5 agenda items.'); return; }
    setItems([...items, { driver: 'system', topic: s.topic, recommendation: s.recommendation, decisionType: s.decisionType, decisionRequired: true }]);
    // Remove from suggestions list
    setSuggestions(prev => prev.filter(x => x !== s));
  }

  function removeItem(idx: number) {
    setItems(items.filter((_, i) => i !== idx));
  }

  async function loadSuggestions() {
    setLoadingSuggestions(true);
    setShowSuggestions(true);
    try {
      const s = await fetchSuggestions();
      setSuggestions(s);
    } catch {
      setSuggestions([]);
    }
    setLoadingSuggestions(false);
  }

  return (
    <div>
      {/* Current agenda items */}
      {items.map((item, i) => (
        <div key={i} style={styles.item}>
          <span style={styles.itemType}>{item.decisionType}</span>
          <span style={{ flex: 1, fontSize: 12 }}>{item.topic}</span>
          <button style={styles.removeBtn} onClick={() => removeItem(i)}>x</button>
        </div>
      ))}

      {/* Manual topic entry */}
      <div style={styles.form}>
        <input style={styles.input} placeholder="Topic" value={topic} onChange={e => setTopic(e.target.value)}
          onKeyDown={e => { if (e.key === 'Enter') addItem(); }} />
        <input style={styles.input} placeholder="Recommendation" value={recommendation} onChange={e => setRecommendation(e.target.value)}
          onKeyDown={e => { if (e.key === 'Enter') addItem(); }} />
        <select style={styles.select} value={decisionType} onChange={e => setDecisionType(e.target.value as DecisionType)}>
          <option value="A">A - Autonomous</option>
          <option value="B">B - Advisory</option>
          <option value="C">C - Controlled</option>
        </select>
        <button style={styles.addBtn} onClick={addItem} disabled={items.length >= 5}>+ Add</button>
      </div>

      {/* Suggest Topics button */}
      <div style={{ marginTop: 10 }}>
        <button style={styles.suggestBtn} onClick={loadSuggestions} disabled={loadingSuggestions}>
          {loadingSuggestions ? '... Loading' : '💡 Suggest Topics'}
        </button>
      </div>

      {/* Topic suggestions panel */}
      {showSuggestions && suggestions.length > 0 && (
        <div style={styles.suggestPanel}>
          <div style={styles.suggestHeader}>
            <span>Suggested Topics</span>
            <button style={{ ...styles.removeBtn, fontSize: 12 }} onClick={() => setShowSuggestions(false)}>Close</button>
          </div>
          {suggestions.map((s, i) => {
            const sc = SOURCE_COLORS[s.source] || SOURCE_COLORS.standing;
            return (
              <div key={i} style={styles.suggestItem} onClick={() => addSuggestion(s)}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                  <span style={{ ...styles.sourceBadge, background: sc.bg, color: sc.color, borderColor: sc.color + '44' }}>
                    {sc.label}
                  </span>
                  <span style={{ ...styles.itemType, fontSize: 9 }}>{s.decisionType}</span>
                  <span style={{ fontSize: 12, fontWeight: 600, color: '#e8eaf0' }}>{s.topic}</span>
                </div>
                <div style={{ fontSize: 11, color: '#7a7f96', marginTop: 2, marginLeft: 28 }}>{s.recommendation}</div>
              </div>
            );
          })}
        </div>
      )}

      {/* Run button */}
      <div style={{ marginTop: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <span style={{ fontSize: 11, color: '#7a7f96' }}>{items.length}/5 items</span>
        <button
          style={{ ...styles.runBtn, opacity: items.length === 0 || disabled ? 0.4 : 1 }}
          onClick={() => onRun(items)}
          disabled={items.length === 0 || disabled}
        >
          Run Meeting with Agenda
        </button>
      </div>
    </div>
  );
}

const styles: Record<string, React.CSSProperties> = {
  item: { display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px', background: '#12121f', borderRadius: 6, marginBottom: 4, border: '1px solid #252540' },
  itemType: { width: 22, height: 22, borderRadius: 4, background: '#8b5cf633', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 10, fontWeight: 700, color: '#8b5cf6', flexShrink: 0 },
  removeBtn: { background: 'none', border: 'none', color: '#ef4444', cursor: 'pointer', fontWeight: 700, fontSize: 14, fontFamily: 'inherit' },
  form: { display: 'flex', gap: 6, marginTop: 12 },
  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' },
  addBtn: { padding: '8px 14px', borderRadius: 6, border: '1px solid #8b5cf6', background: '#8b5cf633', color: '#e8eaf0', fontWeight: 600, fontSize: 12, cursor: 'pointer', fontFamily: 'inherit' },
  runBtn: { padding: '10px 20px', borderRadius: 8, border: 'none', background: 'linear-gradient(135deg, #8b5cf6, #6366f1)', color: '#fff', fontWeight: 700, fontSize: 13, cursor: 'pointer', fontFamily: 'inherit' },
  suggestBtn: { padding: '8px 16px', borderRadius: 8, border: '1px solid #f59e0b44', background: '#f59e0b11', color: '#f59e0b', fontWeight: 600, fontSize: 12, cursor: 'pointer', fontFamily: 'inherit', width: '100%' },
  suggestPanel: { marginTop: 8, padding: 10, background: '#0a0a14', borderRadius: 8, border: '1px solid #252540', maxHeight: 280, overflowY: 'auto' as const },
  suggestHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8, fontSize: 11, fontWeight: 700, color: '#7a7f96', textTransform: 'uppercase' as const, letterSpacing: 0.5 },
  suggestItem: { padding: '8px 10px', borderRadius: 6, border: '1px solid #252540', marginBottom: 4, cursor: 'pointer', transition: 'background 0.15s', background: '#12121f' },
  sourceBadge: { display: 'inline-block', padding: '2px 6px', borderRadius: 4, fontSize: 9, fontWeight: 700, letterSpacing: 0.3, border: '1px solid' },
};