(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')) +
' ';
[row.brief, row.env_brief, 'Owner: ' + (row.owner_username || '—')]
.filter(Boolean)
.forEach((line) => {
html += '- ' + escapeHtml(String(line)) + '
';
});
html += ' |
';
});
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 = '';
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);
}
})();