diff --git a/public/views/control.js b/public/views/control.js index 081bc80..b1a3086 100644 --- a/public/views/control.js +++ b/public/views/control.js @@ -76,6 +76,13 @@ function statusPill(s) { return el('span', { class: 'badge', style: { background: 'transparent', border: `1px solid ${colors[s] || 'var(--border)'}`, color: colors[s] || 'var(--muted)' } }, s || '—'); } +function typeBadge(t) { + const type = t === 'feature' ? 'feature' : 'bug'; + const c = type === 'feature' ? '#7aa2e0' : '#e0b24b'; + return el('span', { class: 'badge', style: { background: 'transparent', border: `1px solid ${c}`, color: c } }, + type === 'feature' ? '✨ feature' : '🐞 bug'); +} + // ---- group cache (used by approve/instances dropdowns) ---------------------- let groupsCache = []; @@ -298,24 +305,31 @@ async function renderReleases(panel) { async function renderTickets(panel) { let filter = 'open'; + let typeFilter = ''; const list = el('div'); const detail = el('div', { style: { marginTop: '0.9rem' } }); const statusSel = select([{ value: '', label: 'all' }, 'open', 'closed'], filter); statusSel.onchange = () => { filter = statusSel.value; loadList(); }; + const typeSel = select([{ value: '', label: 'all' }, { value: 'bug', label: '🐞 bug' }, { value: 'feature', label: '✨ feature' }], typeFilter); + typeSel.onchange = () => { typeFilter = typeSel.value; loadList(); }; async function loadList() { mount(list, el('p', { class: 'muted' }, 'Loading tickets…')); + const qs = []; + if (filter) qs.push(`status=${encodeURIComponent(filter)}`); + if (typeFilter) qs.push(`type=${encodeURIComponent(typeFilter)}`); let rows; - try { rows = await api.get(`${A}/tickets${filter ? `?status=${encodeURIComponent(filter)}` : ''}`); } + try { rows = await api.get(`${A}/tickets${qs.length ? `?${qs.join('&')}` : ''}`); } catch (e) { return mount(list, errBox(e)); } rows = rows.slice().sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0)); const body = rows.map(t => el('tr', {}, td(el('a', { href: '#', onclick: (e) => { e.preventDefault(); openTicket(t.id); } }, el('strong', {}, t.subject || t.title || `Ticket #${t.id}`))), + td(typeBadge(t.type)), td(el('span', { class: 'muted', style: { fontSize: '0.72rem' } }, t.label || t.email || '—')), td(statusPill(t.status)), td(el('span', { class: 'muted', style: { fontSize: '0.72rem' } }, fmtTime(t.created_at))))); - mount(list, rows.length ? table(['Subject', 'From', 'Status', 'Created'], body) : el('p', { class: 'muted' }, 'No tickets.')); + mount(list, rows.length ? table(['Subject', 'Type', 'From', 'Status', 'Created'], body) : el('p', { class: 'muted' }, 'No tickets.')); } async function openTicket(id) { @@ -348,6 +362,7 @@ async function renderTickets(panel) { el('div', { class: 'card', style: { display: 'grid', gap: '0.6rem' } }, el('div', { class: 'term-bar' }, el('span', { class: 'term-title' }, t.subject || t.title || `Ticket #${id}`), + typeBadge(t.type), statusPill(t.status), el('span', { style: { marginLeft: 'auto' } }, btn(t.status === 'closed' ? 'Reopen' : 'Close', () => patch({ status: t.status === 'closed' ? 'open' : 'closed' }, 'status updated'), 'ghost'))), @@ -364,6 +379,7 @@ async function renderTickets(panel) { el('div', { class: 'term-bar' }, el('span', { class: 'term-title' }, '◆ Tickets'), el('span', { style: { marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: '0.4rem' } }, el('span', { class: 'muted', style: { fontSize: '0.78rem' } }, 'status'), statusSel, + el('span', { class: 'muted', style: { fontSize: '0.78rem' } }, 'type'), typeSel, btn('Refresh', () => loadList(), 'ghost'))), list, detail); loadList();