';
}
html +=
'
' +
panelOpen +
'';
briefLines.forEach((line) => {
html += ' ' + escapeHtml(line) + ' ';
});
if (openUrl) {
html += ' Click to open full report ';
}
html += ' |
';
});
tbody.innerHTML = html;
bindTreeInteractions(tbody);
bindReportTagFilters(tbody, handleReportTagClick);
}
function applyReportTagFilter(tagId) {
listContext.filterTags = [tagId];
listContext.tagMode = listContext.tagMode || 'and';
grouped = false;
app.setAttribute('data-grouped', '0');
page = 1;
updateFilterBanner(listContext, filterBanner);
const p = new URLSearchParams(window.location.search);
p.set('view', 'reports');
p.delete('tag');
p.append('tag', tagId);
p.set('tag_mode', listContext.tagMode);
p.delete('group');
p.delete('page');
history.replaceState(null, '', basePath() + '/?' + p.toString());
const modeWrap = document.getElementById('tag-mode-wrap');
if (modeWrap) modeWrap.hidden = false;
const modeSel = document.getElementById('tag-mode-select');
if (modeSel) modeSel.value = listContext.tagMode;
load();
}
function handleReportTagClick(tagId, ev) {
ev.stopPropagation();
ev.preventDefault();
if (grouped) {
applyReportTagFilter(tagId);
return;
}
const xhr = new XMLHttpRequest();
xhr.open(
'GET',
basePath() +
'/api/reports.php?page=1&per_page=2&tag=' +
encodeURIComponent(tagId) +
'&tag_mode=and&sort=received_at_ms&dir=desc&since_ms=0',
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) {
navigateTo(basePath() + '/?view=report&id=' + data.items[0].id);
return;
}
applyReportTagFilter(tagId);
};
xhr.send();
}
function load() {
if (loading) return;
loading = true;
statusEl.textContent = t('reports.loading');
let qs =
'page=' +
page +
'&per_page=' +
perPage +
'&sort=' +
encodeURIComponent(apiSortKey(sort)) +
'&dir=' +
encodeURIComponent(dir) +
'&since_ms=' +
encodeURIComponent(String(lastVisitMs()));
if (listContext.searchQuery) {
qs += '&q=' + encodeURIComponent(listContext.searchQuery);
} else if (listContext.similarTo > 0) {
qs += '&similar_to=' + encodeURIComponent(String(listContext.similarTo));
} else {
if (grouped) qs += '&group=1';
if (listContext.filterDevice) {
qs += '&filter_device=' + encodeURIComponent(listContext.filterDevice);
}
if (listContext.filterAbi) {
qs += '&filter_abi=' + encodeURIComponent(listContext.filterAbi);
}
if (listContext.filterOsSdk > 0) {
qs += '&filter_os_sdk=' + encodeURIComponent(String(listContext.filterOsSdk));
}
if (listContext.filterOsRelease) {
qs += '&filter_os_release=' + encodeURIComponent(listContext.filterOsRelease);
}
if (listContext.filterAppVersion) {
qs += '&filter_app_version=' + encodeURIComponent(listContext.filterAppVersion);
}
if (listContext.filterBuild) {
qs += '&filter_build=' + encodeURIComponent(listContext.filterBuild);
}
if (listContext.filterTags && listContext.filterTags.length) {
listContext.filterTags.forEach((tagId) => {
qs += '&tag=' + encodeURIComponent(tagId);
});
qs += '&tag_mode=' + encodeURIComponent(listContext.tagMode || 'and');
}
}
const xhr = new XMLHttpRequest();
xhr.open('GET', basePath() + '/api/reports.php?' + qs, true);
xhr.onload = function () {
loading = false;
let data;
try {
data = JSON.parse(xhr.responseText);
} catch {
statusEl.textContent = 'Invalid response';
return;
}
if (!data.ok) {
statusEl.textContent = data.error || 'Load failed';
return;
}
lastItems = data.items || [];
refreshTableChrome();
renderPagination(Number(data.total) || 0);
statusEl.textContent =
'Showing ' +
(data.items?.length || 0) +
' of ' +
(data.total || 0) +
(grouped ? ' (grouped)' : '');
};
xhr.onerror = function () {
loading = false;
statusEl.textContent = 'Network error';
};
xhr.send();
}
initTagEditButtons(tbody, load);
const searchInput = document.getElementById('reports-search-input');
const searchSuggest = document.getElementById('reports-search-suggest');
const searchClear = document.getElementById('reports-search-clear');
let suggestTimer = null;
function hideSuggest() {
if (searchSuggest) {
searchSuggest.hidden = true;
searchSuggest.innerHTML = '';
}
}
function applySearchQuery(q, pushUrl) {
listContext.similarTo = 0;
listContext.filterDevice = '';
listContext.filterAbi = '';
listContext.filterOsSdk = 0;
listContext.filterOsRelease = '';
listContext.filterAppVersion = '';
listContext.filterBuild = '';
listContext.searchQuery = (q || '').trim();
grouped = false;
app.setAttribute('data-grouped', '0');
page = 1;
updateFilterBanner(listContext, filterBanner);
if (searchInput) searchInput.value = listContext.searchQuery;
if (searchClear) searchClear.hidden = !listContext.searchQuery;
if (pushUrl) {
const url =
basePath() +
'/?view=reports' +
(listContext.searchQuery ? '&q=' + encodeURIComponent(listContext.searchQuery) : '');
history.replaceState(null, '', url);
}
load();
}
function fetchSuggest(q) {
if (!searchSuggest || q.length < 2) {
hideSuggest();
return;
}
const xhr = new XMLHttpRequest();
xhr.open(
'GET',
basePath() +
'/api/reports.php?suggest=1&q=' +
encodeURIComponent(q) +
'&per_page=10',
true
);
xhr.onload = function () {
let data;
try {
data = JSON.parse(xhr.responseText);
} catch {
hideSuggest();
return;
}
if (!data.ok || !data.suggestions || !data.suggestions.length) {
hideSuggest();
return;
}
searchSuggest.innerHTML = data.suggestions
.map(
(s) =>
'
'
)
.join('');
searchSuggest.hidden = false;
};
xhr.send();
}
if (searchInput) {
if (listContext.searchQuery) {
searchInput.value = listContext.searchQuery;
if (searchClear) searchClear.hidden = false;
}
searchInput.addEventListener('input', () => {
const q = searchInput.value;
if (searchClear) searchClear.hidden = !q.trim();
hideSuggest();
clearTimeout(suggestTimer);
const trimmed = q.trim();
if (trimmed.length < 2) return;
suggestTimer = setTimeout(() => fetchSuggest(trimmed), 1000);
});
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
clearTimeout(suggestTimer);
hideSuggest();
applySearchQuery(searchInput.value.trim(), true);
}
if (e.key === 'Escape') {
hideSuggest();
}
});
}
function selectSearchSuggestion(term) {
if (!searchInput) return;
const t = (term || '').trim();
if (!t) return;
const raw = searchInput.value;
const withoutTrailing = raw.replace(/\s+$/, '');
const lastSpace = withoutTrailing.lastIndexOf(' ');
searchInput.value =
lastSpace < 0
? t
: withoutTrailing.slice(0, lastSpace + 1) + t;
if (searchClear) searchClear.hidden = !searchInput.value.trim();
searchInput.focus();
}
if (searchSuggest) {
searchSuggest.addEventListener('click', (e) => {
const btn = e.target.closest('[data-suggest]');
if (!btn) return;
const term = btn.getAttribute('data-suggest') || '';
selectSearchSuggestion(term);
hideSuggest();
});
}
if (searchClear) {
searchClear.addEventListener('click', () => {
if (searchInput) searchInput.value = '';
searchClear.hidden = true;
hideSuggest();
applySearchQuery('', true);
});
}
applyColgroup(grouped);
load();
}
const TAG_QUICK_COLORS = [
'#5c6b82', '#3d8bfd', '#6d28d9', '#b45309', '#047857',
'#dc2626', '#d97706', '#0891b2', '#be185d', '#4f46e5',
];
function canTagEdit() {
return document.body.getAttribute('data-can-tag-edit') === '1';
}
function slugTagId(label) {
return String(label)
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 32);
}
function renderTagPill(tag, removable) {
let html =
'
' +
escapeHtml(tag.label || tag.id);
if (removable) {
html +=
'';
}
return html + '';
}
function tagsApiUrl(entityType) {
return basePath() + (entityType === 'ticket' ? '/api/ticket_tags.php' : '/api/report_tags.php');
}
const TICKET_META_IDS = ['ticket', 'issue'];
function isTicketMetaId(id) {
return TICKET_META_IDS.indexOf(id) >= 0;
}
function stripTicketMetaTags(tags) {
return (tags || []).filter((t) => !isTicketMetaId(t.id));
}
function tagsJson(tags) {
return JSON.stringify(tags || []);
}
function createTagEditor(cfg) {
const entityType = cfg.entityType === 'ticket' ? 'ticket' : 'report';
const state = {
tags: [...(cfg.initialTags || [])],
workflow: [],
presets: [],
entityId: cfg.entityId ?? cfg.reportId ?? cfg.ticketId ?? 0,
entityType,
};
const els = cfg.elements;
function renderList() {
if (!els.list) return;
if (!state.tags.length) {
els.list.innerHTML = '
No custom tags yet.';
return;
}
els.list.innerHTML = state.tags
.map(
(t, i) =>
'
' +
renderTagPill(t, true) +
''
)
.join('');
els.list.querySelectorAll('.tag-remove').forEach((btn) => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-remove-id');
state.tags = state.tags.filter((t) => t.id !== id);
renderAll();
});
});
els.list.querySelectorAll('input[data-color-idx]').forEach((inp) => {
inp.addEventListener('input', () => {
const idx = Number(inp.getAttribute('data-color-idx'));
if (state.tags[idx]) state.tags[idx].bg = inp.value;
renderPreview();
});
});
}
function renderPreview() {
if (!els.preview) return;
els.preview.innerHTML = state.tags.length
? state.tags.map((t) => renderTagPill(t, false)).join('')
: '
No custom tags';
}
function presetChipHtml(p) {
return (
'
'
);
}
function wirePresetChips(root) {
if (!root) return;
root.querySelectorAll('.tag-preset-chip').forEach((btn) => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-preset-id');
const p =
state.workflow.find((x) => x.id === id) ||
state.presets.find((x) => x.id === id);
if (!p || state.tags.some((t) => t.id === p.id)) return;
if (entityType === 'ticket' && isTicketMetaId(p.id)) {
state.tags = stripTicketMetaTags(state.tags);
}
state.tags.push({ ...p });
renderAll();
});
});
}
function renderPresets() {
if (!els.presetBar || !els.presetChips) return;
const wf = state.workflow || [];
const hist = (state.presets || []).filter(
(p) => !wf.some((w) => w.id === p.id)
);
if (!wf.length && !hist.length) {
els.presetBar.hidden = true;
return;
}
els.presetBar.hidden = false;
let html = '';
if (wf.length) {
html +=
'
' +
escapeHtml(t('tag.workflow')) +
'' +
wf.map(presetChipHtml).join('') +
'
';
}
if (hist.length) {
html +=
'
' +
escapeHtml(t('tag.suggestions')) +
'' +
hist.map(presetChipHtml).join('') +
'
';
}
els.presetChips.innerHTML = html;
wirePresetChips(els.presetChips);
}
function renderAll() {
renderList();
renderPreview();
}
function setStatus(msg, ok) {
if (!els.status) return;
els.status.textContent = msg || '';
els.status.classList.toggle('tag-editor-status--err', ok === false);
}
function addFromForm(form) {
const label = (form.label?.value || '').trim();
const bg = form.bg?.value || '#5c6b82';
if (!label) return;
const id = slugTagId(label);
if (!id) return;
const reserved = ['new', 'java', 'ndk', 'android'];
if (reserved.includes(id)) {
setStatus('Reserved tag id: ' + id, false);
return;
}
const existing = state.tags.findIndex((t) => t.id === id);
const entry = { id, label, bg };
if (existing >= 0) state.tags[existing] = entry;
else state.tags.push(entry);
form.label.value = '';
renderAll();
setStatus('', true);
}
if (els.form) {
els.form.addEventListener('submit', (e) => {
e.preventDefault();
addFromForm(els.form);
});
}
if (els.saveBtn) {
els.saveBtn.addEventListener('click', () => {
setStatus(t('tag.saving'), true);
const sentTags = state.tags.map((t) => ({ ...t }));
const xhr = new XMLHttpRequest();
xhr.open('POST', tagsApiUrl(state.entityType), true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function () {
let data;
try {
data = JSON.parse(xhr.responseText);
} catch {
setStatus('Invalid response', false);
return;
}
if (!data.ok) {
setStatus(data.hint || data.error || 'Save failed', false);
return;
}
state.tags = data.tags || [];
renderAll();
const adjusted = tagsJson(sentTags) !== tagsJson(state.tags);
setStatus(adjusted ? 'Saved (type/status tags normalized)' : 'Saved', true);
if (cfg.onSaved) cfg.onSaved(state.tags, state.entityId);
};
xhr.onerror = function () {
setStatus(t('reports.network_error'), false);
};
xhr.send(JSON.stringify({ id: state.entityId, tags: state.tags }));
});
}
function loadFromApi() {
const xhr = new XMLHttpRequest();
xhr.open('GET', tagsApiUrl(state.entityType) + '?id=' + state.entityId, true);
xhr.onload = function () {
let data;
try {
data = JSON.parse(xhr.responseText);
} catch {
return;
}
if (data.ok) {
state.tags = data.tags || [];
state.workflow = data.workflow || [];
state.presets = data.presets || [];
renderAll();
renderPresets();
}
};
xhr.send();
}
renderAll();
if (cfg.fetchPresets) loadFromApi();
return {
setTags(tags) {
state.tags = tags || [];
renderAll();
},
getTags() {
return state.tags;
},
reload() {
loadFromApi();
},
};
}
let tagModalEditor = null;
function openTagModal(entityId, entityType) {
const modal = document.getElementById('tag-modal');
const editorEl = document.getElementById('tag-editor-modal');
if (!modal || !editorEl || !canTagEdit()) return;
const et = entityType === 'ticket' ? 'ticket' : 'report';
editorEl.setAttribute('data-report-id', et === 'report' ? String(entityId) : '0');
editorEl.setAttribute('data-ticket-id', et === 'ticket' ? String(entityId) : '0');
editorEl.setAttribute('data-entity-type', et);
modal.hidden = false;
modal.setAttribute('aria-hidden', 'false');
tagModalEditor = createTagEditor({
entityId: Number(entityId),
entityType: et,
initialTags: [],
fetchPresets: true,
elements: {
list: document.getElementById('tag-modal-list'),
preview: document.getElementById('tag-modal-preview'),
form: document.getElementById('tag-modal-add'),
presetBar: document.getElementById('tag-modal-presets'),
presetChips: document.getElementById('tag-modal-preset-chips'),
saveBtn: document.getElementById('tag-modal-save'),
status: document.getElementById('tag-modal-status'),
},
onSaved() {
if (typeof reportsReloadRef === 'function') reportsReloadRef();
if (typeof window.ticketsReloadRef === 'function') window.ticketsReloadRef();
},
});
}
window.openTagModal = openTagModal;
function closeTagModal() {
const modal = document.getElementById('tag-modal');
if (!modal) return;
modal.hidden = true;
modal.setAttribute('aria-hidden', 'true');
tagModalEditor = null;
}
let reportsReloadRef = null;
function initTagModal() {
document.querySelectorAll('[data-tag-modal-close]').forEach((el) => {
el.addEventListener('click', closeTagModal);
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeTagModal();
});
}
function initTagEditorReadOnly() {
const preview = document.getElementById('tag-editor-preview');
const dataEl =
document.getElementById('ticket-custom-tags-data') ||
document.getElementById('report-custom-tags-data');
if (!preview || !dataEl || canTagEdit()) return;
try {
const tags = JSON.parse(dataEl.textContent || '[]');
preview.innerHTML = tags.length
? tags.map((t) => renderTagPill(t, false)).join('')
: '
No custom tags';
} catch {
preview.innerHTML = '
—';
}
}
function initTagEditorDetail() {
const root = document.getElementById('tag-editor');
if (!root) return;
const entityType = root.getAttribute('data-entity-type') === 'ticket' ? 'ticket' : 'report';
const entityId = Number(
root.getAttribute(entityType === 'ticket' ? 'data-ticket-id' : 'data-report-id')
);
const canEdit = entityType === 'ticket' ? !!root.querySelector('#tag-editor-list') : canTagEdit();
if (!canEdit) {
initTagEditorReadOnly();
return;
}
let initial = [];
const dataEl =
document.getElementById('ticket-custom-tags-data') ||
document.getElementById('report-custom-tags-data');
if (dataEl) {
try {
initial = JSON.parse(dataEl.textContent || '[]');
} catch { /* ignore */ }
}
createTagEditor({
entityId,
entityType,
initialTags: initial,
fetchPresets: true,
elements: {
list: document.getElementById('tag-editor-list'),
preview: document.getElementById('tag-editor-preview'),
form: document.getElementById('tag-editor-add'),
presetBar: document.getElementById('tag-preset-bar'),
presetChips: document.getElementById('tag-preset-chips'),
saveBtn: document.getElementById('tag-editor-save'),
status: document.getElementById('tag-editor-status'),
},
});
}
function initTicketDetail() {
const id = document.body.getAttribute('data-ticket-id');
if (!id) return;
const tree = document.getElementById('detail-tree');
if (tree) bindDetailTree(tree);
let assignees = [];
const assigneeDataEl = document.getElementById('ticket-assignees-data');
if (assigneeDataEl) {
try {
assignees = JSON.parse(assigneeDataEl.textContent || '[]');
if (!Array.isArray(assignees)) assignees = [];
} catch {
assignees = [];
}
}
const assigneeEditor = document.getElementById('assignee-editor');
const assigneePick = document.getElementById('assignee-user-pick');
const assigneeAddBtn = document.getElementById('assignee-add-btn');
function renderAssignees() {
if (!assigneeEditor) return;
if (!assignees.length) {
assigneeEditor.innerHTML = '
—';
return;
}
assigneeEditor.innerHTML = assignees
.map(function (a, idx) {
const role = a.role ? '
(' + escapeHtml(a.role) + ')' : '';
return (
'
' +
escapeHtml(a.username) +
role +
' '
);
})
.join('');
assigneeEditor.querySelectorAll('.assignee-remove').forEach(function (btn) {
btn.addEventListener('click', function () {
const i = Number(btn.getAttribute('data-idx'));
assignees.splice(i, 1);
renderAssignees();
});
});
}
function loadUsersForAssignees() {
if (!assigneePick) return;
const xhr = new XMLHttpRequest();
xhr.open('GET', basePath() + '/api/users.php', true);
xhr.onload = function () {
let data;
try {
data = JSON.parse(xhr.responseText);
} catch {
return;
}
if (!data.ok || !Array.isArray(data.users)) return;
data.users.forEach(function (u) {
const opt = document.createElement('option');
opt.value = u.username;
opt.textContent = u.username + (u.role ? ' (' + u.role + ')' : '');
opt.dataset.role = u.role || '';
assigneePick.appendChild(opt);
});
};
xhr.send();
}
if (assigneeEditor) {
renderAssignees();
loadUsersForAssignees();
if (assigneeAddBtn && assigneePick) {
assigneeAddBtn.addEventListener('click', function () {
const username = assigneePick.value;
if (!username) return;
const role = assigneePick.selectedOptions[0]?.dataset.role || '';
if (assignees.some(function (a) { return a.username === username; })) return;
assignees.push({ username: username, role: role });
renderAssignees();
});
}
}
const attachmentList = document.getElementById('ticket-attachment-list');
const linkForm = document.getElementById('ticket-link-form');
const fileForm = document.getElementById('ticket-file-form');
const linkStatus = document.getElementById('ticket-link-status');
const fileStatus = document.getElementById('ticket-file-status');
function renderAttachmentItem(att) {
const li = document.createElement('li');
li.className = 'ticket-attachment ticket-attachment--' + (att.kind || 'file');
li.dataset.attachmentId = String(att.id);
let main = '';
const label = escapeHtml(att.provider_label || 'File');
if (att.kind === 'link' && att.external_url) {
main =
'
' +
escapeHtml(att.title || att.external_url) +
'';
} else if (att.download_url) {
const size =
att.size_bytes > 0
? '
(' + (att.size_bytes / 1024).toFixed(1) + ' KB)'
: '';
main =
'
' +
escapeHtml(att.title || att.original_name || 'file') +
'' +
size;
} else {
main = '
' + escapeHtml(att.title || '') + '';
}
li.innerHTML =
'
' +
label +
' ' +
main +
'
' +
escapeHtml(att.created_by || '') +
'' +
'
';
li.querySelector('.attachment-remove').addEventListener('click', function () {
if (!confirm('Remove attachment?')) return;
const xhr = new XMLHttpRequest();
xhr.open('DELETE', basePath() + '/api/ticket_attachments.php?id=' + att.id, true);
xhr.onload = function () {
let data;
try {
data = JSON.parse(xhr.responseText);
} catch {
return;
}
if (data.ok) li.remove();
};
xhr.send();
});
return li;
}
if (linkForm) {
linkForm.addEventListener('submit', function (e) {
e.preventDefault();
const fd = new FormData(linkForm);
const url = String(fd.get('url') || '').trim();
if (!url) return;
if (linkStatus) linkStatus.textContent = 'Saving…';
const xhr = new XMLHttpRequest();
xhr.open('POST', basePath() + '/api/ticket_attachments.php', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function () {
let data;
try {
data = JSON.parse(xhr.responseText);
} catch {
if (linkStatus) linkStatus.textContent = 'Invalid response';
return;
}
if (!data.ok) {
if (linkStatus) linkStatus.textContent = data.error || 'Failed';
return;
}
if (attachmentList && data.attachment) {
attachmentList.appendChild(renderAttachmentItem(data.attachment));
}
linkForm.reset();
if (linkStatus) linkStatus.textContent = 'Attached';
};
xhr.onerror = function () {
if (linkStatus) linkStatus.textContent = t('reports.network_error');
};
xhr.send(
JSON.stringify({
ticket_id: Number(id),
title: fd.get('title'),
url: url,
})
);
});
}
if (fileForm) {
fileForm.addEventListener('submit', function (e) {
e.preventDefault();
const fd = new FormData(fileForm);
fd.append('ticket_id', id);
if (fileStatus) fileStatus.textContent = 'Uploading…';
const xhr = new XMLHttpRequest();
xhr.open('POST', basePath() + '/api/ticket_attachments.php', true);
xhr.onload = function () {
let data;
try {
data = JSON.parse(xhr.responseText);
} catch {
if (fileStatus) fileStatus.textContent = 'Invalid response';
return;
}
if (!data.ok) {
if (fileStatus) fileStatus.textContent = data.error || 'Failed';
return;
}
if (attachmentList && data.attachment) {
attachmentList.appendChild(renderAttachmentItem(data.attachment));
}
fileForm.reset();
if (fileStatus) fileStatus.textContent = 'Uploaded';
};
xhr.onerror = function () {
if (fileStatus) fileStatus.textContent = t('reports.network_error');
};
xhr.send(fd);
});
}
if (attachmentList) {
attachmentList.querySelectorAll('.attachment-remove').forEach(function (btn) {
btn.addEventListener('click', function () {
const attId = btn.getAttribute('data-id');
if (!confirm('Remove attachment?')) return;
const xhr = new XMLHttpRequest();
xhr.open('DELETE', basePath() + '/api/ticket_attachments.php?id=' + attId, true);
xhr.onload = function () {
let data;
try {
data = JSON.parse(xhr.responseText);
} catch {
return;
}
if (data.ok) btn.closest('.ticket-attachment')?.remove();
};
xhr.send();
});
});
}
const commentForm = document.getElementById('ticket-comment-form');
const commentStatus = document.getElementById('ticket-comment-status');
const commentList = document.getElementById('ticket-comment-list');
if (commentForm) {
commentForm.addEventListener('submit', function (e) {
e.preventDefault();
const fd = new FormData(commentForm);
const bodyText = String(fd.get('body') || '').trim();
if (!bodyText) return;
if (commentStatus) commentStatus.textContent = 'Posting…';
const xhr = new XMLHttpRequest();
xhr.open('POST', basePath() + '/api/ticket_comments.php', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function () {
let data;
try {
data = JSON.parse(xhr.responseText);
} catch {
if (commentStatus) commentStatus.textContent = 'Invalid response';
return;
}
if (!data.ok) {
if (commentStatus) commentStatus.textContent = data.error || 'Failed';
return;
}
const c = data.comment;
if (commentList && c) {
const li = document.createElement('li');
li.className = 'ticket-comment';
const when = c.created_at_ms
? new Date(Number(c.created_at_ms)).toISOString().replace('T', ' ').slice(0, 19)
: '';
li.innerHTML =
'
';
li.querySelector('.ticket-comment-body').textContent = c.body;
commentList.appendChild(li);
}
commentForm.reset();
if (commentStatus) commentStatus.textContent = 'Posted';
};
xhr.onerror = function () {
if (commentStatus) commentStatus.textContent = t('reports.network_error');
};
xhr.send(JSON.stringify({ ticket_id: Number(id), body: bodyText }));
});
}
const form = document.getElementById('ticket-edit-form');
if (!form) return;
const statusEl = document.getElementById('ticket-save-status');
const ratingSel = form.querySelector('select[name="rating"]');
const starsPreview = form.querySelector('.ticket-stars-preview .star-rating');
if (ratingSel && starsPreview) {
ratingSel.addEventListener('change', () => {
const n = Math.max(0, Math.min(5, Number(ratingSel.value) || 0));
let html = '';
for (let i = 1; i <= 5; i++) {
html += '
';
}
starsPreview.innerHTML = html;
});
}
form.addEventListener('submit', (e) => {
e.preventDefault();
if (statusEl) statusEl.textContent = 'Saving…';
const fd = new FormData(form);
const body = {
id: Number(form.getAttribute('data-ticket-id')),
title: fd.get('title'),
brief: fd.get('brief'),
body: fd.get('body'),
owner_username: fd.get('owner_username'),
verified_by_username: fd.get('verified_by_username'),
rating: Number(fd.get('rating')),
workflow_state: fd.get('workflow_state'),
assignees: assignees,
};
const xhr = new XMLHttpRequest();
xhr.open('PUT', basePath() + '/api/ticket_update.php', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function () {
let data;
try {
data = JSON.parse(xhr.responseText);
} catch {
if (statusEl) statusEl.textContent = 'Invalid response';
return;
}
if (!data.ok) {
if (statusEl) statusEl.textContent = data.error || 'Save failed';
return;
}
if (statusEl) statusEl.textContent = 'Saved';
};
xhr.onerror = function () {
if (statusEl) statusEl.textContent = t('reports.network_error');
};
xhr.send(JSON.stringify(body));
});
}
function initTagFilterBar(listContext) {
const chipsEl = document.getElementById('tag-filter-chips');
const modeWrap = document.getElementById('tag-mode-wrap');
const modeSel = document.getElementById('tag-mode-select');
if (!chipsEl) return;
function renderFilterChips(workflow) {
const active = listContext.filterTags || [];
chipsEl.innerHTML = (workflow || [])
.map((tag) => {
const on = active.includes(tag.id);
return (
'
' +
escapeHtml(tag.label) +
''
);
})
.join('');
}
if (modeSel) {
modeSel.value = listContext.tagMode === 'or' ? 'or' : 'and';
modeSel.addEventListener('change', () => {
if (!(listContext.filterTags && listContext.filterTags.length)) return;
const p = new URLSearchParams(window.location.search);
p.set('view', 'reports');
p.set('tag_mode', modeSel.value === 'or' ? 'or' : 'and');
p.delete('page');
window.location.href = basePath() + '/?' + p.toString();
});
}
const xhr = new XMLHttpRequest();
xhr.open('GET', basePath() + '/api/tag_catalog.php', true);
xhr.onload = function () {
let data;
try {
data = JSON.parse(xhr.responseText);
} catch {
return;
}
if (!data.ok) return;
renderFilterChips(data.workflow || []);
if (modeWrap) {
modeWrap.hidden = !(listContext.filterTags && listContext.filterTags.length);
}
};
xhr.send();
}
function initTagEditButtons(tbody, reloadFn) {
reportsReloadRef = reloadFn;
if (!tbody) return;
tbody.addEventListener('click', (e) => {
const btn = e.target.closest('.tag-edit-btn');
if (!btn) return;
e.stopPropagation();
e.preventDefault();
openTagModal(Number(btn.getAttribute('data-report-id')), 'report');
});
}
function boot() {
initTheme();
initNav();
initTagModal();
initReportDetail();
initTicketDetail();
initTagEditorDetail();
initReportsApp();
}
function onReady(fn) {
const run = () => {
if (window.CrashI18n && window.CrashI18n.ready) {
window.CrashI18n.ready.then(fn);
} else {
fn();
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', run);
} else {
run();
}
}
onReady(boot);
})();