mirror of
git://f0xx.org/ac/ac-be-tickets
synced 2026-07-29 03:57:37 +03:00
initial
This commit is contained in:
373
public/assets/js/tickets.js
Normal file
373
public/assets/js/tickets.js
Normal file
@@ -0,0 +1,373 @@
|
||||
(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);
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user