(function () { function t(key, params) { return window.CrashI18n ? window.CrashI18n.t(key, params) : key; } function basePath() { return document.body.getAttribute('data-base-path') || ''; } function escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function formatTime(ms) { if (!ms) return '—'; const d = new Date(Number(ms)); return d.toISOString().replace('T', ' ').slice(0, 19); } function formatStars(n) { const score = Math.max(0, Math.min(5, Number(n) || 0)); let html = ''; for (let i = 1; i <= 5; i++) { html += ''; } return html + ''; } function renderTagPill(tag) { const id = String(tag.id || '').trim(); const label = escapeHtml(tag.label || tag.id); const bg = escapeHtml(tag.bg || '#5c6b82'); if (!id) { return '' + label + ''; } return ( '' ); } function canTagEdit() { return document.body.getAttribute('data-can-tag-edit') === '1'; } function initTicketsApp() { const app = document.getElementById('tickets-app'); const tbody = document.getElementById('tickets-tbody'); const statusEl = document.getElementById('tickets-status'); const pagination = document.getElementById('tickets-pagination'); const perPageSel = document.getElementById('tickets-per-page'); const tagFilter = document.getElementById('tickets-tag-filter'); const thead = document.querySelector('#tickets-table thead'); if (!app || !tbody) return; let page = 1; let perPage = 50; let sort = 'opened_at_ms'; let dir = 'desc'; let tag = ''; function load() { statusEl.textContent = t('reports.loading'); const q = '?page=' + page + '&per_page=' + perPage + '&sort=' + encodeURIComponent(sort) + '&dir=' + encodeURIComponent(dir) + (tag ? '&tag=' + encodeURIComponent(tag) : ''); const xhr = new XMLHttpRequest(); xhr.open('GET', basePath() + '/api/tickets.php' + q, true); xhr.onload = function () { let data; try { data = JSON.parse(xhr.responseText); } catch { statusEl.textContent = t('reports.invalid_response'); return; } if (!data.ok) { statusEl.textContent = data.hint || data.error || t('reports.load_failed'); return; } renderRows(data.items || []); renderPagination(data.total || 0, data.page || 1); const from = data.total ? (data.page - 1) * data.per_page + 1 : 0; const to = Math.min(data.page * data.per_page, data.total); statusEl.textContent = t('reports.status_showing', { from, to, total: data.total, }); }; xhr.onerror = function () { statusEl.textContent = t('reports.network_error'); }; xhr.send(); } window.ticketsReloadRef = load; function renderRows(items) { if (!items.length) { tbody.innerHTML = '' + escapeHtml(t('tickets.empty')) + ''; return; } let html = ''; items.forEach((row, i) => { const briefId = 'tkt-brief-' + row.id + '-' + i; const openUrl = basePath() + '/?view=ticket&id=' + row.id; const issueTitle = escapeHtml(row.title || ''); const issueBrief = row.brief ? '
' + escapeHtml(row.brief) + '
' : ''; html += ''; html += ''; html += '' + issueTitle + '' + issueBrief + ''; html += '' + formatTime(row.opened_at_ms) + ''; html += '' + escapeHtml(row.env_brief || '') + '
' + escapeHtml(row.device_model || '') + ''; html += '' + formatStars(row.rating) + ''; const tagEditBtn = canTagEdit() ? '' : ''; const tags = (row.tags || []).map(renderTagPill).join(''); html += '
' + tags + tagEditBtn + '
'; html += ''; html += '

' + escapeHtml(t('row.open_report')) + '

'; }); tbody.innerHTML = html; bindRows(); } function applyTagFilter(tagId) { tag = tagId; if (tagFilter) { let found = false; tagFilter.querySelectorAll('option').forEach((opt) => { if (opt.value === tagId) found = true; }); if (!found) { const opt = document.createElement('option'); opt.value = tagId; opt.textContent = tagId; tagFilter.appendChild(opt); } tagFilter.value = tagId; } page = 1; load(); } function activateTag(tagId, ev) { if (!tagId) return; ev.stopPropagation(); ev.preventDefault(); const xhr = new XMLHttpRequest(); xhr.open( 'GET', basePath() + '/api/tickets.php?page=1&per_page=2&tag=' + encodeURIComponent(tagId), true ); xhr.onload = function () { let data; try { data = JSON.parse(xhr.responseText); } catch { return; } if (!data.ok) return; const total = Number(data.total || 0); if (total === 1 && data.items && data.items[0] && data.items[0].id) { window.location.href = basePath() + '/?view=ticket&id=' + encodeURIComponent(String(data.items[0].id)); return; } applyTagFilter(tagId); }; xhr.send(); } function bindRows() { tbody.querySelectorAll('.report-tree-toggle').forEach((btn) => { btn.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); const controls = btn.getAttribute('aria-controls'); const row = controls ? document.getElementById(controls) : null; if (!row) return; const open = row.hidden; row.hidden = !open; btn.setAttribute('aria-expanded', open ? 'true' : 'false'); }); }); tbody.querySelectorAll('[data-href]').forEach((el) => { const href = el.getAttribute('data-href'); if (!href) return; const go = () => { window.location.href = href; }; el.addEventListener('click', (e) => { if (e.target.closest('.report-tree-toggle, .tag-edit-btn, .report-tag--filter')) return; go(); }); el.addEventListener('keydown', (e) => { if (e.target.closest('.report-tree-toggle, .tag-edit-btn, .report-tag--filter')) return; if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(); } }); }); tbody.querySelectorAll('.tag-edit-btn').forEach((btn) => { btn.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); if (typeof window.openTagModal === 'function') { window.openTagModal(Number(btn.getAttribute('data-ticket-id')), 'ticket'); } }); }); tbody.querySelectorAll('.report-tag--filter').forEach((btn) => { btn.addEventListener('click', (e) => { activateTag(btn.getAttribute('data-tag-id') || '', e); }); }); } function renderPagination(total, currentPage) { if (!pagination) return; page = currentPage; const pages = Math.max(1, Math.ceil(total / perPage)); const prev = page > 1 ? page - 1 : null; const next = page < pages ? page + 1 : null; let html = '
'; html += ''; html += 'Page ' + page + ' / ' + pages + ' · ' + total + ' tickets'; html += '
'; pagination.innerHTML = html; pagination.querySelectorAll('.page-btn').forEach((btn) => { btn.addEventListener('click', () => { const p = Number(btn.getAttribute('data-page')); if (!p) return; page = p; load(); }); }); } if (perPageSel) { perPageSel.addEventListener('change', () => { perPage = Number(perPageSel.value) || 50; page = 1; load(); }); } if (tagFilter) { tagFilter.addEventListener('change', () => { tag = tagFilter.value || ''; page = 1; load(); }); } if (thead) { thead.querySelectorAll('.sortable').forEach((th) => { th.addEventListener('click', () => { const key = th.getAttribute('data-sort'); if (!key) return; if (sort === key) { dir = dir === 'asc' ? 'desc' : 'asc'; } else { sort = key; dir = 'desc'; } thead.querySelectorAll('.sortable').forEach((x) => { x.classList.toggle('sortable--active', x === th); }); page = 1; load(); }); }); } window.__ticketsReload = function () { page = 1; load(); }; load(); } function onReady(fn) { if (window.CrashI18n && window.CrashI18n.ready) { window.CrashI18n.ready.then(fn); } else { fn(); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => onReady(initTicketsApp)); } else { onReady(initTicketsApp); } })();