Upload via GUI (340 Dateien)

This commit is contained in:
2026-10-26 21:19:25 +00:00
parent 12699f0918
commit 2452849c74
7 changed files with 42 additions and 12 deletions
+5 -1
View File
@@ -31,8 +31,12 @@ $action = $body['action'] ?? '';
function api_close_banned_tickets(PDO $pdo, string $matchType, string $value): void function api_close_banned_tickets(PDO $pdo, string $matchType, string $value): void
{ {
$col = $matchType === 'email' ? 'requester_email' : sql_email_domain('requester_email'); $col = $matchType === 'email' ? 'requester_email' : sql_email_domain('requester_email');
$pdo->prepare("UPDATE tickets SET status='closed',closed_at=?,updated_at=? WHERE $col=? AND status NOT IN ('closed')") $before = $pdo->prepare("SELECT id,status FROM tickets WHERE $col=? AND status NOT IN ('closed')");
$before->execute([$value]);
$rows = $before->fetchAll();
$pdo->prepare("UPDATE tickets SET status='closed',resolution_code='spam',resolution_note='Im Rahmen einer Sperre geschlossen',closed_at=?,updated_at=? WHERE $col=? AND status NOT IN ('closed')")
->execute([db_now(), db_now(), $value]); ->execute([db_now(), db_now(), $value]);
ticket_sla_transition_rows($pdo, $rows, 'closed', (int) ($GLOBALS['__api_staff']['id'] ?? 0) ?: null);
} }
if ($action === 'create') { if ($action === 'create') {
+19 -4
View File
@@ -248,6 +248,8 @@ function api_ticket_summary(array $ticket): array
'requesterName' => $ticket['requester_name'], 'requesterName' => $ticket['requester_name'],
'requesterEmail' => $ticket['requester_email'], 'requesterEmail' => $ticket['requester_email'],
'status' => $ticket['status'], 'status' => $ticket['status'],
'resolutionCode' => $ticket['resolution_code'] ?? null,
'resolutionNote' => $ticket['resolution_note'] ?? null,
'priority' => [ 'priority' => [
'id' => (int) $ticket['priority_id'], 'id' => (int) $ticket['priority_id'],
'name' => $ticket['priority_name'], 'name' => $ticket['priority_name'],
@@ -261,14 +263,16 @@ function api_ticket_summary(array $ticket): array
'id' => (int) $ticket['assigned_staff_id'], 'id' => (int) $ticket['assigned_staff_id'],
'name' => $ticket['staff_name'], 'name' => $ticket['staff_name'],
], ],
'isOverdue' => !empty($ticket['resolution_due_at']) 'slaPaused' => !empty($ticket['sla_paused_at']) && ticket_sla_is_paused_status((string) $ticket['status']),
'slaPausedAt' => $ticket['sla_paused_at'] ?? null,
'isOverdue' => !in_array($ticket['status'], ['resolved', 'closed'], true) && (!empty($ticket['is_overdue']) || (empty($ticket['sla_paused_at']) && !empty($ticket['resolution_due_at'])
&& strtotime($ticket['resolution_due_at']) < time() && strtotime($ticket['resolution_due_at']) < time()
&& !in_array($ticket['status'], ['resolved', 'closed'], true), )),
// Fertig formatierter Fälligkeitstext ("in 3 Std 12 Min" / "vor 2 Std 5 Min // Fertig formatierter Fälligkeitstext ("in 3 Std 12 Min" / "vor 2 Std 5 Min
// (überfällig)") – bewusst serverseitig über dieselbe time_until()-Funktion wie im // (überfällig)") – bewusst serverseitig über dieselbe time_until()-Funktion wie im
// Web berechnet, statt das Datumsformat in Kotlin nachzubauen (siehe die Lehren aus // Web berechnet, statt das Datumsformat in Kotlin nachzubauen (siehe die Lehren aus
// den PHP-Datums-Bugs dieser Session: eine einzige Quelle der Wahrheit fürs Format). // den PHP-Datums-Bugs dieser Session: eine einzige Quelle der Wahrheit fürs Format).
'dueLabel' => !empty($ticket['resolution_due_at']) ? time_until($ticket['resolution_due_at']) : null, 'dueLabel' => !empty($ticket['sla_paused_at']) && ticket_sla_is_paused_status((string) $ticket['status']) ? 'Pausiert' : (!empty($ticket['resolution_due_at']) ? time_until($ticket['resolution_due_at']) : null),
'isSpam' => !empty($ticket['is_spam']), 'isSpam' => !empty($ticket['is_spam']),
'createdAt' => $ticket['created_at'], 'createdAt' => $ticket['created_at'],
'updatedAt' => $ticket['updated_at'], 'updatedAt' => $ticket['updated_at'],
@@ -304,7 +308,7 @@ function api_ticket_collaborators(PDO $pdo, int $ticketId): array
return array_map(fn($r) => ['id' => (int) $r['id'], 'email' => (string) $r['email'], 'name' => (string) ($r['name'] ?? '')], $s->fetchAll()); 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 function api_add_ticket_message(PDO $pdo, array $staff, array $ticket, string $type, string $rawMessage, ?string $setStatus, ?string $signatureChoice = null, ?string $resolutionCode = null, string $resolutionNote = ''): int
{ {
$permission = $type === 'note' ? 'post_note' : 'reply_ticket'; $permission = $type === 'note' ? 'post_note' : 'reply_ticket';
api_require_permission($staff, $permission, (int) $ticket['department_id']); api_require_permission($staff, $permission, (int) $ticket['department_id']);
@@ -352,6 +356,9 @@ function api_add_ticket_message(PDO $pdo, array $staff, array $ticket, string $t
} }
if (in_array($newStatus, ['resolved', 'closed'], true)) { if (in_array($newStatus, ['resolved', 'closed'], true)) {
api_require_permission($staff, 'close_ticket', (int) $ticket['department_id']); api_require_permission($staff, 'close_ticket', (int) $ticket['department_id']);
if (!isset(ticket_resolution_reasons()[$resolutionCode ?? ''])) {
api_error(422, 'RESOLUTION_CODE_REQUIRED', 'Bitte wählen Sie einen Abschlussgrund.');
}
} }
} }
$now = db_now(); $now = db_now();
@@ -370,15 +377,23 @@ function api_add_ticket_message(PDO $pdo, array $staff, array $ticket, string $t
} else { } else {
$firstResponse = empty($ticket['first_responded_at']); $firstResponse = empty($ticket['first_responded_at']);
$autoAssign = empty($ticket['assigned_staff_id']); $autoAssign = empty($ticket['assigned_staff_id']);
$terminal = in_array($newStatus, ['resolved', 'closed'], true);
$sql = 'UPDATE tickets SET status = ?, updated_at = ?' $sql = 'UPDATE tickets SET status = ?, updated_at = ?'
. ($terminal ? ', resolution_code = ?, resolution_note = ?' : '')
. ($newStatus === 'resolved' && empty($ticket['resolved_at']) ? ', resolved_at = ?' : '')
. ($newStatus === 'closed' && empty($ticket['closed_at']) ? ', closed_at = ?' : '')
. ($firstResponse ? ', first_responded_at = ?' : '') . ($firstResponse ? ', first_responded_at = ?' : '')
. ($autoAssign ? ', assigned_staff_id = ?' : '') . ($autoAssign ? ', assigned_staff_id = ?' : '')
. ' WHERE id = ?'; . ' WHERE id = ?';
$args = [$newStatus, $now]; $args = [$newStatus, $now];
if ($terminal) { $args[] = $resolutionCode; $args[] = trim($resolutionNote); }
if ($newStatus === 'resolved' && empty($ticket['resolved_at'])) $args[] = $now;
if ($newStatus === 'closed' && empty($ticket['closed_at'])) $args[] = $now;
if ($firstResponse) $args[] = $now; if ($firstResponse) $args[] = $now;
if ($autoAssign) $args[] = $staff['id']; if ($autoAssign) $args[] = $staff['id'];
$args[] = $ticket['id']; $args[] = $ticket['id'];
$pdo->prepare($sql)->execute($args); $pdo->prepare($sql)->execute($args);
ticket_sla_status_transition($pdo, (int) $ticket['id'], (string) $ticket['status'], $newStatus, (int) $staff['id']);
save_mentions($pdo, (int) $ticket['id'], $threadId, (int) $staff['id'], $message, $ticket); save_mentions($pdo, (int) $ticket['id'], $threadId, (int) $staff['id'], $message, $ticket);
log_activity($pdo, (int) $ticket['id'], (int) $staff['id'], 'Öffentliche Antwort gesendet (Mobile App)'); log_activity($pdo, (int) $ticket['id'], (int) $staff['id'], 'Öffentliche Antwort gesendet (Mobile App)');
} }
+2 -2
View File
@@ -18,7 +18,7 @@ $myOpen = (int) $stmt->fetchColumn();
$stmt = $pdo->prepare( $stmt = $pdo->prepare(
"SELECT COUNT(*) FROM tickets "SELECT COUNT(*) FROM tickets
WHERE resolution_due_at IS NOT NULL AND resolution_due_at < ? WHERE status NOT IN ('resolved','closed') AND (is_overdue = 1 OR (sla_paused_at IS NULL AND resolution_due_at IS NOT NULL AND resolution_due_at < ?))
AND status NOT IN ('resolved','closed'){$deptFilter}" AND status NOT IN ('resolved','closed'){$deptFilter}"
); );
$stmt->execute([db_now(), ...$deptParams]); $stmt->execute([db_now(), ...$deptParams]);
@@ -86,7 +86,7 @@ if ($departmentIds === null) {
"SELECT d.id, d.name, "SELECT d.id, d.name,
COUNT(CASE WHEN t.status NOT IN ('resolved','closed') THEN 1 END) AS open_count, COUNT(CASE WHEN t.status NOT IN ('resolved','closed') THEN 1 END) AS open_count,
COUNT(CASE WHEN t.status NOT IN ('resolved','closed') COUNT(CASE WHEN t.status NOT IN ('resolved','closed')
AND t.resolution_due_at IS NOT NULL AND t.resolution_due_at < ? AND (t.is_overdue = 1 OR (t.sla_paused_at IS NULL AND t.resolution_due_at IS NOT NULL AND t.resolution_due_at < ?))
THEN 1 END) AS overdue_count THEN 1 END) AS overdue_count
FROM departments d FROM departments d
LEFT JOIN tickets t ON t.department_id = d.id LEFT JOIN tickets t ON t.department_id = d.id
+3 -1
View File
@@ -151,6 +151,7 @@ if ($action === 'send') {
if ($autoAssign) $args[] = $staff['id']; if ($autoAssign) $args[] = $staff['id'];
$args[] = $ticket['id']; $args[] = $ticket['id'];
$pdo->prepare($sql)->execute($args); $pdo->prepare($sql)->execute($args);
ticket_sla_status_transition($pdo, (int) $ticket['id'], (string) $ticket['status'], 'answered', (int) $staff['id']);
if ($autoAssign) log_activity($pdo, (int) $ticket['id'], (int) $staff['id'], 'Automatisch zugewiesen (Live-Chat, Mobile App)'); if ($autoAssign) log_activity($pdo, (int) $ticket['id'], (int) $staff['id'], 'Automatisch zugewiesen (Live-Chat, Mobile App)');
save_mentions($pdo, (int) $ticket['id'], $threadId, (int) $staff['id'], $message, $ticket); save_mentions($pdo, (int) $ticket['id'], $threadId, (int) $staff['id'], $message, $ticket);
@@ -167,8 +168,9 @@ if ($action === 'end') {
} }
$now = db_now(); $now = db_now();
$pdo->prepare( $pdo->prepare(
"UPDATE tickets SET status='closed', closed_at=COALESCE(closed_at, ?), updated_at=?, locked_by=NULL, locked_at=NULL WHERE id=?" "UPDATE tickets SET status='closed', resolution_code='other', resolution_note='Live-Chat beendet', closed_at=COALESCE(closed_at, ?), updated_at=?, locked_by=NULL, locked_at=NULL WHERE id=?"
)->execute([$now, $now, $ticket['id']]); )->execute([$now, $now, $ticket['id']]);
ticket_sla_status_transition($pdo, (int) $ticket['id'], (string) $ticket['status'], 'closed', (int) $staff['id']);
log_activity($pdo, (int) $ticket['id'], (int) $staff['id'], 'Live-Chat beendet (Mobile App)'); log_activity($pdo, (int) $ticket['id'], (int) $staff['id'], 'Live-Chat beendet (Mobile App)');
finalize_work_session($pdo, (int) $ticket['id'], (int) $staff['id']); finalize_work_session($pdo, (int) $ticket['id'], (int) $staff['id']);
@@ -16,5 +16,7 @@ if ($number === '') {
} }
$pdo = get_db(); $pdo = get_db();
$ticket = api_find_ticket($pdo, $staff, $number); $ticket = api_find_ticket($pdo, $staff, $number);
$threadId = api_add_ticket_message($pdo, $staff, $ticket, $type, $message, $setStatus, $signatureChoice); $resolutionCode = isset($body['resolutionCode']) ? trim((string) $body['resolutionCode']) : null;
$resolutionNote = trim((string) ($body['resolutionNote'] ?? ''));
$threadId = api_add_ticket_message($pdo, $staff, $ticket, $type, $message, $setStatus, $signatureChoice, $resolutionCode, $resolutionNote);
api_json(['threadId' => $threadId], 201); api_json(['threadId' => $threadId], 201);
@@ -29,6 +29,12 @@ if (array_key_exists('status', $body) && $body['status'] !== $ticket['status'])
api_error(422, 'INVALID_STATUS', 'Der Status ist ungültig.'); api_error(422, 'INVALID_STATUS', 'Der Status ist ungültig.');
} }
api_require_permission($staff, in_array($status, ['resolved', 'closed'], true) ? 'close_ticket' : 'reply_ticket', (int) $ticket['department_id']); api_require_permission($staff, in_array($status, ['resolved', 'closed'], true) ? 'close_ticket' : 'reply_ticket', (int) $ticket['department_id']);
if (in_array($status, ['resolved', 'closed'], true)) {
$resolutionCode = trim((string) ($body['resolutionCode'] ?? ''));
if (!isset(ticket_resolution_reasons()[$resolutionCode])) api_error(422, 'RESOLUTION_CODE_REQUIRED', 'Bitte wählen Sie einen Abschlussgrund.');
$updates[] = 'resolution_code = ?'; $params[] = $resolutionCode;
$updates[] = 'resolution_note = ?'; $params[] = trim((string) ($body['resolutionNote'] ?? ''));
}
$updates[] = 'status = ?'; $updates[] = 'status = ?';
$params[] = $status; $params[] = $status;
if ($status === 'resolved' && empty($ticket['resolved_at'])) { if ($status === 'resolved' && empty($ticket['resolved_at'])) {
@@ -90,6 +96,7 @@ $params[] = $now;
$params[] = $ticket['id']; $params[] = $ticket['id'];
$pdo->prepare('UPDATE tickets SET ' . implode(', ', $updates) . ' WHERE id = ?')->execute($params); $pdo->prepare('UPDATE tickets SET ' . implode(', ', $updates) . ' WHERE id = ?')->execute($params);
if (array_key_exists('status', $body) && $body['status'] !== $ticket['status']) { if (array_key_exists('status', $body) && $body['status'] !== $ticket['status']) {
ticket_sla_status_transition($pdo, (int) $ticket['id'], (string) $ticket['status'], (string) $body['status'], (int) $staff['id']);
[$label] = status_label((string) $body['status']); [$label] = status_label((string) $body['status']);
log_activity($pdo, (int) $ticket['id'], (int) $staff['id'], 'Status geändert: ' . $label . ' (Mobile App)'); log_activity($pdo, (int) $ticket['id'], (int) $staff['id'], 'Status geändert: ' . $label . ' (Mobile App)');
} }
+3 -3
View File
@@ -70,13 +70,13 @@ if ($sla !== '') {
$now = db_now(); $now = db_now();
$riskThreshold = date('Y-m-d H:i:s', time() + 4 * 3600); $riskThreshold = date('Y-m-d H:i:s', time() + 4 * 3600);
if ($sla === 'overdue') { if ($sla === 'overdue') {
$where[] = "t.resolution_due_at IS NOT NULL AND t.resolution_due_at < ? AND t.status NOT IN ('resolved','closed')"; $where[] = "t.resolution_due_at IS NOT NULL AND (t.is_overdue = 1 OR (t.sla_paused_at IS NULL AND t.resolution_due_at < ?)) AND t.status NOT IN ('resolved','closed')";
$params[] = $now; $params[] = $now;
} elseif ($sla === 'at_risk') { } elseif ($sla === 'at_risk') {
$where[] = "t.resolution_due_at IS NOT NULL AND t.resolution_due_at >= ? AND t.resolution_due_at < ? AND t.status NOT IN ('resolved','closed')"; $where[] = "t.sla_paused_at IS NULL AND t.resolution_due_at IS NOT NULL AND t.resolution_due_at >= ? AND t.resolution_due_at < ? AND t.status NOT IN ('resolved','closed')";
array_push($params, $now, $riskThreshold); array_push($params, $now, $riskThreshold);
} elseif ($sla === 'on_time') { } elseif ($sla === 'on_time') {
$where[] = "(t.resolution_due_at IS NULL OR t.resolution_due_at >= ?) AND t.status NOT IN ('resolved','closed')"; $where[] = "t.sla_paused_at IS NULL AND (t.resolution_due_at IS NULL OR t.resolution_due_at >= ?) AND t.status NOT IN ('resolved','closed')";
$params[] = $riskThreshold; $params[] = $riskThreshold;
} }
} }