@@ -1,268 +1,270 @@
'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 ? . permissions ) || r . defaultAgentPermissions || { } ;
const boxes = r . permissionKeys . map ( ( k ) => ( { k , cb : h ( 'input' , { type : 'checkbox' , checked : ! ! perms [ k ] } ) } ) ) ;
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' ) ,
h ( 'div' , { class : 'checks' } , ... boxes . map ( ( { k , cb } ) => h ( 'label' , { class : 'check' } , cb , 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 ( 'selec t' , { } , ... 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 ( 'selec t' , { } , ... 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 ( 'inpu t' , { type : 'checkbox' , checked : x ? . actionNotifyAdmin ? ? true } ) , nStaff : h ( 'inpu t' , { 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' , { } , 'Bedingun gen ') , 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 setz en' ) , 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 : 'cli ck' , 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 ( 'inpu t' , { 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 = 'a ssista nt.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 ( 'selec t' , { } , ... opt ( named ( o . priorities ) , s . priorityId || '' , '— Standard —' ) ) ;
const days = h ( 'inpu t' , { 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 ( 't d' , { } , 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 ( 'inpu t' , { 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 ( 't d' , { } , x . result === 'none' ? h ( 'button' , { class : 'btn ghost sm' , onclick : ( ) => { tab = 'answers' ; paintBar ( ) ; edit ( null , x . question ) ; } } , 'Antwort anlegen' ) : null ) ) ) ) ,
h ( 'div' , { class : 'row p ager ' } , 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 === 'En ter ' ) 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 : 'Roll en' , perm : 'manage_roles ' , custom : adminRoles , add : true } ,
{ key : 'escalation s' , label : 'Eskalationsregeln ' , perm : 'manage_escalation s' , custom : adminEscalations , add : true } ,
{ key : 'business_hour s' , label : 'Geschäftszeit en' , perm : 'manage_business_hours' , custom : adminBusinessHours } ,
{ key : 'email_template s' , label : 'E-Mail-Vorla gen' , perm : 'manage_email_template s' , custom : adminEmailTemplates } ,
{ key : 'ticket_rule s' , label : 'Ticket-Regel n' , admin : true , custom : adminTicketRules , add : true } ,
) ;
'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 ( 'inpu t' , { value : x ? . name || '' } ) , active : h ( 'input' , { type : 'checkbox' , checked : x ? . isActive ? ? true } ) ,
trig : h ( 'select' , { } , ... opt ( TRIGGERS , x ? . triggerType || 'no_reply ') ) , hours : h ( 'inpu t' , { 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 ( 'selec t' , { } , ... opt ( named ( r . priorities ) , x ? . actionSetPriorityId , '— nicht ändern —' ) ) , aStaff : h ( 'selec t' , { } , ... 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 Eskalationsre gel ', h ( 'div ' , { } , h ( 'label' , { } , 'Name ' ) , f . name , h ( 'label' , { class : 'check' } , f . active , 'Aktiv' ) ,
h ( 'label' , { } , 'Auslöser ') , f . trig , h ( 'label' , { } , 'Nach Stund en' ) , 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 ( 'inpu t' , { 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 : 'che ck' } , 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 A ssiste nt (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 ( 'inpu t' , { 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 ( 'selec t' , { } , ... 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 ( 'selec t' , { 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' , 'Fr age', '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 : 'mu ted ' } , '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 Assist ent ' , perm : 'manage_assistant ' , custom : adminAssistant } ,
{ key : 'team s' , label : 'Teams ' , perm : 'manage_team s' , custom : adminTeams , add : true } ,
{ key : 'role s' , label : 'Roll en' , perm : 'manage_roles' , custom : adminRoles , add : true } ,
{ key : 'escalation s' , label : 'Eskalationsre gel n' , perm : 'manage_escalation s' , custom : adminEscalations , add : true } ,
{ key : 'business_hour s' , label : 'Geschäftszeite n' , 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 } ,
) ;