Upload via GUI (360 Dateien)

This commit is contained in:
2026-10-30 21:49:53 +00:00
parent 2837074176
commit 07295c8b77
4 changed files with 111 additions and 10 deletions
+6 -1
View File
@@ -147,7 +147,7 @@ function run_check_escalations(PDO $pdo, bool $verbose = false): array
}
$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
p.name AS priority_name, s.name AS staff_name, s.email AS staff_email, s.is_on_vacation AS staff_on_vacation
FROM tickets t
JOIN departments d ON d.id = t.department_id
JOIN priorities p ON p.id = t.priority_id
@@ -202,6 +202,11 @@ function run_check_escalations(PDO $pdo, bool $verbose = false): array
}
// Zugewiesenen Mitarbeiter benachrichtigen
if ($rule['action_notify_staff'] && !empty($ticket['staff_email'])) {
// Bearbeiter im Urlaub: die Vertretung bekommt die Mail
$escTo = vacation_expand_ids($pdo, [(int) $ticket['assigned_staff_id']]);
$ticket['staff_email'] = $escTo ? (staff_emails_by_ids($pdo, [$escTo[0]])[0] ?? '') : '';
}
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:')
+2 -9
View File
@@ -48,15 +48,8 @@ function run_check_sla(PDO $pdo, bool $verbose = false): array
ticket_sla_log_overdue($pdo, (int) $t['id'], 'resolution', (string) $t['resolution_due_at']);
log_activity($pdo, $t['id'], null, 'SLA überschritten – Ticket als überfällig markiert');
if ($t['department_email']) {
$subject = "SLA überschritten: Ticket #{$t['ticket_number']}";
$inner = email_p('Das folgende Ticket hat die SLA-Lösungsfrist überschritten und benötigt Aufmerksamkeit:')
. email_badge('Ticket', '#' . $t['ticket_number'], '#b91c1c', '#fee2e2')
. email_p('<strong>Betreff:</strong> ' . e($t['subject']), '24px')
. email_button('Ticket ansehen', e(base_url() . '/staff/ticket_view.php?n=' . urlencode($t['ticket_number'])), '#dc2626');
email_send_and_log($t['department_email'], $subject, email_wrap($inner), null, 'SLA-überschritten-Mail (Abteilung)', $t['ticket_number']);
}
notify_admins_overdue_ticket($t);
// Nur die Betroffenen bekommen eine Mail (Bearbeiter bzw. Abteilung), nicht die Postfach-Adresse der Abteilung und nicht alle Admins
notify_overdue_ticket_staff($t);
fcm_notify_overdue($t); // Push auf die Android-App
$newlyOverdue++;
if ($verbose) {
+8
View File
@@ -175,6 +175,14 @@ function fetch_emails_for_account(PDO $pdo, array $account, bool $verbose): arra
// Bei "delete" bleibt die Zuordnung zwar erhalten, die Originalmail ist danach jedoch nicht mehr verschiebbar.
$sourceFolderAfterFetch = $afterFetch === 'archive' ? $archiveFolder : $account['folder'];
// Mails, die DeskHub selbst verschickt hat (zum Beispiel SLA-Meldungen an eine Postfach-Adresse), dürfen keine Tickets erzeugen
if (mail_is_own_system_mail($pdo, (string) $fromEmail, (string) @imap_fetchheader($mbox, $emailNum))) {
$log[] = "[$label] Mail von {$fromEmail} ignoriert (eigene Systemmail, würde sonst ein Ticket erzeugen): {$subject}";
handle_after_fetch($mbox, $emailNum, $afterFetch, $archiveFolder);
$processed++;
continue;
}
// Nachrichtentext: Text-Teil bevorzugt, bei reinen HTML-Mails (Newsletter) nur der sichtbare Text ohne CSS und Skripte
$structure = @imap_fetchstructure($mbox, $emailNum);
$body = $structure ? mail_best_text($structure, fn(string $no) => (string) imap_fetchbody($mbox, $emailNum, $no)) : '';
+95
View File
@@ -1850,6 +1850,101 @@ function run_migrations(PDO $pdo, bool $verbose = false): array
}
}
// Anmelde-Wächter: Protokoll der Anmeldungen sowie gemerkte Geräte und Orte je Mitarbeiter
if (!isset($done['php_0065_login_guard'])) {
try {
$drv = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
$ai = $drv === 'sqlite' ? 'INTEGER PRIMARY KEY AUTOINCREMENT' : 'INT AUTO_INCREMENT PRIMARY KEY';
$eng = $drv === 'sqlite' ? '' : ' ENGINE=InnoDB DEFAULT CHARSET=utf8mb4';
$pdo->exec("CREATE TABLE IF NOT EXISTS login_events (
id $ai,
staff_id INT NULL,
email VARCHAR(255) NOT NULL DEFAULT '',
event VARCHAR(20) NOT NULL,
channel VARCHAR(10) NOT NULL DEFAULT 'web',
ip VARCHAR(64) NOT NULL DEFAULT '',
place VARCHAR(100) NOT NULL DEFAULT '',
device VARCHAR(160) NOT NULL DEFAULT '',
detail VARCHAR(255) NOT NULL DEFAULT '',
token_hash VARCHAR(64) NULL,
token_used TINYINT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL
)$eng");
$pdo->exec('CREATE INDEX idx_login_events_created ON login_events (created_at)');
$pdo->exec('CREATE INDEX idx_login_events_staff ON login_events (staff_id, created_at)');
$pdo->exec('CREATE INDEX idx_login_events_token ON login_events (token_hash)');
$pdo->exec("CREATE TABLE IF NOT EXISTS login_devices (
id $ai,
staff_id INT NOT NULL,
device_hash VARCHAR(64) NOT NULL,
label VARCHAR(160) NOT NULL DEFAULT '',
channel VARCHAR(10) NOT NULL DEFAULT 'web',
first_ip VARCHAR(64) NOT NULL DEFAULT '',
last_ip VARCHAR(64) NOT NULL DEFAULT '',
place VARCHAR(100) NOT NULL DEFAULT '',
first_seen DATETIME NOT NULL,
last_seen DATETIME NOT NULL,
UNIQUE (staff_id, device_hash)
)$eng");
$pdo->exec("CREATE TABLE IF NOT EXISTS login_places (
id $ai,
staff_id INT NOT NULL,
place_key VARCHAR(80) NOT NULL,
label VARCHAR(100) NOT NULL DEFAULT '',
first_seen DATETIME NOT NULL,
last_seen DATETIME NOT NULL,
UNIQUE (staff_id, place_key)
)$eng");
$pdo->prepare('INSERT INTO schema_migrations (migration) VALUES (?)')->execute(['php_0065_login_guard']);
$result['applied']++;
if ($verbose) $result['log'][] = ' [OK] php_0065_login_guard';
} catch (Throwable $e) {
$result['errors'][] = 'php_0065_login_guard: ' . $e->getMessage();
if ($verbose) $result['log'][] = ' [FAIL] php_0065_login_guard - ' . $e->getMessage();
}
}
// Abwesenheit im Profil: Welche Tickets an die Vertretung weitergegeben wurden (für die Rückgabe nach dem Urlaub)
if (!isset($done['php_0066_vacation_moves'])) {
try {
$drv = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
$ai = $drv === 'sqlite' ? 'INTEGER PRIMARY KEY AUTOINCREMENT' : 'INT AUTO_INCREMENT PRIMARY KEY';
$eng = $drv === 'sqlite' ? '' : ' ENGINE=InnoDB DEFAULT CHARSET=utf8mb4';
$pdo->exec("CREATE TABLE IF NOT EXISTS vacation_moves (
id $ai,
ticket_id INT NOT NULL,
from_staff_id INT NOT NULL,
to_staff_id INT NOT NULL,
created_at DATETIME NOT NULL,
returned_at DATETIME NULL
)$eng");
$pdo->exec('CREATE INDEX idx_vacation_moves_from ON vacation_moves (from_staff_id, returned_at)');
$pdo->prepare('INSERT INTO schema_migrations (migration) VALUES (?)')->execute(['php_0066_vacation_moves']);
$result['applied']++;
if ($verbose) $result['log'][] = ' [OK] php_0066_vacation_moves';
} catch (Throwable $e) {
$result['errors'][] = 'php_0066_vacation_moves: ' . $e->getMessage();
if ($verbose) $result['log'][] = ' [FAIL] php_0066_vacation_moves - ' . $e->getMessage();
}
}
// Reparatur: Entwarnungs-Notizen der öffentlichen API waren doppelt maskiert (&quot; statt ")
if (!isset($done['php_0067_fix_api_resolve_notes'])) {
try {
$rows = $pdo->query("SELECT t.id, t.message FROM ticket_threads t JOIN tickets k ON k.id = t.ticket_id
WHERE k.source = 'api' AND t.author_type = 'system' AND t.is_internal_note = 1
AND (t.message LIKE '%&quot;%' OR t.message LIKE '%&amp;%' OR t.message LIKE '%&#039;%')")->fetchAll();
$upd = $pdo->prepare('UPDATE ticket_threads SET message = ? WHERE id = ?');
foreach ($rows as $r) $upd->execute([html_entity_decode((string) $r['message'], ENT_QUOTES | ENT_HTML5, 'UTF-8'), $r['id']]);
$pdo->prepare('INSERT INTO schema_migrations (migration) VALUES (?)')->execute(['php_0067_fix_api_resolve_notes']);
$result['applied']++;
if ($verbose) $result['log'][] = ' [OK] php_0067_fix_api_resolve_notes (' . count($rows) . ' Notizen repariert)';
} catch (Throwable $e) {
$result['errors'][] = 'php_0067_fix_api_resolve_notes: ' . $e->getMessage();
if ($verbose) $result['log'][] = ' [FAIL] php_0067_fix_api_resolve_notes - ' . $e->getMessage();
}
}
return $result;
}