Upload via GUI (340 Dateien)
This commit is contained in:
@@ -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') {
|
||||||
|
|||||||
@@ -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)');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user