V0.1
This commit is contained in:
283
public/js/pages/ticket-detail.js
Normal file
283
public/js/pages/ticket-detail.js
Normal file
@@ -0,0 +1,283 @@
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 =
|
||||
'<tr><td colspan="3" class="muted">Noch keine Ereignisse.</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = events
|
||||
.map(
|
||||
(ev) => `
|
||||
<tr>
|
||||
<td class="events-table-time">${esc(formatDateTime(ev.createdAt))}</td>
|
||||
<td><span class="badge event-type-badge ${eventTypeBadgeClass[ev.type] || ''}">${esc(eventTypeLabel[ev.type] || ev.type)}</span></td>
|
||||
<td class="events-table-desc">${eventInhaltHtml(ev)}</td>
|
||||
</tr>`,
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
|
||||
function showViewMode() {
|
||||
panelView.hidden = false;
|
||||
panelEdit.hidden = true;
|
||||
}
|
||||
|
||||
function showEditMode() {
|
||||
panelView.hidden = true;
|
||||
panelEdit.hidden = false;
|
||||
}
|
||||
|
||||
async function viewTicketDetail(id) {
|
||||
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;
|
||||
}
|
||||
|
||||
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',
|
||||
'<p class="error tv-form-err">Mindestens eine Datei auswählen.</p>',
|
||||
);
|
||||
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',
|
||||
'<p class="error tv-form-err">Beschreibung oder Gerät auswählen.</p>',
|
||||
);
|
||||
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 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);
|
||||
} catch (e) {
|
||||
if (isAuthRedirectError(e)) return;
|
||||
showError(e.message || 'Fehler');
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
Reference in New Issue
Block a user