'use strict'; // Zusätzliche Verwaltungsbereiche (wie in der Android-App): Teams, Rollen, Eskalationen, // Geschäftszeiten, E-Mail-Vorlagen und Ticket-Regeln. Jeder Bereich rendert sich selbst in `body`. const PERM_LABELS = { reply_ticket: 'Tickets beantworten', close_ticket: 'Tickets lösen/schließen', delete_ticket: 'Tickets löschen', transfer_ticket: 'Tickets verschieben', post_note: 'Interne Notizen', assign_ticket: 'Tickets zuweisen', manage_canned: 'Textbausteine verwalten', view_reports: 'Berichte ansehen', create_ticket: 'Tickets erstellen', manage_kb: 'Wissensdatenbank', manage_departments: 'Abteilungen', manage_priorities: 'Prioritäten / SLA', manage_help_topics: 'Hilfe-Themen', manage_email_addresses: 'E-Mail-Adressen', manage_email_templates: 'E-Mail-Vorlagen', manage_organizations: 'Organisationen', manage_teams: 'Teams', manage_roles: 'Rollen', manage_staff: 'Mitarbeiter', view_audit_log: 'Admin-Protokoll', manage_escalations: 'Eskalationsregeln', view_time_entries: 'Zeiterfassung', manage_business_hours: 'Geschäftszeiten', }; const WEEKDAYS = { 1: 'Montag', 2: 'Dienstag', 3: 'Mittwoch', 4: 'Donnerstag', 5: 'Freitag', 6: 'Samstag', 0: 'Sonntag' }; const opt = (arr, val, empty) => [empty ? h('option', { value: '' }, empty) : null, ...arr.map(([v, l]) => h('option', { value: v, selected: String(v) === String(val ?? '') }, l))]; const named = (list) => list.map((x) => [x.id, x.name]); const numOrNull = (v) => (v === '' || v == null ? null : Number(v)); const row = (...kids) => h('div', { class: 'row' }, ...kids); const delBtn = (fn) => h('button', { class: 'btn danger sm', onclick: (e) => { e.stopPropagation(); if (confirm('Wirklich löschen?')) fn(); } }, '✕'); const table = (heads, rows) => h('div', { class: 'panel' }, rows.length ? h('table', {}, h('tr', {}, ...heads.map((t) => h('th', {}, t))), ...rows) : h('div', { class: 'muted' }, 'Keine Einträge.')); // ─── Teams ─────────────────────────────────────────────────────────────────── async function adminTeams(body, addBtn) { const load = async () => { const r = await run(() => api('teams.php')); if (!r) return; clear(body).append(table(['Name', 'Leitung', 'Mitglieder', 'Aktiv', 'Aktualisiert', ''], r.items.map((t) => h('tr', { class: 'click', onclick: () => edit(t, r) }, h('td', {}, t.name), h('td', {}, t.leadName || '—'), h('td', {}, t.memberCount), h('td', {}, h('input', { type: 'checkbox', checked: t.isActive, onclick: (e) => e.stopPropagation(), onchange: () => run(async () => { await post('teams.php', { action: 'toggle', id: t.id }); load(); }) })), h('td', { class: 'muted' }, t.updatedAt ? new Date(t.updatedAt.replace(' ', 'T')).toLocaleString(window.I18N?.locale || 'de-DE', { dateStyle: 'short', timeStyle: 'short' }) : ''), h('td', {}, delBtn(() => run(async () => { await post('teams.php', { action: 'delete', id: t.id }); load(); }, 'Gelöscht'))))))); addBtn.onclick = () => edit(null, r); }; function edit(t, r) { const name = h('input', { value: t?.name || '' }), notes = h('textarea', { value: t?.notes || '' }); const lead = h('select', {}, ...opt(named(r.staffOptions), t?.leadId, '— keine —')); const active = h('input', { type: 'checkbox', checked: t ? t.isActive : true }), noAlerts = h('input', { type: 'checkbox', checked: !!t?.noAlerts }); const members = r.staffOptions.map((s) => ({ s, cb: h('input', { type: 'checkbox', checked: t?.memberIds?.includes(s.id) }) })); modal(t ? 'Team bearbeiten' : 'Neues Team', h('div', {}, h('label', {}, 'Name'), name, h('label', {}, 'Leitung'), lead, h('label', { class: 'check' }, active, 'Aktiv'), h('label', { class: 'check' }, noAlerts, 'Benachrichtigung bei Ticketzuweisung deaktivieren'), h('label', {}, 'Administratornotizen (intern)'), notes, h('label', {}, 'Mitglieder'), h('div', { class: 'checks' }, ...members.map(({ s, cb }) => h('label', { class: 'check' }, cb, s.name)))), [{ label: 'Speichern', fn: async (close) => { if (await run(() => post('teams.php', { action: 'save', id: t?.id ?? null, name: name.value, leadId: numOrNull(lead.value), isActive: active.checked, noAlerts: noAlerts.checked, notes: notes.value || null, memberIds: members.filter((m) => m.cb.checked).map((m) => m.s.id) }), 'Gespeichert')) { close(); load(); } } }]); } load(); } // ─── Rollen ────────────────────────────────────────────────────────────────── async function adminRoles(body, addBtn) { const load = async () => { const r = await run(() => api('roles.php')); if (!r) return; clear(body).append(table(['Rolle', 'Status', 'Zugewiesen', 'Rechte', ''], r.items.map((x) => h('tr', { class: 'click', onclick: () => edit(x, r) }, h('td', {}, x.name), h('td', {}, x.isActive ? 'Aktiv' : 'Deaktiviert'), h('td', {}, x.staffCount), h('td', { class: 'muted' }, Object.values(x.permissions).filter(Boolean).length + ' von ' + r.permissionKeys.length), h('td', {}, delBtn(() => run(async () => { await post('roles.php', { action: 'delete', id: x.id }); load(); }, 'Gelöscht'))))))); addBtn.onclick = () => edit(null, r); }; function edit(x, r) { const name = h('input', { value: x?.name || '' }), notes = h('textarea', { value: x?.notes || '' }); const active = h('input', { type: 'checkbox', checked: x ? x.isActive : true }); const perms = (x?.effectivePermissions) || (x?.permissions) || r.defaultAgentPermissions || {}; const boxes = r.permissionKeys.map((k) => ({ k, cb: h('input', { type: 'checkbox', checked: !!perms[k] }) })); const byKey = Object.fromEntries(boxes.map((b) => [b.k, b])); const groups = (r.permissionGroups && r.permissionGroups.length ? r.permissionGroups : [{ title: 'Rechte', keys: r.permissionKeys }]); modal(x ? 'Rolle bearbeiten' : 'Neue Rolle', h('div', {}, h('label', {}, 'Name'), name, h('label', { class: 'check' }, active, 'Aktiv (deaktivierte Rolle wird ignoriert)'), h('label', {}, 'Administratornotizen (intern)'), notes, h('label', {}, 'Rechte'), ...groups.map((g) => h('div', {}, h('h4', {}, g.title), h('div', { class: 'checks' }, ...g.keys.filter((k) => byKey[k]).map((k) => h('label', { class: 'check' }, byKey[k].cb, r.permissionLabels?.[k] || PERM_LABELS[k] || k)))))), [{ label: 'Speichern', fn: async (close) => { if (await run(() => post('roles.php', { action: 'save', id: x?.id ?? null, name: name.value, isActive: active.checked, notes: notes.value || null, permissions: Object.fromEntries(boxes.map(({ k, cb }) => [k, cb.checked])) }), 'Gespeichert')) { close(); load(); } } }]); } load(); } // ─── Eskalationsregeln ─────────────────────────────────────────────────────── const TRIGGERS = [['no_reply', 'Keine Antwort seit …'], ['no_update', 'Keine Aktualisierung seit …'], ['overdue', 'Überfällig seit …'], ['open_too_long', 'Zu lange offen seit …']]; async function adminEscalations(body, addBtn) { const load = async () => { const r = await run(() => api('escalation_rules.php')); if (!r) return; clear(body).append(table(['Regel', 'Auslöser', 'Angewendet', 'Aktiv', ''], r.rules.map((x) => h('tr', { class: 'click', onclick: () => edit(x, r) }, h('td', {}, x.name), h('td', {}, `${(TRIGGERS.find((t) => t[0] === x.triggerType) || [, x.triggerType])[1].replace('…', x.triggerHours + ' Std.')}`), h('td', {}, x.appliedCount), h('td', {}, h('input', { type: 'checkbox', checked: x.isActive, onclick: (e) => e.stopPropagation(), onchange: () => run(async () => { await post('escalation_rules.php', { action: 'toggle', id: x.id }); load(); }) })), h('td', {}, delBtn(() => run(async () => { await post('escalation_rules.php', { action: 'delete', id: x.id }); load(); }, 'Gelöscht'))))))); addBtn.onclick = () => edit(null, r); }; function edit(x, r) { const f = { name: h('input', { value: x?.name || '' }), active: h('input', { type: 'checkbox', checked: x?.isActive ?? true }), trig: h('select', {}, ...opt(TRIGGERS, x?.triggerType || 'no_reply')), hours: h('input', { type: 'number', min: 1, value: x?.triggerHours ?? 24 }), cStatus: h('select', {}, ...opt(state.meta.statuses.map((s) => [s.value, s.label]), x?.conditionStatus, 'Beliebig')), cPrio: h('select', {}, ...opt(named(r.priorities), x?.conditionPriorityId, 'Beliebig')), cDept: h('select', {}, ...opt(named(r.departments), x?.conditionDeptId, 'Beliebig')), aPrio: h('select', {}, ...opt(named(r.priorities), x?.actionSetPriorityId, '— nicht ändern —')), aStaff: h('select', {}, ...opt(named(r.staffOptions), x?.actionAssignStaffId, '— nicht ändern —')), aStatus: h('select', {}, ...opt([['open', 'Offen'], ['pending', 'Wartend'], ['on_hold', 'Zurückgestellt']], x?.actionSetStatus, '— nicht ändern —')), nAdmin: h('input', { type: 'checkbox', checked: x?.actionNotifyAdmin ?? true }), nStaff: h('input', { type: 'checkbox', checked: !!x?.actionNotifyStaff }), }; modal(x ? 'Regel bearbeiten' : 'Neue Eskalationsregel', h('div', {}, h('label', {}, 'Name'), f.name, h('label', { class: 'check' }, f.active, 'Aktiv'), h('label', {}, 'Auslöser'), f.trig, h('label', {}, 'Nach Stunden'), f.hours, h('h3', {}, 'Bedingungen'), h('label', {}, 'Status'), f.cStatus, h('label', {}, 'Priorität'), f.cPrio, h('label', {}, 'Abteilung'), f.cDept, h('h3', {}, 'Aktionen'), h('label', {}, 'Priorität setzen'), f.aPrio, h('label', {}, 'Zuweisen an'), f.aStaff, h('label', {}, 'Status setzen'), f.aStatus, h('label', { class: 'check' }, f.nAdmin, 'Admins benachrichtigen'), h('label', { class: 'check' }, f.nStaff, 'Zuständigen benachrichtigen')), [{ label: 'Speichern', fn: async (close) => { if (await run(() => post('escalation_rules.php', { action: 'save', id: x?.id ?? null, name: f.name.value, isActive: f.active.checked, triggerType: f.trig.value, triggerHours: +f.hours.value || 24, conditionStatus: f.cStatus.value || null, conditionPriorityId: numOrNull(f.cPrio.value), conditionDeptId: numOrNull(f.cDept.value), actionSetPriorityId: numOrNull(f.aPrio.value), actionAssignStaffId: numOrNull(f.aStaff.value), actionSetStatus: f.aStatus.value || null, actionNotifyAdmin: f.nAdmin.checked, actionNotifyStaff: f.nStaff.checked, sortOrder: x?.sortOrder ?? 0 }), 'Gespeichert')) { close(); load(); } } }]); } load(); } // ─── Geschäftszeiten ───────────────────────────────────────────────────────── async function adminBusinessHours(body, addBtn) { addBtn.classList.add('hidden'); const load = async () => { const r = await run(() => api('business_hours.php')); if (!r) return; const enabled = h('input', { type: 'checkbox', checked: r.enabled }); const rows = r.hours.map((x) => ({ x, open: h('input', { type: 'checkbox', checked: x.isOpen }), from: h('input', { type: 'time', value: x.openTime }), to: h('input', { type: 'time', value: x.closeTime }) })); const hDate = h('input', { type: 'date' }), hName = h('input', { placeholder: 'Bezeichnung (z. B. Neujahr)' }); clear(body).append(h('div', { class: 'panel' }, h('label', { class: 'check' }, enabled, 'SLA-Uhr nur innerhalb der Geschäftszeiten laufen lassen'), h('table', {}, ...rows.map(({ x, open, from, to }) => h('tr', {}, h('td', {}, WEEKDAYS[x.weekday]), h('td', {}, open), h('td', {}, from), h('td', {}, to)))), row(h('button', { class: 'btn', onclick: () => run(() => post('business_hours.php', { action: 'save_hours', enabled: enabled.checked, hours: rows.map(({ x, open, from, to }) => ({ weekday: x.weekday, isOpen: open.checked, openTime: from.value || '09:00', closeTime: to.value || '17:00' })) }), 'Gespeichert') }, 'Speichern'))), h('h3', {}, 'Feiertage / Schließtage'), h('div', { class: 'panel' }, row(hDate, hName, h('button', { class: 'btn', onclick: () => hDate.value && hName.value && run(async () => { await post('business_hours.php', { action: 'add_holiday', date: hDate.value, name: hName.value }); load(); }) }, 'Hinzufügen')), ...r.holidays.map((hd) => row(h('span', { class: 'grow' }, `${hd.date} – ${hd.name}`), delBtn(() => run(async () => { await post('business_hours.php', { action: 'delete_holiday', id: hd.id }); load(); })))))); }; load(); } // ─── E-Mail-Vorlagen ───────────────────────────────────────────────────────── async function adminEmailTemplates(body, addBtn) { addBtn.classList.add('hidden'); const load = async () => { const r = await run(() => api('email_templates.php')); if (!r) return; clear(body).append(table(['Vorlage', 'Angepasst'], r.items.map((t) => h('tr', { class: 'click', onclick: () => edit(t) }, h('td', {}, t.label), h('td', {}, t.isCustomized ? '✓' : ''))))); }; function edit(t) { const subject = h('input', { value: t.subject }), text = h('textarea', { value: t.body, class: 'tall' }); modal(t.label, h('div', {}, h('label', {}, 'Betreff'), subject, h('label', {}, 'Text'), text, h('div', { class: 'muted' }, 'Platzhalter: ' + t.placeholders.join(' '))), [{ label: 'Speichern', fn: async (close) => { if (await run(() => post('email_templates.php', { action: 'save', key: t.key, subject: subject.value, body: text.value }), 'Gespeichert')) { close(); load(); } } }, ...(t.isCustomized ? [{ label: 'Zurücksetzen', cls: 'ghost', fn: async (close) => { if (confirm('Auf Standard zurücksetzen?') && await run(() => post('email_templates.php', { action: 'reset', key: t.key }), 'Zurückgesetzt')) { close(); load(); } } }] : [])]); } load(); } // ─── Ticket-Regeln ─────────────────────────────────────────────────────────── async function adminTicketRules(body, addBtn) { const load = async () => { const r = await run(() => api('ticket_rules.php')); if (!r) return; const label = (list, v) => (list.find((o) => o.value === v) || { label: v }).label; const acEnabled = h('input', { type: 'checkbox', checked: r.autoCloseEnabled }), acDays = h('input', { type: 'number', min: 1, value: r.autoCloseDays, class: 'narrow' }); clear(body).append( h('div', { class: 'panel' }, h('b', {}, 'Automatisch schließen'), row(h('label', { class: 'check' }, acEnabled, 'Beantwortete Tickets nach'), acDays, h('span', {}, 'Tagen schließen'), h('button', { class: 'btn sm', onclick: () => run(() => post('ticket_rules.php', { action: 'save_auto_close', enabled: acEnabled.checked, days: +acDays.value || 7 }), 'Gespeichert') }, 'Speichern'))), table(['Regel', 'Bedingungen', 'Aktionen', 'Aktiv', ''], r.rules.map((x) => h('tr', { class: 'click', onclick: () => edit(x, r) }, h('td', {}, x.name), h('td', { class: 'muted' }, x.conditions.map((c) => `${label(r.fieldOptions, c.field)} ${label(r.operatorOptions, c.operator)} „${c.value}“`).join(x.matchType === 'any' ? ' ODER ' : ' UND ')), h('td', { class: 'muted' }, x.actions.map((a) => label(r.actionOptions, a.type) + (a.valueLabel ? `: ${a.valueLabel}` : '')).join(', ')), h('td', {}, h('input', { type: 'checkbox', checked: x.isActive, onclick: (e) => e.stopPropagation(), onchange: () => run(async () => { await post('ticket_rules.php', { action: 'toggle', id: x.id }); load(); }) })), h('td', {}, delBtn(() => run(async () => { await post('ticket_rules.php', { action: 'delete', id: x.id }); load(); }, 'Gelöscht'))))))); addBtn.onclick = () => edit(null, r); }; function edit(x, r) { const name = h('input', { value: x?.name || '' }), match = h('select', {}, ...opt([['all', 'Alle Bedingungen müssen zutreffen'], ['any', 'Mindestens eine Bedingung']], x?.matchType || 'all')); const active = h('input', { type: 'checkbox', checked: x?.isActive ?? true }), sort = h('input', { type: 'number', value: x?.sortOrder ?? 0 }); const conds = h('div'), acts = h('div'); const valueSource = { set_department: r.departments, set_priority: r.priorities, set_help_topic: r.helpTopics, assign_staff: r.staffOptions, auto_reply: r.cannedResponses }; const addCond = (c) => { const rowEl = h('div', { class: 'row rule' }, h('select', { 'data-f': 'field' }, ...opt(r.fieldOptions.map((o) => [o.value, o.label]), c?.field)), h('select', { 'data-f': 'operator' }, ...opt(r.operatorOptions.map((o) => [o.value, o.label]), c?.operator)), h('input', { 'data-f': 'value', value: c?.value || '', placeholder: 'Wert' }), h('button', { class: 'btn ghost sm', onclick: () => rowEl.remove() }, '✕')); conds.append(rowEl); }; const addAct = (a) => { const rowEl = h('div', { class: 'row rule' }), valHolder = h('span', { class: 'grow' }); const type = h('select', { 'data-f': 'type', onchange: () => paint(type.value, '') }, ...opt(r.actionOptions.map((o) => [o.value, o.label]), a?.type)); const paint = (t, v) => { clear(valHolder); if (t === 'reject') return; const src = valueSource[t]; valHolder.append(src ? h('select', { 'data-f': 'value' }, ...opt(named(src), v)) : h('input', { 'data-f': 'value', value: v || '', placeholder: 'Wert' })); }; rowEl.append(type, valHolder, h('button', { class: 'btn ghost sm', onclick: () => rowEl.remove() }, '✕')); acts.append(rowEl); paint(type.value, a?.value); }; (x?.conditions?.length ? x.conditions : [null]).forEach(addCond); (x?.actions?.length ? x.actions : [null]).forEach(addAct); const collect = (box) => [...box.children].map((el) => Object.fromEntries([...el.querySelectorAll('[data-f]')].map((i) => [i.dataset.f, i.value]))); modal(x ? 'Regel bearbeiten' : 'Neue Ticket-Regel', h('div', {}, h('label', {}, 'Name'), name, h('label', {}, 'Verknüpfung'), match, h('label', { class: 'check' }, active, 'Aktiv'), h('label', {}, 'Reihenfolge'), sort, h('h3', {}, 'Wenn …'), conds, h('button', { class: 'btn ghost sm', onclick: () => addCond(null) }, '+ Bedingung'), h('h3', {}, 'Dann …'), acts, h('button', { class: 'btn ghost sm', onclick: () => addAct(null) }, '+ Aktion')), [{ label: 'Speichern', fn: async (close) => { if (await run(() => post('ticket_rules.php', { action: 'save', id: x?.id ?? null, name: name.value, matchType: match.value, isActive: active.checked, sortOrder: +sort.value || 0, conditions: collect(conds).map((c) => ({ field: c.field, operator: c.operator, value: c.value || '' })), actions: collect(acts).map((a) => ({ type: a.type, value: a.value || '' })) }), 'Gespeichert')) { close(); load(); } } }]); } load(); } // ─── Digitaler Assistent (Einstellungen, zusätzliche Antworten, Fragen-Log, Gleichbedeutende Wörter, Ausprobieren) ─── async function adminAssistant(body) { const A = 'assistant.php'; const tabs = [['settings', 'Einstellungen'], ['answers', 'Zusätzliche Antworten'], ['log', 'Fragen-Log'], ['synonyms', 'Gleichbedeutende Wörter'], ['test', 'Frage ausprobieren']]; let tab = 'settings'; const logF = { result: '', period: '30', q: '', page: 1 }; const bar = h('div', { class: 'tabs wrap' }), view = h('div'); body.append(bar, view); const paintBar = () => clear(bar).append(...tabs.map(([k, l]) => h('button', { class: 'tab' + (tab === k ? ' active' : ''), onclick: () => { tab = k; paintBar(); paint(); } }, l))); const paint = () => ({ settings: pSettings, answers: pAnswers, log: pLog, synonyms: pSynonyms, test: pTest })[tab](); async function pSettings() { const o = await run(() => api(A, { query: { section: 'overview' } })); if (!o) return; const s = o.settings, d = o.defaults; const en = h('input', { type: 'checkbox', checked: s.enabled }), mode = h('select', {}, ...opt([['offline_only', 'Nur wenn der Live-Chat ausgeschaltet oder niemand erreichbar ist (Standard)'], ['always', 'Immer (zum Testen)']], s.mode)); const name = h('input', { value: s.name, maxlength: 60, placeholder: d.name }), greet = h('textarea', { rows: 3, maxlength: 500, placeholder: d.greeting }, s.greeting), none = h('textarea', { rows: 2, maxlength: 500, placeholder: d.noAnswer }, s.noAnswer); const dept = h('select', {}, ...opt(named(o.departments), s.departmentId || '', '— Standardabteilung (bzw. Abteilung des gewählten Themas) —')); const prio = h('select', {}, ...opt(named(o.priorities), s.priorityId || '', '— Standard —')); const days = h('input', { type: 'number', min: 0, max: 3650, value: s.logDays, style: 'max-width:140px' }); const st = o.state; clear(view).append(h('div', { class: 'panel' }, !o.dbReady ? h('div', { class: 'warnbox' }, 'Die Datenbank ist noch nicht aktuell. Bitte im Adminportal unter System-Update „Datenbank aktualisieren“ klicken.') : null, h('p', { class: 'muted' }, 'Der Assistent beantwortet Fragen von Kunden aus der Wissensdatenbank und aus den zusätzlichen Antworten. Findet er nichts, bietet er ein Ticket an. Es wird kein externer KI-Dienst verwendet.'), h('p', {}, 'Zustand jetzt: ', h('b', {}, !s.enabled ? 'ausgeschaltet' : st.activeNow ? 'aktiv (Kunden sehen den Assistenten)' : 'bereit, aber gerade nicht sichtbar'), h('span', { class: 'muted' }, ` · Live-Chat ${st.liveChatEnabled ? 'an' : 'aus'} · Wissensdatenbank im Portal ${st.kbEnabled ? 'an' : 'aus'}`)), h('label', { class: 'check' }, en, 'Digitalen Assistenten einschalten'), h('label', {}, 'Wann ist er aktiv?'), mode, h('label', {}, 'Name des Assistenten'), name, h('label', {}, 'Begrüßung'), greet, h('label', {}, 'Text, wenn er keine Antwort findet'), none, h('label', {}, 'Abteilung für Tickets des Assistenten'), dept, h('label', {}, 'Priorität für Tickets des Assistenten'), prio, h('label', {}, 'Fragen-Log: Aufbewahrung in Tagen (0 = nichts speichern)'), days, h('div', { class: 'row top' }, h('button', { class: 'btn', onclick: () => run(() => post(A, { action: 'save_settings', enabled: en.checked, mode: mode.value, name: name.value, greeting: greet.value, noAnswer: none.value, departmentId: numOrNull(dept.value) || 0, priorityId: numOrNull(prio.value) || 0, logDays: +days.value || 0 }), 'Gespeichert').then(pSettings) }, 'Speichern')))); } async function pAnswers() { const r = await run(() => api(A, { query: { section: 'answers' } })); if (!r) return; const add = h('button', { class: 'btn', onclick: () => edit(null) }, '+ Antwort hinzufügen'); clear(view).append(h('div', { class: 'row' }, h('span', { class: 'grow muted' }, 'Diese Antworten haben Vorrang vor der Wissensdatenbank. Der Assistent findet sie über die Wörter in der Frage und in den Stichwörtern.'), add), table(['Frage / Thema', 'Stichwörter', 'Genutzt', 'Status', ''], r.items.map((a) => h('tr', { class: 'click', onclick: () => edit(a) }, h('td', {}, a.title, h('div', { class: 'muted' }, a.answer.slice(0, 90))), h('td', { class: 'muted' }, a.keywords), h('td', {}, a.hits + '×'), h('td', {}, a.isActive ? 'Aktiv' : 'Aus'), h('td', {}, delBtn(() => run(async () => { await post(A, { action: 'delete_answer', id: a.id }); pAnswers(); }, 'Gelöscht'))))))); } function edit(a, prefill) { const title = h('input', { value: a?.title ?? prefill ?? '', maxlength: 255, placeholder: 'z. B. Wie lange dauert der Versand?' }), kw = h('input', { value: a?.keywords ?? '', maxlength: 1000, placeholder: 'z. B. Lieferzeit, Paket, Lieferung' }); const ans = h('textarea', { rows: 6, maxlength: 4000, placeholder: 'Reiner Text. Adressen mit https:// werden anklickbar.' }, a?.answer ?? ''), act = h('input', { type: 'checkbox', checked: a ? a.isActive : true }); modal(a ? 'Antwort bearbeiten' : 'Neue Antwort', h('div', {}, h('label', {}, 'Frage / Thema *'), title, h('label', {}, 'Stichwörter (optional, durch Komma getrennt)'), kw, h('label', {}, 'Antwort *'), ans, h('label', { class: 'check' }, act, 'Aktiv')), [{ label: 'Speichern', fn: async (close) => { if (await run(() => post(A, { action: 'save_answer', id: a?.id ?? 0, title: title.value, keywords: kw.value, answer: ans.value, isActive: act.checked }), 'Gespeichert')) { close(); tab = 'answers'; paintBar(); pAnswers(); } } }]); } async function pLog() { const r = await run(() => api(A, { query: { section: 'log', result: logF.result, period: logF.period, q: logF.q, page: logF.page } })); if (!r) return; const reset = () => { logF.page = 1; pLog(); }; const tiles = h('div', { class: 'cards' }, ...Object.entries(r.filters).map(([k, label]) => h('div', { class: 'stat link' + (logF.result === k ? ' active' : ''), onclick: () => { logF.result = logF.result === k ? '' : k; reset(); } }, h('div', { class: 'n' }, r.stats[k] ?? 0), h('div', { class: 'muted' }, label)))); const sel = h('select', { onchange: (e) => { logF.result = e.target.value; reset(); } }, ...opt(Object.entries(r.filters), logF.result, 'Alle Ergebnisse')); const per = h('select', { onchange: (e) => { logF.period = e.target.value; reset(); } }, ...opt([['today', 'Heute'], ['7', 'Letzte 7 Tage'], ['30', 'Letzte 30 Tage'], ['all', 'Gesamter Zeitraum']], logF.period)); const q = h('input', { value: logF.q, placeholder: 'Suchen…', onkeydown: (e) => { if (e.key === 'Enter') { logF.q = e.target.value; reset(); } } }); const clearBtn = h('button', { class: 'btn ghost sm', onclick: () => confirm('Das gesamte Fragen-Log löschen?') && run(async () => { await post(A, { action: 'clear_log' }); reset(); }, 'Log geleert') }, 'Log leeren'); clear(view).append(h('p', { class: 'muted' }, 'Zeigt, was Kunden den Assistenten gefragt haben und ob er eine Antwort fand. Gespeichert werden nur Frage und Ergebnis (kein Name, keine E-Mail-Adresse).'), tiles, h('div', { class: 'row' }, sel, per, q, h('button', { class: 'btn secondary', onclick: () => { logF.q = q.value; reset(); } }, 'Filtern'), h('span', { class: 'grow' }), clearBtn), r.top.length ? h('div', { class: 'panel' }, h('b', {}, 'Häufige Fragen ohne Antwort'), ...r.top.map((t) => h('div', { class: 'row' }, h('span', { class: 'grow' }, `${t.question} (${t.n}×)`), h('button', { class: 'btn ghost sm', onclick: () => { tab = 'answers'; paintBar(); edit(null, t.question); } }, 'Antwort dazu anlegen')))) : null, table(['Zeit', 'Frage', 'Ergebnis', 'Antwort / Ticket', ''], r.items.map((x) => h('tr', {}, h('td', { class: 'muted' }, fmtDate(x.createdAt)), h('td', {}, x.question), h('td', {}, (r.filters[x.result] || x.result) + (x.helpful === null ? '' : x.helpful ? ' 👍' : ' 👎')), h('td', { class: 'muted' }, x.ticketNumber ? '#' + x.ticketNumber : (x.matchedTitle || '')), h('td', {}, x.result === 'none' ? h('button', { class: 'btn ghost sm', onclick: () => { tab = 'answers'; paintBar(); edit(null, x.question); } }, 'Antwort anlegen') : null)))), h('div', { class: 'row pager' }, h('button', { class: 'btn ghost sm', disabled: r.page <= 1, onclick: () => { logF.page--; pLog(); } }, '‹'), h('span', { class: 'muted' }, `${r.total} Einträge · Seite ${r.page} von ${r.pages}`), h('button', { class: 'btn ghost sm', disabled: r.page >= r.pages, onclick: () => { logF.page++; pLog(); } }, '›'))); } async function pSynonyms() { const o = await run(() => api(A, { query: { section: 'overview' } })); if (!o) return; const t = h('textarea', { rows: 8, maxlength: 5000, placeholder: 'Zugang, Einladung, beitreten\nGeld zurück, Erstattung, Rückzahlung' }, o.settings.synonyms); clear(view).append(h('div', { class: 'panel' }, h('p', { class: 'muted' }, 'Wörter, die Kunden und Artikel unterschiedlich nennen, aber dasselbe meinen. Eine Gruppe je Zeile, durch Komma getrennt, zum Beispiel „Zugang, Einladung, beitreten“. Einige Gruppen sind eingebaut, deine ergänzen oder überschreiben sie.'), t, h('div', { class: 'row top' }, h('button', { class: 'btn', onclick: () => run(async () => { const r = await post(A, { action: 'save_synonyms', text: t.value }); toast(`Gespeichert (${r.groups} Gruppen)`); }) }, 'Speichern')))); } function pTest() { const q = h('input', { placeholder: 'z. B. Wie setze ich mein Passwort zurück?', maxlength: 300 }), out = h('div'); const go = () => run(async () => { const r = await post(A, { action: 'test', question: q.value }); clear(out).append(r.found ? h('div', { class: 'panel' }, h('div', { class: 'muted' }, (r.result.source === 'custom' ? 'Zusätzliche Antwort' : 'Wissensdatenbank') + ': ' + r.result.title), h('p', { style: 'white-space:pre-wrap' }, r.result.text), ...r.result.others.map((x) => h('div', { class: 'muted' }, 'Weiterer Treffer: ' + x))) : h('div', { class: 'muted' }, 'Keine passende Antwort: Der Assistent würde anbieten, ein Ticket zu erstellen. Lege dafür eine zusätzliche Antwort an oder ergänze die Wissensdatenbank.')); }); q.addEventListener('keydown', (e) => { if (e.key === 'Enter') go(); }); clear(view).append(h('div', { class: 'panel' }, h('p', { class: 'muted' }, 'Zeigt, was der Assistent auf eine Kundenfrage antworten würde (ohne Zähler und ohne Ticket).'), h('div', { class: 'row' }, q, h('button', { class: 'btn', onclick: go }, 'Testen')), out)); } paintBar(); paint(); } ADMIN.push( { key: 'assistant', label: 'Digitaler Assistent', perm: 'manage_assistant', custom: adminAssistant }, { key: 'teams', label: 'Teams', perm: 'manage_teams', custom: adminTeams, add: true }, { key: 'roles', label: 'Rollen', perm: 'manage_roles', custom: adminRoles, add: true }, { key: 'escalations', label: 'Eskalationsregeln', perm: 'manage_escalations', custom: adminEscalations, add: true }, { key: 'business_hours', label: 'Geschäftszeiten', perm: 'manage_business_hours', custom: adminBusinessHours }, { key: 'email_templates', label: 'E-Mail-Vorlagen', perm: 'manage_email_templates', custom: adminEmailTemplates }, { key: 'ticket_rules', label: 'Ticket-Regeln', admin: true, custom: adminTicketRules, add: true }, );