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: = e(plugin_setting('beispiel', 'last_cron', 'noch nie')) ?>
+
+ | Ticket | Ereignis | Zeit |
+
+ | #= (int) $r['ticket_id'] ?> | = e($r['event']) ?> | = e($r['created_at']) ?> |
+ | 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 00000000..a7645a8d
Binary files /dev/null and b/Plugins/plugins/tagesbericht-1.0.0.zip differ
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
' : '')
+ . 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
+= e($message) ?>
+= e($error) ?>
+
+
+
+
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 @@
+