// Dashy — main app (API-backed) const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "light", "accent": "#2A6FDB", "density": "cozy", "showTags": true }/*EDITMODE-END*/; const ACCENTS = ['#2A6FDB', '#1F8A5B', '#D97757', '#7A5AF8']; function useApiData(authed) { const [data, setData] = React.useState({ tasks: [], users: [], audit: [] }); const [loading, setLoading] = React.useState(true); React.useEffect(() => { if (!authed) return; let mounted = true; const load = async () => { try { const [tasks, users, audit] = await Promise.all([ api.getTasks(), api.getUsers(), api.getAudit() ]); if (mounted) { setData({ tasks, users, audit }); setLoading(false); } } catch (e) { console.error("Failed to load data:", e); } }; load(); const off = api.subscribe(load); return () => { mounted = false; off(); }; }, [authed]); return { ...data, loading }; } function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [authed, setAuthed] = React.useState(!!api.token); // Optional: Decode token to get meId if needed, but for prototype we just assume rod if not known yet, // or decode JWT using a simple base64 parse. const [meId, setMeId] = React.useState(() => { if (api.token) { try { return JSON.parse(atob(api.token.split('.')[1])).sub; } catch(e) {} } return 'rod'; }); const { tasks, users: dbUsers, audit, loading } = useApiData(authed); const [tab, setTab] = React.useState('overview'); const [adding, setAdding] = React.useState(null); const [openTaskId, setOpenTaskId] = React.useState(null); const [showLogs, setShowLogs] = React.useState(false); const [showSettings, setShowSettings] = React.useState(false); const [headsUp, setHeadsUp] = React.useState([ { id: 'h1', kind: 'unsuccessful', taskId: 't7', title: 'WO #2188 auto-marked Unsuccessful', sub: 'Two missed bookings — assigned to Kirra for review' }, { id: 'h2', kind: 'billing', taskId: 't4', title: 'Form response switched to Billing', sub: 'K. Wynne · originally Service Booking' }, ]); if (!authed) { return { try { await api.login(id, pwd); setMeId(id); setAuthed(true); // Fire & forget audit log api.addAudit({ actor: id, action: 'login', summary: 'Signed in' }).catch(console.error); } catch (e) { alert("Login failed: " + e.message); } }} />; } if (loading) return ; const userMap = Object.fromEntries(dbUsers.map(u => [u.id, u])); const merge = (id) => { const base = findUser(id); if (!base && !userMap[id]) return null; const live = userMap[id] || {}; if (!base) { return { id, name: live.name, role: live.role, hue: live.hue, initials: live.initials, photo: live.photo || null, account_type: live.account_type || 'standard' }; } return { ...base, name: live.name || base.name, role: live.role || base.role, photo: live.photo || null, account_type: live.account_type || 'standard' }; }; const me = merge(meId) || merge('rod'); const isAdmin = me && me.account_type === 'admin'; const openTask = tasks.find(x => x.id === openTaskId); const addTask = async ({ title, description, assignee, priority }) => { try { const t = await api.createTask({ title, description, assignee_id: assignee, priority, added_by: meId, source: 'manual', status: 'open', tags: [] }); await api.addAudit({ actor: meId, action: 'task_created', summary: 'Created task "' + title + '" for ' + (merge(assignee)||{}).name, target: t.id }); setAdding(null); } catch(e) { console.error(e); alert("Failed to create task"); } }; const moveTask = async (taskId, toUserId) => { try { await api.updateTask(taskId, { assignee_id: toUserId }); await api.addAudit({ actor: meId, action: 'task_moved', summary: 'Moved task to ' + (merge(toUserId)||{}).name, target: taskId }); } catch(e) {} }; const setPriority = async (taskId, p) => { try { await api.updateTask(taskId, { priority: p }); } catch(e) {} }; const completeTask = async (taskId) => { try { await api.updateTask(taskId, { status: 'closed' }); await api.addAudit({ actor: meId, action: 'task_completed', summary: 'Marked task as completed', target: taskId }); setOpenTaskId(null); } catch(e) { console.error(e); alert("Failed to complete task: " + e.message); } }; const dismissHU = (id) => setHeadsUp(h => h.filter(x => x.id !== id)); const openTaskFromAnywhere = (id) => { setOpenTaskId(id); setShowLogs(false); }; // Map API fields to frontend component expectations const frontendTasks = tasks.map(t => ({ ...t, assignee: t.assignee_id, addedBy: t.added_by, addedAt: t.added_at, tags: t.tags.map(tagObj => tagObj.tag) })); const frontendAudit = audit.map(a => ({ ...a, actor: a.actor, action: a.action, summary: a.summary, target: a.target, at: a.at })); const mappedOpenTask = frontendTasks.find(x => x.id === openTaskId); return (
setAdding(meId)} onLogs={() => setShowLogs(true)} onProfile={() => setShowSettings(true)} />
{tab === 'overview' && ( setOpenTaskId(task.id)} onAddFor={(uid) => setAdding(uid)} onMoveTask={moveTask} /> )} {tab !== 'overview' && ( setOpenTaskId(task.id)} onAddFor={(uid) => setAdding(uid)} /> )}
setAdding(null)} onSubmit={addTask} defaultAssignee={adding} me={me} /> {mappedOpenTask && ( setOpenTaskId(null)} onMove={moveTask} onPriority={setPriority} onComplete={() => completeTask(mappedOpenTask.id)} /> )} {showLogs && ( setShowLogs(false)} wide> )} {showSettings && ( setShowSettings(false)} onSave={async (edits) => { // Not implemented on backend yet for user updating, mock success setShowSettings(false); }} onLogout={() => { api.logout(); setAuthed(false); setShowSettings(false); }} onSwitchUser={async (id) => { try { await api.login(id, "password123"); setMeId(id); setShowSettings(false); } catch(e) { alert("Failed to switch user"); } }} /> )}
); } function BootSplash() { return (
loading from API…
); } function DashyTweaks({ t, setTweak }) { return ( setTweak('theme', v)} /> setTweak('accent', v)} /> setTweak('density', v)} /> setTweak('showTags', v)} /> ); } function ThemeBridge() { const [t] = useTweaks(TWEAK_DEFAULTS); React.useEffect(() => { document.documentElement.dataset.theme = t.theme; document.documentElement.style.setProperty('--accent', t.accent); document.documentElement.dataset.showTags = t.showTags ? 'on' : 'off'; }, [t.theme, t.accent, t.showTags]); return null; } const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<>);