import {
apiDelete,
apiGet,
apiPost,
apiPut,
authFetchStatus,
isAuthRedirectError,
} from './api.js';
/** Zwischenspeicher für GET /integrations/teamviewer/connections (Benutzer → Geräte). */
let tvSessionsCache = null;
const ticketStatusLabel = {
OPEN: 'Offen',
WAITING: 'Warte auf Rückmeldung',
DONE: 'Erledigt',
};
const ticketPriorityLabel = {
LOW: 'Niedrig',
MEDIUM: 'Mittel',
HIGH: 'Hoch',
};
const eventTypeLabel = {
NOTE: 'Notiz',
CALL: 'Anruf',
REMOTE: 'Remote',
PART: 'Ersatzteil benötigt',
SYSTEM: 'System',
};
const eventTypeBadgeClass = {
NOTE: 'event-type-note',
CALL: 'event-type-call',
REMOTE: 'event-type-remote',
PART: 'event-type-part',
SYSTEM: 'event-type-system',
};
/** Anzeige Dauer aus gespeicherten Sekunden (TeamViewer start/end). */
function formatRemoteDurationDe(totalSec) {
if (totalSec == null || totalSec < 0) return '';
const n = Math.floor(Number(totalSec));
const m = Math.floor(n / 60);
const s = n % 60;
if (m === 0) return `${s} Sek.`;
if (s === 0) return `${m} Min.`;
return `${m} Min. ${s} Sek.`;
}
/** HTML für die Inhaltsspalte (nur server-/formularbekannte Typen) */
function eventInhaltHtml(ev) {
const t = ev.type;
if (t === 'CALL') {
let h = `
Beschreibung
${esc(ev.description)}
`;
if (ev.callbackNumber) {
h += `
Rückrufnummer: ${esc(ev.callbackNumber)}
`;
}
return `${h}
`;
}
if (t === 'REMOTE') {
let h = `Beschreibung
${esc(ev.description)}
`;
if (ev.teamviewerId) {
h += `
Gerät-ID (TeamViewer): ${esc(ev.teamviewerId)}
`;
}
if (ev.remoteDurationSeconds != null) {
h += `
Remote-Dauer: ${esc(formatRemoteDurationDe(ev.remoteDurationSeconds))}
`;
}
return `${h}
`;
}
if (t === 'PART') {
let h = `Artikelnummer: ${esc(ev.articleNumber || '')}
`;
if (ev.description && String(ev.description).trim()) {
h += `
Bemerkung
${esc(ev.description)}
`;
}
return `${h}
`;
}
return `${esc(ev.description)}
`;
}
function fillTvDeviceSelect() {
const userSel = document.getElementById('tv-user-select');
const devSel = document.getElementById('tv-conn-select');
if (!devSel) return;
const ukey = userSel?.value ?? '';
devSel.innerHTML =
'';
if (!ukey || !tvSessionsCache) {
devSel.disabled = true;
return;
}
const u = (tvSessionsCache.users || []).find((x) => x.userKey === ukey);
const devices = u?.devices || [];
if (devices.length === 0) {
devSel.disabled = true;
return;
}
devSel.disabled = false;
devSel.innerHTML +=
devices
.map(
(d) =>
``,
)
.join('');
}
async function loadTeamViewerConnectionsIntoSelect() {
const userSel = document.getElementById('tv-user-select');
const devSel = document.getElementById('tv-conn-select');
const hint = document.getElementById('tv-conn-hint');
if (!userSel || !devSel) return;
userSel.innerHTML = '';
devSel.innerHTML = '';
devSel.disabled = true;
if (hint) hint.textContent = '';
try {
const data = await apiGet('/integrations/teamviewer/connections');
tvSessionsCache = data;
const users = data.users || [];
userSel.innerHTML =
'' +
users
.map((u) => {
const label =
u.username && u.username !== '_unbekannt'
? u.username
: u.userid
? `Benutzer ${u.userid}`
: 'Unbekannt (Benutzer)';
return ``;
})
.join('');
devSel.innerHTML =
'';
devSel.disabled = true;
userSel.onchange = () => fillTvDeviceSelect();
if (hint) {
const ndev = users.reduce((n, u) => n + (u.devices?.length || 0), 0);
if (users.length) {
hint.textContent = `${users.length} Benutzer, ${ndev} Gerät(e)/Session(s).`;
} else {
hint.textContent =
data.meta?.recordCount === 0
? 'Keine Verbindungen in den letzten 7 Tagen.'
: 'Keine gruppierten Einträge (TeamViewer-Antwort prüfen).';
}
}
} catch (e) {
tvSessionsCache = null;
userSel.innerHTML = '';
devSel.innerHTML = '';
devSel.disabled = true;
if (hint) hint.textContent = e.message || 'Fehler';
}
}
function syncEventFormFieldGroups(form) {
const sel = form.querySelector('#ev-type-sel');
if (!sel) return;
const v = sel.value;
form.querySelectorAll('.ev-field-group').forEach((el) => {
const show = el.getAttribute('data-ev-type') === v;
el.hidden = !show;
el.querySelectorAll('input, textarea').forEach((inp) => {
const name = inp.getAttribute('name');
let req = false;
if (show) {
if (v === 'NOTE' && name === 'description_note') req = true;
if (v === 'CALL' && (name === 'description_call' || name === 'callbackNumber'))
req = true;
if (v === 'REMOTE' && name === 'description_remote') req = false;
if (v === 'PART' && name === 'articleNumber') req = true;
}
inp.required = req;
});
});
if (v === 'REMOTE') {
loadTeamViewerConnectionsIntoSelect();
}
}
function buildEventPostBody(ticketId, fd) {
const type = fd.get('type');
const base = { ticketId, type };
if (type === 'NOTE') {
return { ...base, description: fd.get('description_note') };
}
if (type === 'CALL') {
return {
...base,
description: fd.get('description_call'),
callbackNumber: fd.get('callbackNumber'),
};
}
if (type === 'REMOTE') {
const body = {
...base,
description: fd.get('description_remote'),
};
const tv = fd.get('teamviewerDevice');
if (tv && String(tv).trim()) body.teamviewerId = String(tv).trim();
return body;
}
if (type === 'PART') {
return {
...base,
articleNumber: fd.get('articleNumber'),
description: fd.get('description_part') || '',
};
}
return base;
}
function esc(s) {
return String(s ?? '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
function extrasName(m) {
const x = m?.extras;
if (!x || typeof x !== 'object') return '';
return String(x.Name || '').trim();
}
function parseRoute() {
const pathOnly = (location.hash.slice(1) || '/home').split('?')[0];
const raw = pathOnly.replace(/^\/+/, '');
const parts = raw.split('/').filter(Boolean);
return { parts };
}
function formatDateTime(iso) {
try {
return new Date(iso).toLocaleString('de-DE');
} catch {
return '—';
}
}
const appEl = document.getElementById('app');
function updateNav(st) {
const nav = document.getElementById('main-nav');
if (!nav) return;
if (!st.loggedIn) {
nav.innerHTML = '';
return;
}
const isAdmin = st.user?.role === 'admin';
nav.innerHTML = `
Start
Maschinen
Tickets
${isAdmin ? 'OptionenBenutzer' : ''}
${esc(st.user.username)}
`;
const btn = document.getElementById('btn-logout');
if (btn) {
btn.onclick = async () => {
try {
await apiPost('/auth/logout', {});
} catch {
/* ignore */
}
updateNav({ loggedIn: false });
location.hash = '#/login';
};
}
}
async function viewBootstrap() {
appEl.innerHTML = `
Erster Administrator
Es ist noch kein Benutzer angelegt. Legen Sie das erste Admin-Konto an (min. 8 Zeichen Passwort).
`;
document.getElementById('form-boot').onsubmit = async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
await apiPost('/auth/bootstrap', {
username: fd.get('username'),
password: fd.get('password'),
});
location.hash = '#/home';
};
}
async function viewLogin() {
appEl.innerHTML = `
`;
document.getElementById('form-login').onsubmit = async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
try {
await apiPost('/auth/login', {
username: fd.get('username'),
password: fd.get('password'),
});
location.hash = '#/home';
} catch (err) {
appEl.querySelector('.auth-err')?.remove();
const p = document.createElement('p');
p.className = 'error auth-err';
p.textContent = err.message || 'Anmeldung fehlgeschlagen';
document.getElementById('form-login').prepend(p);
}
};
}
function formatDeSyncDateTime(iso) {
if (!iso) return '—';
try {
return new Date(iso).toLocaleString('de-DE', {
day: 'numeric',
month: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
} catch {
return '—';
}
}
async function viewOptions() {
const data = await apiGet('/settings/integrations');
let syncStatus = { lastSyncAt: null, entries: [] };
try {
syncStatus = await apiGet('/ldap/sync-status');
} catch {
/* z. B. ältere Server ohne Route */
}
const ldap = data.ldap || {};
const L = (k) => esc(ldap[k] ?? '');
const userFilterVal = esc(ldap.userSearchFilter || ldap.userFilter || '');
const syncMin = ldap.syncIntervalMinutes ?? 1440;
const tv = data.teamviewer || {};
const bearer = esc(tv.bearerToken || tv.apiToken || '');
const tvNotes = esc(tv.notes ?? tv.apiNotes ?? '');
const lastSyncLabel = formatDeSyncDateTime(syncStatus.lastSyncAt);
const logEntries = Array.isArray(syncStatus.entries) ? syncStatus.entries : [];
const logRows =
logEntries.length > 0
? logEntries
.map(
(e) => `
| ${esc(formatDeSyncDateTime(e.finishedAt))} |
${e.triggerType === 'automatic' ? 'Automatisch' : 'Manuell'} |
${e.status === 'success' ? 'Erfolg' : 'Fehler'} |
${esc(String(e.usersSynced ?? 0))} |
${e.errorMessage ? esc(e.errorMessage) : '—'} |
`,
)
.join('')
: `| Noch keine Einträge. |
`;
appEl.innerHTML = `
← Start
Optionen
Integrationen für das CRM. Werte werden in der Datenbank gespeichert.
`;
const ldapBody = document.getElementById('ldap-section-body');
const ldapToggle = document.getElementById('ldap-toggle');
const chev = ldapToggle.querySelector('.ldap-chevron');
function setLdapOpen(open) {
ldapBody.hidden = !open;
ldapToggle.setAttribute('aria-expanded', String(open));
chev.textContent = open ? '▲' : '▼';
}
ldapToggle.onclick = () => setLdapOpen(ldapBody.hidden);
document.getElementById('btn-ldap-sync-now').onclick = async () => {
const btn = document.getElementById('btn-ldap-sync-now');
btn.disabled = true;
try {
await apiPost('/ldap/sync', {});
await route();
} catch (err) {
alert(err.message || String(err));
} finally {
btn.disabled = false;
}
};
document.getElementById('form-opt').onsubmit = async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
await apiPut('/settings/integrations', {
ldap: {
serverUrl: fd.get('ldap_serverUrl'),
bindDn: fd.get('ldap_bindDn'),
bindPassword: fd.get('ldap_bindPassword'),
searchBase: fd.get('ldap_searchBase'),
userSearchFilter: fd.get('ldap_userSearchFilter'),
usernameAttribute: fd.get('ldap_usernameAttribute'),
firstNameAttribute: fd.get('ldap_firstNameAttribute'),
lastNameAttribute: fd.get('ldap_lastNameAttribute'),
syncIntervalMinutes: fd.get('ldap_syncIntervalMinutes'),
syncEnabled: fd.get('ldap_syncEnabled') === 'on',
},
teamviewer: {
bearerToken: fd.get('tv_bearerToken'),
notes: fd.get('tv_notes'),
},
});
route();
};
}
async function viewUsers() {
const users = await apiGet('/users');
appEl.innerHTML = `
← Start
Benutzer
| Benutzer |
Rolle |
Quelle |
Aktiv |
|
${users
.map(
(u) => `
| ${esc(u.username)} |
${u.role === 'admin' ? 'Admin' : 'Benutzer'} |
${u.source === 'ldap' ? 'LDAP' : 'Lokal'} |
${u.active ? 'Ja' : 'Nein'} |
${u.source === 'local' ? `` : ''}
|
`,
)
.join('')}
`;
document.getElementById('form-new-user').onsubmit = async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
await apiPost('/users', {
username: fd.get('username'),
password: fd.get('password'),
role: fd.get('role'),
});
e.target.reset();
route();
};
appEl.querySelectorAll('.btn-pw').forEach((btn) => {
btn.onclick = async () => {
const uid = btn.getAttribute('data-id');
const pw = window.prompt('Neues Passwort (min. 8 Zeichen):');
if (!pw || pw.length < 8) return;
await apiPut(`/users/${uid}`, { password: pw });
route();
};
});
appEl.querySelectorAll('.btn-toggle').forEach((btn) => {
btn.onclick = async () => {
const uid = btn.getAttribute('data-id');
const active = btn.getAttribute('data-active') === '1';
await apiPut(`/users/${uid}`, { active: !active });
route();
};
});
appEl.querySelectorAll('.btn-del-user').forEach((btn) => {
btn.onclick = async () => {
if (!window.confirm('Benutzer wirklich löschen?')) return;
const uid = btn.getAttribute('data-id');
await apiDelete(`/users/${uid}`);
route();
};
});
}
async function route() {
const { parts } = parseRoute();
appEl.innerHTML = 'Lade …
';
let st;
try {
st = await authFetchStatus();
} catch {
st = { needsBootstrap: false, loggedIn: false, user: null };
}
updateNav(st);
try {
if (st.needsBootstrap) {
if (parts[0] !== 'bootstrap') {
location.hash = '#/bootstrap';
return;
}
await viewBootstrap();
return;
}
if (!st.loggedIn) {
if (parts[0] !== 'login') {
location.hash = '#/login';
return;
}
await viewLogin();
return;
}
if (parts[0] === 'login' || parts[0] === 'bootstrap') {
location.hash = '#/home';
return;
}
const isAdmin = st.user?.role === 'admin';
if (parts[0] === 'options' && parts.length === 1) {
if (!isAdmin) {
location.hash = '#/home';
return;
}
await viewOptions();
return;
}
if (parts[0] === 'users' && parts.length === 1) {
if (!isAdmin) {
location.hash = '#/home';
return;
}
await viewUsers();
return;
}
if (parts[0] === 'home' && parts.length === 1) {
await viewHome();
return;
}
if (parts[0] === 'machines' && parts.length === 1) {
await viewMachineList();
return;
}
if (parts[0] === 'machines' && parts.length === 2) {
await viewMachineDetail(parts[1]);
return;
}
if (parts[0] === 'tickets' && parts.length === 1) {
await viewTicketList();
return;
}
if (parts[0] === 'tickets' && parts.length === 2) {
await viewTicketDetail(parts[1]);
return;
}
location.hash = '#/home';
} catch (e) {
if (isAuthRedirectError(e)) {
updateNav({ loggedIn: false, user: null, needsBootstrap: false });
return;
}
appEl.innerHTML = `${esc(e.message)}
`;
}
}
/** Einheitlicher Platzhalter, wenn noch keine Gruppenzeile gesetzt war */
const ANLAGEN_GRUPPE_PLACEHOLDER = '\u2014';
/** Baut das extras-Objekt aus dem Maschinen-Bearbeitungsformular (Anlagenliste). */
function collectExtrasFromMachineForm(form, machine) {
const prev =
machine.extras && typeof machine.extras === 'object'
? JSON.parse(JSON.stringify(machine.extras))
: {};
const beschr = prev._beschriftungZeile9;
const werte = prev._werteAlsListe;
const gruppe = prev._gruppeZeile7;
if (
Array.isArray(beschr) &&
Array.isArray(werte) &&
beschr.length === werte.length
) {
const n = beschr.length;
const newWerte = [];
for (let i = 0; i < n; i++) {
newWerte.push(String(form.elements[`extra_wert_${i}`]?.value ?? ''));
}
prev._werteAlsListe = newWerte;
return prev;
}
const out = {};
for (const k of Object.keys(prev)) {
if (k.startsWith('_')) out[k] = prev[k];
}
const kvKeys = Object.keys(prev).filter((k) => !k.startsWith('_'));
for (let i = 0; i < kvKeys.length; i++) {
const key = kvKeys[i];
out[key] = String(form.elements[`extras_kv_val_${i}`]?.value ?? '');
}
return out;
}
function extrasTableHtml(extras, opts) {
const editable = opts && opts.editable === true;
if (!extras || typeof extras !== 'object') {
if (editable) {
return `
Anlagenliste (ITT)
Keine Anlagendaten gespeichert — nichts zu bearbeiten.
`;
}
return '';
}
const beschr = extras._beschriftungZeile9;
const gruppe = extras._gruppeZeile7;
const werte = extras._werteAlsListe;
if (
Array.isArray(beschr) &&
Array.isArray(werte) &&
beschr.length === werte.length
) {
const g =
Array.isArray(gruppe) && gruppe.length === beschr.length
? gruppe
: null;
if (editable) {
const body = beschr
.map((label, i) => {
const v = werte[i];
const grp = g ? String(g[i] ?? '') : '';
return `
| ${esc(grp)} |
${esc(String(label || `Spalte ${i + 1}`))} |
|
`;
})
.join('');
return `
Anlagenliste (bearbeiten)
Nur die Spalte „Wert“ ist bearbeitbar. Gruppe und Beschreibung bleiben wie importiert.
`;
}
/** Zeilen mit gleicher laufender Gruppe; Titelzeile nur bei Wechsel */
if (g) {
const n = beschr.length;
const groupForRow = [];
let cur = '';
for (let i = 0; i < n; i++) {
const raw = g[i] != null ? String(g[i]).trim() : '';
if (raw) cur = raw;
groupForRow[i] = cur || ANLAGEN_GRUPPE_PLACEHOLDER;
}
const segments = [];
let segStart = 0;
for (let i = 1; i <= n; i++) {
if (i === n || groupForRow[i] !== groupForRow[i - 1]) {
segments.push({ from: segStart, to: i, key: groupForRow[segStart] });
segStart = i;
}
}
const gruppenTitel = (key) =>
key === ANLAGEN_GRUPPE_PLACEHOLDER ? 'Sonstiges' : key;
const tbodies = segments
.map((seg, idx) => {
const spacer =
idx > 0
? ` |
`
: '';
const titel = gruppenTitel(seg.key);
const kopf = `| ${esc(titel)} |
`;
const zeilen = [];
for (let i = seg.from; i < seg.to; i++) {
const v = werte[i];
const show = v !== '' && v != null;
const label = String(beschr[i] || `Spalte ${i + 1}`);
zeilen.push(
`| ${esc(label)} | ${esc(String(v ?? ''))} |
`,
);
}
return `${spacer}${kopf}${zeilen.join('')}`;
})
.join('');
return `
Anlagenliste (alle Spalten · Zeile 9 = Beschreibung)
Gruppe = Zeile 7 im Blatt „Anlagen“, Beschreibung = Zeile 9. Zusammenhängende Zeilen mit gleicher Gruppe sind zusammengefasst.
`;
}
const body = beschr
.map((label, i) => {
const v = werte[i];
const show = v !== '' && v != null;
const grp = '';
return `
| ${esc(grp)} |
${esc(String(label || `Spalte ${i + 1}`))} |
${esc(String(v ?? ''))} |
`;
})
.join('');
return `
Anlagenliste (alle Spalten · Zeile 9 = Beschreibung)
Gruppe = Zeile 7 im Blatt „Anlagen“, Beschreibung = Zeile 9.
`;
}
const kvEntries = Object.entries(extras).filter(([k]) => !k.startsWith('_'));
if (editable) {
if (kvEntries.length === 0) {
return `
Daten aus Anlagenliste (ITT)
Keine zusätzlichen Felder — nichts zu bearbeiten.
`;
}
const rows = kvEntries
.map(
([k, v], i) =>
`
| ${esc(k)} |
|
`,
)
.join('');
return `
Daten aus Anlagenliste (ITT)
Nur die Werte sind bearbeitbar; Feldnamen sind fest.
`;
}
const rows = kvEntries
.filter(([, v]) => v !== '' && v != null)
.map(
([k, v]) =>
`| ${esc(k)} | ${esc(String(v))} |
`,
)
.join('');
if (!rows) return '';
return `
Daten aus Anlagenliste (ITT)
`;
}
async function viewMachineList() {
const machines = await apiGet('/machines');
appEl.innerHTML = `
`;
const inp = document.getElementById('machine-filter');
const tbody = document.querySelector('#machine-table tbody');
inp.addEventListener('input', () => {
const q = inp.value.toLowerCase().trim();
tbody.querySelectorAll('tr').forEach((tr) => {
tr.hidden = !!(q && !tr.textContent.toLowerCase().includes(q));
});
});
}
async function viewMachineDetail(id) {
const m = await apiGet(`/machines/${id}`);
const ortName = extrasName(m);
const renderView = () => {
appEl.innerHTML = `
← Alle Maschinen
${esc(m.name)}
Typ: ${esc(m.typ)}
Seriennummer: ${esc(m.seriennummer)}
Standort: ${esc(m.standort)}
${ortName ? `
Name (Anlagenliste): ${esc(ortName)}
` : ''}
${extrasTableHtml(m.extras)}
`;
document.getElementById('btn-m-del').onclick = async () => {
if (!confirm('Maschine wirklich löschen?')) return;
await apiDelete(`/machines/${id}`);
location.hash = '#/machines';
};
document.getElementById('btn-m-edit').onclick = () => renderEdit();
};
const renderEdit = () => {
appEl.innerHTML = `
← Alle Maschinen
${esc(m.name)}
`;
const form = document.getElementById('form-m');
form.onsubmit = async (e) => {
e.preventDefault();
const body = {
name: form.elements.name.value,
typ: form.elements.typ.value,
seriennummer: form.elements.seriennummer.value,
standort: form.elements.standort.value,
extras: collectExtrasFromMachineForm(form, m),
};
await apiPut(`/machines/${id}`, body);
route();
};
document.getElementById('m-cancel').onclick = () => renderView();
document.getElementById('btn-m-del-edit').onclick = async () => {
if (!confirm('Maschine wirklich löschen?')) return;
await apiDelete(`/machines/${id}`);
location.hash = '#/machines';
};
};
renderView();
}
const statusBadgeClass = { OPEN: 'badge-open', WAITING: 'badge-waiting', DONE: 'badge-done' };
const priorityBadgeClass = { HIGH: 'badge-high', MEDIUM: 'badge-medium', LOW: 'badge-low' };
async function viewHome() {
const tickets = await apiGet('/tickets?open=1');
const eventsLists =
tickets.length === 0
? []
: await Promise.all(tickets.map((t) => apiGet(`/tickets/${t.id}/events`)));
const openCount = tickets.filter((t) => t.status === 'OPEN').length;
const waitingCount = tickets.filter((t) => t.status === 'WAITING').length;
const listHtml =
tickets.length === 0
? 'Keine offenen Tickets.
'
: `${tickets
.map((t, i) => {
const events = eventsLists[i] || [];
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 mBlock = t.machine
? `
Maschine: ${esc(t.machine.seriennummer)}${mn ? ` · ${esc(mn)}` : ''} · Typ: ${esc(t.machine.typ)}
`
: '';
const evChrono = [...events].reverse();
const eventBoxes =
evChrono.length === 0
? '
Noch keine Ereignisse in der Historie.
'
: evChrono
.map(
(ev) => `
`,
)
.join('');
return `
${esc(t.title)}
${esc(ticketStatusLabel[t.status])}
${esc(ticketPriorityLabel[t.priority])}
${t.machine ? `Maschine: ${machineLabel}` : 'Keine Maschine'}
Standort: ${standort}
Erstellt: ${esc(formatDateTime(t.createdAt))}
Aktualisiert: ${esc(formatDateTime(t.updatedAt))}
Ticket öffnen →
Beschreibung
${esc(t.description)}
${mBlock}
${eventBoxes}
`;
})
.join('')}
`;
appEl.innerHTML = `
Offene Tickets
${openCount} Offen
${waitingCount} Wartend
gesamt: ${tickets.length}
${listHtml}
`;
}
function ticketListQuery() {
const q = new URLSearchParams(location.hash.split('?')[1] || '');
const p = new URLSearchParams();
if (q.get('status')) p.set('status', q.get('status'));
if (q.get('priority')) p.set('priority', q.get('priority'));
if (q.get('machineId')) p.set('machineId', q.get('machineId'));
const s = p.toString();
return s ? `?${s}` : '';
}
async function viewTicketList() {
const qs = ticketListQuery();
const hashParams = new URLSearchParams(location.hash.split('?')[1] || '');
const [tickets, allMachines] = await Promise.all([
apiGet(`/tickets${qs}`),
apiGet('/machines'),
]);
const mid = hashParams.get('machineId') || '';
appEl.innerHTML = `
Tickets
Filter
| Titel | Status | Priorität | Maschine |
${tickets
.map(
(t) => `
| ${esc(t.title)} |
${esc(ticketStatusLabel[t.status])} |
${esc(ticketPriorityLabel[t.priority])} |
${t.machine ? esc(t.machine.seriennummer) : ''}${t.machine ? (extrasName(t.machine) ? ` · ${esc(extrasName(t.machine))}` : '') : ''} |
`,
)
.join('')}
`;
const selFm = document.getElementById('sel-fm');
if (mid) selFm.value = mid;
document.getElementById('form-new-ticket').onsubmit = async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
await apiPost('/tickets', {
machineId: fd.get('machineId'),
title: fd.get('title'),
description: fd.get('description'),
});
e.target.reset();
route();
};
document.getElementById('form-filter').onsubmit = (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const p = new URLSearchParams();
if (fd.get('status')) p.set('status', fd.get('status'));
if (fd.get('priority')) p.set('priority', fd.get('priority'));
if (fd.get('machineId')) p.set('machineId', fd.get('machineId'));
const q = p.toString();
location.hash = `#/tickets${q ? `?${q}` : ''}`;
};
}
async function viewTicketDetail(id) {
const [ticket, events] = await Promise.all([
apiGet(`/tickets/${id}`),
apiGet(`/tickets/${id}/events`),
]);
const mn = ticket.machine ? extrasName(ticket.machine) : '';
appEl.innerHTML = `
← Zurück
${esc(ticket.title)}
Status: ${esc(ticketStatusLabel[ticket.status])}
Priorität: ${esc(ticketPriorityLabel[ticket.priority])}
Beschreibung:
${esc(ticket.description)}
${ticket.machine ? `
Maschine: ${esc(ticket.machine.seriennummer)}${mn ? ` · ${esc(mn)}` : ''}
` : ''}
Historie
| Zeitpunkt |
Art |
Inhalt |
${
events.length === 0
? `| Noch keine Ereignisse. |
`
: events
.map(
(ev) => `
| ${esc(formatDateTime(ev.createdAt))} |
${esc(eventTypeLabel[ev.type] || ev.type)} |
${eventInhaltHtml(ev)} |
`,
)
.join('')
}
Weiteres Ticket für diese Maschine
Optional.
`;
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);
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 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);
route();
};
document.getElementById('form-t2').onsubmit = async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
await apiPost('/tickets', {
machineId: ticket.machineId,
title: fd.get('title'),
description: fd.get('description'),
});
e.target.reset();
route();
};
document.getElementById('btn-t-edit').onclick = () => {
document.getElementById('tick-view').innerHTML = `
`;
document.getElementById('form-tu').onsubmit = async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
await apiPut(`/tickets/${id}`, Object.fromEntries(fd.entries()));
route();
};
document.getElementById('tu-cancel').onclick = () => route();
};
}
window.addEventListener('unhandledrejection', (ev) => {
if (isAuthRedirectError(ev.reason)) {
ev.preventDefault();
}
});
window.addEventListener('hashchange', route);
if (!location.hash) location.hash = '#/home';
route();