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