210 lines
7.8 KiB
JavaScript
210 lines
7.8 KiB
JavaScript
import { apiGet, isAuthRedirectError } from '../api.js';
|
|
import { guard } from '../core/auth-guard.js';
|
|
import {
|
|
ticketStatusLabel,
|
|
ticketPriorityLabel,
|
|
eventTypeLabel,
|
|
eventTypeBadgeClass,
|
|
statusBadgeClass,
|
|
priorityBadgeClass,
|
|
} from '../core/constants.js';
|
|
import { esc, formatDateTime, extrasName } from '../core/utils.js';
|
|
import { bindAttachmentPreview } from '../core/attachment-preview.js';
|
|
import {
|
|
eventInhaltHtml,
|
|
sortEventsChronologicalWithAttachmentsLast,
|
|
} from '../core/ticket-events.js';
|
|
|
|
const loadingEl = document.getElementById('page-loading');
|
|
const mainEl = document.getElementById('page-main');
|
|
const errEl = document.getElementById('page-error');
|
|
const listEl = document.getElementById('home-ticket-list');
|
|
const listMineEl = document.getElementById('home-ticket-list-mine');
|
|
const emptyEl = document.getElementById('home-empty');
|
|
const tpl = document.getElementById('tpl-home-ticket');
|
|
|
|
function formatAssigneeLabel(ticket) {
|
|
const u = ticket.assignedTo;
|
|
if (!u) return '—';
|
|
const name = [u.firstName, u.lastName].filter(Boolean).join(' ').trim();
|
|
return name || u.username || u.id;
|
|
}
|
|
|
|
function showError(msg) {
|
|
loadingEl.hidden = true;
|
|
mainEl.hidden = true;
|
|
errEl.hidden = false;
|
|
errEl.textContent = msg;
|
|
}
|
|
|
|
function renderEventBoxes(events) {
|
|
const evChrono = sortEventsChronologicalWithAttachmentsLast(events);
|
|
if (evChrono.length === 0) {
|
|
return '<p class="muted home-ticket-no-events">Noch keine Ereignisse in der Historie.</p>';
|
|
}
|
|
return evChrono
|
|
.map(
|
|
(ev) => `
|
|
<div class="home-event-box">
|
|
<div class="home-event-box-header">
|
|
<span class="badge event-type-badge ${eventTypeBadgeClass[ev.type] || ''}">${esc(eventTypeLabel[ev.type] || ev.type)}</span>
|
|
<time class="home-event-box-time" datetime="${esc(ev.createdAt)}">${esc(formatDateTime(ev.createdAt))}</time>
|
|
</div>
|
|
<div class="home-event-box-body">${eventInhaltHtml(ev)}</div>
|
|
</div>`,
|
|
)
|
|
.join('');
|
|
}
|
|
|
|
/** @param {'open' | 'mine'} listKind */
|
|
function fillTicketCard(node, t, events, listKind) {
|
|
const id = t.id;
|
|
const detailId = `home-ticket-detail-${id}`;
|
|
node.dataset.ticketId = id;
|
|
|
|
const btn = node.querySelector('.home-ticket-collapse-btn');
|
|
const panel = node.querySelector('.home-ticket-collapsible');
|
|
panel.id = detailId;
|
|
btn.setAttribute('aria-controls', detailId);
|
|
|
|
const mn = t.machine ? extrasName(t.machine) : '';
|
|
const machineLabel = t.machine
|
|
? `${esc(t.machine.seriennummer)}${mn ? ` · ${esc(mn)}` : ''}`
|
|
: '';
|
|
const standort = t.machine ? esc(t.machine.standort) : '—';
|
|
|
|
const headEl = node.querySelector('.home-ticket-head');
|
|
headEl.classList.toggle('home-ticket-head-overdue', Boolean(t.isOverdue));
|
|
|
|
const titleA = node.querySelector('.js-ticket-link');
|
|
titleA.href = `/ticket.html?id=${encodeURIComponent(id)}`;
|
|
titleA.textContent = t.title;
|
|
|
|
const st = node.querySelector('.js-status');
|
|
st.textContent = ticketStatusLabel[t.status];
|
|
st.className = `badge js-status ${statusBadgeClass[t.status] || ''}`;
|
|
|
|
const pr = node.querySelector('.js-priority');
|
|
pr.textContent = ticketPriorityLabel[t.priority];
|
|
pr.className = `badge js-priority ${priorityBadgeClass[t.priority] || ''}`;
|
|
|
|
const assignTag = node.querySelector('.js-assignee-tag');
|
|
if (assignTag) {
|
|
const name = formatAssigneeLabel(t);
|
|
if (listKind === 'mine') {
|
|
assignTag.textContent = name;
|
|
assignTag.hidden = false;
|
|
assignTag.className = 'badge js-assignee-tag badge-assignee';
|
|
assignTag.title = `Zugewiesen: ${name}`;
|
|
} else {
|
|
const hasOther = Boolean(t.assignedTo);
|
|
assignTag.textContent = hasOther ? name : 'Nicht zugewiesen';
|
|
assignTag.hidden = false;
|
|
assignTag.className = hasOther
|
|
? 'badge js-assignee-tag badge-assignee'
|
|
: 'badge js-assignee-tag badge-assignee badge-assignee--none';
|
|
assignTag.title = hasOther ? `Zugewiesen: ${name}` : 'Noch niemandem zugewiesen';
|
|
}
|
|
}
|
|
|
|
const metaM = node.querySelector('.js-meta-machine');
|
|
metaM.innerHTML = t.machine
|
|
? `<span class="muted">Maschine:</span> ${machineLabel}`
|
|
: '<span class="muted">Keine Maschine</span>';
|
|
|
|
node.querySelector('.js-meta-standort').textContent = standort;
|
|
node.querySelector('.js-meta-created').textContent = formatDateTime(t.createdAt);
|
|
node.querySelector('.js-meta-updated').textContent = formatDateTime(t.updatedAt);
|
|
const assigneeEl = node.querySelector('.js-meta-assignee');
|
|
if (assigneeEl) assigneeEl.textContent = formatAssigneeLabel(t);
|
|
|
|
const openA = node.querySelector('.js-ticket-open');
|
|
openA.href = `/ticket.html?id=${encodeURIComponent(id)}`;
|
|
|
|
node.querySelector('.js-desc').textContent = t.description;
|
|
|
|
const mBlock = node.querySelector('.js-machine-block');
|
|
if (t.machine) {
|
|
const m = t.machine;
|
|
mBlock.innerHTML = `<p class="home-ticket-machine"><strong>Maschine:</strong> <a href="/machine.html?id=${esc(m.id)}">${esc(m.seriennummer)}</a>${mn ? ` · ${esc(mn)}` : ''} · <strong>Typ:</strong> ${esc(m.typ)}</p>`;
|
|
} else {
|
|
mBlock.innerHTML = '';
|
|
}
|
|
|
|
node.querySelector('.js-events').innerHTML = renderEventBoxes(events);
|
|
|
|
const chev = btn.querySelector('.home-ticket-chevron');
|
|
btn.onclick = () => {
|
|
const willShow = panel.hidden;
|
|
panel.hidden = !willShow;
|
|
btn.setAttribute('aria-expanded', String(willShow));
|
|
chev.textContent = willShow ? '▲' : '▼';
|
|
};
|
|
}
|
|
|
|
function renderTicketListInto(container, tickets, eventsLists, listKind) {
|
|
container.innerHTML = '';
|
|
tickets.forEach((t, i) => {
|
|
const frag = tpl.content.cloneNode(true);
|
|
const article = frag.querySelector('.home-ticket-card');
|
|
fillTicketCard(article, t, eventsLists[i] || [], listKind);
|
|
container.appendChild(article);
|
|
});
|
|
}
|
|
|
|
async function run() {
|
|
const [ticketsAll, ticketsMine] = await Promise.all([
|
|
apiGet('/tickets?open=1&assignedTo=not_me'),
|
|
apiGet('/tickets?open=1&assignedTo=me'),
|
|
]);
|
|
|
|
const eventsAll =
|
|
ticketsAll.length === 0
|
|
? []
|
|
: await Promise.all(ticketsAll.map((t) => apiGet(`/tickets/${t.id}/events`)));
|
|
const eventsMine =
|
|
ticketsMine.length === 0
|
|
? []
|
|
: await Promise.all(ticketsMine.map((t) => apiGet(`/tickets/${t.id}/events`)));
|
|
|
|
const openCount = ticketsAll.filter((t) => t.status === 'OPEN').length;
|
|
const waitingCount = ticketsAll.filter((t) => t.status === 'WAITING').length;
|
|
document.getElementById('kpi-open').textContent = `${openCount} Offen`;
|
|
document.getElementById('kpi-waiting').textContent = `${waitingCount} Wartend`;
|
|
document.getElementById('kpi-total').textContent = `gesamt: ${ticketsAll.length}`;
|
|
|
|
const mineOpen = ticketsMine.filter((t) => t.status === 'OPEN').length;
|
|
const mineWaiting = ticketsMine.filter((t) => t.status === 'WAITING').length;
|
|
document.getElementById('kpi-mine-open').textContent = `${mineOpen} Offen`;
|
|
document.getElementById('kpi-mine-waiting').textContent = `${mineWaiting} Wartend`;
|
|
document.getElementById('kpi-mine-total').textContent = `gesamt: ${ticketsMine.length}`;
|
|
|
|
if (ticketsAll.length === 0) {
|
|
emptyEl.hidden = false;
|
|
emptyEl.textContent = 'Keine offenen Tickets.';
|
|
listEl.innerHTML = '';
|
|
} else {
|
|
emptyEl.hidden = true;
|
|
renderTicketListInto(listEl, ticketsAll, eventsAll, 'open');
|
|
}
|
|
|
|
renderTicketListInto(listMineEl, ticketsMine, eventsMine, 'mine');
|
|
}
|
|
|
|
async function init() {
|
|
const st = await guard({ activeNav: 'start' });
|
|
if (!st) return;
|
|
loadingEl.hidden = true;
|
|
mainEl.hidden = false;
|
|
bindAttachmentPreview(document.body);
|
|
|
|
try {
|
|
await run();
|
|
} catch (e) {
|
|
if (isAuthRedirectError(e)) return;
|
|
showError(e.message || 'Fehler');
|
|
}
|
|
}
|
|
|
|
init();
|