import { api, toast } from '/static/js/api.js'; const errorBox = document.getElementById('error'); const showErr = (e) => { errorBox.textContent = e.message || String(e); }; // ---- tabs ---- const tabs = document.querySelectorAll('[role=tab]'); tabs.forEach((btn) => btn.addEventListener('click', () => { tabs.forEach((b) => b.setAttribute('aria-selected', b === btn ? 'true' : 'false')); document.querySelectorAll('[role=tabpanel]').forEach((p) => { p.hidden = true; }); document.getElementById('tab-' + btn.dataset.tab).hidden = false; loaders[btn.dataset.tab](); })); // ---- customers ---- const dlg = document.getElementById('customer-dialog'); let editingCustomer = null; // null = creating let consultants = []; function credsFromForm(type) { switch (type) { case 'jira': return { email: val('c-jira-email'), apiToken: val('c-jira-token') }; case 'azure_devops': return { organization: val('c-ado-org'), project: val('c-ado-project'), pat: val('c-ado-pat') }; case 'youtrack': return { permanentToken: val('c-yt-token') }; case 'wekan': return { username: val('c-wekan-user'), password: val('c-wekan-pass') }; default: return {}; } } const val = (id) => document.getElementById(id).value.trim(); function credsEntered(type) { const c = credsFromForm(type); return Object.values(c).some(Boolean); } document.getElementById('c-type').addEventListener('change', (e) => { document.querySelectorAll('.creds').forEach((f) => { f.hidden = true; }); document.getElementById('creds-' + e.target.value).hidden = false; document.getElementById('f-baseurl').style.display = e.target.value === 'demo' ? 'none' : ''; }); async function loadConsultants() { const res = await api('GET', '/api/v1/admin/users?role=consultant&limit=200'); consultants = res.users; const sel = document.getElementById('c-consultants'); sel.replaceChildren(...consultants.map((u) => { const o = document.createElement('option'); o.value = u.id; o.textContent = `${u.name} <${u.email}>`; return o; })); } function openCustomerDialog(customer) { editingCustomer = customer || null; document.getElementById('customer-dialog-title').textContent = customer ? `Edit ${customer.name}` : 'New customer'; document.getElementById('c-name').value = customer ? customer.name : ''; document.getElementById('c-type').value = customer ? customer.ticketing.type : 'jira'; document.getElementById('c-project').value = customer ? customer.ticketing.projectKey : ''; document.getElementById('c-baseurl').value = customer ? customer.ticketing.baseUrl : ''; document.getElementById('c-poll').value = customer ? customer.ticketing.pollIntervalSec : 60; document.getElementById('c-budget').value = customer ? customer.defaultBudget : 1000; document.querySelectorAll('.creds input').forEach((i) => { i.value = ''; }); document.getElementById('c-type').dispatchEvent(new Event('change')); const sel = document.getElementById('c-consultants'); [...sel.options].forEach((o) => { o.selected = customer ? customer.consultantIds.includes(o.value) : false; }); document.getElementById('c-test-result').textContent = customer && customer.ticketing.hasCredentials ? 'Stored credentials are kept unless you enter new ones.' : ''; dlg.showModal(); } document.getElementById('customer-new').addEventListener('click', async () => { try { await loadConsultants(); openCustomerDialog(null); } catch (e) { showErr(e); } }); document.getElementById('c-cancel').addEventListener('click', () => dlg.close()); document.getElementById('c-test').addEventListener('click', async () => { const out = document.getElementById('c-test-result'); out.textContent = 'Testing…'; const type = val('c-type'); try { let res; if (editingCustomer && !credsEntered(type)) { res = await api('POST', `/api/v1/admin/customers/${editingCustomer.id}/test-connection`, {}); } else { res = await api('POST', '/api/v1/admin/customers/test-connection', { type, baseUrl: val('c-baseurl'), projectKey: val('c-project'), credentials: credsFromForm(type), }); } out.textContent = res.ok ? `✓ Connected (${res.latencyMs} ms)` : `✗ ${res.error}`; out.style.color = res.ok ? 'var(--ok)' : 'var(--err)'; } catch (e) { out.textContent = '✗ ' + e.message; out.style.color = 'var(--err)'; } }); document.getElementById('customer-form').addEventListener('submit', async (e) => { e.preventDefault(); const type = val('c-type'); const body = { name: val('c-name'), type, baseUrl: val('c-baseurl'), projectKey: val('c-project'), pollIntervalSec: parseInt(val('c-poll'), 10) || 60, defaultBudget: parseFloat(val('c-budget')) || 0, consultantIds: [...document.getElementById('c-consultants').selectedOptions].map((o) => o.value), }; if (!editingCustomer || credsEntered(type)) body.credentials = credsFromForm(type); try { if (editingCustomer) { body.version = editingCustomer.version; await api('PATCH', `/api/v1/admin/customers/${editingCustomer.id}`, body); } else { await api('POST', '/api/v1/admin/customers', body); } dlg.close(); toast('Customer saved.', 'ok'); loadCustomers(); } catch (err) { if (err.status === 409) toast('Conflict: ' + err.message, 'err'); else showErr(err); } }); async function loadCustomers() { try { const res = await api('GET', '/api/v1/admin/customers?includeArchived=true'); const host = document.getElementById('customer-list'); host.replaceChildren(...res.customers.map(renderCustomerCard)); if (!res.customers.length) { host.textContent = 'No customers yet — create the first one.'; } } catch (e) { showErr(e); } } function renderCustomerCard(c) { const card = document.createElement('div'); card.className = 'card'; const syncBadge = c.ticketing.lastSyncStatus === 'ok' ? `sync ok` : c.ticketing.lastSyncStatus === 'error' ? `sync error` : 'never synced'; card.innerHTML = `
${esc(c.ticketing.type)} · project ${esc(c.ticketing.projectKey || '—')} · poll ${c.ticketing.pollIntervalSec}s · budget ${c.defaultBudget} · ${c.consultantIds.length} consultant(s) ${c.ticketing.lastSyncAt && !c.ticketing.lastSyncAt.startsWith('0001') ? ' · last sync ' + new Date(c.ticketing.lastSyncAt).toLocaleString() : ''}
No sync workers running.
'; if (Array.isArray(s.syncWorkers) && s.syncWorkers.length) { workers = s.syncWorkers.map((wkr) => `${esc(JSON.stringify(s.jobs))}
`; } catch (e) { host.textContent = e.message; } } document.getElementById('status-refresh').addEventListener('click', loadStatus); // ---- audit ---- let auditCursor = ''; async function loadAudit(reset = true) { try { const entity = encodeURIComponent(document.getElementById('audit-entity').value.trim()); if (reset) auditCursor = ''; const res = await api('GET', `/api/v1/admin/audit-log?entityId=${entity}&cursor=${auditCursor}`); auditCursor = res.nextCursor; const rows = res.entries.map((e) => { const tr = document.createElement('tr'); const cells = [ new Date(e.at).toLocaleString(), e.actorId, e.action, `${e.entity}/${e.entityId}`, e.diff ? JSON.stringify(e.diff) : '', ]; cells.forEach((c) => { const td = document.createElement('td'); td.textContent = c; tr.appendChild(td); }); return tr; }); const tbody = document.querySelector('#audit-table tbody'); if (reset) tbody.replaceChildren(...rows); else tbody.append(...rows); } catch (e) { showErr(e); } } document.getElementById('audit-apply').addEventListener('click', () => loadAudit(true)); document.getElementById('audit-more').addEventListener('click', () => loadAudit(false)); function esc(s) { return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[c])); } const loaders = { customers: loadCustomers, users: loadUsers, settings: loadSettings, status: loadStatus, audit: () => loadAudit(true), }; loadCustomers();