// 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: [], workspace: null, deletedTasks: [] }); const [loading, setLoading] = React.useState(true); React.useEffect(() => { if (!authed) return; let mounted = true; const load = async () => { try { const [tasks, users, audit, workspace, deletedTasks] = await Promise.all([ api.getTasks(), api.getUsers(), api.getAudit(), api.getWorkspace(), api.getDeletedTasks().catch(() => []) // Catch if not admin or error ]); if (mounted) { setData({ tasks, users, audit, workspace, deletedTasks }); 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, workspace, deletedTasks, loading } = useApiData(authed); const [tab, setTab] = React.useState('overview'); React.useEffect(() => { window.dbUsers = dbUsers; }, [dbUsers]); 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 { await api.login(id, pwd); setMeId(id); setAuthed(true); api.addAudit({ actor: id, action: 'login', summary: 'Signed in' }).catch(console.error); }} />; } 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 reopenTask = async (taskId) => { try { await api.updateTask(taskId, { status: 'open' }); await api.addAudit({ actor: meId, action: 'task_reopened', summary: 'Reopened task', target: taskId }); setOpenTaskId(null); } catch(e) { console.error(e); alert("Failed to reopen task: " + e.message); } }; const editTaskDesc = async (taskId, newDesc) => { try { await api.updateTask(taskId, { description: newDesc }); await api.addAudit({ actor: meId, action: 'task_edited', summary: 'Updated task description', target: taskId }); } catch(e) { console.error(e); alert("Failed to update description: " + e.message); } }; const deleteTask = async (taskId) => { try { const t = tasks.find(x => x.id === taskId); await api.deleteTask(taskId); await api.addAudit({ actor: meId, action: 'task_deleted', summary: 'Permanently deleted task' + (t ? ': ' + t.title : ''), target: taskId }); setOpenTaskId(null); } catch(e) { console.error(e); alert("Failed to delete task: " + e.message); } }; const restoreTask = async (taskId) => { try { await api.restoreTask(taskId); await api.addAudit({ actor: meId, action: 'task_restored', summary: 'Restored task from trash', target: taskId }); if (tab === 'deleted') setTab('overview'); } catch(e) { console.error(e); alert("Failed to restore 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)} workspace={workspace} />
{tab === 'overview' && ( setOpenTaskId(task.id)} onAddFor={(uid) => setAdding(uid)} onMoveTask={moveTask} /> )} {tab === 'deleted' && ( ({ ...t, assignee: t.assignee_id, addedBy: t.added_by, addedAt: t.added_at, tags: t.tags.map(tagObj => tagObj.tag) }))} onRestore={restoreTask} /> )} {tab !== 'overview' && tab !== 'deleted' && ( setOpenTaskId(task.id)} onAddFor={(uid) => setAdding(uid)} /> )}
setAdding(null)} onSubmit={addTask} defaultAssignee={adding} me={me} dbUsers={dbUsers} /> {mappedOpenTask && ( setOpenTaskId(null)} onMove={moveTask} onPriority={setPriority} onComplete={() => completeTask(mappedOpenTask.id)} onReopen={() => reopenTask(mappedOpenTask.id)} onEditDesc={(newDesc) => editTaskDesc(mappedOpenTask.id, newDesc)} onDeleteTask={() => deleteTask(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"); } }} onCreateUser={async (u) => { try { const id = u.name.split(' ')[0].toLowerCase() + Math.floor(Math.random()*100); await api.createUser({ id, name: u.name, role: u.role, hue: Math.floor(Math.random() * 360), initials: u.name.split(' ').map(s=>s[0]).join('').slice(0,2).toUpperCase(), account_type: u.account_type, password: "password123" }); await api.addAudit({ actor: meId, action: 'user_created', summary: 'Added ' + u.name + ' (' + (u.account_type||'standard') + ')', target: id }); } catch(e) { console.error(e); alert("Failed to create user: " + e.message); } }} onDeleteUser={async (id) => { try { const u = userMap[id]; await api.deleteUser(id); await api.addAudit({ actor: meId, action: 'user_deleted', summary: 'Removed ' + (u?u.name:id), target: null }); } catch(e) { console.error(e); alert("Failed to delete user"); } }} onUpdateUserRole={async (id, edits) => { try { await api.updateUser(id, edits); await api.addAudit({ actor: meId, action: 'user_updated', summary: 'Updated ' + (userMap[id]?userMap[id].name:id) + ' permissions', target: null }); } catch(e) { console.error(e); alert("Failed to update user"); } }} onChangePassword={async (oldPwd, newPwd) => { await api.changePassword(meId, oldPwd, newPwd); await api.addAudit({ actor: meId, action: 'password_changed', summary: 'Updated password', target: meId }); }} workspace={workspace} onUpdateWorkspace={async (edits) => { try { await api.updateWorkspace(edits); await api.addAudit({ actor: meId, action: 'workspace_updated', summary: 'Updated workspace settings', target: null }); } catch(e) { alert("Failed to update workspace: " + e.message); } }} /> )}
); } 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(<>);