Upload via GUI (94 Dateien)

This commit is contained in:
2026-10-24 20:15:15 +00:00
parent 9108a34774
commit cbf6139e74
45 changed files with 3478 additions and 0 deletions
+220
View File
@@ -0,0 +1,220 @@
<?php
// Störungsmitteilung und Rundmail. Alle Funktionen beginnen mit rundmail_.
const RUNDMAIL = 'rundmail';
const RUNDMAIL_BATCH = 25;
/** Vorlagen für typische Anlässe. @return array<string,array{0:string,1:string}> */
function rundmail_presets(): array
{
return [
'wartung' => ['Geplante Wartung', "Guten Tag {name},\n\nwir führen am [Datum] von [Uhrzeit] bis [Uhrzeit] eine Wartung durch. In dieser Zeit ist unser Dienst möglicherweise nicht erreichbar.\n\nIhr offenes Ticket bleibt davon unberührt, wir melden uns danach bei Ihnen.\n\nVielen Dank für Ihr Verständnis."],
'stoerung' => ['Aktuelle Störung', "Guten Tag {name},\n\nzurzeit gibt es eine Störung: [Beschreibung]. Wir arbeiten an der Behebung und halten Sie auf dem Laufenden.\n\nIhr offenes Ticket ist dadurch möglicherweise ebenfalls betroffen. Sie müssen nichts weiter tun.\n\nVielen Dank für Ihre Geduld."],
'behoben' => ['Störung behoben', "Guten Tag {name},\n\ndie Störung ist behoben. Alles läuft wieder wie gewohnt.\n\nSollte bei Ihnen noch etwas nicht funktionieren, antworten Sie einfach auf Ihr Ticket.\n\nVielen Dank für Ihre Geduld."],
];
}
/** Gültige Adresse eines echten Kunden (keine Platzhalter, keine anonymisierten oder gesperrten Adressen). */
function rundmail_usable_email(string $email): bool
{
$email = strtolower(trim($email));
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
if (str_ends_with($email, '.invalid') || str_ends_with($email, '@deleted.local') || str_starts_with($email, 'anon_')) return false;
if (function_exists('is_banned') && is_banned($email)) return false;
return true;
}
/** Abteilungen, an deren Kunden der Mitarbeiter schreiben darf (null = alle). */
function rundmail_allowed_departments(array $staff): ?array
{
$ids = staff_department_ids($staff);
return $ids === null ? null : array_map('intval', $ids);
}
/** Auswahl für das Formular. @return list<array{key:string,label:string}> */
function rundmail_groups(PDO $pdo, array $staff): array
{
$allowed = rundmail_allowed_departments($staff);
$out = [['key' => 'open', 'label' => 'Alle Kunden mit offenen Tickets' . ($allowed === null ? '' : ' (meiner Abteilungen)')]];
foreach ($pdo->query('SELECT id, name FROM departments ORDER BY name')->fetchAll() as $d) {
if ($allowed !== null && !in_array((int) $d['id'], $allowed, true)) continue;
$out[] = ['key' => 'department:' . $d['id'], 'label' => 'Kunden mit offenen Tickets in Abteilung „' . $d['name'] . '“'];
}
foreach ($pdo->query('SELECT id, name FROM organizations ORDER BY name')->fetchAll() as $o) {
$out[] = ['key' => 'organization:' . $o['id'], 'label' => 'Organisation „' . $o['name'] . '“' . ($allowed === null ? ' (alle Mitglieder)' : ' (Mitglieder mit offenem Ticket)')];
}
return $out;
}
/** @return array{type:string,id:?int,label:string}|null */
function rundmail_parse_group(PDO $pdo, string $key): ?array
{
if ($key === 'open') return ['type' => 'open', 'id' => null, 'label' => 'Kunden mit offenen Tickets'];
if (!preg_match('/^(department|organization):(\d+)$/', $key, $m)) return null;
$table = $m[1] === 'department' ? 'departments' : 'organizations';
$s = $pdo->prepare("SELECT name FROM {$table} WHERE id = ?");
$s->execute([(int) $m[2]]);
$name = $s->fetchColumn();
if ($name === false) return null;
return ['type' => $m[1], 'id' => (int) $m[2], 'label' => ($m[1] === 'department' ? 'Abteilung ' : 'Organisation ') . $name];
}
/** Empfänger der Gruppe, einmal je Adresse. @return list<array{email:string,name:string}> */
function rundmail_recipients(PDO $pdo, array $staff, string $type, ?int $id): array
{
$allowed = rundmail_allowed_departments($staff);
if ($type === 'department' && $allowed !== null && !in_array((int) $id, $allowed, true)) return [];
$open = "t.status NOT IN ('resolved','closed') AND t.is_spam = 0 AND t.merged_into IS NULL";
$params = [];
$deptSql = '';
if ($type === 'department') { $deptSql = ' AND t.department_id = ?'; $params[] = (int) $id; }
elseif ($allowed !== null) {
if (!$allowed) return [];
$deptSql = ' AND t.department_id IN (' . implode(',', array_fill(0, count($allowed), '?')) . ')';
$params = array_merge($params, $allowed);
}
if ($type === 'organization') {
if ($allowed === null) {
$stmt = $pdo->prepare('SELECT email, name FROM users WHERE organization_id = ? ORDER BY id');
$stmt->execute([(int) $id]);
} else {
$stmt = $pdo->prepare("SELECT DISTINCT u.email, u.name FROM users u JOIN tickets t ON t.user_id = u.id WHERE u.organization_id = ? AND {$open}{$deptSql} ORDER BY u.id");
$stmt->execute(array_merge([(int) $id], $params));
}
} else {
$stmt = $pdo->prepare("SELECT t.requester_email AS email, t.requester_name AS name FROM tickets t WHERE {$open}{$deptSql} ORDER BY t.id");
$stmt->execute($params);
}
$seen = [];
$out = [];
foreach ($stmt->fetchAll() as $r) {
$mail = strtolower(trim((string) $r['email']));
if (isset($seen[$mail]) || !rundmail_usable_email($mail)) continue;
$seen[$mail] = true;
$out[] = ['email' => $mail, 'name' => trim((string) $r['name'])];
}
return $out;
}
/** Betreff und Mail-Text für einen Empfänger. @return array{0:string,1:string} */
function rundmail_render(array $msg, string $name): array
{
$name = $name !== '' ? $name : 'Kunde';
$text = str_replace('{name}', $name, (string) $msg['body']);
$html = '<div>' . nl2br(e($text)) . '</div>'
. '<p style="color:#6b7280;font-size:12px;margin-top:20px">Sie erhalten diese Nachricht, weil Sie bei uns ein offenes Ticket haben bzw. zur genannten Kundengruppe gehören. Bitte antworten Sie bei Fragen auf Ihr Ticket.</p>';
if (function_exists('email_wrap')) $html = email_wrap($html);
return [(string) $msg['subject'], $html];
}
/** Versand einer einzelnen Mail (für Tests austauschbar über $GLOBALS['rundmail_send']). */
function rundmail_send(string $to, string $subject, string $html): bool
{
if (isset($GLOBALS['rundmail_send']) && is_callable($GLOBALS['rundmail_send'])) return (bool) ($GLOBALS['rundmail_send'])($to, $subject, $html);
return (bool) send_mail($to, $subject, $html);
}
/** Prüft Betreff und Text. */
function rundmail_validate(string $subject, string $body): ?string
{
if (trim($subject) === '' || mb_strlen($subject) > 150) return 'Bitte einen Betreff angeben (höchstens 150 Zeichen).';
if (trim($body) === '' || mb_strlen($body) > 5000) return 'Bitte einen Text angeben (höchstens 5000 Zeichen).';
if (preg_match('/[\r\n]/', $subject)) return 'Der Betreff darf nur eine Zeile sein.';
return null;
}
/** Legt die Nachricht an und startet den Versand. @return array{ok:bool,error?:string,id?:int,total?:int} */
function rundmail_create(PDO $pdo, array $staff, string $groupKey, string $subject, string $body): array
{
if ($err = rundmail_validate($subject, $body)) return ['ok' => false, 'error' => $err];
$group = rundmail_parse_group($pdo, $groupKey);
if (!$group) return ['ok' => false, 'error' => 'Bitte eine Empfängergruppe wählen.'];
$isAdmin = ($staff['role'] ?? '') === 'admin';
$recent = $pdo->prepare('SELECT COUNT(*) FROM plugin_rundmail_messages WHERE sender_id = ? AND created_at >= ?');
$recent->execute([(int) $staff['id'], date('Y-m-d H:i:s', time() - 3600)]);
if ((int) $recent->fetchColumn() >= ($isAdmin ? 20 : 5)) return ['ok' => false, 'error' => 'Sie haben in der letzten Stunde schon viele Rundmails verschickt. Bitte später noch einmal.'];
$rcpt = rundmail_recipients($pdo, $staff, $group['type'], $group['id']);
if (!$rcpt) return ['ok' => false, 'error' => 'In dieser Gruppe gibt es keine Empfänger (oder Sie dürfen ihr nicht schreiben).'];
$max = $isAdmin ? 2000 : 200;
if (count($rcpt) > $max) return ['ok' => false, 'error' => 'Zu viele Empfänger (' . count($rcpt) . '). Erlaubt sind höchstens ' . $max . '.' . ($isAdmin ? '' : ' Größere Verteiler darf nur ein Administrator senden.')];
$pdo->prepare("INSERT INTO plugin_rundmail_messages (subject, body, group_type, group_id, group_label, sender_id, sender_name, status, total, created_at) VALUES (?,?,?,?,?,?,?,'sending',?,?)")
->execute([trim($subject), trim($body), $group['type'], $group['id'], mb_substr($group['label'], 0, 190), (int) $staff['id'], mb_substr((string) $staff['name'], 0, 120), count($rcpt), db_now()]);
$id = (int) $pdo->lastInsertId();
$ins = $pdo->prepare('INSERT INTO plugin_rundmail_queue (message_id, email, name) VALUES (?,?,?)');
foreach ($rcpt as $r) $ins->execute([$id, $r['email'], mb_substr($r['name'], 0, 120)]);
rundmail_process($pdo, $id, RUNDMAIL_BATCH);
return ['ok' => true, 'id' => $id, 'total' => count($rcpt)];
}
/** Verschickt die nächsten Mails (einer Nachricht oder der ältesten offenen). @return int Zahl der bearbeiteten Empfänger */
function rundmail_process(PDO $pdo, ?int $messageId = null, int $batch = RUNDMAIL_BATCH): int
{
if ($messageId === null) {
$messageId = (int) $pdo->query("SELECT id FROM plugin_rundmail_messages WHERE status = 'sending' ORDER BY id LIMIT 1")->fetchColumn();
if (!$messageId) return 0;
}
$m = $pdo->prepare("SELECT * FROM plugin_rundmail_messages WHERE id = ? AND status = 'sending'");
$m->execute([$messageId]);
$msg = $m->fetch();
if (!$msg) return 0;
$q = $pdo->prepare("SELECT * FROM plugin_rundmail_queue WHERE message_id = ? AND status = 'pending' ORDER BY id LIMIT " . max(1, $batch));
$q->execute([$messageId]);
$n = 0;
foreach ($q->fetchAll() as $row) {
[$subject, $html] = rundmail_render($msg, (string) $row['name']);
try {
$ok = rundmail_send((string) $row['email'], $subject, $html);
$err = $ok ? null : 'Versand fehlgeschlagen (E-Mail-Versand eingerichtet?)';
} catch (Throwable $e) {
$ok = false;
$err = mb_substr($e->getMessage(), 0, 240);
}
$pdo->prepare('UPDATE plugin_rundmail_queue SET status = ?, error = ?, sent_at = ? WHERE id = ?')->execute([$ok ? 'sent' : 'failed', $err, db_now(), $row['id']]);
$n++;
}
$c = $pdo->prepare("SELECT SUM(CASE WHEN status = 'sent' THEN 1 ELSE 0 END), SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END), SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) FROM plugin_rundmail_queue WHERE message_id = ?");
$c->execute([$messageId]);
[$sent, $failed, $pending] = array_map('intval', array_values($c->fetch(PDO::FETCH_NUM)));
if ($pending === 0) $pdo->prepare("UPDATE plugin_rundmail_messages SET sent = ?, failed = ?, status = 'done', finished_at = ? WHERE id = ?")->execute([$sent, $failed, db_now(), $messageId]);
else $pdo->prepare('UPDATE plugin_rundmail_messages SET sent = ?, failed = ? WHERE id = ?')->execute([$sent, $failed, $messageId]);
return $n;
}
/** Hintergrundlauf: nächste Mails der laufenden Nachrichten versenden. */
function rundmail_tick(PDO $pdo): int
{
return rundmail_process($pdo, null, RUNDMAIL_BATCH);
}
/** Bricht den Versand ab (noch nicht gesendete Mails entfallen). Verfasser oder Administrator. */
function rundmail_cancel(PDO $pdo, array $staff, int $id): bool
{
$s = $pdo->prepare("SELECT sender_id FROM plugin_rundmail_messages WHERE id = ? AND status = 'sending'");
$s->execute([$id]);
$owner = $s->fetchColumn();
if ($owner === false || (($staff['role'] ?? '') !== 'admin' && (int) $owner !== (int) $staff['id'])) return false;
$pdo->prepare("UPDATE plugin_rundmail_queue SET status = 'cancelled' WHERE message_id = ? AND status = 'pending'")->execute([$id]);
$c = $pdo->prepare("SELECT SUM(CASE WHEN status = 'sent' THEN 1 ELSE 0 END), SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) FROM plugin_rundmail_queue WHERE message_id = ?");
$c->execute([$id]);
[$sent, $failed] = array_map('intval', array_values($c->fetch(PDO::FETCH_NUM)));
$pdo->prepare("UPDATE plugin_rundmail_messages SET status = 'cancelled', sent = ?, failed = ?, finished_at = ? WHERE id = ?")->execute([$sent, $failed, db_now(), $id]);
return true;
}
/** Probemail an den angemeldeten Mitarbeiter. */
function rundmail_test(array $staff, string $subject, string $body): array
{
if ($err = rundmail_validate($subject, $body)) return ['ok' => false, 'error' => $err];
[$s, $html] = rundmail_render(['subject' => '[TEST] ' . trim($subject), 'body' => trim($body)], (string) $staff['name']);
$ok = rundmail_send((string) $staff['email'], $s, $html);
return $ok ? ['ok' => true] : ['ok' => false, 'error' => 'Die Probemail konnte nicht gesendet werden (E-Mail-Versand eingerichtet?).'];
}
/** Verlauf: Administratoren sehen alle, Mitarbeiter ihre eigenen. */
function rundmail_history(PDO $pdo, array $staff, int $limit = 40): array
{
if (($staff['role'] ?? '') === 'admin') return $pdo->query('SELECT * FROM plugin_rundmail_messages ORDER BY id DESC LIMIT ' . max(1, $limit))->fetchAll();
$s = $pdo->prepare('SELECT * FROM plugin_rundmail_messages WHERE sender_id = ? ORDER BY id DESC LIMIT ' . max(1, $limit));
$s->execute([(int) $staff['id']]);
return $s->fetchAll();
}
@@ -0,0 +1,27 @@
CREATE TABLE IF NOT EXISTS plugin_rundmail_messages (
id INT AUTO_INCREMENT PRIMARY KEY,
subject VARCHAR(200) NOT NULL,
body TEXT NOT NULL,
group_type VARCHAR(20) NOT NULL,
group_id INT NULL,
group_label VARCHAR(190) NOT NULL DEFAULT '',
sender_id INT NULL,
sender_name VARCHAR(120) NOT NULL DEFAULT '',
status VARCHAR(12) NOT NULL DEFAULT 'sending',
total INT NOT NULL DEFAULT 0,
sent INT NOT NULL DEFAULT 0,
failed INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
finished_at DATETIME NULL,
KEY idx_plugin_rundmail_msg_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS plugin_rundmail_queue (
id INT AUTO_INCREMENT PRIMARY KEY,
message_id INT NOT NULL,
email VARCHAR(190) NOT NULL,
name VARCHAR(120) NOT NULL DEFAULT '',
status VARCHAR(10) NOT NULL DEFAULT 'pending',
error VARCHAR(250) NULL,
sent_at DATETIME NULL,
KEY idx_plugin_rundmail_queue (message_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
@@ -0,0 +1,27 @@
CREATE TABLE IF NOT EXISTS plugin_rundmail_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
subject TEXT NOT NULL,
body TEXT NOT NULL,
group_type TEXT NOT NULL,
group_id INTEGER NULL,
group_label TEXT NOT NULL DEFAULT '',
sender_id INTEGER NULL,
sender_name TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'sending',
total INTEGER NOT NULL DEFAULT 0,
sent INTEGER NOT NULL DEFAULT 0,
failed INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
finished_at TEXT NULL
);
CREATE INDEX IF NOT EXISTS idx_plugin_rundmail_msg_status ON plugin_rundmail_messages (status);
CREATE TABLE IF NOT EXISTS plugin_rundmail_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message_id INTEGER NOT NULL,
email TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'pending',
error TEXT NULL,
sent_at TEXT NULL
);
CREATE INDEX IF NOT EXISTS idx_plugin_rundmail_queue ON plugin_rundmail_queue (message_id, status)
+110
View File
@@ -0,0 +1,110 @@
<?php
// Seite des Rundmail-Plugins. Verfügbar: $staff, $pdo.
$isAdmin = ($staff['role'] ?? '') === 'admin';
$message = null;
$error = null;
$form = ['group' => (string) ($_POST['group'] ?? ''), 'subject' => (string) ($_POST['subject'] ?? ''), 'body' => (string) ($_POST['body'] ?? '')];
$preview = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$action = $_POST['action'] ?? '';
if ($action === 'preview') {
$g = rundmail_parse_group($pdo, $form['group']);
$err = rundmail_validate($form['subject'], $form['body']);
if (!$g) $error = 'Bitte eine Empfängergruppe wählen.';
elseif ($err) $error = $err;
else {
$rcpt = rundmail_recipients($pdo, $staff, $g['type'], $g['id']);
if (!$rcpt) $error = 'In dieser Gruppe gibt es keine Empfänger (oder Sie dürfen ihr nicht schreiben).';
else $preview = ['group' => $g, 'rcpt' => $rcpt];
}
} elseif ($action === 'send') {
if (empty($_POST['confirm'])) { $error = 'Bitte bestätigen, dass Text und Empfänger geprüft sind.'; }
else {
$r = rundmail_create($pdo, $staff, $form['group'], $form['subject'], $form['body']);
if ($r['ok']) {
$message = 'Die Nachricht geht an ' . $r['total'] . ' Empfänger. Der Versand läuft im Hintergrund, den Stand sehen Sie unten.';
$form = ['group' => '', 'subject' => '', 'body' => ''];
} else $error = $r['error'];
}
} elseif ($action === 'test') {
$r = rundmail_test($staff, $form['subject'], $form['body']);
if ($r['ok']) $message = 'Die Probemail ist an ' . $staff['email'] . ' unterwegs.'; else $error = $r['error'];
} elseif ($action === 'cancel') {
if (rundmail_cancel($pdo, $staff, (int) ($_POST['id'] ?? 0))) $message = 'Der Versand wurde abgebrochen.'; else $error = 'Dieser Versand lässt sich nicht abbrechen.';
}
}
// Offene Versände weiterschieben, damit der Stand beim Ansehen nicht stehen bleibt
if ($_SERVER['REQUEST_METHOD'] !== 'POST') rundmail_tick($pdo);
$groups = rundmail_groups($pdo, $staff);
$history = rundmail_history($pdo, $staff);
$presets = rundmail_presets();
$statusLabels = ['sending' => 'Läuft', 'done' => 'Fertig', 'cancelled' => 'Abgebrochen'];
?>
<h1>Störungsmitteilung und Rundmail</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:860px;margin-bottom:16px">
<?= csrf_field() ?>
<h3>Neue Nachricht</h3>
<p class="muted" style="font-size:.85rem">Jede Mail geht einzeln an den Kunden (keine sichtbare Empfängerliste). Gedacht für Kunden mit offenen Tickets, etwa bei einer Wartung oder Störung. Mit <code>{name}</code> wird der Name eingesetzt.<?= $isAdmin ? '' : ' Sie sehen nur Gruppen Ihrer Abteilungen.' ?></p>
<label>Empfänger</label>
<select name="group" required>
<option value="">– Gruppe wählen –</option>
<?php foreach ($groups as $g): ?><option value="<?= e($g['key']) ?>" <?= $form['group'] === $g['key'] ? 'selected' : '' ?>><?= e($g['label']) ?></option><?php endforeach; ?>
</select>
<label style="margin-top:8px">Vorlage</label>
<select id="rm-preset" onchange="var p=window.RM_PRESETS[this.value];if(p){document.getElementById('rm-subject').value=p[0];document.getElementById('rm-body').value=p[1];}">
<option value="">– eigene Nachricht –</option>
<?php foreach ($presets as $k => $p): ?><option value="<?= e($k) ?>"><?= e($p[0]) ?></option><?php endforeach; ?>
</select>
<script>window.RM_PRESETS = <?= json_encode($presets, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP) ?>;</script>
<label style="margin-top:8px">Betreff</label>
<input type="text" id="rm-subject" name="subject" maxlength="150" value="<?= e($form['subject']) ?>" required>
<label style="margin-top:8px">Text</label>
<textarea id="rm-body" name="body" rows="9" maxlength="5000" required><?= e($form['body']) ?></textarea>
<div class="btn-row">
<button type="submit" name="action" value="preview" class="btn">Empfänger prüfen</button>
<button type="submit" name="action" value="test" class="btn secondary">Probemail an mich</button>
</div>
</form>
<?php if ($preview): ?>
<form method="post" class="card" style="max-width:860px;margin-bottom:16px;border-left:4px solid #b45309">
<?= csrf_field() ?>
<input type="hidden" name="action" value="send">
<input type="hidden" name="group" value="<?= e($form['group']) ?>">
<input type="hidden" name="subject" value="<?= e($form['subject']) ?>">
<input type="hidden" name="body" value="<?= e($form['body']) ?>">
<h3>Vor dem Senden prüfen</h3>
<p><strong><?= count($preview['rcpt']) ?></strong> Empfänger: <?= e($preview['group']['label']) ?></p>
<p class="muted" style="font-size:.84rem">Zum Beispiel: <?= e(implode(', ', array_map(fn($r) => $r['email'], array_slice($preview['rcpt'], 0, 5)))) ?><?= count($preview['rcpt']) > 5 ? ' …' : '' ?></p>
<div style="border:1px solid var(--border,#e3e9ee);border-radius:8px;padding:12px;margin:8px 0;font-size:.9rem"><strong><?= e($form['subject']) ?></strong><br><br><?= nl2br(e(str_replace('{name}', $preview['rcpt'][0]['name'] ?: 'Kunde', $form['body']))) ?></div>
<label style="font-weight:400"><input type="checkbox" name="confirm" value="1" style="width:auto"> Text und Empfänger sind geprüft, die Nachricht kann nicht zurückgeholt werden.</label>
<div class="btn-row"><button type="submit" class="btn">An <?= count($preview['rcpt']) ?> Empfänger senden</button></div>
</form>
<?php endif; ?>
<div class="card">
<h3><?= $isAdmin ? 'Verlauf aller Nachrichten' : 'Ihre gesendeten Nachrichten' ?></h3>
<?php if (!$history): ?><p class="muted">Noch nichts gesendet.</p><?php else: ?>
<table style="width:100%">
<thead><tr><th>Wann</th><th>Betreff</th><th>Empfänger</th><th>Von</th><th>Stand</th><th></th></tr></thead>
<tbody>
<?php foreach ($history as $h): ?>
<tr>
<td class="muted" style="white-space:nowrap"><?= e(format_dt((string) $h['created_at'])) ?></td>
<td><?= e((string) $h['subject']) ?></td>
<td><?= e((string) $h['group_label']) ?></td>
<td><?= e((string) $h['sender_name']) ?></td>
<td><?= e($statusLabels[$h['status']] ?? $h['status']) ?><div class="muted" style="font-size:.76rem"><?= (int) $h['sent'] ?> von <?= (int) $h['total'] ?> gesendet<?= (int) $h['failed'] ? ', <span style="color:#b42318">' . (int) $h['failed'] . ' fehlgeschlagen</span>' : '' ?></div></td>
<td><?php if ($h['status'] === 'sending' && ($isAdmin || (int) $h['sender_id'] === (int) $staff['id'])): ?><form method="post" onsubmit="return confirm('Versand abbrechen? Noch nicht gesendete Mails entfallen.')"><?= csrf_field() ?><input type="hidden" name="action" value="cancel"><input type="hidden" name="id" value="<?= (int) $h['id'] ?>"><button class="btn secondary" type="submit" style="padding:2px 10px;font-size:.78rem">Abbrechen</button></form><?php endif; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
+9
View File
@@ -0,0 +1,9 @@
{
"slug": "rundmail",
"name": "Störungsmitteilung und Rundmail",
"version": "1.0.0",
"description": "Eine Nachricht an viele Kunden gleichzeitig, zum Beispiel bei einer Wartung oder einem Ausfall. Empfänger sind Kunden mit offenen Tickets, die einer Abteilung oder die einer Organisation. Jede Mail geht einzeln raus, der Versand läuft im Hintergrund und wird protokolliert. Mitarbeiter dürfen nur an Kunden ihrer eigenen Abteilungen schreiben.",
"author": "DeskHub",
"requires": "1.0.0",
"permissions": ["hooks.cron", "ui.menu", "db.migrations"]
}
+10
View File
@@ -0,0 +1,10 @@
<?php
// Störungsmitteilung und Rundmail: Versand im Hintergrund.
require_once __DIR__ . '/lib.php';
plugin_add_hook('cron.run', function () {
rundmail_tick(get_db());
});
// Für Administratoren und Mitarbeiter (Mitarbeiter schreiben nur an Kunden ihrer Abteilungen)
plugin_register_menu('Rundmail', 'rundmail');