Upload via GUI (69 Dateien)

This commit is contained in:
2026-10-21 16:45:21 +00:00
parent 5cae8be709
commit dc19180a77
2 changed files with 969 additions and 916 deletions
+51 -4
View File
@@ -190,8 +190,13 @@ function renderShell() {
h('div', { class: 'who' }, h('div', { class: 'avatar' }, initials(state.staff.name)),
h('div', {}, h('div', { class: 'name' }, state.staff.name), h('div', { class: 'role' }, state.staff.role === 'admin' ? 'Administrator' : 'Mitarbeiter'))),
h('button', { class: 'btn ghost sm', onclick: doLogout }, 'Abmelden'));
const navBtn = ([k, ic, label]) => h('button', { class: 'nav', 'data-page': k, onclick: () => go(k) }, h('span', { class: 'nl' }, icon(ic), label), k === 'tickets' ? h('span', { class: 'badge hidden', id: 'badge' }) : null);
const teamKeys = new Set(['availability', 'admin']);
const work = items.filter(([k]) => !teamKeys.has(k)), team = items.filter(([k]) => teamKeys.has(k));
const side = h('nav', { class: 'side' },
...items.map(([k, ic, label]) => h('button', { class: 'nav', 'data-page': k, onclick: () => go(k) }, h('span', { class: 'nl' }, icon(ic), label), k === 'tickets' ? h('span', { class: 'badge hidden', id: 'badge' }) : null)),
...work.map(navBtn),
team.length ? h('div', { class: 'navsep' }) : null,
...team.map(navBtn),
h('div', { class: 'spacer' }),
h('button', { class: 'nav', 'data-page': 'settings', onclick: () => go('settings') }, h('span', { class: 'nl' }, icon('gear'), 'Einstellungen')));
$app.append(h('div', { class: 'shell' }, top, side, h('div', { class: 'mainwrap' }, h('div', { id: 'banners' }), h('main', { class: 'main', id: 'main' }))));
@@ -696,7 +701,16 @@ async function vAvailability(main) {
const note = h('input', { value: r.myNote, placeholder: 'Notiz (optional)' });
main.append(h('h2', {}, 'Verfügbarkeit'), h('div', { class: 'panel' }, h('label', {}, 'Mein Status'), sel, h('label', {}, 'Notiz'), note,
h('div', { class: 'row' }, h('button', { class: 'btn', onclick: () => run(() => post('availability.php', { status: sel.value, note: note.value }), 'Gespeichert') }, 'Speichern'))),
h('h3', {}, 'Team'), h('div', { class: 'panel' }, h('table', {}, ...r.team.map((m) => h('tr', {}, h('td', {}, m.name), h('td', {}, m.isOnVacation ? '🏖 Urlaub' : labels[m.status] || m.status), h('td', { class: 'muted' }, m.note))))));
h('h3', {}, isAdmin() ? 'Team (als Administrator kannst du den Status ändern)' : 'Team'),
h('div', { class: 'panel' }, h('table', {}, ...r.team.map((m) => {
const cells = [h('td', {}, m.name), h('td', {}, m.isOnVacation ? '🏖 Urlaub' : (labels[m.status] || m.status) + (m.autoOffline ? ' (automatisch, inaktiv)' : '')), h('td', { class: 'muted' }, m.note)];
if (isAdmin()) {
const ms = h('select', {}, ...Object.entries(labels).map(([v, l]) => h('option', { value: v, selected: v === m.status }, l)));
const mn = h('input', { value: m.note || '', placeholder: 'Notiz', maxlength: '120' });
cells.push(h('td', {}, ms), h('td', {}, mn), h('td', {}, h('button', { class: 'btn sm', onclick: async () => { const ok = await run(() => post('availability.php', { status: ms.value, note: mn.value, staffId: m.id }), 'Status von ' + m.name + ' geändert'); if (ok) go('availability'); } }, 'Setzen')));
}
return h('tr', {}, ...cells);
}))));
}
// ─── Verwaltung (rechtegesteuert, wie in der Android-App) ───────────────────
@@ -735,11 +749,35 @@ const ADMIN = [
{ key: 'time', label: 'Zeiterfassung', perm: 'view_time_entries', file: 'time_entries.php', readonly: true, list: 'entries', cols: [['Datum', (x) => fmtDate(x.loggedAt)], ['Mitarbeiter', 'staffName'], ['Ticket', 'ticketNumber'], ['Minuten', 'minutes'], ['Notiz', 'note']] },
];
const ADMIN_GROUPS = [
['Verwalten', ['departments', 'priorities', 'sla_plans', 'help_topics', 'kb', 'email_addresses', 'email_templates', 'ticket_rules', 'banlist']],
['Kunden', ['organizations']],
['Agenten', ['staff', 'teams', 'roles', 'status']],
['System', ['audit', 'time', 'escalations', 'business_hours']],
];
const ADMIN_DESC = {
departments: 'Status, Leiter, Mitglieder, E-Mail', priorities: 'Reaktions- und Lösungszeiten', sla_plans: 'Benannte Zeiten je Abteilung oder Thema', help_topics: 'Thema, Typ, Abteilung, Priorität',
kb: 'Artikel und Kategorien', email_addresses: 'Adressen und Postfach-Abruf', email_templates: 'Texte der automatischen E-Mails', ticket_rules: 'Automatisierung und Auto-Antworten', banlist: 'Absender oder Mailanbieter sperren',
organizations: 'Kunden zu Firmen bündeln',
staff: 'Konten, Rollen, Abteilungszugriff', teams: 'Mitarbeiter-Gruppen', roles: 'Individuelle Berechtigungen', status: 'Status ändern, z. B. auf Offline setzen',
audit: 'Änderungen und fehlgeschlagene Mails', time: 'Erfasste Arbeitszeiten', escalations: 'Regeln für überfällige Tickets', business_hours: 'Öffnungszeiten und Feiertage',
};
async function vAdmin(main, sectionKey) {
const sections = adminSections();
if (!sectionKey) {
main.append(h('h2', {}, 'Verwaltung'), h('div', { class: 'tiles' }, ...sections.map((s) => h('div', { class: 'tile', onclick: () => go('admin', s.key) }, s.label))),
h('p', { class: 'muted' }, 'Sichtbar sind nur Bereiche, für die du freigeschaltet bist. Systemnahe Bereiche (Backup, Update, Postfächer, Einstellungen) gibt es nur in der Weboberfläche.'));
// Bereiche zu Gruppen ordnen (gleiche Gruppen wie im Adminportal und in der Android-App)
const entries = new Map(sections.map((s) => [s.key, { label: s.label, open: () => go('admin', s.key) }]));
if (isAdmin()) entries.set('status', { label: 'Status der Mitarbeiter', open: () => go('availability') });
const used = new Set();
const groupsView = ADMIN_GROUPS.map(([title, keys]) => {
const tiles = keys.filter((k) => entries.has(k)).map((k) => { used.add(k); const e = entries.get(k);
return h('div', { class: 'tile', tabindex: '0', onclick: e.open, onkeydown: (ev) => { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); e.open(); } } },
h('div', { class: 'tt' }, e.label), h('div', { class: 'td' }, ADMIN_DESC[k] || '')); });
return tiles.length ? h('section', { class: 'tilegroup' }, h('h3', { class: 'grouplabel' }, title), h('div', { class: 'tiles' }, ...tiles)) : null;
});
const rest = [...entries.keys()].filter((k) => !used.has(k)).map((k) => h('div', { class: 'tile', onclick: entries.get(k).open }, h('div', { class: 'tt' }, entries.get(k).label)));
main.append(h('div', { class: 'pagehead' }, h('div', { class: 'grow' }, h('h2', {}, 'Verwaltung'), h('div', { class: 'sub' }, 'Nur Bereiche, für die du freigeschaltet bist. Backup, Update, Postfächer und Einstellungen gibt es nur in der Weboberfläche.'))),
...groupsView, rest.length ? h('section', { class: 'tilegroup' }, h('h3', { class: 'grouplabel' }, 'Weitere'), h('div', { class: 'tiles' }, ...rest)) : null);
return;
}
const s = sections.find((x) => x.key === sectionKey);
@@ -888,6 +926,15 @@ function openPalette() {
document.body.append(bg); input.focus(); refresh();
}
// Meldet dem Server höchstens einmal pro Minute, dass das Programm bedient wird (sonst setzt der Server nach 30 Minuten ohne Aktivität auf Offline)
let lastPresence = 0;
function presencePing() {
if (!state.staff || Date.now() - lastPresence < 60000) return;
lastPresence = Date.now();
post('presence.php', {}).catch(() => { lastPresence = 0; });
}
['mousedown', 'keydown', 'wheel', 'touchstart'].forEach((ev) => document.addEventListener(ev, presencePing, { passive: true }));
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { const m = document.querySelector('.modal-bg'); if (m) { m.remove(); e.preventDefault(); } return; }
if (!state.staff) return;
+6
View File
@@ -173,6 +173,12 @@ tr.click { cursor: pointer; transition: background .08s; } tr.click:hover td { b
.toast.error { border-left-color: var(--bad); }
.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
.tile { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 20px; cursor: pointer; font-weight: 600; box-shadow: var(--shadow-sm); transition: transform .12s, border-color .12s, box-shadow .12s; }
.tile .tt { font-weight: 600; }
.tile .td { font-weight: 400; font-size: .8rem; color: var(--muted); margin-top: 4px; line-height: 1.35; }
.tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.tilegroup { margin-bottom: 26px; }
.grouplabel { margin: 0 0 10px; font-size: .72rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.navsep { height: 1px; background: var(--chrome-hover); margin: 8px 6px; }
.tile:hover { border-color: var(--accent); transform: translateY(-2px); box-shadow: var(--shadow); color: var(--accent); }
.bulk .btn.ghost { color: inherit; border-color: rgba(255,255,255,.55); } .bulk .btn.ghost:hover { background: rgba(255,255,255,.18); }
.sect { margin: 16px 0 4px; font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: 700; }