import { api, toast } from '/static/js/api.js'; import { subscribe, onPollFallback } from '/static/js/ws.js'; const grid = document.getElementById('board-grid'); const errorBox = document.getElementById('error'); let meId = ''; let isDeveloper = false; let tasks = []; const customerNames = new Map(); const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[c])); function filters() { return { q: document.getElementById('f-q').value.trim(), customerId: document.getElementById('f-customer').value, minBounty: document.getElementById('f-min').value || '0', sort: document.getElementById('f-sort').value, }; } async function load() { try { const f = filters(); const qs = new URLSearchParams(f).toString(); const res = await api('GET', '/api/v1/board?' + qs); tasks = res.tasks; (res.customers || []).forEach((c) => customerNames.set(c.id, c.name)); const sel = document.getElementById('f-customer'); if (sel.options.length === 1) { res.customers.forEach((c) => sel.add(new Option(c.name, c.id))); } render(); } catch (e) { errorBox.textContent = e.message; } } function ageBadge(t) { if (!t.publishedAt) return ''; const days = (Date.now() - new Date(t.publishedAt).getTime()) / 86400000; if (days > 14) return '14d+'; if (days > 7) return '7d+'; return ''; } function render() { grid.replaceChildren(...tasks.map(renderCard)); if (!tasks.length) { grid.innerHTML = '

No published tasks right now. Consultants add you to their pool to share work here.

'; } } function renderCard(t) { const card = document.createElement('div'); card.className = 'card'; const myClaim = (t.claimRequests || []).some((c) => c.developerId === meId); const others = (t.claimRequests || []).filter((c) => c.developerId !== meId).length; card.innerHTML = `
◈ ${t.bounty} ${ageBadge(t)}
${customerNames.has(t.customerId) ? `

${esc(customerNames.get(t.customerId))}

` : ''}

${esc(t.title)}

${esc((t.description || '').slice(0, 180))}

${(t.acceptanceCriteria || []).length} acceptance criteria

${myClaim ? 'requested by you' : ''} ${others ? `${others} other request(s)` : ''} ${!isDeveloper ? 'Open' : myClaim ? '' : ''}
`; const claimBtn = card.querySelector('[data-act=claim]'); if (claimBtn) claimBtn.addEventListener('click', () => openClaim(t)); const wBtn = card.querySelector('[data-act=withdraw]'); if (wBtn) { wBtn.addEventListener('click', async () => { try { await api('POST', `/api/v1/tasks/${t.id}/claim/withdraw`, {}); load(); } catch (e) { toast(e.message, 'err'); } }); } return card; } const dlg = document.getElementById('claim-dialog'); let claimTask = null; function openClaim(t) { claimTask = t; document.getElementById('claim-task-title').textContent = `${t.title} — bounty ${t.bounty}`; dlg.showModal(); } document.getElementById('claim-cancel').addEventListener('click', () => dlg.close()); document.getElementById('claim-form').addEventListener('submit', async (e) => { e.preventDefault(); try { await api('POST', `/api/v1/tasks/${claimTask.id}/claim`, { note: document.getElementById('claim-note').value.trim(), }); dlg.close(); document.getElementById('claim-note').value = ''; toast('Assignment requested — the consultant will review it.', 'ok'); load(); } catch (err) { toast(err.message, 'err'); } }); // saved filters (§11.8) document.getElementById('f-save').addEventListener('click', async () => { try { const me = await api('GET', '/api/v1/auth/me'); const extra = me.user.extra || {}; extra.savedBoardFilters = JSON.stringify(filters()); await api('PATCH', '/api/v1/profile', { extra }); toast('Filters saved as your default.', 'ok'); } catch (e) { toast(e.message, 'err'); } }); async function restoreFilters() { try { const me = await api('GET', '/api/v1/auth/me'); meId = me.user.id; isDeveloper = !!(me.user.roles && me.user.roles.developer); const saved = me.user.extra && me.user.extra.savedBoardFilters; if (saved) { const f = JSON.parse(saved); document.getElementById('f-q').value = f.q || ''; document.getElementById('f-min').value = f.minBounty > 0 ? f.minBounty : ''; document.getElementById('f-sort').value = f.sort || ''; } } catch (e) { /* anonymous — page guard handles it */ } } let timer = null; ['f-q', 'f-min'].forEach((id) => { document.getElementById(id).addEventListener('input', () => { clearTimeout(timer); timer = setTimeout(load, 300); }); }); ['f-customer', 'f-sort'].forEach((id) => { document.getElementById(id).addEventListener('change', load); }); subscribe('board', () => { clearTimeout(timer); timer = setTimeout(load, 400); }); onPollFallback(load); restoreFilters().then(load);