import { api, toast } from '/static/js/api.js'; import { subscribe } from '/static/js/ws.js'; import { attachMentions, mentionHTML } from '/static/js/mention.js'; const root = document.getElementById('task-root'); const errorBox = document.getElementById('error'); const taskId = root.dataset.taskId; const meId = root.dataset.userId; const isConsultant = root.dataset.isConsultant === '1' || root.dataset.isAdmin === '1'; let task = null; const userCache = new Map(); const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[c])); async function userName(id) { if (!id || id.startsWith('system')) return id || 'system'; if (!userCache.has(id)) { try { const res = await api('GET', `/api/v1/users/${id}/card`); userCache.set(id, res.card.name); } catch (e) { userCache.set(id, id.slice(0, 8)); } } return userCache.get(id); } async function load() { try { const res = await api('GET', `/api/v1/tasks/${taskId}`); task = res.task; await render(); } catch (e) { errorBox.textContent = e.message; root.innerHTML = ''; } } const mine = () => task.assignee && task.assignee.kind === 'human' && task.assignee.userId === meId; const myClaim = () => (task.claimRequests || []).some((c) => c.developerId === meId); async function render() { const t = task; const assigneeName = t.assignee ? (t.assignee.kind === 'ai' ? `AI (job ${esc(t.assignee.jobId || '')})` : esc(await userName(t.assignee.userId))) : '—'; const orphan = t.external && t.external.orphaned ? 'orphaned upstream' : ''; const claims = await Promise.all((t.claimRequests || []).map(async (c) => `
${esc(await userName(c.authorId))} · ${new Date(c.at).toLocaleString()}
${esc(t.status)} ${orphan} ${t.external ? `· ${esc(t.external.key)} ↗` : ''} · assignee: ${assigneeName} ${totalMin ? `· time logged: ${Math.floor(totalMin / 60)}h ${totalMin % 60}m` : ''}
${esc(t.description)}
${(t.acceptanceCriteria || []).length ? `No comments yet.
'}| When | Time | Note |
|---|
| When | Who | Event |
|---|
' + body + '
' }); load(); } catch (err) { toast(err.message, 'err'); } }); attachMentions(document.getElementById('comment-body')); const timeForm = document.getElementById('time-form'); if (timeForm) { timeForm.addEventListener('submit', async (e) => { e.preventDefault(); try { await api('POST', `/api/v1/tasks/${taskId}/time`, { minutes: parseInt(document.getElementById('time-minutes').value, 10), note: document.getElementById('time-note').value.trim(), }); load(); } catch (err) { toast(err.message, 'err'); } }); } } // ---- review dialog with per-AC checklist (§11.18) ---- const reviewDlg = document.getElementById('review-dialog'); function openReview() { const host = document.getElementById('review-checklist'); host.replaceChildren(...(task.acceptanceCriteria || []).map((c, i) => { const div = document.createElement('div'); div.innerHTML = ``; return div; })); reviewDlg.showModal(); } document.getElementById('review-cancel').addEventListener('click', () => reviewDlg.close()); async function submitReview(decision) { const checklist = [...document.querySelectorAll('[data-ac]')].map((cb, i) => ({ criterion: task.acceptanceCriteria[i], ok: cb.checked, })); try { await api('POST', `/api/v1/tasks/${taskId}/review`, { decision, note: document.getElementById('review-note').value.trim(), checklist, }); reviewDlg.close(); toast(decision === 'approve' ? 'Approved — bounty awarded.' : 'Changes requested.', 'ok'); load(); } catch (e) { toast(e.message, 'err'); } } document.getElementById('review-approve').addEventListener('click', () => submitReview('approve')); document.getElementById('review-changes').addEventListener('click', () => submitReview('request_changes')); // ---- AI dialog ---- const aiDlg = document.getElementById('ai-dialog'); document.getElementById('ai-cancel').addEventListener('click', () => aiDlg.close()); document.getElementById('ai-form').addEventListener('submit', async (e) => { e.preventDefault(); try { await api('POST', `/api/v1/tasks/${taskId}/assign-ai`, { context: { repositoryUrl: document.getElementById('ai-repo').value.trim(), branch: document.getElementById('ai-branch').value.trim(), instructions: document.getElementById('ai-instructions').value.trim(), }, }); aiDlg.close(); toast('AI job created — the result will arrive for review.', 'ok'); load(); } catch (err) { toast(err.message, 'err'); } }); subscribe('board', (event, data) => { if (data && data.taskId === taskId) setTimeout(load, 200); }); load();