import { apiGet, apiPost, apiPostForm, apiPut, 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, syncEventFormFieldGroups, buildEventPostBody, loadTeamViewerConnectionsIntoSelect, sortEventsChronologicalWithAttachmentsLast, } from '../core/ticket-events.js'; const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; function formatAssigneeLabel(u) { if (!u) return 'Nicht zugewiesen'; const name = [u.firstName, u.lastName].filter(Boolean).join(' ').trim(); return name || u.username || u.id; } function assigneeOptionLabel(u) { const name = [u.firstName, u.lastName].filter(Boolean).join(' ').trim(); return name ? `${name} (${u.username})` : u.username; } const loadingEl = document.getElementById('page-loading'); const badIdEl = document.getElementById('ticket-bad-id'); const errEl = document.getElementById('page-error'); const mainEl = document.getElementById('page-main'); const panelView = document.getElementById('panel-ticket-view'); const panelEdit = document.getElementById('panel-ticket-edit'); function showError(msg) { loadingEl.hidden = true; badIdEl.hidden = true; mainEl.hidden = true; errEl.hidden = false; errEl.textContent = msg; } function fillTicketView(ticket) { document.getElementById('ticket-title').textContent = ticket.title; const stBadge = document.getElementById('t-status-badge'); stBadge.textContent = ticketStatusLabel[ticket.status]; stBadge.className = `badge ${statusBadgeClass[ticket.status] || ''}`; document.getElementById('t-priority-label').textContent = ticketPriorityLabel[ticket.priority]; const slaSel = document.getElementById('t-sla-days'); if (slaSel) { slaSel.value = ticket.slaDays != null && ticket.slaDays !== '' ? String(ticket.slaDays) : ''; } document.getElementById('t-description').textContent = ticket.description; const mrow = document.getElementById('t-machine-row'); if (ticket.machine) { mrow.hidden = false; const mn = extrasName(ticket.machine); const link = document.getElementById('t-machine-link'); link.href = `/machine.html?id=${encodeURIComponent(ticket.machine.id)}`; link.textContent = ticket.machine.seriennummer; document.getElementById('t-machine-suffix').textContent = mn ? ` · ${mn}` : ''; } else { mrow.hidden = true; } const assignLabel = document.getElementById('t-assign-label'); if (assignLabel) { assignLabel.textContent = formatAssigneeLabel(ticket.assignedTo); } } function fillEditForm(ticket) { document.getElementById('tu-title').value = ticket.title; document.getElementById('tu-desc').value = ticket.description; document.getElementById('tu-status').value = ticket.status; document.getElementById('tu-priority').value = ticket.priority; } function renderEvents(events) { const tbody = document.getElementById('events-table-body'); if (events.length === 0) { tbody.innerHTML = 'Noch keine Ereignisse.'; return; } tbody.innerHTML = events .map( (ev) => ` ${esc(formatDateTime(ev.createdAt))} ${esc(eventTypeLabel[ev.type] || ev.type)} ${eventInhaltHtml(ev)} `, ) .join(''); } function showViewMode() { panelView.hidden = false; panelEdit.hidden = true; } function showEditMode() { panelView.hidden = true; panelEdit.hidden = false; } async function viewTicketDetail(id, canEdit) { const [ticket, events] = await Promise.all([ apiGet(`/tickets/${id}`), apiGet(`/tickets/${id}/events`), ]); let currentTicket = ticket; fillTicketView(currentTicket); fillEditForm(currentTicket); showViewMode(); renderEvents(sortEventsChronologicalWithAttachmentsLast(events)); const sect2 = document.getElementById('sect-second-ticket'); if (currentTicket.machineId) { sect2.hidden = false; } else { sect2.hidden = true; } if (!canEdit) { document.getElementById('btn-t-edit').hidden = true; document.getElementById('panel-ticket-edit').hidden = true; const assignSel = document.getElementById('t-assign-user'); const assignP = assignSel?.closest('p'); if (assignP) assignP.hidden = true; const assignRo = document.getElementById('t-assign-readonly'); if (assignRo) assignRo.hidden = false; const slaRd = document.getElementById('t-sla-days'); if (slaRd) slaRd.disabled = true; const evForm = document.getElementById('form-ev'); const evCard = evForm?.closest('.card'); if (evCard) evCard.hidden = true; sect2.hidden = true; const pv = document.getElementById('panel-ticket-view'); if (pv) { const hint = document.createElement('p'); hint.className = 'muted'; hint.textContent = 'Nur Lesen.'; pv.appendChild(hint); } return; } const assignSel = document.getElementById('t-assign-user'); const assignRo = document.getElementById('t-assign-readonly'); if (assignRo) assignRo.hidden = true; if (assignSel) { try { const users = await apiGet('/assignable-users'); assignSel.innerHTML = ''; const optNone = document.createElement('option'); optNone.value = ''; optNone.textContent = '— nicht zugewiesen —'; assignSel.appendChild(optNone); for (const u of users) { const opt = document.createElement('option'); opt.value = u.id; opt.textContent = assigneeOptionLabel(u); assignSel.appendChild(opt); } assignSel.value = currentTicket.assignedTo?.id ?? ''; } catch (err) { assignSel.innerHTML = ''; if (isAuthRedirectError(err)) return; window.alert(err.message || 'Benutzerliste konnte nicht geladen werden.'); } assignSel.onchange = async () => { const v = assignSel.value; const assignedUserId = v === '' ? null : v; try { const updated = await apiPut(`/tickets/${id}`, { assignedUserId }); const evs = await apiGet(`/tickets/${id}/events`); currentTicket = updated; fillTicketView(updated); fillEditForm(updated); assignSel.value = updated.assignedTo?.id ?? ''; renderEvents(sortEventsChronologicalWithAttachmentsLast(evs)); } catch (err) { assignSel.value = currentTicket.assignedTo?.id ?? ''; if (isAuthRedirectError(err)) return; window.alert(err.message || 'Zuweisung konnte nicht gespeichert werden.'); } }; } const slaSel = document.getElementById('t-sla-days'); if (slaSel) { slaSel.onchange = async () => { const v = slaSel.value; const slaDays = v === '' ? null : Number(v); try { const updated = await apiPut(`/tickets/${id}`, { slaDays }); const evs = await apiGet(`/tickets/${id}/events`); currentTicket = updated; fillTicketView(updated); fillEditForm(updated); renderEvents(sortEventsChronologicalWithAttachmentsLast(evs)); } catch (err) { slaSel.value = currentTicket.slaDays != null ? String(currentTicket.slaDays) : ''; if (isAuthRedirectError(err)) return; window.alert(err.message || 'Fehler beim Speichern der Fälligkeit.'); } }; } document.getElementById('btn-t-edit').onclick = () => { fillEditForm(currentTicket); showEditMode(); }; panelEdit.onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(panelEdit); await apiPut(`/tickets/${id}`, Object.fromEntries(fd.entries())); location.reload(); }; document.getElementById('tu-cancel').onclick = () => { fillTicketView(currentTicket); showViewMode(); }; const formEv = document.getElementById('form-ev'); syncEventFormFieldGroups(formEv); formEv.querySelector('#ev-type-sel').onchange = () => syncEventFormFieldGroups(formEv); document.getElementById('btn-tv-reload').onclick = () => loadTeamViewerConnectionsIntoSelect(); formEv.onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(e.target); const evType = fd.get('type'); if (evType === 'ATTACHMENT') { const fileInput = document.getElementById('ev-attachment-files'); const files = fileInput?.files; if (!files || files.length === 0) { formEv.insertAdjacentHTML( 'afterbegin', '

Mindestens eine Datei auswählen.

', ); setTimeout(() => { document.querySelector('.tv-form-err')?.remove(); }, 4000); return; } const formData = new FormData(); formData.append( 'description', String(fd.get('description_attachment') ?? '').trim(), ); for (let i = 0; i < files.length; i += 1) { formData.append('files', files[i]); } await apiPostForm(`/tickets/${id}/events/attachments`, formData); e.target.reset(); syncEventFormFieldGroups(formEv); location.reload(); return; } let body = buildEventPostBody(id, fd); if (body.type === 'REMOTE') { const sel = document.getElementById('tv-conn-select'); const opt = sel?.selectedOptions?.[0]; if (opt?.value) { body.teamviewerId = opt.value; const sd = opt.getAttribute('data-start-date'); const ed = opt.getAttribute('data-end-date'); if (sd) body.teamviewerStartDate = sd; if (ed) body.teamviewerEndDate = ed; const n = opt.getAttribute('data-notes'); if (n && String(n).trim()) body.teamviewerNotes = String(n).trim(); const dn = opt.getAttribute('data-devicename') || ''; const u = String(fd.get('description_remote') ?? '').trim(); if (dn) { body.description = u ? `${u}\n\nTeamViewer-Gerät: ${dn}` : `TeamViewer-Gerät: ${dn}`; } else if (u) { body.description = u; } else { body.description = 'Remote-Session (TeamViewer)'; } } else { body.description = String(body.description ?? '').trim(); if (!body.description) { formEv.insertAdjacentHTML( 'afterbegin', '

Beschreibung oder Gerät auswählen.

', ); setTimeout(() => { document.querySelector('.tv-form-err')?.remove(); }, 4000); return; } } } await apiPost('/events', body); e.target.reset(); syncEventFormFieldGroups(formEv); location.reload(); }; document.getElementById('form-t2').onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(e.target); await apiPost('/tickets', { machineId: currentTicket.machineId, title: fd.get('title'), description: fd.get('description'), }); e.target.reset(); location.reload(); }; } async function init() { const st = await guard({ activeNav: 'tickets' }); if (!st) return; const canEdit = st.user?.canEditCrm === true; const id = new URLSearchParams(location.search).get('id'); if (!id || !UUID.test(id)) { loadingEl.hidden = true; badIdEl.hidden = false; return; } loadingEl.hidden = true; mainEl.hidden = false; bindAttachmentPreview(document.body); try { await viewTicketDetail(id, canEdit); } catch (e) { if (isAuthRedirectError(e)) return; showError(e.message || 'Fehler'); } } init();