// 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.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 });
})();