Files
SDS-CRM/public/js/pages/ticket-detail.js

367 lines
12 KiB
JavaScript

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 =
'<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, 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 = '<option value="">— Fehler beim Laden —</option>';
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',
'<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 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();