Upload via GUI (335 Dateien)

This commit is contained in:
2026-10-24 20:14:48 +00:00
parent b2a0a2d0b8
commit c3b6bd5dcd
6 changed files with 147 additions and 0 deletions
+8
View File
@@ -297,6 +297,14 @@ function api_is_following(int $ticketId): bool
return isset($mine[$ticketId]);
}
/** Cc-Empfänger eines Tickets für die Apps. */
function api_ticket_collaborators(PDO $pdo, int $ticketId): array
{
$s = $pdo->prepare('SELECT id, email, name FROM ticket_collaborators WHERE ticket_id = ? ORDER BY created_at, id');
$s->execute([$ticketId]);
return array_map(fn($r) => ['id' => (int) $r['id'], 'email' => (string) $r['email'], 'name' => (string) ($r['name'] ?? '')], $s->fetchAll());
}
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';
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/common.php';
/**
* Auswertung der Zufriedenheitsabfrage.
* GET ?days=30 (1 bis 365) → {enabled, days, stats:{sent,answered,avg,rate,dist,byStaff,recent}}
* Mitarbeiter sehen nur die Abteilungen, in denen sie arbeiten.
*/
api_require_method('GET');
$staff = api_require_staff();
$pdo = get_db();
$days = max(1, min(365, (int) ($_GET['days'] ?? 30)));
$from = date('Y-m-d 00:00:00', time() - ($days - 1) * 86400);
$to = date('Y-m-d 23:59:59');
$stats = null;
try {
$stats = csat_stats($pdo, $from, $to, staff_department_ids($staff));
} catch (Throwable $e) {
$stats = ['sent' => 0, 'answered' => 0, 'avg' => null, 'rate' => null, 'dist' => [1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 0], 'byStaff' => [], 'recent' => []];
}
$stats['dist'] = array_map(fn($k, $v) => ['rating' => $k, 'count' => $v], array_keys($stats['dist']), $stats['dist']);
api_json(['enabled' => csat_enabled(), 'days' => $days, 'stats' => $stats]);
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/common.php';
/**
* Statusseite für Kunden.
* GET → Zusammenfassung (Gesamtzustand, Dienste, Meldungen); für alle Mitarbeiter lesbar
* POST (nur Administratoren):
* {action:"set_component", id, status}
* {action:"create_incident", title, body?, status?, componentId?, componentState?}
* {action:"update_incident", id, status, body?}
* {action:"delete_incident", id}
* {action:"set_enabled", enabled}
*/
api_require_method('GET', 'POST');
$staff = api_require_staff();
$pdo = get_db();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
api_require_admin($staff);
$b = api_request_body();
$action = (string) ($b['action'] ?? '');
if ($action === 'set_component') {
if (!status_set_component($pdo, (int) ($b['id'] ?? 0), (string) ($b['status'] ?? ''))) api_error(422, 'INVALID_STATUS', 'Ungültiger Dienst oder Zustand.');
} elseif ($action === 'create_incident') {
$cid = (int) ($b['componentId'] ?? 0) ?: null;
$cs = isset($b['componentState']) && $b['componentState'] !== '' ? (string) $b['componentState'] : null;
$r = status_create_incident($pdo, (string) ($b['title'] ?? ''), (string) ($b['body'] ?? ''), (string) ($b['status'] ?? 'investigating'), $cid, $cs, (int) $staff['id']);
if (!$r['ok']) api_error(422, 'INVALID_INCIDENT', $r['error']);
audit_log($staff['id'], 'Statusseite: Meldung veröffentlicht (App)');
} elseif ($action === 'update_incident') {
$r = status_update_incident($pdo, (int) ($b['id'] ?? 0), (string) ($b['status'] ?? ''), (string) ($b['body'] ?? ''), (int) $staff['id'], true);
if (!$r['ok']) api_error(422, 'INVALID_INCIDENT', $r['error']);
} elseif ($action === 'delete_incident') {
status_delete_incident($pdo, (int) ($b['id'] ?? 0));
} elseif ($action === 'set_enabled') {
set_setting('status_page_enabled', !empty($b['enabled']) ? '1' : '0');
} else {
api_error(422, 'UNKNOWN_ACTION', 'Unbekannte Aktion.');
}
}
api_json(['status' => status_summary($pdo)] + ['canManage' => $staff['role'] === 'admin']);
+9
View File
@@ -126,4 +126,13 @@ try {
} catch (Throwable $e) {
$summary['similar'] = ['articles' => [], 'tickets' => []];
}
// Cc-Empfänger, Beobachter und Bewertung des Kunden
try {
$summary['collaborators'] = api_ticket_collaborators($pdo, (int) $ticket['id']);
$summary['followers'] = array_map(fn($f) => ['id' => (int) $f['id'], 'name' => (string) $f['name']], ticket_followers($pdo, (int) $ticket['id']));
$__csat = csat_for_ticket($pdo, (int) $ticket['id']);
$summary['csat'] = $__csat ? ['rating' => $__csat['rating'] !== null ? (int) $__csat['rating'] : null, 'comment' => (string) ($__csat['comment'] ?? ''), 'sentAt' => $__csat['sent_at'], 'answeredAt' => $__csat['answered_at']] : null;
} catch (Throwable $e) {
$summary += ['collaborators' => [], 'followers' => [], 'csat' => null];
}
api_json(['ticket' => $summary]);
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/common.php';
/**
* Cc-Empfänger (Kollaboratoren) eines Tickets: sie bekommen öffentliche Antworten per Mail in Kopie.
* GET ?number=… Liste
* POST {number, action:"add", email, name?}
* POST {number, action:"remove", id}
* Beobachter (Mitarbeiter) werden über ticket_update.php ("following") gesetzt und kommen mit ticket.php.
*/
api_require_method('GET', 'POST');
$staff = api_require_staff();
$pdo = get_db();
$isPost = $_SERVER['REQUEST_METHOD'] === 'POST';
$body = $isPost ? api_request_body() : [];
$number = trim((string) ($isPost ? ($body['number'] ?? '') : ($_GET['number'] ?? '')));
if ($number === '') api_error(422, 'TICKET_NUMBER_REQUIRED', 'Eine Ticketnummer ist erforderlich.');
$ticket = api_find_ticket($pdo, $staff, $number);
if ($isPost) {
$action = (string) ($body['action'] ?? '');
if ($action === 'add') {
$email = trim((string) ($body['email'] ?? ''));
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) api_error(422, 'INVALID_EMAIL', 'Bitte eine gültige E-Mail-Adresse angeben.');
$name = trim(mb_substr((string) ($body['name'] ?? ''), 0, 120));
$dup = $pdo->prepare('SELECT COUNT(*) FROM ticket_collaborators WHERE ticket_id = ? AND LOWER(email) = LOWER(?)');
$dup->execute([$ticket['id'], $email]);
if ((int) $dup->fetchColumn() === 0) {
$pdo->prepare('INSERT INTO ticket_collaborators (ticket_id, email, name, added_by, created_at) VALUES (?,?,?,?,?)')
->execute([$ticket['id'], $email, $name !== '' ? $name : null, $staff['id'], db_now()]);
log_activity($pdo, (int) $ticket['id'], (int) $staff['id'], "Kollaborator hinzugefügt: {$email}");
}
} elseif ($action === 'remove') {
$s = $pdo->prepare('SELECT email FROM ticket_collaborators WHERE id = ? AND ticket_id = ?');
$s->execute([(int) ($body['id'] ?? 0), $ticket['id']]);
$email = $s->fetchColumn();
if ($email !== false) {
$pdo->prepare('DELETE FROM ticket_collaborators WHERE id = ? AND ticket_id = ?')->execute([(int) $body['id'], $ticket['id']]);
log_activity($pdo, (int) $ticket['id'], (int) $staff['id'], "Kollaborator entfernt: {$email}");
}
} else {
api_error(422, 'UNKNOWN_ACTION', 'Unbekannte Aktion.');
}
}
api_json(['collaborators' => api_ticket_collaborators($pdo, (int) $ticket['id'])]);
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/common.php';
/**
* Ticket-Vorlagen zum Vorbelegen des Formulars „Neues Ticket“ in den Apps.
* GET → {templates:[{id,name,description,subject,body,departmentId,helpTopicId,priorityId,isPublic}]}
* Gepflegt werden die Vorlagen im Webbereich (Adminportal → Ticket-Vorlagen).
*/
api_require_method('GET');
api_require_staff();
$pdo = get_db();
$list = [];
try {
$list = array_map('ticket_template_payload', ticket_templates_list($pdo));
} catch (Throwable $e) {
// Datenbank noch nicht aktualisiert: keine Vorlagen
}
api_json(['templates' => $list]);