feat: Archive tab — role-scoped list of archived tasks with search
New /archive page (nav link for all roles) backed by GET /api/v1/archive: - admins see every archived task; - consultants see archived tasks for customers they're assigned to; - developers see archived tasks they were assigned to, claimed, or acted on. Supports full-text ?q= search; cards link to the task detail view. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
// Archive page: archived tasks scoped by role (server-side), with search.
|
||||
import { api } from '/static/js/api.js';
|
||||
|
||||
const host = document.getElementById('archive-list');
|
||||
const errorBox = document.getElementById('error');
|
||||
const search = document.getElementById('archive-search');
|
||||
let names = {};
|
||||
|
||||
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
||||
}[c]));
|
||||
|
||||
function render(tasks) {
|
||||
host.replaceChildren(...tasks.map((t) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
const when = t.updatedAt ? new Date(t.updatedAt).toLocaleString() : '';
|
||||
card.innerHTML = `
|
||||
<div class="spread">
|
||||
${names[t.customerId] ? `<span class="card-project">${esc(names[t.customerId])}</span>` : '<span></span>'}
|
||||
<span class="badge accent">◈ ${t.bounty}</span>
|
||||
</div>
|
||||
<h3 class="mt"><a href="/tasks/${t.id}">${esc(t.title)}</a></h3>
|
||||
<p class="muted">${esc((t.description || '').slice(0, 200))}</p>
|
||||
<p class="muted" style="font-size:0.8rem">archived${when ? ' · ' + esc(when) : ''}</p>`;
|
||||
return card;
|
||||
}));
|
||||
if (!tasks.length) {
|
||||
host.innerHTML = '<p class="muted">No archived tasks match.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
let timer = null;
|
||||
async function load() {
|
||||
try {
|
||||
const q = search.value.trim();
|
||||
const res = await api('GET', '/api/v1/archive' + (q ? '?q=' + encodeURIComponent(q) : ''));
|
||||
names = res.customerNames || {};
|
||||
render(res.tasks || []);
|
||||
} catch (e) { errorBox.textContent = e.message; }
|
||||
}
|
||||
|
||||
search.addEventListener('input', () => { clearTimeout(timer); timer = setTimeout(load, 250); });
|
||||
load();
|
||||
@@ -0,0 +1,9 @@
|
||||
{{define "content"}}
|
||||
<h1>Archive</h1>
|
||||
<div class="error-box" id="error" role="alert"></div>
|
||||
<input type="search" id="archive-search" class="mt"
|
||||
placeholder="Search archived tasks…" aria-label="Search archived tasks"
|
||||
style="max-width:440px; width:100%">
|
||||
<p class="muted" style="margin-top:6px">Archived tasks you can access.</p>
|
||||
<div class="grid mt" id="archive-list"></div>
|
||||
{{end}}
|
||||
@@ -28,6 +28,7 @@
|
||||
{{end}}
|
||||
<a href="/messages" {{if eq .Active "messages"}}aria-current="page"{{end}}>Messages</a>
|
||||
<a href="/metrics" {{if eq .Active "metrics"}}aria-current="page"{{end}}>Metrics</a>
|
||||
<a href="/archive" {{if eq .Active "archive"}}aria-current="page"{{end}}>Archive</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="right">
|
||||
|
||||
Reference in New Issue
Block a user