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