From 27fe8a89824baf8fd3b276a5b6e4d9db967558d7 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Fri, 16 Oct 2026 18:44:25 +0000 Subject: [PATCH] Upload via GUI (10 Dateien) --- .../migrations/001_log.mysql.sql | 6 + .../migrations/001_log.sqlite.sql | 6 + Plugins/plugin-beispiel/pages/log.php | 13 +++ Plugins/plugin-beispiel/plugin.json | 9 ++ Plugins/plugin-beispiel/plugin.php | 35 ++++++ Plugins/plugins/tagesbericht-1.0.0.zip | Bin 0 -> 4857 bytes Plugins/plugins/tagesbericht/lib.php | 106 ++++++++++++++++++ .../tagesbericht/pages/einstellungen.php | 55 +++++++++ Plugins/plugins/tagesbericht/plugin.json | 9 ++ Plugins/plugins/tagesbericht/plugin.php | 10 ++ 10 files changed, 249 insertions(+) create mode 100644 Plugins/plugin-beispiel/migrations/001_log.mysql.sql create mode 100644 Plugins/plugin-beispiel/migrations/001_log.sqlite.sql create mode 100644 Plugins/plugin-beispiel/pages/log.php create mode 100644 Plugins/plugin-beispiel/plugin.json create mode 100644 Plugins/plugin-beispiel/plugin.php create mode 100644 Plugins/plugins/tagesbericht-1.0.0.zip create mode 100644 Plugins/plugins/tagesbericht/lib.php create mode 100644 Plugins/plugins/tagesbericht/pages/einstellungen.php create mode 100644 Plugins/plugins/tagesbericht/plugin.json create mode 100644 Plugins/plugins/tagesbericht/plugin.php diff --git a/Plugins/plugin-beispiel/migrations/001_log.mysql.sql b/Plugins/plugin-beispiel/migrations/001_log.mysql.sql new file mode 100644 index 00000000..b08f93ef --- /dev/null +++ b/Plugins/plugin-beispiel/migrations/001_log.mysql.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS plugin_beispiel_log ( + id INT AUTO_INCREMENT PRIMARY KEY, + ticket_id INT NOT NULL, + event VARCHAR(40) NOT NULL, + created_at DATETIME NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/Plugins/plugin-beispiel/migrations/001_log.sqlite.sql b/Plugins/plugin-beispiel/migrations/001_log.sqlite.sql new file mode 100644 index 00000000..a3782fe8 --- /dev/null +++ b/Plugins/plugin-beispiel/migrations/001_log.sqlite.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS plugin_beispiel_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ticket_id INT NOT NULL, + event VARCHAR(40) NOT NULL, + created_at DATETIME NOT NULL +); diff --git a/Plugins/plugin-beispiel/pages/log.php b/Plugins/plugin-beispiel/pages/log.php new file mode 100644 index 00000000..040ae514 --- /dev/null +++ b/Plugins/plugin-beispiel/pages/log.php @@ -0,0 +1,13 @@ +query('SELECT ticket_id, event, created_at FROM plugin_beispiel_log ORDER BY id DESC LIMIT 50')->fetchAll(); +?> +

Ticket-Protokoll

+

Letzter Plugin-Cron:

+
+ + + + + +
TicketEreignisZeit
#
Noch keine Ereignisse.
diff --git a/Plugins/plugin-beispiel/plugin.json b/Plugins/plugin-beispiel/plugin.json new file mode 100644 index 00000000..c4b46e0c --- /dev/null +++ b/Plugins/plugin-beispiel/plugin.json @@ -0,0 +1,9 @@ +{ + "slug": "beispiel", + "name": "Beispiel: Ticket-Protokoll", + "version": "1.0.0", + "description": "Demonstriert Hooks, Menüpunkt, Seite, API-Endpunkt, Ticket-Seitenleiste, Einstellungen und Datenbank-Migration.", + "author": "DeskHub", + "requires": "1.0.0", + "permissions": ["hooks.tickets", "hooks.cron", "ui.menu", "ui.sidebar", "api.endpoints", "db.migrations"] +} diff --git a/Plugins/plugin-beispiel/plugin.php b/Plugins/plugin-beispiel/plugin.php new file mode 100644 index 00000000..a1700f95 --- /dev/null +++ b/Plugins/plugin-beispiel/plugin.php @@ -0,0 +1,35 @@ +prepare('INSERT INTO plugin_beispiel_log (ticket_id, event, created_at) VALUES (?,?,?)')->execute([$e['ticketId'], 'created', db_now()]); +}); +plugin_add_hook('ticket.status_changed', function (array $e) { + $pdo = get_db(); + $pdo->prepare('INSERT INTO plugin_beispiel_log (ticket_id, event, created_at) VALUES (?,?,?)')->execute([$e['ticketId'], 'status_changed', db_now()]); +}); + +// Baustein in der Ticketansicht (Plugins liefern fertiges HTML und müssen Ausgaben selbst escapen) +plugin_add_filter('ticket.view.sidebar', function (string $html, array $ctx) { + $stmt = get_db()->prepare('SELECT COUNT(*) FROM plugin_beispiel_log WHERE ticket_id = ?'); + $stmt->execute([$ctx['ticketId']]); + return $html . '
Protokoll-Plugin
' + . (int) $stmt->fetchColumn() . ' Ereignis(se) zu diesem Ticket
'; +}); + +plugin_register_menu('Ticket-Protokoll', 'log'); + +plugin_register_api('count', function (array $staff, array $request): array { + return ['count' => (int) get_db()->query('SELECT COUNT(*) FROM plugin_beispiel_log')->fetchColumn()]; +}); + +plugin_add_hook('cron.run', function () { + plugin_set_setting('beispiel', 'last_cron', db_now()); +}); + +// Aufräumen beim Entfernen mit "Daten löschen" +plugin_add_hook('plugin.uninstall.beispiel', function () { + get_db()->exec('DROP TABLE IF EXISTS plugin_beispiel_log'); +}); diff --git a/Plugins/plugins/tagesbericht-1.0.0.zip b/Plugins/plugins/tagesbericht-1.0.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..a7645a8dea1319a19ebe679d54f1aca7491868e4 GIT binary patch literal 4857 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 UMoshQL8{-A?{_A3u>QIG2hWl6{{R30 literal 0 HcmV?d00001 diff --git a/Plugins/plugins/tagesbericht/lib.php b/Plugins/plugins/tagesbericht/lib.php new file mode 100644 index 00000000..71a5f105 --- /dev/null +++ b/Plugins/plugins/tagesbericht/lib.php @@ -0,0 +1,106 @@ + plugin_setting(TAGESBERICHT, 'enabled', '0') === '1', + 'hour' => max(0, min(23, (int) plugin_setting(TAGESBERICHT, 'hour', '7'))), + 'workdays' => plugin_setting(TAGESBERICHT, 'workdays', '1') === '1', + // Leer = alle aktiven Mitarbeiter, sonst Liste von Mitarbeiter-IDs + 'recipients' => array_values(array_filter(array_map('intval', explode(',', (string) plugin_setting(TAGESBERICHT, 'recipients', ''))))), + 'last_sent' => plugin_setting(TAGESBERICHT, 'last_sent', ''), + ]; +} + +/** Empfänger laut Einstellung (nur aktive Mitarbeiter mit E-Mail-Adresse). */ +function tagesbericht_recipients(PDO $pdo, array $ids): array +{ + $rows = $pdo->query("SELECT * FROM staff WHERE is_active = 1 AND email <> ''")->fetchAll(); + return array_values(array_filter($rows, fn($s) => !$ids || in_array((int) $s['id'], $ids, true))); +} + +/** Zahlen und Listen für einen Mitarbeiter; berücksichtigt dessen Abteilungen. */ +function tagesbericht_data(PDO $pdo, array $staff, ?string $now = null): array +{ + $now = $now ?? db_now(); + $since = date('Y-m-d H:i:s', strtotime($now) - 86400); + [$dept, $deptParams] = dept_filter_sql(staff_department_ids($staff), 't.department_id'); + $open = "t.status NOT IN ('resolved','closed') AND t.is_spam = 0"; + + $count = function (string $where, array $params = []) use ($pdo, $dept, $deptParams): int { + $stmt = $pdo->prepare("SELECT COUNT(*) FROM tickets t WHERE {$where}{$dept}"); + $stmt->execute(array_merge($params, $deptParams)); + return (int) $stmt->fetchColumn(); + }; + $oldest = $pdo->prepare("SELECT t.ticket_number, t.subject, t.created_at, t.resolution_due_at, d.name AS department + FROM tickets t LEFT JOIN departments d ON d.id = t.department_id + WHERE {$open}{$dept} ORDER BY t.created_at ASC LIMIT 5"); + $oldest->execute($deptParams); + + return [ + 'open' => $count($open), + 'unassigned' => $count($open . ' AND t.assigned_staff_id IS NULL'), + 'mine' => $count($open . ' AND t.assigned_staff_id = ?', [(int) $staff['id']]), + 'overdue' => $count($open . ' AND t.resolution_due_at IS NOT NULL AND t.resolution_due_at < ?', [$now]), + 'created' => $count('t.is_spam = 0 AND t.created_at >= ?', [$since]), + 'resolved' => $count('t.is_spam = 0 AND t.resolved_at IS NOT NULL AND t.resolved_at >= ?', [$since]), + 'oldest' => $oldest->fetchAll(), + ]; +} + +function tagesbericht_html(array $staff, array $d): string +{ + $card = function (string $label, int $value, string $color) { + return '
' + . '
' . $value . '
' + . '
' . e($label) . '
'; + }; + $rows = ''; + foreach ($d['oldest'] as $t) { + $late = $t['resolution_due_at'] && $t['resolution_due_at'] < db_now(); + $rows .= '' . e($t['ticket_number']) . ' ' + . e(mb_strimwidth((string) $t['subject'], 0, 60, '…')) . '
' + . e((string) $t['department']) . ' · seit ' . e(date('d.m.Y', strtotime($t['created_at']))) + . ($late ? ' · überfällig' : '') . ''; + } + $inner = email_p('Guten Morgen ' . e(explode(' ', trim((string) $staff['name']))[0]) . ', hier ist deine Ticket-Übersicht:') + . '' + . $card('Offen', $d['open'], '#0f6f6a') . $card('Unzugewiesen', $d['unassigned'], '#d97706') . $card('Überfällig', $d['overdue'], '#dc2626') + . '' + . $card('Mir zugewiesen', $d['mine'], '#2563eb') . $card('Neu (24 h)', $d['created'], '#475569') . $card('Erledigt (24 h)', $d['resolved'], '#15803d') + . '
' + . ($rows !== '' ? '

Älteste offene Tickets

' . $rows . '
' : '') + . email_button('Zum Ticketsystem', base_url() . '/staff/tickets.php'); + return email_wrap($inner); +} + +/** Sendet den Bericht an alle Empfänger; liefert Anzahl erfolgreich versendeter Mails. */ +function tagesbericht_send(PDO $pdo, ?array $only = null): int +{ + $cfg = tagesbericht_config(); + $sent = 0; + foreach ($only ?? tagesbericht_recipients($pdo, $cfg['recipients']) as $staff) { + $d = tagesbericht_data($pdo, $staff); + if ($d['open'] === 0 && $d['created'] === 0 && $d['resolved'] === 0) continue; // nichts zu berichten + $subject = 'Tagesbericht: ' . $d['open'] . ' offen, ' . $d['overdue'] . ' überfällig'; + if (send_mail($staff['email'], $subject, tagesbericht_html($staff, $d))) $sent++; + } + return $sent; +} + +/** Wird alle paar Minuten aufgerufen: sendet einmal pro Tag ab der eingestellten Uhrzeit. */ +function tagesbericht_tick(PDO $pdo, ?int $time = null): int +{ + $cfg = tagesbericht_config(); + $time = $time ?? time(); + if (!$cfg['enabled']) return 0; + if ((int) date('G', $time) < $cfg['hour']) return 0; + if ($cfg['workdays'] && (int) date('N', $time) > 5) return 0; + $today = date('Y-m-d', $time); + if ($cfg['last_sent'] === $today) return 0; + plugin_set_setting(TAGESBERICHT, 'last_sent', $today); // zuerst merken, damit parallele Aufrufe nicht doppelt senden + return tagesbericht_send($pdo); +} diff --git a/Plugins/plugins/tagesbericht/pages/einstellungen.php b/Plugins/plugins/tagesbericht/pages/einstellungen.php new file mode 100644 index 00000000..b1572217 --- /dev/null +++ b/Plugins/plugins/tagesbericht/pages/einstellungen.php @@ -0,0 +1,55 @@ + 0) $message = 'Testbericht an ' . $staff['email'] . ' gesendet.'; + else $error = 'Kein Bericht gesendet: Entweder gibt es nichts zu berichten oder der Mailversand ist nicht eingerichtet.'; + } +} + +$cfg = tagesbericht_config(); +$allStaff = $pdo->query("SELECT id, name, email FROM staff WHERE is_active = 1 ORDER BY name")->fetchAll(); +?> +

Tagesbericht

+
+
+ +
+ + + + + + + +

Empfänger

+

Ohne Auswahl erhalten alle aktiven Mitarbeiter den Bericht. Jeder sieht nur Tickets seiner Abteilungen.

+ + + + +

Zuletzt gesendet:

+
+
+ +
+ + +

Schickt dir den Bericht sofort zum Ausprobieren (an ).

+ +
diff --git a/Plugins/plugins/tagesbericht/plugin.json b/Plugins/plugins/tagesbericht/plugin.json new file mode 100644 index 00000000..bc3f3044 --- /dev/null +++ b/Plugins/plugins/tagesbericht/plugin.json @@ -0,0 +1,9 @@ +{ + "slug": "tagesbericht", + "name": "Tagesbericht per E-Mail", + "version": "1.0.0", + "description": "Sendet Mitarbeitern täglich eine Zusammenfassung: offene, unzugewiesene und überfällige Tickets, Zugänge und Erledigungen der letzten 24 Stunden sowie die ältesten offenen Tickets. Jeder sieht nur seine Abteilungen.", + "author": "DeskHub", + "requires": "1.0.0", + "permissions": ["hooks.cron", "ui.menu"] +} diff --git a/Plugins/plugins/tagesbericht/plugin.php b/Plugins/plugins/tagesbericht/plugin.php new file mode 100644 index 00000000..ed5cebb3 --- /dev/null +++ b/Plugins/plugins/tagesbericht/plugin.php @@ -0,0 +1,10 @@ +