mirror of
git://f0xx.org/ac/ac-be-tickets
synced 2026-07-29 05:58:55 +03:00
374 lines
11 KiB
JavaScript
374 lines
11 KiB
JavaScript
(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, '>')
|
|
.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 =
|
|
'<span class="star-rating" role="img" aria-label="' + score + ' out of 5">';
|
|
for (let i = 1; i <= 5; i++) {
|
|
html += '<span class="star' + (i <= score ? ' star--on' : '') + '"></span>';
|
|
}
|
|
return html + '</span>';
|
|
}
|
|
|
|
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 '<span class="report-tag" style="--tag-bg:' + bg + '">' + label + '</span>';
|
|
}
|
|
return (
|
|
'<button type="button" class="report-tag report-tag--filter" data-tag-id="' +
|
|
escapeHtml(id) +
|
|
'" style="--tag-bg:' +
|
|
bg +
|
|
'" title="Filter by tag">' +
|
|
label +
|
|
'</button>'
|
|
);
|
|
}
|
|
|
|
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 =
|
|
'<tr><td colspan="6" class="muted">' + escapeHtml(t('tickets.empty')) + '</td></tr>';
|
|
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
|
|
? '<div class="ticket-issue-brief muted">' + escapeHtml(row.brief) + '</div>'
|
|
: '';
|
|
html +=
|
|
'<tr class="report-row report-row--nav" data-href="' +
|
|
escapeHtml(openUrl) +
|
|
'" tabindex="0" role="link">';
|
|
html +=
|
|
'<td class="report-tree-cell"><button type="button" class="report-tree-toggle" aria-expanded="false" aria-controls="' +
|
|
briefId +
|
|
'"><span class="report-tree-arrow"></span></button></td>';
|
|
html +=
|
|
'<td class="ticket-col-issue"><strong class="ticket-issue-title">' +
|
|
issueTitle +
|
|
'</strong>' +
|
|
issueBrief +
|
|
'</td>';
|
|
html += '<td>' + formatTime(row.opened_at_ms) + '</td>';
|
|
html +=
|
|
'<td class="ticket-col-env"><span>' +
|
|
escapeHtml(row.env_brief || '') +
|
|
'</span><br><span class="muted">' +
|
|
escapeHtml(row.device_model || '') +
|
|
'</span></td>';
|
|
html += '<td>' + formatStars(row.rating) + '</td>';
|
|
const tagEditBtn = canTagEdit()
|
|
? '<button type="button" class="tag-edit-btn" data-ticket-id="' +
|
|
row.id +
|
|
'" title="Edit tags">✎</button>'
|
|
: '';
|
|
const tags = (row.tags || []).map(renderTagPill).join('');
|
|
html +=
|
|
'<td class="col-tags"><div class="report-tags">' +
|
|
tags +
|
|
tagEditBtn +
|
|
'</div></td>';
|
|
html += '</tr>';
|
|
html +=
|
|
'<tr class="report-brief-row" id="' +
|
|
briefId +
|
|
'" hidden><td colspan="6"><div class="report-brief-panel report-row--nav" data-href="' +
|
|
escapeHtml(openUrl) +
|
|
'" tabindex="0" role="link"><p class="muted">' +
|
|
escapeHtml(t('row.open_report')) +
|
|
'</p><ul class="report-brief-lines">';
|
|
[row.brief, row.env_brief, 'Owner: ' + (row.owner_username || '—')]
|
|
.filter(Boolean)
|
|
.forEach((line) => {
|
|
html += '<li>' + escapeHtml(String(line)) + '</li>';
|
|
});
|
|
html += '</ul></div></td></tr>';
|
|
});
|
|
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 = '<div class="pagination-inner">';
|
|
html +=
|
|
'<button type="button" class="btn page-btn" data-page="' +
|
|
(prev || '') +
|
|
'" ' +
|
|
(prev ? '' : 'disabled') +
|
|
'>← Previous</button>';
|
|
html +=
|
|
'<span class="page-info">Page ' +
|
|
page +
|
|
' / ' +
|
|
pages +
|
|
' · ' +
|
|
total +
|
|
' tickets</span>';
|
|
html +=
|
|
'<button type="button" class="btn page-btn" data-page="' +
|
|
(next || '') +
|
|
'" ' +
|
|
(next ? '' : 'disabled') +
|
|
'>Next →</button></div>';
|
|
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);
|
|
}
|
|
})();
|