From 95434a42fe05b41cfa598dec3b34308ea9d3b587 Mon Sep 17 00:00:00 2001 From: Jukka Lampikoski Date: Fri, 13 Mar 2026 19:24:57 +0200 Subject: [PATCH] Add timer/scheduler to ticket automation rules Adds optional delay_days + delay_condition (no_activity) to ticket rules. When a ticket has no activity for X days, timed rules automatically apply actions (e.g., escalate priority to urgent). Checked on each ticket list load. Co-Authored-By: Claude Opus 4.6 --- api.php | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ db.php | 13 +++++++++--- index.html | 14 +++++++++++++ script.js | 8 ++++++++ 4 files changed, 90 insertions(+), 3 deletions(-) diff --git a/api.php b/api.php index e1f747c..1937dc6 100644 --- a/api.php +++ b/api.php @@ -3242,6 +3242,62 @@ switch ($action) { } unset($tc); + // Ajastinsääntöjen tarkistus (delay_days + no_activity) + $timedRules = array_filter(dbLoadTicketRules($comp['id']), function($r) { + return $r['enabled'] && $r['delay_days'] > 0 && $r['delay_condition'] === 'no_activity'; + }); + if (!empty($timedRules)) { + foreach ($tickets as &$tc) { + if (in_array($tc['status'], ['suljettu', 'ratkaistu'])) continue; + $lastActivity = $tc['updated'] ?? $tc['created']; + $inactiveDays = (strtotime($now) - strtotime($lastActivity)) / 86400; + foreach ($timedRules as $rule) { + if ($inactiveDays < $rule['delay_days']) continue; + // Tarkista ehdot (from/to/subject) + $match = true; + if (!empty($rule['from_contains'])) { + if (stripos(($tc['from_email'] ?? '') . ' ' . ($tc['from_name'] ?? ''), $rule['from_contains']) === false) $match = false; + } + if (!empty($rule['subject_contains'])) { + if (stripos($tc['subject'] ?? '', $rule['subject_contains']) === false) $match = false; + } + if (!empty($rule['to_contains'])) { + if (stripos($tc['to_email'] ?? '', $rule['to_contains']) === false) $match = false; + } + if (!$match) continue; + // Sovella toimenpiteet + $changed = false; + if (!empty($rule['set_priority']) && ($tc['priority'] ?? '') !== $rule['set_priority']) { + $tc['priority'] = $rule['set_priority']; + $changed = true; + } + if (!empty($rule['status_set']) && ($tc['status'] ?? '') !== $rule['status_set']) { + $tc['status'] = $rule['status_set']; + $changed = true; + } + if (!empty($rule['type_set']) && ($tc['type'] ?? '') !== $rule['type_set']) { + $tc['type'] = $rule['type_set']; + $changed = true; + } + if (!empty($rule['set_tags'])) { + $ruleTags = array_map('trim', explode(',', $rule['set_tags'])); + $existingTags = $tc['tags'] ?? []; + $merged = array_values(array_unique(array_merge($existingTags, $ruleTags))); + if ($merged !== $existingTags) { + $tc['tags'] = $merged; + $changed = true; + } + } + if ($changed) { + $tc['updated'] = $now; + dbSaveTicket($comp['id'], $tc); + } + break; // Vain yksi ajastinsääntö per tiketti + } + } + unset($tc); + } + // Resolve mailbox names for this company $mailboxes = dbLoadMailboxes($comp['id']); $mailboxNames = []; @@ -3967,6 +4023,8 @@ switch ($action) { 'set_priority' => $input['set_priority'] ?? '', 'set_tags' => trim($input['set_tags'] ?? ''), 'auto_close_days' => intval($input['auto_close_days'] ?? 0), + 'delay_days' => intval($input['delay_days'] ?? 0), + 'delay_condition' => trim($input['delay_condition'] ?? ''), 'enabled' => $input['enabled'] ?? true, ]; diff --git a/db.php b/db.php index 11436b7..88d6b0b 100644 --- a/db.php +++ b/db.php @@ -680,6 +680,8 @@ function initDatabase(): void { "ALTER TABLE tickets ADD COLUMN to_email VARCHAR(255) DEFAULT '' AFTER from_name", "ALTER TABLE tickets ADD COLUMN bcc TEXT DEFAULT '' AFTER cc", "ALTER TABLE ticket_messages ADD COLUMN attachments TEXT DEFAULT '' AFTER zammad_article_id", + "ALTER TABLE ticket_rules ADD COLUMN delay_days INT DEFAULT 0 AFTER auto_close_days", + "ALTER TABLE ticket_rules ADD COLUMN delay_condition VARCHAR(50) DEFAULT '' AFTER delay_days", ]; foreach ($alters as $sql) { try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ } @@ -1657,6 +1659,8 @@ function dbLoadTicketRules(string $companyId): array { foreach ($rules as &$r) { $r['priority'] = (int)$r['priority']; $r['auto_close_days'] = (int)$r['auto_close_days']; + $r['delay_days'] = (int)($r['delay_days'] ?? 0); + $r['delay_condition'] = $r['delay_condition'] ?? ''; $r['enabled'] = !empty($r['enabled']); unset($r['company_id']); } @@ -1665,14 +1669,15 @@ function dbLoadTicketRules(string $companyId): array { function dbSaveTicketRule(string $companyId, array $rule): void { _dbExecute(" - INSERT INTO ticket_rules (id, company_id, name, from_contains, subject_contains, to_contains, priority, tag, assign_to, status_set, type_set, set_priority, set_tags, auto_close_days, enabled) - VALUES (:id, :company_id, :name, :from_contains, :subject_contains, :to_contains, :priority, :tag, :assign_to, :status_set, :type_set, :set_priority, :set_tags, :auto_close_days, :enabled) + INSERT INTO ticket_rules (id, company_id, name, from_contains, subject_contains, to_contains, priority, tag, assign_to, status_set, type_set, set_priority, set_tags, auto_close_days, delay_days, delay_condition, enabled) + VALUES (:id, :company_id, :name, :from_contains, :subject_contains, :to_contains, :priority, :tag, :assign_to, :status_set, :type_set, :set_priority, :set_tags, :auto_close_days, :delay_days, :delay_condition, :enabled) ON DUPLICATE KEY UPDATE name = VALUES(name), from_contains = VALUES(from_contains), subject_contains = VALUES(subject_contains), to_contains = VALUES(to_contains), priority = VALUES(priority), tag = VALUES(tag), assign_to = VALUES(assign_to), status_set = VALUES(status_set), type_set = VALUES(type_set), set_priority = VALUES(set_priority), set_tags = VALUES(set_tags), - auto_close_days = VALUES(auto_close_days), enabled = VALUES(enabled) + auto_close_days = VALUES(auto_close_days), delay_days = VALUES(delay_days), + delay_condition = VALUES(delay_condition), enabled = VALUES(enabled) ", [ 'id' => $rule['id'], 'company_id' => $companyId, @@ -1688,6 +1693,8 @@ function dbSaveTicketRule(string $companyId, array $rule): void { 'set_priority' => $rule['set_priority'] ?? '', 'set_tags' => $rule['set_tags'] ?? '', 'auto_close_days' => $rule['auto_close_days'] ?? 0, + 'delay_days' => $rule['delay_days'] ?? 0, + 'delay_condition' => $rule['delay_condition'] ?? '', 'enabled' => !empty($rule['enabled']) ? 1 : 0, ]); } diff --git a/index.html b/index.html index eea94e2..e1930cb 100644 --- a/index.html +++ b/index.html @@ -1292,6 +1292,20 @@ +
+ +
+
+ + +
+
+ + +
diff --git a/script.js b/script.js index 76c6942..8c94ba8 100644 --- a/script.js +++ b/script.js @@ -2342,6 +2342,10 @@ function renderRules() { if (r.set_priority) actions.push('Prioriteetti → ' + (priorityLabels[r.set_priority] || r.set_priority)); if (r.set_tags) actions.push('Tagit: #' + r.set_tags.split(',').map(t => t.trim()).join(' #')); if (r.auto_close_days) actions.push('Auto-close: ' + r.auto_close_days + 'pv'); + if (r.delay_days && r.delay_condition) { + const condLabel = r.delay_condition === 'no_activity' ? 'ei aktiviteettia' : r.delay_condition; + actions.push('⏱ Ajastin: ' + r.delay_days + 'pv (' + condLabel + ')'); + } return `
${esc(r.name)}
@@ -2373,6 +2377,8 @@ function showRuleForm(rule) { document.getElementById('rule-form-priority').value = rule ? (rule.set_priority || '') : ''; document.getElementById('rule-form-tags').value = rule ? (rule.set_tags || '') : ''; document.getElementById('rule-form-autoclose').value = rule ? (rule.auto_close_days || '') : ''; + document.getElementById('rule-form-delay-days').value = rule ? (rule.delay_days || '') : ''; + document.getElementById('rule-form-delay-condition').value = rule ? (rule.delay_condition || '') : ''; editingRuleId = rule ? rule.id : null; } @@ -2397,6 +2403,8 @@ document.getElementById('btn-save-rule').addEventListener('click', async () => { set_priority: document.getElementById('rule-form-priority').value, set_tags: document.getElementById('rule-form-tags').value.trim(), auto_close_days: parseInt(document.getElementById('rule-form-autoclose').value) || 0, + delay_days: parseInt(document.getElementById('rule-form-delay-days').value) || 0, + delay_condition: document.getElementById('rule-form-delay-condition').value, enabled: true, }; const existingId = document.getElementById('rule-form-id').value;