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 tasks = [];
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;
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)}
${esc((t.description || '').slice(0, 180))}
${(t.acceptanceCriteria || []).length} acceptance criteria
${myClaim ? 'requested by you' : ''}
${others ? `${others} other request(s)` : ''}
${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;
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);