Upload via GUI (290 Dateien)
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* api/staff/v1/assistant.php: Verwaltung des digitalen Assistenten für Android- und Windows-App (wie staff/assistant.php).
|
||||
* Nur Administratoren und Mitarbeiter mit dem Recht "Digitalen Assistenten verwalten".
|
||||
*
|
||||
* GET ?section=overview -> Einstellungen, Zustand, Abteilungen, Prioritäten, Synonyme
|
||||
* GET ?section=answers -> zusätzliche Antworten
|
||||
* GET ?section=log&result=&period=&q=&page= -> Fragen-Log mit Kennzahlen und häufigen Fragen ohne Antwort
|
||||
* POST {action: save_settings|save_answer|delete_answer|save_synonyms|clear_log|test, ...}
|
||||
*/
|
||||
require_once __DIR__ . '/common.php';
|
||||
require_once __DIR__ . '/../../../includes/assistant.php';
|
||||
|
||||
api_require_method('GET', 'POST');
|
||||
$staff = api_require_staff();
|
||||
if ($staff['role'] !== 'admin' && !has_permission($staff, 'manage_assistant')) {
|
||||
api_error(403, 'FORBIDDEN', 'Für diese Aktion fehlt die erforderliche Berechtigung.');
|
||||
}
|
||||
$pdo = get_db();
|
||||
|
||||
/** Sind die Tabellen schon da? (sonst: Datenbank aktualisieren) */
|
||||
function assistant_api_db_ready(PDO $pdo): bool
|
||||
{
|
||||
try { $pdo->query('SELECT id FROM assistant_answers LIMIT 1'); return true; } catch (Throwable $e) { return false; }
|
||||
}
|
||||
|
||||
function assistant_api_answer(array $a): array
|
||||
{
|
||||
return ['id' => (int) $a['id'], 'title' => $a['title'], 'keywords' => (string) ($a['keywords'] ?? ''), 'answer' => $a['answer'],
|
||||
'isActive' => (bool) $a['is_active'], 'hits' => (int) $a['hits'], 'updatedAt' => $a['updated_at']];
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
$section = (string) ($_GET['section'] ?? 'overview');
|
||||
if ($section === 'overview') {
|
||||
$texts = assistant_texts();
|
||||
api_json([
|
||||
'dbReady' => assistant_api_db_ready($pdo),
|
||||
'settings' => [
|
||||
'enabled' => assistant_enabled(),
|
||||
'mode' => get_setting('assistant_mode', 'offline_only') === 'always' ? 'always' : 'offline_only',
|
||||
'name' => get_setting('assistant_name', ''),
|
||||
'greeting' => get_setting('assistant_greeting', ''),
|
||||
'noAnswer' => get_setting('assistant_no_answer', ''),
|
||||
'departmentId' => (int) get_setting('assistant_department_id', '0'),
|
||||
'priorityId' => (int) get_setting('assistant_priority_id', '0'),
|
||||
'logDays' => assistant_log_days(),
|
||||
'synonyms' => get_setting('assistant_synonyms', ''),
|
||||
],
|
||||
'defaults' => ['name' => $texts['name'], 'greeting' => $texts['greeting'], 'noAnswer' => $texts['no_answer']],
|
||||
'state' => [
|
||||
'activeNow' => assistant_active($pdo),
|
||||
'liveChatEnabled' => get_setting('live_chat_enabled', '0') === '1',
|
||||
'kbEnabled' => get_setting('kb_enabled', '1') === '1',
|
||||
'helpdeskOffline' => get_setting('helpdesk_status', 'online') === 'offline',
|
||||
],
|
||||
'departments' => array_map(fn($d) => ['id' => (int) $d['id'], 'name' => $d['name']], $pdo->query('SELECT id, name FROM departments WHERE is_active = 1 ORDER BY name')->fetchAll()),
|
||||
'priorities' => array_map(fn($p) => ['id' => (int) $p['id'], 'name' => $p['name']], $pdo->query('SELECT id, name FROM priorities ORDER BY sort_order')->fetchAll()),
|
||||
]);
|
||||
}
|
||||
if ($section === 'answers') {
|
||||
try {
|
||||
$items = array_map('assistant_api_answer', $pdo->query('SELECT * FROM assistant_answers ORDER BY title')->fetchAll());
|
||||
} catch (Throwable $e) {
|
||||
api_error(503, 'DB_OUTDATED', 'Die Datenbank ist noch nicht aktuell. Bitte im Adminportal unter System-Update „Datenbank aktualisieren“ klicken.');
|
||||
}
|
||||
api_json(['items' => $items]);
|
||||
}
|
||||
if ($section === 'log') {
|
||||
$period = in_array($_GET['period'] ?? '', ['today', '7', '30', 'all'], true) ? (string) $_GET['period'] : '30';
|
||||
$filter = ['result' => (string) ($_GET['result'] ?? ''), 'period' => $period, 'q' => trim((string) ($_GET['q'] ?? ''))];
|
||||
try {
|
||||
$page = assistant_log_list($pdo, $filter, max(1, (int) ($_GET['page'] ?? 1)));
|
||||
$stats = assistant_log_stats($pdo, $period);
|
||||
$top = assistant_log_unanswered($pdo, $period);
|
||||
} catch (Throwable $e) {
|
||||
api_error(503, 'DB_OUTDATED', 'Das Log ist noch nicht verfügbar. Bitte im Adminportal unter System-Update „Datenbank aktualisieren“ klicken.');
|
||||
}
|
||||
api_json([
|
||||
'filters' => assistant_log_filters(),
|
||||
'stats' => $stats,
|
||||
'top' => $top,
|
||||
'total' => $page['total'], 'pages' => $page['pages'], 'page' => $page['page'],
|
||||
'items' => array_map(fn(array $r) => [
|
||||
'id' => (int) $r['id'], 'question' => $r['question'], 'result' => $r['result'], 'matchedTitle' => $r['matched_title'],
|
||||
'ticketNumber' => $r['ticket_number'], 'helpful' => $r['helpful'] === null ? null : ((int) $r['helpful'] === 1), 'createdAt' => $r['created_at'],
|
||||
], $page['rows']),
|
||||
]);
|
||||
}
|
||||
api_error(422, 'INVALID_SECTION', 'Unbekannter Bereich.');
|
||||
}
|
||||
|
||||
$body = api_request_body();
|
||||
$action = (string) ($body['action'] ?? '');
|
||||
|
||||
if ($action === 'save_settings') {
|
||||
$dept = (int) ($body['departmentId'] ?? 0);
|
||||
$prio = (int) ($body['priorityId'] ?? 0);
|
||||
if ($dept > 0) { $c = $pdo->prepare('SELECT COUNT(*) FROM departments WHERE id = ? AND is_active = 1'); $c->execute([$dept]); if (!(int) $c->fetchColumn()) $dept = 0; }
|
||||
if ($prio > 0) { $c = $pdo->prepare('SELECT COUNT(*) FROM priorities WHERE id = ?'); $c->execute([$prio]); if (!(int) $c->fetchColumn()) $prio = 0; }
|
||||
set_setting('assistant_enabled', !empty($body['enabled']) ? '1' : '0');
|
||||
set_setting('assistant_mode', ($body['mode'] ?? 'offline_only') === 'always' ? 'always' : 'offline_only');
|
||||
set_setting('assistant_name', mb_substr(trim(strip_tags((string) ($body['name'] ?? ''))), 0, 60));
|
||||
set_setting('assistant_greeting', mb_substr(trim(strip_tags((string) ($body['greeting'] ?? ''))), 0, 500));
|
||||
set_setting('assistant_no_answer', mb_substr(trim(strip_tags((string) ($body['noAnswer'] ?? ''))), 0, 500));
|
||||
set_setting('assistant_department_id', (string) $dept);
|
||||
set_setting('assistant_priority_id', (string) $prio);
|
||||
set_setting('assistant_log_days', (string) max(0, min(3650, (int) ($body['logDays'] ?? 90))));
|
||||
audit_log((int) $staff['id'], 'Einstellungen des digitalen Assistenten geändert (App)');
|
||||
api_json(['ok' => true]);
|
||||
}
|
||||
|
||||
if ($action === 'save_synonyms') {
|
||||
$text = mb_substr(trim(strip_tags((string) ($body['text'] ?? ''))), 0, 5000);
|
||||
set_setting('assistant_synonyms', $text);
|
||||
audit_log((int) $staff['id'], 'Gleichbedeutende Wörter des digitalen Assistenten geändert (App)');
|
||||
api_json(['ok' => true, 'groups' => count(assistant_custom_synonyms($text))]);
|
||||
}
|
||||
|
||||
if ($action === 'save_answer') {
|
||||
if (!assistant_api_db_ready($pdo)) api_error(503, 'DB_OUTDATED', 'Die Datenbank ist noch nicht aktuell. Bitte „Datenbank aktualisieren“ ausführen.');
|
||||
$id = (int) ($body['id'] ?? 0);
|
||||
$title = trim(mb_substr((string) ($body['title'] ?? ''), 0, 255));
|
||||
$keywords = trim(mb_substr((string) ($body['keywords'] ?? ''), 0, 1000));
|
||||
$answer = trim(mb_substr((string) ($body['answer'] ?? ''), 0, 4000));
|
||||
$active = !empty($body['isActive']) ? 1 : 0;
|
||||
if ($title === '' || $answer === '') api_error(422, 'FIELDS_REQUIRED', 'Bitte eine Frage (Titel) und die Antwort angeben.');
|
||||
$now = db_now();
|
||||
if ($id > 0) {
|
||||
$pdo->prepare('UPDATE assistant_answers SET title=?, keywords=?, answer=?, is_active=?, updated_at=? WHERE id=?')->execute([$title, $keywords ?: null, $answer, $active, $now, $id]);
|
||||
audit_log((int) $staff['id'], "Antwort des digitalen Assistenten geändert (App): {$title}");
|
||||
} else {
|
||||
$pdo->prepare('INSERT INTO assistant_answers (title, keywords, answer, is_active, created_at, updated_at) VALUES (?,?,?,?,?,?)')->execute([$title, $keywords ?: null, $answer, $active, $now, $now]);
|
||||
$id = (int) $pdo->lastInsertId();
|
||||
audit_log((int) $staff['id'], "Antwort des digitalen Assistenten angelegt (App): {$title}");
|
||||
}
|
||||
$row = $pdo->prepare('SELECT * FROM assistant_answers WHERE id = ?');
|
||||
$row->execute([$id]);
|
||||
api_json(['item' => assistant_api_answer($row->fetch())]);
|
||||
}
|
||||
|
||||
if ($action === 'delete_answer') {
|
||||
$id = (int) ($body['id'] ?? 0);
|
||||
$t = $pdo->prepare('SELECT title FROM assistant_answers WHERE id = ?');
|
||||
$t->execute([$id]);
|
||||
$title = $t->fetchColumn();
|
||||
if ($title === false) api_error(404, 'NOT_FOUND', 'Die Antwort gibt es nicht.');
|
||||
$pdo->prepare('DELETE FROM assistant_answers WHERE id = ?')->execute([$id]);
|
||||
audit_log((int) $staff['id'], "Antwort des digitalen Assistenten gelöscht (App): {$title}");
|
||||
api_json(['ok' => true]);
|
||||
}
|
||||
|
||||
if ($action === 'clear_log') {
|
||||
try {
|
||||
$n = $pdo->exec('DELETE FROM assistant_log');
|
||||
} catch (Throwable $e) {
|
||||
api_error(503, 'DB_OUTDATED', 'Das Log ist noch nicht verfügbar. Bitte „Datenbank aktualisieren“ ausführen.');
|
||||
}
|
||||
audit_log((int) $staff['id'], 'Fragen-Log des digitalen Assistenten geleert (App)');
|
||||
api_json(['ok' => true, 'deleted' => (int) $n]);
|
||||
}
|
||||
|
||||
if ($action === 'test') {
|
||||
$q = trim(mb_substr((string) ($body['question'] ?? ''), 0, 300));
|
||||
if ($q === '') api_error(422, 'QUESTION_REQUIRED', 'Bitte eine Frage eingeben.');
|
||||
$r = assistant_search($pdo, $q, false);
|
||||
api_json(['found' => $r !== null, 'result' => $r ? ['source' => $r['source'], 'title' => $r['title'], 'text' => $r['text'], 'others' => array_map(fn($o) => $o['title'], $r['others'])] : null]);
|
||||
}
|
||||
|
||||
api_error(422, 'INVALID_ACTION', 'Unbekannte Aktion.');
|
||||
@@ -2,6 +2,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../../includes/bootstrap.php';
|
||||
require_once __DIR__ . '/../../../includes/ticket_extras.php'; // falls bootstrap.php noch die alte Version ist
|
||||
require_once __DIR__ . '/../../../includes/totp.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
@@ -147,6 +148,7 @@ function api_require_staff(): array
|
||||
// auch App-Nutzung hält die automatischen Hintergrund-Jobs am Laufen.
|
||||
pseudo_cron_schedule_background();
|
||||
|
||||
$GLOBALS['__api_staff'] = $staff; // für api_is_following()
|
||||
return $staff;
|
||||
}
|
||||
|
||||
@@ -271,9 +273,30 @@ function api_ticket_summary(array $ticket): array
|
||||
'isSpam' => !empty($ticket['is_spam']),
|
||||
'createdAt' => $ticket['created_at'],
|
||||
'updatedAt' => $ticket['updated_at'],
|
||||
'snoozedUntil' => !empty($ticket['snoozed_until']) && $ticket['snoozed_until'] > db_now() ? $ticket['snoozed_until'] : null,
|
||||
'following' => api_is_following((int) $ticket['id']),
|
||||
];
|
||||
}
|
||||
|
||||
/** Beobachtet der angemeldete Mitarbeiter dieses Ticket? (für die App) */
|
||||
function api_is_following(int $ticketId): bool
|
||||
{
|
||||
static $mine = null;
|
||||
if ($mine === null) {
|
||||
$mine = [];
|
||||
try {
|
||||
$staff = $GLOBALS['__api_staff'] ?? null;
|
||||
if ($staff) {
|
||||
$stmt = get_db()->prepare('SELECT ticket_id FROM ticket_followers WHERE staff_id = ?');
|
||||
$stmt->execute([(int) $staff['id']]);
|
||||
$mine = array_flip(array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN)));
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
}
|
||||
}
|
||||
return isset($mine[$ticketId]);
|
||||
}
|
||||
|
||||
function api_add_ticket_message(PDO $pdo, array $staff, array $ticket, string $type, string $rawMessage, ?string $setStatus, ?string $signatureChoice = null): int
|
||||
{
|
||||
$permission = $type === 'note' ? 'post_note' : 'reply_ticket';
|
||||
|
||||
Reference in New Issue
Block a user