import { api, toast } from '/static/js/api.js'; import { lineChart, barChart } from '/static/js/charts.js'; const errorBox = document.getElementById('error'); const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[c])); let me = null; function rangeQS() { const from = document.getElementById('m-from').value; const to = document.getElementById('m-to').value; const p = new URLSearchParams(); if (from) p.set('from', from); if (to) p.set('to', to); return p; } function card(label, value, hint) { return `

${esc(label)}

${esc(value)}

${hint ? `

${esc(hint)}

` : ''}
`; } const weekLabel = (iso) => { const d = new Date(iso); return `${d.getMonth() + 1}/${d.getDate()}`; }; async function loadDeveloper() { const res = await api('GET', '/api/v1/developer/metrics?' + rangeQS()); document.getElementById('dev-metrics').hidden = false; document.getElementById('dev-cards').innerHTML = card('Total bounty earned', res.totalBounty) + card('Tasks completed', res.tasksCompleted) + card('Approval rate', `${Math.round(res.approvalRate * 100)}%`, `${res.approved} approved ยท ${res.changesRequested} change requests`) + card('Avg assigned โ†’ approved', `${res.avgAssignToApproveHours.toFixed(1)} h`) + card('Time logged', `${Math.floor(res.timeLoggedMinutes / 60)}h ${res.timeLoggedMinutes % 60}m`); lineChart(document.getElementById('dev-weekly'), res.weekly.map((p) => ({ label: weekLabel(p.week), value: p.amount })), { label: 'weekly earnings' }); barChart(document.getElementById('dev-customers'), res.perCustomer.map((g) => ({ label: g.key, value: g.amount })), { label: 'earnings per customer' }); document.getElementById('m-csv').href = '/api/v1/developer/metrics?format=csv&' + rangeQS(); } async function loadConsultant() { const qs = rangeQS(); const cust = document.getElementById('m-customer').value; if (cust) qs.set('customerId', cust); const res = await api('GET', '/api/v1/consultant/metrics?' + qs); document.getElementById('cons-metrics').hidden = false; const sel = document.getElementById('m-customer'); if (sel.options.length === 1) { res.customers.forEach((c) => sel.add(new Option(c.name, c.id))); } document.getElementById('cons-cards').innerHTML = card('Bounty awarded', res.totalBounty) + card('Tasks completed', res.tasksCompleted) + card('Atomization lead time', `${res.atomizationLeadHours.toFixed(1)} h`, 'imported โ†’ published') + card('Open board depth', res.openBoardDepth, 'published, unclaimed tasks'); lineChart(document.getElementById('cons-weekly'), res.weekly.map((p) => ({ label: weekLabel(p.week), value: p.amount })), { label: 'weekly awards' }); barChart(document.getElementById('cons-devs'), res.perDeveloper.map((g) => ({ label: g.key, value: g.amount })), { label: 'per developer' }); barChart(document.getElementById('cons-customers'), res.perCustomer.map((g) => ({ label: g.key, value: g.amount })), { label: 'per customer' }); document.getElementById('m-csv').href = '/api/v1/consultant/metrics?format=csv&' + qs; } async function loadLeaderboard() { const res = await api('GET', '/api/v1/leaderboard?' + rangeQS()); const tbody = document.querySelector('#leaderboard tbody'); tbody.replaceChildren(...res.leaderboard.map((row, i) => { const tr = document.createElement('tr'); tr.innerHTML = `${i + 1}${esc(row.name)}${row.tasks}โ—ˆ ${row.amount}`; return tr; })); if (!res.leaderboard.length) { tbody.innerHTML = 'No bounties awarded yet.'; } } async function loadAll() { errorBox.textContent = ''; try { if (me.user.roles.developer) await loadDeveloper(); if (me.user.roles.consultant || me.user.roles.admin) await loadConsultant(); await loadLeaderboard(); } catch (e) { errorBox.textContent = e.message; } } document.getElementById('m-apply').addEventListener('click', loadAll); document.getElementById('m-customer').addEventListener('change', loadConsultant); document.getElementById('lb-optout').addEventListener('change', async (e) => { try { await api('PATCH', '/api/v1/profile', { settings: { leaderboardOptOut: e.target.checked } }); toast(e.target.checked ? 'You are hidden from the leaderboard.' : 'You are visible on the leaderboard.', 'ok'); loadLeaderboard(); } catch (err) { toast(err.message, 'err'); } }); (async () => { me = await api('GET', '/api/v1/auth/me'); document.getElementById('lb-optout').checked = !!me.user.settings.leaderboardOptOut; loadAll(); })();