Upload via GUI (10 Dateien)

This commit is contained in:
2026-10-16 18:44:25 +00:00
parent 9ef6edd6bb
commit 27fe8a8982
10 changed files with 249 additions and 0 deletions
@@ -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;
@@ -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
);
+13
View File
@@ -0,0 +1,13 @@
<?php
// Seite des Beispiel-Plugins. Verfügbar: $staff, $pdo.
$rows = $pdo->query('SELECT ticket_id, event, created_at FROM plugin_beispiel_log ORDER BY id DESC LIMIT 50')->fetchAll();
?>
<h1>Ticket-Protokoll</h1>
<p class="muted">Letzter Plugin-Cron: <?= e(plugin_setting('beispiel', 'last_cron', 'noch nie')) ?></p>
<div class="card"><table>
<thead><tr><th>Ticket</th><th>Ereignis</th><th>Zeit</th></tr></thead>
<tbody>
<?php foreach ($rows as $r): ?><tr><td>#<?= (int) $r['ticket_id'] ?></td><td><?= e($r['event']) ?></td><td><?= e($r['created_at']) ?></td></tr><?php endforeach; ?>
<?php if (!$rows): ?><tr><td colspan="3" class="muted">Noch keine Ereignisse.</td></tr><?php endif; ?>
</tbody>
</table></div>
+9
View File
@@ -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"]
}
+35
View File
@@ -0,0 +1,35 @@
<?php
// Beispiel-Plugin: protokolliert Ticket-Ereignisse in einer eigenen Tabelle.
// Wird beim Aktivieren geladen; hier werden nur Erweiterungen registriert.
plugin_add_hook('ticket.created', function (array $e) {
$pdo = get_db();
$pdo->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 . '<div class="card" style="margin-top:16px;padding:14px 16px"><strong>Protokoll-Plugin</strong><br>'
. (int) $stmt->fetchColumn() . ' Ereignis(se) zu diesem Ticket</div>';
});
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');
});
Binary file not shown.
+106
View File
@@ -0,0 +1,106 @@
<?php
/** Logik des Tagesbericht-Plugins (ohne Seiten-/Hook-Code, damit sie einzeln testbar ist). */
const TAGESBERICHT = 'tagesbericht';
function tagesbericht_config(): array
{
return [
'enabled' => 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 '<td style="padding:6px"><div style="background:#f5f7f9;border-radius:8px;padding:12px;text-align:center">'
. '<div style="font-size:24px;font-weight:700;color:' . $color . '">' . $value . '</div>'
. '<div style="font-size:12px;color:#64748b">' . e($label) . '</div></div></td>';
};
$rows = '';
foreach ($d['oldest'] as $t) {
$late = $t['resolution_due_at'] && $t['resolution_due_at'] < db_now();
$rows .= '<tr><td style="padding:6px 0;border-top:1px solid #e2e8f0;font-size:13px"><strong>' . e($t['ticket_number']) . '</strong> '
. e(mb_strimwidth((string) $t['subject'], 0, 60, '…')) . '<br><span style="color:#64748b;font-size:12px">'
. e((string) $t['department']) . ' · seit ' . e(date('d.m.Y', strtotime($t['created_at'])))
. ($late ? ' · <span style="color:#dc2626">überfällig</span>' : '') . '</span></td></tr>';
}
$inner = email_p('Guten Morgen ' . e(explode(' ', trim((string) $staff['name']))[0]) . ', hier ist deine Ticket-Übersicht:')
. '<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr>'
. $card('Offen', $d['open'], '#0f6f6a') . $card('Unzugewiesen', $d['unassigned'], '#d97706') . $card('Überfällig', $d['overdue'], '#dc2626')
. '</tr><tr>'
. $card('Mir zugewiesen', $d['mine'], '#2563eb') . $card('Neu (24 h)', $d['created'], '#475569') . $card('Erledigt (24 h)', $d['resolved'], '#15803d')
. '</tr></table>'
. ($rows !== '' ? '<p style="margin:20px 0 6px;font-weight:700;font-size:14px">Älteste offene Tickets</p><table role="presentation" width="100%" cellpadding="0" cellspacing="0">' . $rows . '</table>' : '')
. 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);
}
@@ -0,0 +1,55 @@
<?php
// Einstellungsseite des Tagesbericht-Plugins. Verfügbar: $staff, $pdo.
$message = null;
$error = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$action = $_POST['action'] ?? '';
if ($action === 'save') {
plugin_set_setting(TAGESBERICHT, 'enabled', !empty($_POST['enabled']) ? '1' : '0');
plugin_set_setting(TAGESBERICHT, 'hour', (string) max(0, min(23, (int) ($_POST['hour'] ?? 7))));
plugin_set_setting(TAGESBERICHT, 'workdays', !empty($_POST['workdays']) ? '1' : '0');
$ids = array_map('intval', (array) ($_POST['recipients'] ?? []));
plugin_set_setting(TAGESBERICHT, 'recipients', implode(',', array_filter($ids)));
$message = 'Einstellungen gespeichert.';
} elseif ($action === 'test') {
$n = tagesbericht_send($pdo, [$staff]);
if ($n > 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();
?>
<h1>Tagesbericht</h1>
<?php if ($message): ?><div class="flash success"><?= e($message) ?></div><?php endif; ?>
<?php if ($error): ?><div class="flash error"><?= e($error) ?></div><?php endif; ?>
<form method="post" class="card" style="max-width:640px">
<?= csrf_field() ?>
<input type="hidden" name="action" value="save">
<label><input type="checkbox" name="enabled" value="1" <?= $cfg['enabled'] ? 'checked' : '' ?>> Täglichen Bericht per E-Mail senden</label>
<label for="hour">Ab Uhrzeit (Serverzeit)</label>
<select id="hour" name="hour" style="max-width:140px">
<?php for ($h = 0; $h < 24; $h++): ?><option value="<?= $h ?>" <?= $cfg['hour'] === $h ? 'selected' : '' ?>><?= sprintf('%02d:00', $h) ?></option><?php endfor; ?>
</select>
<label style="margin-top:10px"><input type="checkbox" name="workdays" value="1" <?= $cfg['workdays'] ? 'checked' : '' ?>> Nur an Werktagen (Mo–Fr)</label>
<h3>Empfänger</h3>
<p class="muted" style="font-size:.85rem">Ohne Auswahl erhalten alle aktiven Mitarbeiter den Bericht. Jeder sieht nur Tickets seiner Abteilungen.</p>
<?php foreach ($allStaff as $s): ?>
<label style="display:block"><input type="checkbox" name="recipients[]" value="<?= (int) $s['id'] ?>" <?= in_array((int) $s['id'], $cfg['recipients'], true) ? 'checked' : '' ?>> <?= e($s['name']) ?> <span class="muted">(<?= e($s['email']) ?>)</span></label>
<?php endforeach; ?>
<p class="muted" style="font-size:.82rem;margin-top:14px">Zuletzt gesendet: <?= e($cfg['last_sent'] !== '' ? $cfg['last_sent'] : 'noch nie') ?></p>
<div class="btn-row"><button type="submit" class="btn">Speichern</button></div>
</form>
<form method="post" class="card" style="max-width:640px;margin-top:16px">
<?= csrf_field() ?>
<input type="hidden" name="action" value="test">
<p style="margin-top:0">Schickt dir den Bericht sofort zum Ausprobieren (an <?= e($staff['email']) ?>).</p>
<button type="submit" class="btn secondary">Testbericht an mich senden</button>
</form>
+9
View File
@@ -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"]
}
+10
View File
@@ -0,0 +1,10 @@
<?php
// Tagesbericht-Plugin: sendet Mitarbeitern täglich eine Ticket-Zusammenfassung per E-Mail.
require_once __DIR__ . '/lib.php';
plugin_add_hook('cron.run', function () {
tagesbericht_tick(get_db());
});
// Einstellungen: nur für Admins sichtbar (siehe pages/einstellungen.php)
plugin_register_menu('Tagesbericht', 'einstellungen', 'manage_email_addresses');