Benutzer, Ticketzuweißungen
This commit is contained in:
@@ -1 +1,6 @@
|
||||
/* Benutzer – bei Bedarf seiten-spezifische Styles */
|
||||
|
||||
.users-table .user-role-select {
|
||||
min-width: 11rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -652,7 +652,13 @@ code {
|
||||
/* ═══════════════════════════════════════════════════
|
||||
Startseite — Offene Tickets
|
||||
════════════════════════════════════════════════════ */
|
||||
.home-open-tickets { gap: 1rem; }
|
||||
.home-open-tickets { gap: 1.75rem; }
|
||||
|
||||
.home-ticket-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.home-kpi-bar {
|
||||
display: flex;
|
||||
@@ -841,6 +847,24 @@ code {
|
||||
.badge-waiting { background: rgba(158, 106, 3, 0.15); color: var(--amber-fg); border-color: rgba(210, 153, 34, 0.3); }
|
||||
.badge-done { background: rgba(110, 118, 129, 0.12); color: var(--text-muted); border-color: var(--border); }
|
||||
|
||||
/* Zuweisung (Name) — gleiche Badge-Form wie Status */
|
||||
.badge-assignee {
|
||||
background: rgba(88, 166, 255, 0.12);
|
||||
color: var(--accent-hi);
|
||||
border-color: rgba(88, 166, 255, 0.35);
|
||||
font-weight: 500;
|
||||
max-width: 14rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.badge-assignee--none {
|
||||
background: rgba(110, 118, 129, 0.12);
|
||||
color: var(--text-muted);
|
||||
border-color: var(--border);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Priority badge variants */
|
||||
.badge-high { background: rgba(185, 28, 28, 0.18); color: var(--red-fg); border-color: rgba(248, 81, 73, 0.35); }
|
||||
.badge-medium { background: rgba(158, 106, 3, 0.15); color: var(--amber-fg); border-color: rgba(210, 153, 34, 0.3); }
|
||||
|
||||
@@ -7,6 +7,16 @@ window.addEventListener('unhandledrejection', (ev) => {
|
||||
}
|
||||
});
|
||||
|
||||
/** @param {object|null|undefined} user – aus /auth/status */
|
||||
export function canEditCrm(user) {
|
||||
return user?.canEditCrm === true;
|
||||
}
|
||||
|
||||
/** @param {object|null|undefined} user – aus /auth/status */
|
||||
export function canAdmin(user) {
|
||||
return user?.canAdmin === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ needsAdmin?: boolean, activeNav?: string }} opts
|
||||
* @returns {Promise<object|null>} Session-Status oder null bei Redirect
|
||||
@@ -40,7 +50,7 @@ export async function guard(opts = {}) {
|
||||
location.href = '/start.html';
|
||||
return null;
|
||||
}
|
||||
if (needsAdmin && st.user?.role !== 'admin') {
|
||||
if (needsAdmin && !canAdmin(st.user)) {
|
||||
location.href = '/start.html';
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export function updateNav(st, activeNav = '') {
|
||||
nav.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
const isAdmin = st.user?.role === 'admin';
|
||||
const isAdmin = st.user?.canAdmin === true;
|
||||
const na = (key) => (activeNav === key ? 'nav-active' : '');
|
||||
nav.innerHTML = `
|
||||
<a href="/start.html" class="${na('start')}">Start</a>
|
||||
|
||||
@@ -71,9 +71,24 @@ function showEditMode() {
|
||||
}
|
||||
|
||||
async function viewMachineDetail(id, options = {}) {
|
||||
const { startInEditMode = false } = options;
|
||||
const { startInEditMode = false, canEdit = true } = options;
|
||||
const m = await apiGet(`/machines/${id}`);
|
||||
|
||||
const btnEdit = document.getElementById('btn-m-edit');
|
||||
const btnDup = document.getElementById('btn-m-dup');
|
||||
const btnDel = document.getElementById('btn-m-del');
|
||||
const btnDelEdit = document.getElementById('btn-m-del-edit');
|
||||
if (!canEdit) {
|
||||
btnEdit.hidden = true;
|
||||
btnDup.hidden = true;
|
||||
btnDel.hidden = true;
|
||||
btnDelEdit.hidden = true;
|
||||
document.getElementById('machine-extras-edit').innerHTML = '';
|
||||
fillView(m);
|
||||
showViewMode();
|
||||
return;
|
||||
}
|
||||
|
||||
fillView(m);
|
||||
if (startInEditMode) {
|
||||
fillEdit(m);
|
||||
@@ -143,10 +158,11 @@ async function viewMachineDetail(id, options = {}) {
|
||||
async function init() {
|
||||
const st = await guard({ activeNav: 'machines' });
|
||||
if (!st) return;
|
||||
const canEdit = st.user?.canEditCrm === true;
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const id = params.get('id');
|
||||
const startInEditMode = params.get('edit') === '1';
|
||||
const startInEditMode = canEdit && params.get('edit') === '1';
|
||||
if (!id || !UUID.test(id)) {
|
||||
loadingEl.hidden = true;
|
||||
badIdEl.hidden = false;
|
||||
@@ -157,8 +173,10 @@ async function init() {
|
||||
mainEl.hidden = false;
|
||||
|
||||
try {
|
||||
await viewMachineDetail(id, { startInEditMode });
|
||||
if (startInEditMode) {
|
||||
await viewMachineDetail(id, { startInEditMode, canEdit });
|
||||
if (!canEdit && params.get('edit') === '1') {
|
||||
history.replaceState(null, '', `/machine.html?id=${encodeURIComponent(id)}`);
|
||||
} else if (startInEditMode) {
|
||||
history.replaceState(null, '', `/machine.html?id=${encodeURIComponent(id)}`);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -53,34 +53,39 @@ function initNewMachineCollapse() {
|
||||
toggle.onclick = () => setOpen(body.hidden);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
async function run(canEdit) {
|
||||
const machines = await apiGet('/machines');
|
||||
document.getElementById('machine-count').textContent = String(machines.length);
|
||||
renderRows(machines);
|
||||
|
||||
const newCard = document.getElementById('machines-new-card');
|
||||
if (newCard) newCard.hidden = !canEdit;
|
||||
|
||||
initNewMachineCollapse();
|
||||
|
||||
const formNew = document.getElementById('form-new-machine');
|
||||
formNew.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(formNew);
|
||||
const btn = formNew.querySelector('button[type="submit"]');
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const body = Object.fromEntries(fd.entries());
|
||||
const created = await apiPost('/machines', {
|
||||
name: body.name,
|
||||
typ: body.typ,
|
||||
seriennummer: body.seriennummer,
|
||||
standort: body.standort,
|
||||
listStatus: body.listStatus || '',
|
||||
});
|
||||
location.href = `/machine.html?id=${encodeURIComponent(created.id)}`;
|
||||
} catch (err) {
|
||||
alert(err.message || 'Anlegen fehlgeschlagen.');
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
if (canEdit) {
|
||||
formNew.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(formNew);
|
||||
const btn = formNew.querySelector('button[type="submit"]');
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const body = Object.fromEntries(fd.entries());
|
||||
const created = await apiPost('/machines', {
|
||||
name: body.name,
|
||||
typ: body.typ,
|
||||
seriennummer: body.seriennummer,
|
||||
standort: body.standort,
|
||||
listStatus: body.listStatus || '',
|
||||
});
|
||||
location.href = `/machine.html?id=${encodeURIComponent(created.id)}`;
|
||||
} catch (err) {
|
||||
alert(err.message || 'Anlegen fehlgeschlagen.');
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const inp = document.getElementById('machine-filter');
|
||||
const tbody = document.getElementById('machine-table-body');
|
||||
@@ -95,10 +100,11 @@ async function run() {
|
||||
async function init() {
|
||||
const st = await guard({ activeNav: 'machines' });
|
||||
if (!st) return;
|
||||
const canEdit = st.user?.canEditCrm === true;
|
||||
loadingEl.hidden = true;
|
||||
mainEl.hidden = false;
|
||||
try {
|
||||
await run();
|
||||
await run(canEdit);
|
||||
} catch (e) {
|
||||
if (isAuthRedirectError(e)) return;
|
||||
showError(e.message || 'Fehler');
|
||||
|
||||
@@ -19,9 +19,17 @@ 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;
|
||||
@@ -48,7 +56,8 @@ function renderEventBoxes(events) {
|
||||
.join('');
|
||||
}
|
||||
|
||||
function fillTicketCard(node, t, events) {
|
||||
/** @param {'open' | 'mine'} listKind */
|
||||
function fillTicketCard(node, t, events, listKind) {
|
||||
const id = t.id;
|
||||
const detailId = `home-ticket-detail-${id}`;
|
||||
node.dataset.ticketId = id;
|
||||
@@ -79,6 +88,25 @@ function fillTicketCard(node, t, events) {
|
||||
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}`
|
||||
@@ -87,6 +115,8 @@ function fillTicketCard(node, t, events) {
|
||||
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)}`;
|
||||
@@ -112,32 +142,53 @@ function fillTicketCard(node, t, events) {
|
||||
};
|
||||
}
|
||||
|
||||
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 tickets = await apiGet('/tickets?open=1');
|
||||
const eventsLists =
|
||||
tickets.length === 0
|
||||
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(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;
|
||||
: 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: ${tickets.length}`;
|
||||
document.getElementById('kpi-total').textContent = `gesamt: ${ticketsAll.length}`;
|
||||
|
||||
listEl.innerHTML = '';
|
||||
if (tickets.length === 0) {
|
||||
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;
|
||||
tickets.forEach((t, i) => {
|
||||
const frag = tpl.content.cloneNode(true);
|
||||
const article = frag.querySelector('.home-ticket-card');
|
||||
fillTicketCard(article, t, eventsLists[i] || []);
|
||||
listEl.appendChild(article);
|
||||
});
|
||||
renderTicketListInto(listEl, ticketsAll, eventsAll, 'open');
|
||||
}
|
||||
|
||||
renderTicketListInto(listMineEl, ticketsMine, eventsMine, 'mine');
|
||||
}
|
||||
|
||||
async function init() {
|
||||
@@ -146,6 +197,7 @@ async function init() {
|
||||
loadingEl.hidden = true;
|
||||
mainEl.hidden = false;
|
||||
bindAttachmentPreview(document.body);
|
||||
|
||||
try {
|
||||
await run();
|
||||
} catch (e) {
|
||||
|
||||
@@ -21,6 +21,17 @@ import {
|
||||
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');
|
||||
@@ -65,6 +76,11 @@ function fillTicketView(ticket) {
|
||||
} else {
|
||||
mrow.hidden = true;
|
||||
}
|
||||
|
||||
const assignLabel = document.getElementById('t-assign-label');
|
||||
if (assignLabel) {
|
||||
assignLabel.textContent = formatAssigneeLabel(ticket.assignedTo);
|
||||
}
|
||||
}
|
||||
|
||||
function fillEditForm(ticket) {
|
||||
@@ -103,7 +119,7 @@ function showEditMode() {
|
||||
panelEdit.hidden = false;
|
||||
}
|
||||
|
||||
async function viewTicketDetail(id) {
|
||||
async function viewTicketDetail(id, canEdit) {
|
||||
const [ticket, events] = await Promise.all([
|
||||
apiGet(`/tickets/${id}`),
|
||||
apiGet(`/tickets/${id}/events`),
|
||||
@@ -123,6 +139,72 @@ async function viewTicketDetail(id) {
|
||||
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 () => {
|
||||
@@ -259,6 +341,7 @@ async function viewTicketDetail(id) {
|
||||
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)) {
|
||||
@@ -273,7 +356,7 @@ async function init() {
|
||||
bindAttachmentPreview(document.body);
|
||||
|
||||
try {
|
||||
await viewTicketDetail(id);
|
||||
await viewTicketDetail(id, canEdit);
|
||||
} catch (e) {
|
||||
if (isAuthRedirectError(e)) return;
|
||||
showError(e.message || 'Fehler');
|
||||
|
||||
@@ -61,10 +61,13 @@ function renderTicketRows(tickets) {
|
||||
.join('');
|
||||
}
|
||||
|
||||
async function run() {
|
||||
async function run(canEdit) {
|
||||
const qs = ticketListQuery();
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
|
||||
const newCard = document.getElementById('tickets-new-card');
|
||||
if (newCard) newCard.hidden = !canEdit;
|
||||
|
||||
const [tickets, allMachines] = await Promise.all([
|
||||
apiGet(`/tickets${qs}`),
|
||||
apiGet('/machines'),
|
||||
@@ -81,18 +84,20 @@ async function run() {
|
||||
|
||||
renderTicketRows(tickets);
|
||||
|
||||
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();
|
||||
fillMachineSelects(allMachines, mid);
|
||||
location.reload();
|
||||
};
|
||||
if (canEdit) {
|
||||
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();
|
||||
fillMachineSelects(allMachines, mid);
|
||||
location.reload();
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('form-filter').onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
@@ -109,10 +114,11 @@ async function run() {
|
||||
async function init() {
|
||||
const st = await guard({ activeNav: 'tickets' });
|
||||
if (!st) return;
|
||||
const canEdit = st.user?.canEditCrm === true;
|
||||
loadingEl.hidden = true;
|
||||
mainEl.hidden = false;
|
||||
try {
|
||||
await run();
|
||||
await run(canEdit);
|
||||
} catch (e) {
|
||||
if (isAuthRedirectError(e)) return;
|
||||
showError(e.message || 'Fehler');
|
||||
|
||||
@@ -18,6 +18,20 @@ function formatName(u) {
|
||||
return a.length ? a.map((x) => esc(String(x))).join(' ') : '—';
|
||||
}
|
||||
|
||||
function roleOptionsHtml(current) {
|
||||
const opts = [
|
||||
['viewer', 'Viewer'],
|
||||
['after_sales', 'After-Sales'],
|
||||
['admin', 'Administrator'],
|
||||
];
|
||||
return opts
|
||||
.map(
|
||||
([val, label]) =>
|
||||
`<option value="${esc(val)}"${current === val ? ' selected' : ''}>${esc(label)}</option>`,
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
|
||||
function renderRows(users) {
|
||||
const tbody = document.getElementById('users-table-body');
|
||||
tbody.innerHTML = users
|
||||
@@ -26,7 +40,11 @@ function renderRows(users) {
|
||||
<tr data-id="${esc(u.id)}">
|
||||
<td>${esc(u.username)}</td>
|
||||
<td class="muted">${formatName(u)}</td>
|
||||
<td><span class="badge">${u.role === 'admin' ? 'Admin' : 'Benutzer'}</span></td>
|
||||
<td>
|
||||
<select id="role-${esc(u.id)}" class="user-role-select" data-user-id="${esc(u.id)}" data-role-prev="${esc(u.role)}" aria-label="Rolle">
|
||||
${roleOptionsHtml(u.role)}
|
||||
</select>
|
||||
</td>
|
||||
<td class="muted">${u.source === 'ldap' ? 'LDAP' : 'Lokal'}</td>
|
||||
<td>${u.active ? 'Ja' : 'Nein'}</td>
|
||||
<td class="users-actions">
|
||||
@@ -56,6 +74,24 @@ async function run() {
|
||||
};
|
||||
|
||||
const root = document.getElementById('page-main');
|
||||
root.querySelectorAll('.user-role-select').forEach((sel) => {
|
||||
sel.addEventListener('change', async () => {
|
||||
const id = sel.getAttribute('data-user-id');
|
||||
const prev = sel.getAttribute('data-role-prev');
|
||||
const role = sel.value;
|
||||
sel.disabled = true;
|
||||
try {
|
||||
await apiPut(`/users/${id}`, { role });
|
||||
sel.setAttribute('data-role-prev', role);
|
||||
} catch (e) {
|
||||
window.alert(e.message || 'Rolle konnte nicht gespeichert werden.');
|
||||
sel.value = prev;
|
||||
} finally {
|
||||
sel.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
root.querySelectorAll('.btn-pw').forEach((btn) => {
|
||||
btn.onclick = async () => {
|
||||
const uid = btn.getAttribute('data-id');
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<tbody id="machine-table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card options-section ldap-section machines-new-machine">
|
||||
<div class="card options-section ldap-section machines-new-machine" id="machines-new-card">
|
||||
<button
|
||||
type="button"
|
||||
class="ldap-section-toggle"
|
||||
|
||||
@@ -16,20 +16,38 @@
|
||||
<p id="page-loading" class="muted">Lade …</p>
|
||||
<p id="page-error" class="error" hidden></p>
|
||||
<div id="page-main" class="stack home-open-tickets" hidden>
|
||||
<div class="home-kpi-bar">
|
||||
<h2>Offene Tickets</h2>
|
||||
<div class="home-kpi-pills">
|
||||
<span class="kpi-pill"
|
||||
><span id="kpi-open" class="badge badge-open">0 Offen</span></span
|
||||
>
|
||||
<span class="kpi-pill"
|
||||
><span id="kpi-waiting" class="badge badge-waiting">0 Wartend</span></span
|
||||
>
|
||||
<span id="kpi-total" class="kpi-pill muted">gesamt: 0</span>
|
||||
<section class="home-ticket-section" aria-labelledby="home-heading-open">
|
||||
<div class="home-kpi-bar">
|
||||
<h2 id="home-heading-open">Offene Tickets</h2>
|
||||
<div class="home-kpi-pills">
|
||||
<span class="kpi-pill"
|
||||
><span id="kpi-open" class="badge badge-open">0 Offen</span></span
|
||||
>
|
||||
<span class="kpi-pill"
|
||||
><span id="kpi-waiting" class="badge badge-waiting">0 Wartend</span></span
|
||||
>
|
||||
<span id="kpi-total" class="kpi-pill muted">gesamt: 0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="home-ticket-list" class="home-ticket-list"></div>
|
||||
<p id="home-empty" class="muted" hidden>Keine offenen Tickets.</p>
|
||||
<div id="home-ticket-list" class="home-ticket-list"></div>
|
||||
<p id="home-empty" class="muted" hidden>Keine offenen Tickets.</p>
|
||||
</section>
|
||||
|
||||
<section class="home-ticket-section" aria-labelledby="home-heading-mine">
|
||||
<div class="home-kpi-bar">
|
||||
<h2 id="home-heading-mine">Meine Tickets</h2>
|
||||
<div class="home-kpi-pills">
|
||||
<span class="kpi-pill"
|
||||
><span id="kpi-mine-open" class="badge badge-open">0 Offen</span></span
|
||||
>
|
||||
<span class="kpi-pill"
|
||||
><span id="kpi-mine-waiting" class="badge badge-waiting">0 Wartend</span></span
|
||||
>
|
||||
<span id="kpi-mine-total" class="kpi-pill muted">gesamt: 0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="home-ticket-list-mine" class="home-ticket-list"></div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -49,12 +67,14 @@
|
||||
<a class="js-ticket-link" href="#">Titel</a>
|
||||
<span class="badge js-status"></span>
|
||||
<span class="badge js-priority"></span>
|
||||
<span class="badge js-assignee-tag badge-assignee" title="Zugewiesen"></span>
|
||||
</div>
|
||||
<div class="home-ticket-meta-row">
|
||||
<span class="js-meta-machine"></span>
|
||||
<span><span class="muted">Standort:</span> <span class="js-meta-standort"></span></span>
|
||||
<span><span class="muted">Erstellt:</span> <span class="js-meta-created"></span></span>
|
||||
<span><span class="muted">Aktualisiert:</span> <span class="js-meta-updated"></span></span>
|
||||
<span><span class="muted">Zugewiesen:</span> <span class="js-meta-assignee"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
<a class="home-ticket-open js-ticket-open" href="#">Ticket öffnen →</a>
|
||||
|
||||
@@ -29,6 +29,15 @@
|
||||
<span id="t-status-badge" class="badge"></span>
|
||||
</p>
|
||||
<p><strong>Priorität:</strong> <span id="t-priority-label"></span></p>
|
||||
<p>
|
||||
<label for="t-assign-user"><strong>Zugewiesen an</strong></label>
|
||||
<select id="t-assign-user" aria-label="Benutzer zuweisen">
|
||||
<option value="">— lädt … —</option>
|
||||
</select>
|
||||
</p>
|
||||
<p id="t-assign-readonly" class="muted" hidden>
|
||||
<strong>Zugewiesen an:</strong> <span id="t-assign-label"></span>
|
||||
</p>
|
||||
<p>
|
||||
<label for="t-sla-days"><strong>Fälligkeit (Bearbeitungszeit)</strong></label>
|
||||
<select id="t-sla-days" aria-label="Fälligkeit in Tagen">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<p id="page-error" class="error" hidden></p>
|
||||
<div id="page-main" class="stack" hidden>
|
||||
<h2>Tickets</h2>
|
||||
<div class="card">
|
||||
<div class="card" id="tickets-new-card">
|
||||
<h3>Neues Ticket</h3>
|
||||
<form id="form-new-ticket" class="stack ticket-new-form">
|
||||
<div class="ticket-form-machine">
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
<label
|
||||
>Rolle
|
||||
<select name="role">
|
||||
<option value="user">Benutzer</option>
|
||||
<option value="viewer">Viewer</option>
|
||||
<option value="after_sales" selected>After-Sales</option>
|
||||
<option value="admin">Administrator</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
Reference in New Issue
Block a user