// Automations — rule builder, templates, webhooks, approvals (() => { const { Card, Badge, Switch } = window.TechyFuelOSDesignSystem_be0222; // ── Config maps ─────────────────────────────────────────────────── const TRIGGER_TYPES = [ { id: 'task_status_change', label: 'Task status changes', icon: 'refresh-cw', group: 'Tasks' }, { id: 'due_date_approaching', label: 'Due date is approaching', icon: 'clock', group: 'Tasks' }, { id: 'task_created', label: 'New task is created', icon: 'circle-plus', group: 'Tasks' }, { id: 'task_assigned', label: 'Task is assigned to member', icon: 'user-check', group: 'Tasks' }, { id: 'invoice_paid', label: 'Invoice is marked Paid', icon: 'check-circle', group: 'Finance' }, { id: 'schedule_weekly', label: 'Every week (day of week)', icon: 'calendar', group: 'Schedule' }, { id: 'schedule_monthly', label: 'Every month (day of month)', icon: 'calendar-days', group: 'Schedule' }, ]; const ACTION_TYPES = [ { id: 'change_status', label: 'Change task status', icon: 'arrow-right-circle' }, { id: 'assign_task', label: 'Assign task to member', icon: 'user-plus' }, { id: 'notify_client', label: 'Notify client via portal', icon: 'bell' }, { id: 'send_reminder', label: 'Send email reminder', icon: 'mail' }, { id: 'create_task', label: 'Create a new task', icon: 'circle-plus' }, { id: 'webhook', label: 'Call a webhook URL', icon: 'webhook' }, ]; const STATUSES = ['backlog','todo','in_progress','review','done']; const STATUS_LABELS = { backlog: 'Backlog', todo: 'To do', in_progress: 'In progress', review: 'In review', done: 'Done' }; const DAYS = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']; const TONE = { pending: 'warning', approved: 'success', rejected: 'danger', enabled: 'success', disabled: 'neutral' }; function fmtDate(ts) { if (!ts) return '—'; return new Date(ts).toLocaleDateString('en', { month: 'short', day: 'numeric', year: 'numeric' }); } function fmtTime(ts) { if (!ts) return '—'; return new Date(ts).toLocaleString('en', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); } // ── Rule description builder ────────────────────────────────────── function describeRule(rule) { const tc = rule.trigger_config || {}; const ac = rule.action_config || {}; let trigger = ''; switch (rule.trigger_type) { case 'task_status_change': trigger = `task moves to "${STATUS_LABELS[tc.to_status] || tc.to_status || 'any'}"`; break; case 'due_date_approaching': trigger = `due date is ${tc.days_before || 1} day(s) away`; break; case 'task_created': trigger = 'new task is created'; break; case 'task_assigned': trigger = 'task is assigned'; break; case 'invoice_paid': trigger = 'invoice is paid'; break; case 'schedule_weekly': trigger = `every ${tc.day_of_week || 'Monday'}`; break; case 'schedule_monthly': trigger = `every month on the ${tc.day_of_month || 1}${ordinal(tc.day_of_month || 1)}`; break; default: trigger = rule.trigger_type; } let action = ''; switch (rule.action_type) { case 'change_status': action = `move task to "${STATUS_LABELS[ac.new_status] || ac.new_status}"`; break; case 'assign_task': action = `assign to member`; break; case 'notify_client': action = 'notify the client via portal'; break; case 'send_reminder': action = `send email reminder`; break; case 'create_task': action = `create task "${ac.task_title || 'untitled'}"`; break; case 'webhook': action = `call webhook`; break; default: action = rule.action_type; } return `When ${trigger} → ${action}`; } function ordinal(n) { const s = ['th','st','nd','rd']; const v = n % 100; return s[(v - 20) % 10] || s[v] || s[0]; } // ── Automation engine (client-side) ────────────────────────────── async function runAutomationEngine(rules, team, onLog) { if (!window.API || !rules.length) return; const now = new Date(); const todayStr = now.toISOString().slice(0, 10); const dayOfWeek = DAYS[now.getDay() === 0 ? 6 : now.getDay() - 1]; const dayOfMonth = now.getDate(); let ran = 0; for (const rule of rules) { if (!rule.enabled) continue; const tc = rule.trigger_config || {}; const ac = rule.action_config || {}; try { if (rule.trigger_type === 'schedule_weekly') { const lastRun = rule.last_run_at ? rule.last_run_at.slice(0, 10) : ''; if (tc.day_of_week !== dayOfWeek || lastRun === todayStr) continue; await executeAction(rule, ac, team, null); await window.API.updateRule(rule.id, { run_count: (rule.run_count || 0) + 1, last_run_at: now.toISOString() }); onLog?.(`✅ "${rule.name}" ran (${dayOfWeek} schedule)`); ran++; } if (rule.trigger_type === 'schedule_monthly') { const lastRun = rule.last_run_at ? rule.last_run_at.slice(0, 10) : ''; if (Number(tc.day_of_month) !== dayOfMonth || lastRun === todayStr) continue; await executeAction(rule, ac, team, null); await window.API.updateRule(rule.id, { run_count: (rule.run_count || 0) + 1, last_run_at: now.toISOString() }); onLog?.(`✅ "${rule.name}" ran (monthly day ${dayOfMonth})`); ran++; } if (rule.trigger_type === 'due_date_approaching') { const r = await window.API.getTasks(); const tasks = r.data || []; const targetDays = Number(tc.days_before || 1); const matching = tasks.filter(t => { if (!t.due_date || t.status === 'done') return false; const diff = Math.round((new Date(t.due_date) - now) / 86400000); return diff === targetDays; }); for (const task of matching) { await executeAction(rule, ac, team, task); ran++; } if (matching.length) { await window.API.updateRule(rule.id, { run_count: (rule.run_count || 0) + matching.length, last_run_at: now.toISOString() }); onLog?.(`✅ "${rule.name}" → ${matching.length} task(s) matched`); } } } catch {} } return ran; } async function executeAction(rule, ac, team, task) { if (!window.API) return; switch (rule.action_type) { case 'change_status': if (task && ac.new_status) await window.API.updateTask(task.id, { status: ac.new_status }); break; case 'assign_task': if (task && ac.member_id) await window.API.updateTask(task.id, { assigned_to: ac.member_id }); break; case 'create_task': if (ac.task_title) { const payload = { title: ac.task_title, status: ac.task_status || 'todo', priority: ac.task_priority || 'medium' }; if (ac.assigned_member_id) payload.assigned_to = ac.assigned_member_id; await window.API.createTask(payload); } break; case 'webhook': if (ac.webhook_url) { try { await fetch(ac.webhook_url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rule: rule.name, trigger: rule.trigger_type, task: task || null, timestamp: new Date().toISOString() }) }); } catch {} } break; case 'send_reminder': // Log for now — full email requires server console.log('[TF Automation] Reminder:', rule.name, task?.title); break; } } // ── Rule builder modal ──────────────────────────────────────────── function RuleModal({ open, rule, team, projects, onClose, onSave }) { const [form, setForm] = React.useState({ name: '', trigger_type: 'task_status_change', trigger_config: {}, action_type: 'change_status', action_config: {}, enabled: true }); const [saving, setSaving] = React.useState(false); React.useEffect(() => { if (open) setForm(rule ? { name: rule.name, trigger_type: rule.trigger_type, trigger_config: rule.trigger_config || {}, action_type: rule.action_type, action_config: rule.action_config || {}, enabled: rule.enabled !== false } : { name: '', trigger_type: 'task_status_change', trigger_config: {}, action_type: 'change_status', action_config: {}, enabled: true }); }, [open, rule]); function setF(k, v) { setForm(f => ({ ...f, [k]: v })); } function setTC(k, v) { setForm(f => ({ ...f, trigger_config: { ...f.trigger_config, [k]: v } })); } function setAC(k, v) { setForm(f => ({ ...f, action_config: { ...f.action_config, [k]: v } })); } async function handleSave() { if (!form.name.trim()) return; setSaving(true); try { await onSave(form, rule?.id); onClose(); } finally { setSaving(false); } } if (!open) return null; const inputS = { width: '100%', height: 36, padding: '0 10px', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-md)', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', outline: 'none', boxSizing: 'border-box' }; const selectS = { ...inputS }; const labelS = { display: 'block', fontSize: 'var(--text-xs)', fontWeight: 'var(--fw-bold)', color: 'var(--text-muted)', marginBottom: 6, textTransform: 'uppercase', letterSpacing: '0.07em' }; return (
e.stopPropagation()}>

{rule ? 'Edit automation' : 'New automation rule'}

{/* Name */}
setF('name', e.target.value)} placeholder="e.g. Notify client when task is done" />
{/* IF — Trigger */}
IF (Trigger)
{/* Trigger config fields */} {form.trigger_type === 'task_status_change' && (
)} {form.trigger_type === 'due_date_approaching' && (
setTC('days_before', Number(e.target.value))} />
)} {form.trigger_type === 'schedule_weekly' && (
)} {form.trigger_type === 'schedule_monthly' && (
)}
{/* THEN — Action */}
THEN (Action)
{/* Action config fields */} {form.action_type === 'change_status' && (
)} {form.action_type === 'assign_task' && (
)} {form.action_type === 'create_task' && (
setAC('task_title', e.target.value)} placeholder="Task title…" />
)} {form.action_type === 'send_reminder' && (
setAC('message', e.target.value)} placeholder="Reminder message…" />
)} {form.action_type === 'webhook' && (
setAC('webhook_url', e.target.value)} placeholder="https://…" />
)}
); } // ── Template builder modal ──────────────────────────────────────── function TemplateModal({ open, template, team, projects, onClose, onSave }) { const emptyTask = { title: '', status: 'todo', priority: 'medium', due_offset_days: '', assigned_role: '' }; const [form, setForm] = React.useState({ name: '', description: '', tasks: [{ ...emptyTask }] }); const [saving, setSaving] = React.useState(false); React.useEffect(() => { if (open) setForm(template ? { name: template.name, description: template.description || '', tasks: template.tasks?.length ? template.tasks : [{ ...emptyTask }] } : { name: '', description: '', tasks: [{ ...emptyTask }] }); }, [open, template]); function setF(k, v) { setForm(f => ({ ...f, [k]: v })); } function setTask(i, k, v) { setForm(f => { const tasks = [...f.tasks]; tasks[i] = { ...tasks[i], [k]: v }; return { ...f, tasks }; }); } function addTask() { setForm(f => ({ ...f, tasks: [...f.tasks, { ...emptyTask }] })); } function removeTask(i) { setForm(f => ({ ...f, tasks: f.tasks.filter((_, j) => j !== i) })); } async function handleSave() { if (!form.name.trim()) return; setSaving(true); try { await onSave({ name: form.name, description: form.description, tasks: form.tasks.filter(t => t.title.trim()) }, template?.id); onClose(); } finally { setSaving(false); } } if (!open) return null; const inputS = { width: '100%', height: 33, padding: '0 8px', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-sm)', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', outline: 'none', boxSizing: 'border-box' }; const labelS = { display: 'block', fontSize: 'var(--text-xs)', fontWeight: 'var(--fw-bold)', color: 'var(--text-muted)', marginBottom: 5, textTransform: 'uppercase', letterSpacing: '0.06em' }; return (
e.stopPropagation()}>

{template ? 'Edit template' : 'New task template'}

setF('name', e.target.value)} placeholder="e.g. New client onboarding" />
setF('description', e.target.value)} placeholder="What is this template for?" />
Tasks ({form.tasks.length})
{form.tasks.map((task, i) => (
setTask(i, 'title', e.target.value)} /> setTask(i, 'due_offset_days', e.target.value)} />
))}
); } // ── Webhook modal ───────────────────────────────────────────────── function WebhookModal({ open, webhook, onClose, onSave }) { const ALL_EVENTS = ['task.created','task.completed','task.assigned','invoice.paid','project.created','client.added']; const [form, setForm] = React.useState({ name: '', url: '', events: [], secret: '', enabled: true }); const [saving, setSaving] = React.useState(false); const [testing, setTesting] = React.useState(false); const [testResult, setTestResult] = React.useState(null); React.useEffect(() => { if (open) setForm(webhook ? { name: webhook.name, url: webhook.url, events: webhook.events || [], secret: webhook.secret || '', enabled: webhook.enabled !== false } : { name: '', url: '', events: [], secret: '', enabled: true }); }, [open, webhook]); function toggleEvent(e) { setForm(f => ({ ...f, events: f.events.includes(e) ? f.events.filter(x => x !== e) : [...f.events, e] })); } async function handleSave() { if (!form.url.trim()) return; setSaving(true); try { await onSave(form, webhook?.id); onClose(); } finally { setSaving(false); } } async function testWebhook() { if (!form.url.trim()) return; setTesting(true); setTestResult(null); try { const res = await fetch(form.url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ event: 'test', source: 'TechyFuel OS', timestamp: new Date().toISOString() }) }); setTestResult({ ok: res.ok, status: res.status }); } catch { setTestResult({ ok: false, status: 'Network error' }); } setTesting(false); } if (!open) return null; const inputS = { width: '100%', height: 36, padding: '0 10px', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-md)', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', outline: 'none', boxSizing: 'border-box' }; const labelS = { display: 'block', fontSize: 'var(--text-xs)', fontWeight: 'var(--fw-bold)', color: 'var(--text-muted)', marginBottom: 6, textTransform: 'uppercase', letterSpacing: '0.07em' }; return (
e.stopPropagation()}>

{webhook ? 'Edit webhook' : 'New webhook'}

setForm(f => ({ ...f, name: e.target.value }))} placeholder="e.g. Slack notifier" />
setForm(f => ({ ...f, url: e.target.value }))} placeholder="https://your-server.com/webhook" />
{testResult &&
{testResult.ok ? `✓ Success (HTTP ${testResult.status})` : `✗ Failed: ${testResult.status}`}
}
setForm(f => ({ ...f, secret: e.target.value }))} placeholder="Sent as X-TF-Secret header" type="password" />
{ALL_EVENTS.map(ev => ( ))}
); } // ── Apply template modal ────────────────────────────────────────── function ApplyTemplateModal({ open, template, team, projects, onClose, onApply }) { const [projectId, setProjectId] = React.useState(''); const [applying, setApplying] = React.useState(false); const [done, setDone] = React.useState(null); React.useEffect(() => { if (open) { setProjectId(''); setDone(null); } }, [open]); async function handleApply() { if (!template) return; setApplying(true); try { const r = await window.API.applyTemplate(template.id, projectId || null); setDone(r.data?.length || 0); } finally { setApplying(false); } } if (!open || !template) return null; return (
e.stopPropagation()}>

Apply template

This will create {template.tasks?.length || 0} tasks from "{template.name}".

{done !== null ? (
{done} tasks created!
) : ( <>
)}
); } // ── Main Automations component ──────────────────────────────────── function Automations() { useLucide(); const [tab, setTab] = React.useState('rules'); const [rules, setRules] = React.useState([]); const [templates, setTemplates] = React.useState([]); const [webhooks, setWebhooks] = React.useState([]); const [approvals, setApprovals] = React.useState([]); const [team, setTeam] = React.useState([]); const [projects, setProjects] = React.useState([]); const [loading, setLoading] = React.useState(true); const [ruleModal, setRuleModal] = React.useState(null); // null | 'new' | rule object const [tmplModal, setTmplModal] = React.useState(null); const [whModal, setWhModal] = React.useState(null); const [applyModal, setApplyModal] = React.useState(null); // template to apply const [runLog, setRunLog] = React.useState([]); const [running, setRunning] = React.useState(false); const [approvalFilter, setApprovalFilter] = React.useState('pending'); const pendingCount = approvals.filter(a => a.status === 'pending').length; React.useEffect(() => { if (!window.API) { setLoading(false); return; } (async () => { try { const [rr, tr, pr, wr, ar, tmr] = await Promise.all([ window.API.getRules(), window.API.getTeam(), window.API.getProjects(), window.API.getWebhooks(), window.API.getTaskApprovals(), window.API.getTemplates(), ]); if (rr.data) setRules(rr.data); if (tr.data) setTeam(tr.data); if (pr.data) setProjects(pr.data); if (wr.data) setWebhooks(wr.data); if (ar.data) setApprovals(ar.data); if (tmr.data) setTemplates(tmr.data); } catch {} setLoading(false); })(); }, []); async function saveRule(form, id) { const payload = { name: form.name, trigger_type: form.trigger_type, trigger_config: form.trigger_config, action_type: form.action_type, action_config: form.action_config, enabled: form.enabled }; if (id) { const { data } = await window.API.updateRule(id, payload); if (data) setRules(prev => prev.map(r => r.id === id ? { ...r, ...data } : r)); } else { const { data } = await window.API.createRule(payload); if (data) setRules(prev => [data, ...prev]); } } async function saveTemplate(form, id) { if (id) { const { data } = await window.API.updateTemplate(id, form); if (data) setTemplates(prev => prev.map(t => t.id === id ? { ...t, ...data } : t)); } else { const { data } = await window.API.createTemplate(form); if (data) setTemplates(prev => [data, ...prev]); } } async function saveWebhook(form, id) { if (id) { const { data } = await window.API.updateWebhook(id, form); if (data) setWebhooks(prev => prev.map(w => w.id === id ? { ...w, ...data } : w)); } else { const { data } = await window.API.createWebhook(form); if (data) setWebhooks(prev => [data, ...prev]); } } async function toggleRule(id, enabled) { await window.API.updateRule(id, { enabled }); setRules(prev => prev.map(r => r.id === id ? { ...r, enabled } : r)); } async function deleteRule(id) { await window.API.deleteRule(id); setRules(prev => prev.filter(r => r.id !== id)); } async function handleRunEngine() { setRunning(true); setRunLog([]); const logs = []; await runAutomationEngine(rules, team, msg => { logs.push(msg); setRunLog([...logs]); }); if (!logs.length) setRunLog(['ℹ️ No rules matched current conditions.']); // Refresh rules (run counts) const { data } = await window.API.getRules(); if (data) setRules(data); setRunning(false); } async function resolveApproval(id, status, taskId, comment) { const nextStatus = status === 'approved' ? 'in_progress' : 'todo'; await window.API.resolveApproval(id, status, comment || null, taskId, status === 'approved' ? nextStatus : null); setApprovals(prev => prev.map(a => a.id === id ? { ...a, status, comment: comment || null, resolved_at: new Date().toISOString() } : a)); } const TABS = [ { id: 'rules', label: 'Automation rules', icon: 'zap', count: rules.filter(r => r.enabled).length }, { id: 'templates', label: 'Task templates', icon: 'layout-list', count: templates.length }, { id: 'webhooks', label: 'Webhooks', icon: 'webhook', count: webhooks.length }, { id: 'approvals', label: 'Approvals', icon: 'shield-check', count: pendingCount }, ]; const btnS = { display: 'inline-flex', alignItems: 'center', gap: 7, height: 36, padding: '0 14px', background: 'var(--blue-600)', color: '#fff', border: 'none', borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-brand)', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', fontWeight: 'var(--fw-semibold)', cursor: 'pointer' }; const outS = { display: 'inline-flex', alignItems: 'center', gap: 7, height: 36, padding: '0 14px', background: 'var(--slate-0)', color: 'var(--text-body)', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-md)', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', fontWeight: 'var(--fw-semibold)', cursor: 'pointer' }; return (
{/* Header */}

Automations

{rules.filter(r => r.enabled).length} active rules · {webhooks.filter(w => w.enabled).length} webhooks

{tab === 'rules' && <> } {tab === 'templates' && } {tab === 'webhooks' && }
{/* Tab nav */}
{TABS.map(t => ( ))}
{loading &&
Loading…
} {/* ── Rules tab ── */} {!loading && tab === 'rules' && (
{runLog.length > 0 && (
{runLog.map((l, i) =>
{l}
)}
)} {rules.length === 0 && (
No automation rules yet
Create rules like "When task is Done → notify client" or "Every Monday → create standup task"
)} {rules.map(rule => { const trig = TRIGGER_TYPES.find(t => t.id === rule.trigger_type); const act = ACTION_TYPES.find(a => a.id === rule.action_type); return (
{rule.name}
{describeRule(rule)}
{rule.run_count > 0 && Ran {rule.run_count}×{rule.last_run_at ? ` · ${fmtTime(rule.last_run_at)}` : ''}}
{rule.enabled ? 'On' : 'Off'}
{/* Visual IF/THEN chips */}
IF {trig?.label || rule.trigger_type} THEN {act?.label || rule.action_type}
); })}
)} {/* ── Templates tab ── */} {!loading && tab === 'templates' && (
{templates.length === 0 && (
📋
No templates yet
Create reusable task lists for recurring projects like "Client onboarding" or "Monthly reporting"
)} {templates.map(tmpl => (
{tmpl.name}
{tmpl.description &&
{tmpl.description}
}
{tmpl.tasks?.length || 0} tasks
{(tmpl.tasks || []).slice(0, 3).map((t, i) => (
{t.title} {t.due_offset_days && · day {t.due_offset_days}}
))} {(tmpl.tasks?.length || 0) > 3 &&
+{tmpl.tasks.length - 3} more…
}
))}
)} {/* ── Webhooks tab ── */} {!loading && tab === 'webhooks' && (
Webhooks fire a POST request to your URL with JSON payload when events occur in TechyFuel OS.
{webhooks.length === 0 && (
🔗
No webhooks configured
Connect TechyFuel OS to Slack, Zapier, or your own backend
)} {webhooks.map(wh => (
{wh.name}
{wh.url}
{wh.last_status && = 200 && wh.last_status < 300 ? 'success' : 'danger'} size="sm">{wh.last_status}} {wh.last_triggered_at && {fmtTime(wh.last_triggered_at)}}
{wh.events?.length > 0 && (
{wh.events.map(e => {e})}
)}
))}
)} {/* ── Approvals tab ── */} {!loading && tab === 'approvals' && (
{['pending','approved','rejected'].map(s => ( ))}
{approvals.filter(a => a.status === approvalFilter).length === 0 && (
No {approvalFilter} approvals
)} {approvals.filter(a => a.status === approvalFilter).map(appr => (
{appr.tasks?.title || 'Task approval'} {appr.status}
Requested by {appr.team_members?.name || '—'} · {fmtDate(appr.created_at)} {appr.resolved_at && ` · Resolved ${fmtDate(appr.resolved_at)}`}
{appr.comment &&
"{appr.comment}"
} {appr.status === 'pending' && (
)}
))}
)} {/* Modals */} setRuleModal(null)} onSave={saveRule} /> setTmplModal(null)} onSave={saveTemplate} /> setWhModal(null)} onSave={saveWebhook} /> setApplyModal(null)} />
); } Object.assign(window, { Automations }); })();