Files
deskhub/DeskHub-Webseite/cron/check_escalations.php
T
2026-10-26 21:19:30 +00:00

235 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// Direkter Web-Zugriff (Browser/URL) wird geblockt. Ausnahmen: echter CLI-Cronjob
// sowie Einbindung per require_once aus dem authentifizierten Backend (staff/settings.php),
// das vor dem require_once die Konstante DESKHUB_CRON_RUNNER definiert.
if (php_sapi_name() !== 'cli' && !defined('DESKHUB_CRON_RUNNER')) { http_response_code(403); exit('Kein Web-Zugriff.'); }
/**
* cron/check_escalations.php
*
* Prüft alle aktiven Eskalationsregeln und führt definierte Aktionen aus.
*
* Trigger-Typen:
* no_reply – Ticket seit X Stunden ohne Mitarbeiter-Antwort
* no_update – Ticket seit X Stunden nicht aktualisiert
* overdue – Ticket ist überfällig (resolution_due_at überschritten)
* open_too_long – Ticket seit X Stunden offen
*
* Aktionen:
* - Priorität erhöhen
* - Mitarbeiter neu zuweisen
* - Status ändern
* - Admin benachrichtigen
* - Zugewiesenen Mitarbeiter benachrichtigen
*
* Crontab: 0 * * * * php /pfad/zu/deskhub/cron/check_escalations.php
*
* Kann außerdem manuell aus dem Backend (Einstellungen → Cron-Jobs) ausgeführt werden.
*/
require_once __DIR__ . '/../includes/bootstrap.php';
/**
* Führt die Eskalationsprüfung aus. Kann vom Cron und manuell aus settings.php aufgerufen werden.
*
* @return array{applied: int, skipped: int, log: string[]}
*/
function run_check_escalations(PDO $pdo, bool $verbose = false): array
{
$now = db_now();
$applied = 0;
$skipped = 0;
$log = [];
// Alle aktiven Eskalationsregeln laden
try {
$rules = $pdo->query(
'SELECT er.*, p.name AS prio_name, s.name AS staff_name
FROM escalation_rules er
LEFT JOIN priorities p ON p.id = er.action_set_priority_id
LEFT JOIN staff s ON s.id = er.action_assign_staff_id
WHERE er.is_active = 1
ORDER BY er.sort_order, er.id'
)->fetchAll();
} catch (Throwable $e) {
$log[] = "[" . date('Y-m-d H:i:s') . "] Tabelle escalation_rules nicht gefunden – Migration ausstehend.";
return ['applied' => 0, 'skipped' => 0, 'log' => $log];
}
if (!$rules) {
$log[] = "[" . date('Y-m-d H:i:s') . "] Keine aktiven Eskalationsregeln.";
return ['applied' => 0, 'skipped' => 0, 'log' => $log];
}
// Bereits eskalierte Ticket-Regel-Paare laden (verhindert Doppel-Eskalation)
$alreadyDone = [];
$logRows = $pdo->query('SELECT rule_id, ticket_id FROM escalation_log')->fetchAll();
foreach ($logRows as $lr) {
$alreadyDone[$lr['rule_id'] . '_' . $lr['ticket_id']] = true;
}
foreach ($rules as $rule) {
// ── Betroffene Tickets finden ─────────────────────────────────────────────
$where = ["t.status NOT IN ('resolved','closed')"];
$params = [];
$hours = (int)$rule['trigger_hours'];
// Bedingungen aus der Regel
if ($rule['condition_status']) {
$statusList = array_map('trim', explode(',', $rule['condition_status']));
$ph = implode(',', array_fill(0, count($statusList), '?'));
$where[] = "t.status IN ($ph)";
$params = array_merge($params, $statusList);
}
if ($rule['condition_priority_id']) {
$where[] = 't.priority_id = ?';
$params[] = (int)$rule['condition_priority_id'];
}
if ($rule['condition_dept_id']) {
$where[] = 't.department_id = ?';
$params[] = (int)$rule['condition_dept_id'];
}
// Trigger-spezifische Bedingung
$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
$hoursDiff = $driver === 'sqlite'
? "((julianday('now', 'localtime') - julianday(%s)) * 24)"
: "TIMESTAMPDIFF(HOUR, %s, NOW())";
switch ($rule['trigger_type']) {
case 'no_reply':
// Kein Mitarbeiter-Thread seit X Stunden
$where[] = sprintf(
"NOT EXISTS (
SELECT 1 FROM ticket_threads tt
WHERE tt.ticket_id = t.id AND tt.author_type = 'staff'
AND " . str_replace('%s', 'tt.created_at', $hoursDiff) . " < ?
)",
''
);
// Simpler: letzter Staff-Thread ist älter als X Stunden ODER kein Staff-Thread vorhanden
// Rebuild properly:
$where[count($where)-1] = $driver === 'sqlite'
? "((julianday('now', 'localtime') - julianday(t.created_at)) * 24) >= ?
AND NOT EXISTS (
SELECT 1 FROM ticket_threads tt
WHERE tt.ticket_id = t.id AND tt.author_type = 'staff'
AND ((julianday('now', 'localtime') - julianday(tt.created_at)) * 24) < ?
)"
: "TIMESTAMPDIFF(HOUR, t.created_at, NOW()) >= ?
AND NOT EXISTS (
SELECT 1 FROM ticket_threads tt
WHERE tt.ticket_id = t.id AND tt.author_type = 'staff'
AND TIMESTAMPDIFF(HOUR, tt.created_at, NOW()) < ?
)";
$params[] = $hours;
$params[] = $hours;
break;
case 'no_update':
$where[] = $driver === 'sqlite'
? "((julianday('now', 'localtime') - julianday(t.updated_at)) * 24) >= ?"
: "TIMESTAMPDIFF(HOUR, t.updated_at, NOW()) >= ?";
$params[] = $hours;
break;
case 'overdue':
$where[] = 't.is_overdue = 1';
$where[] = 't.resolution_due_at IS NOT NULL';
break;
case 'open_too_long':
$where[] = $driver === 'sqlite'
? "((julianday('now', 'localtime') - julianday(t.created_at)) * 24) >= ?"
: "TIMESTAMPDIFF(HOUR, t.created_at, NOW()) >= ?";
$params[] = $hours;
break;
}
$sql = 'SELECT t.*, d.name AS department_name, d.email AS department_email,
p.name AS priority_name, s.name AS staff_name, s.email AS staff_email
FROM tickets t
JOIN departments d ON d.id = t.department_id
JOIN priorities p ON p.id = t.priority_id
LEFT JOIN staff s ON s.id = t.assigned_staff_id
WHERE ' . implode(' AND ', $where);
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$tickets = $stmt->fetchAll();
foreach ($tickets as $ticket) {
$key = $rule['id'] . '_' . $ticket['id'];
if (isset($alreadyDone[$key])) {
$skipped++;
continue;
}
// ── Aktionen ausführen ────────────────────────────────────────────────
$changes = [];
if ($rule['action_set_priority_id']) {
$pdo->prepare('UPDATE tickets SET priority_id = ?, updated_at = ? WHERE id = ?')
->execute([$rule['action_set_priority_id'], $now, $ticket['id']]);
$changes[] = 'Priorität gesetzt auf: ' . ($rule['prio_name'] ?? '?');
}
if ($rule['action_assign_staff_id']) {
$pdo->prepare('UPDATE tickets SET assigned_staff_id = ?, updated_at = ? WHERE id = ?')
->execute([$rule['action_assign_staff_id'], $now, $ticket['id']]);
$changes[] = 'Zugewiesen an: ' . ($rule['staff_name'] ?? '?');
}
if ($rule['action_set_status']) {
$pdo->prepare('UPDATE tickets SET status = ?, updated_at = ? WHERE id = ?')
->execute([$rule['action_set_status'], $now, $ticket['id']]);
ticket_sla_status_transition($pdo, (int) $ticket['id'], (string) $ticket['status'], (string) $rule['action_set_status']);
$changes[] = 'Status gesetzt auf: ' . $rule['action_set_status'];
}
// System-Notiz im Ticket
$noteText = "Eskalationsregel „{$rule['name']}“ ausgelöst: " . implode(', ', $changes ?: ['Benachrichtigung']);
$pdo->prepare(
'INSERT INTO ticket_threads (ticket_id, author_type, message, is_internal_note, created_at) VALUES (?,?,?,1,?)'
)->execute([$ticket['id'], 'system', $noteText, $now]);
log_activity($pdo, $ticket['id'], null, $noteText);
// Admin benachrichtigen
if ($rule['action_notify_admin']) {
notify_admins_overdue_ticket(array_merge($ticket, [
'subject' => "[Eskalation: {$rule['name']}] " . $ticket['subject'],
]));
}
// Zugewiesenen Mitarbeiter benachrichtigen
if ($rule['action_notify_staff'] && !empty($ticket['staff_email'])) {
$subj = "Eskalation: Ticket #{$ticket['ticket_number']} – {$ticket['subject']}";
$inner = email_p('Die Eskalationsregel <strong>' . e($rule['name']) . '</strong> wurde für folgendes Ticket ausgelöst:')
. email_badge('Ticket', '#' . $ticket['ticket_number'], '#b91c1c', '#fee2e2')
. email_p('<strong>Aktionen:</strong> ' . e(implode(', ', $changes ?: ['Benachrichtigung'])), '24px')
. email_button('Ticket öffnen', e(base_url() . '/staff/ticket_view.php?n=' . urlencode($ticket['ticket_number'])), '#dc2626');
email_send_and_log($ticket['staff_email'], $subj, email_wrap($inner), null, 'Eskalations-Mail', $ticket['ticket_number']);
}
// Als erledigt markieren
$pdo->prepare(
'INSERT INTO escalation_log (rule_id, ticket_id, applied_at) VALUES (?,?,?)'
)->execute([$rule['id'], $ticket['id'], $now]);
$alreadyDone[$key] = true;
$applied++;
}
}
set_setting('cron_check_escalations_last_run', $now);
$log[] = "[" . date('Y-m-d H:i:s') . "] Eskalations-Check abgeschlossen. $applied Regel(n) angewendet, $skipped bereits erledigt.";
return ['applied' => $applied, 'skipped' => $skipped, 'log' => $log];
}
if (php_sapi_name() === 'cli') {
$result = run_check_escalations(get_db(), true);
foreach ($result['log'] as $line) {
echo $line . "\n";
}
}