Files
deskhub/Plugins/plugins/schichtuebergabe/lib.php
T
2026-10-24 20:15:15 +00:00

238 lines
15 KiB
PHP

<?php
// Schichtübergabe. Alle Funktionen beginnen mit schicht_.
const SCHICHT = 'schichtuebergabe';
function schicht_keep_days(): int
{
return max(1, min(365, (int) plugin_setting(SCHICHT, 'keep_days', '30')));
}
/** Abteilungen eines Mitarbeiters (Hauptabteilung und weitere); null = Administrator, sieht alle. */
function schicht_departments(PDO $pdo, array $staff): ?array
{
if (($staff['role'] ?? '') === 'admin') return null;
$ids = [];
if (!empty($staff['department_id'])) $ids[] = (int) $staff['department_id'];
$s = $pdo->prepare('SELECT department_id FROM staff_department WHERE staff_id = ?');
$s->execute([(int) $staff['id']]);
foreach ($s->fetchAll(PDO::FETCH_COLUMN) as $d) $ids[] = (int) $d;
return array_values(array_unique($ids));
}
/** SQL-Bedingung und Parameter: Notizen, die dieser Mitarbeiter sehen darf (für alle oder für eine seiner Abteilungen). */
function schicht_visible_sql(PDO $pdo, array $staff, string $alias = 'n'): array
{
$depts = schicht_departments($pdo, $staff);
if ($depts === null) return ['1=1', []];
if (!$depts) return ["{$alias}.department_id IS NULL", []];
return ["({$alias}.department_id IS NULL OR {$alias}.department_id IN (" . implode(',', array_fill(0, count($depts), '?')) . '))', $depts];
}
/** Notiz anlegen. @return array{ok:bool,error?:string,id?:int} */
function schicht_create(PDO $pdo, array $staff, string $message, ?int $departmentId, bool $important): array
{
$message = trim($message);
if ($message === '') return ['ok' => false, 'error' => 'Bitte eine Notiz schreiben.'];
if (mb_strlen($message) > 3000) return ['ok' => false, 'error' => 'Die Notiz ist zu lang (höchstens 3000 Zeichen).'];
if ($departmentId !== null) {
$own = schicht_departments($pdo, $staff);
if ($own !== null && !in_array($departmentId, $own, true)) return ['ok' => false, 'error' => 'Sie können Notizen nur für Ihre eigenen Abteilungen schreiben.'];
$d = $pdo->prepare('SELECT COUNT(*) FROM departments WHERE id = ?');
$d->execute([$departmentId]);
if (!(int) $d->fetchColumn()) return ['ok' => false, 'error' => 'Die Abteilung gibt es nicht.'];
}
$pdo->prepare('INSERT INTO plugin_schichtuebergabe_notes (staff_id, department_id, message, is_important, created_at) VALUES (?,?,?,?,?)')
->execute([(int) $staff['id'], $departmentId, $message, $important ? 1 : 0, db_now()]);
$id = (int) $pdo->lastInsertId();
// Der Verfasser hat sie natürlich gelesen
$pdo->prepare('INSERT INTO plugin_schichtuebergabe_reads (note_id, staff_id, read_at) VALUES (?,?,?)')->execute([$id, (int) $staff['id'], db_now()]);
return ['ok' => true, 'id' => $id];
}
/** Ungelesene Notizen für einen Mitarbeiter (innerhalb der Aufbewahrungsfrist). */
function schicht_unread(PDO $pdo, array $staff, bool $onlyImportant = false): array
{
[$cond, $params] = schicht_visible_sql($pdo, $staff);
$sql = "SELECT n.*, s.name AS author FROM plugin_schichtuebergabe_notes n LEFT JOIN staff s ON s.id = n.staff_id
WHERE {$cond} AND n.created_at >= ? AND NOT EXISTS (SELECT 1 FROM plugin_schichtuebergabe_reads r WHERE r.note_id = n.id AND r.staff_id = ?)"
. ($onlyImportant ? ' AND n.is_important = 1' : '') . ' ORDER BY n.id DESC LIMIT 50';
$stmt = $pdo->prepare($sql);
$stmt->execute(array_merge($params, [date('Y-m-d H:i:s', time() - schicht_keep_days() * 86400), (int) $staff['id']]));
return $stmt->fetchAll();
}
function schicht_unread_count(PDO $pdo, array $staff): int
{
return count(schicht_unread($pdo, $staff));
}
/** Alle sichtbaren Notizen (neueste zuerst) mit Leseinfo. */
function schicht_list(PDO $pdo, array $staff, int $limit = 60): array
{
[$cond, $params] = schicht_visible_sql($pdo, $staff);
$stmt = $pdo->prepare("SELECT n.*, s.name AS author, d.name AS department,
(SELECT COUNT(*) FROM plugin_schichtuebergabe_reads r WHERE r.note_id = n.id AND r.staff_id = ?) AS is_read
FROM plugin_schichtuebergabe_notes n LEFT JOIN staff s ON s.id = n.staff_id LEFT JOIN departments d ON d.id = n.department_id
WHERE {$cond} AND n.created_at >= ? ORDER BY n.id DESC LIMIT " . max(1, $limit));
$stmt->execute(array_merge([(int) $staff['id']], $params, [date('Y-m-d H:i:s', time() - schicht_keep_days() * 86400)]));
return $stmt->fetchAll();
}
/** Markiert die Notizen als gelesen. */
function schicht_mark_read(PDO $pdo, array $staff, array $noteIds): void
{
foreach ($noteIds as $id) {
$id = (int) $id;
$has = $pdo->prepare('SELECT COUNT(*) FROM plugin_schichtuebergabe_reads WHERE note_id = ? AND staff_id = ?');
$has->execute([$id, (int) $staff['id']]);
if ((int) $has->fetchColumn()) continue;
$pdo->prepare('INSERT INTO plugin_schichtuebergabe_reads (note_id, staff_id, read_at) VALUES (?,?,?)')->execute([$id, (int) $staff['id'], db_now()]);
}
}
/** Wer hat die Notiz gelesen? @return list<string> */
function schicht_readers(PDO $pdo, int $noteId): array
{
$s = $pdo->prepare('SELECT s.name FROM plugin_schichtuebergabe_reads r JOIN staff s ON s.id = r.staff_id WHERE r.note_id = ? ORDER BY r.read_at');
$s->execute([$noteId]);
return array_map('strval', $s->fetchAll(PDO::FETCH_COLUMN));
}
/** Löschen darf der Verfasser und jeder Administrator. */
function schicht_delete(PDO $pdo, array $staff, int $id): bool
{
$s = $pdo->prepare('SELECT staff_id FROM plugin_schichtuebergabe_notes WHERE id = ?');
$s->execute([$id]);
$owner = $s->fetchColumn();
if ($owner === false) return false;
if (($staff['role'] ?? '') !== 'admin' && (int) $owner !== (int) $staff['id']) return false;
$pdo->prepare('DELETE FROM plugin_schichtuebergabe_reads WHERE note_id = ?')->execute([$id]);
$pdo->prepare('DELETE FROM plugin_schichtuebergabe_notes WHERE id = ?')->execute([$id]);
return true;
}
/** Aufräumen: Notizen nach Ablauf der Frist löschen (höchstens einmal pro Stunde). */
function schicht_cleanup(PDO $pdo): int
{
if (time() - (int) plugin_setting(SCHICHT, 'last_cleanup', '0') < 3600) return 0;
plugin_set_setting(SCHICHT, 'last_cleanup', (string) time());
$cut = date('Y-m-d H:i:s', time() - schicht_keep_days() * 86400);
$pdo->prepare('DELETE FROM plugin_schichtuebergabe_reads WHERE note_id IN (SELECT id FROM plugin_schichtuebergabe_notes WHERE created_at < ?)')->execute([$cut]);
$del = $pdo->prepare('DELETE FROM plugin_schichtuebergabe_notes WHERE created_at < ?');
$del->execute([$cut]);
return $del->rowCount();
}
/** Beschriftung des Menüpunkts mit Zähler. Ohne angemeldeten Mitarbeiter oder bei Fehlern der einfache Name. */
function schicht_menu_label(): string
{
try {
$staff = function_exists('current_staff') ? current_staff() : null;
if ($staff) {
$n = schicht_unread_count(get_db(), $staff);
if ($n > 0) return 'Schichtübergabe (' . $n . ' neu)';
}
} catch (Throwable $e) {
// Der Menüpunkt darf nie wegen eines Fehlers fehlen
}
return 'Schichtübergabe';
}
/** Wichtige Notizen für die Seitenleiste: ungelesene und die eigenen der letzten 12 Stunden (so sieht der Verfasser, dass sie ankommt). */
function schicht_sidebar_notes(PDO $pdo, array $staff): array
{
$byId = [];
foreach (schicht_unread($pdo, $staff, true) as $n) $byId[(int) $n['id']] = $n + ['own' => (int) $n['staff_id'] === (int) $staff['id']];
$own = $pdo->prepare("SELECT n.*, s.name AS author FROM plugin_schichtuebergabe_notes n LEFT JOIN staff s ON s.id = n.staff_id WHERE n.staff_id = ? AND n.is_important = 1 AND n.created_at >= ? ORDER BY n.id DESC LIMIT 5");
$own->execute([(int) $staff['id'], date('Y-m-d H:i:s', time() - 12 * 3600)]);
foreach ($own->fetchAll() as $n) $byId[(int) $n['id']] = $n + ['own' => true];
krsort($byId);
return array_values($byId);
}
/** HTML für die Seitenleiste eines Tickets: wichtige ungelesene Notizen und die eigenen wichtigen der letzten Stunden. */
function schicht_sidebar_html(PDO $pdo, ?array $staff): string
{
if (!$staff) return '';
$notes = schicht_sidebar_notes($pdo, $staff);
if (!$notes) return '';
$html = '<div class="card" style="margin-bottom:14px;border-left:4px solid #dc2626"><h3 style="margin:0 0 6px;font-size:.95rem">Wichtige Schichtübergabe</h3>';
foreach (array_slice($notes, 0, 3) as $n) {
$who = !empty($n['own']) ? 'Ihre Notiz' : (string) $n['author'];
$readers = !empty($n['own']) ? count(array_filter(schicht_readers($pdo, (int) $n['id']), fn($r) => $r !== (string) ($staff['name'] ?? ''))) : null;
$html .= '<p style="margin:0 0 8px;font-size:.85rem"><strong>' . e($who) . '</strong> <span class="muted">' . e(format_dt((string) $n['created_at'])) . ($readers !== null ? ' · von ' . $readers . ' gelesen' : '') . '</span><br>' . nl2br(e(mb_strimwidth((string) $n['message'], 0, 300, '…'))) . '</p>';
}
$html .= '<a class="btn secondary" href="/staff/plugin.php?p=' . SCHICHT . '&amp;page=schichtuebergabe">Alle Notizen öffnen</a></div>';
return $html;
}
/** Ausgeblendete eigene Notizen (je Mitarbeiter, nur die Bestätigung auf der Übersicht). @return list<int> */
function schicht_hidden_own(array $staff): array
{
$raw = (string) plugin_setting(SCHICHT, 'hidden_own_' . (int) $staff['id'], '');
return $raw === '' ? [] : array_map('intval', explode(',', $raw));
}
function schicht_hide_own(array $staff, int $noteId): void
{
$ids = array_slice(array_values(array_unique(array_merge(schicht_hidden_own($staff), [$noteId]))), -50);
plugin_set_setting(SCHICHT, 'hidden_own_' . (int) $staff['id'], implode(',', $ids));
}
/** Eigene Notizen der letzten 12 Stunden (für die Bestätigung auf der Übersicht), mit den Namen der Leser. */
function schicht_own_recent(PDO $pdo, array $staff): array
{
$s = $pdo->prepare('SELECT n.*, d.name AS department FROM plugin_schichtuebergabe_notes n LEFT JOIN departments d ON d.id = n.department_id WHERE n.staff_id = ? AND n.created_at >= ? ORDER BY n.id DESC LIMIT 5');
$s->execute([(int) $staff['id'], date('Y-m-d H:i:s', time() - 12 * 3600)]);
$hidden = schicht_hidden_own($staff);
$rows = array_values(array_filter($s->fetchAll(), fn($r) => !in_array((int) $r['id'], $hidden, true)));
foreach ($rows as &$r) $r['readers'] = array_values(array_filter(schicht_readers($pdo, (int) $r['id']), fn($n) => $n !== (string) ($staff['name'] ?? '')));
return $rows;
}
/**
* Deutlicher Hinweis oben auf den Seiten des Mitarbeiterbereichs (also schon direkt nach der Anmeldung):
* - solange es ungelesene Notizen anderer gibt, auf jeder Seite (rot bei wichtigen); auf der Seite der Schichtübergabe selbst entfällt er;
* - die eigenen Notizen der letzten 12 Stunden als ruhige Bestätigung auf der Übersicht, mit den Namen derer, die sie gelesen haben.
*/
function schicht_banner_html(PDO $pdo, ?array $staff, string $activePage = ''): string
{
if (!$staff || $activePage === 'plugin_' . SCHICHT) return '';
$html = '';
$notes = schicht_unread($pdo, $staff);
if ($notes) {
usort($notes, fn($a, $b) => [(int) $b['is_important'], (int) $b['id']] <=> [(int) $a['is_important'], (int) $a['id']]);
$important = count(array_filter($notes, fn($n) => (int) $n['is_important'] === 1));
$color = $important ? '#dc2626' : '#0f6f6a';
$bg = $important ? '#fef2f2' : '#ecfdf5';
$count = count($notes);
$html .= '<div class="schicht-banner" role="status" style="margin:0 0 16px;padding:14px 18px;border-radius:12px;border:1px solid ' . $color . ';border-left-width:6px;background:' . $bg . ';color:#16202a">'
. '<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">'
. '<strong style="font-size:1rem">' . ($important ? '⚠ ' : '') . 'Schichtübergabe: ' . $count . ($count === 1 ? ' neue Notiz' : ' neue Notizen') . ($important ? ' (' . $important . ' wichtig)' : '') . '</strong>'
. '<div style="display:flex;gap:8px;flex-wrap:wrap"><a class="btn secondary" href="/staff/plugin.php?p=' . SCHICHT . '&amp;page=schichtuebergabe">Öffnen</a>'
. '<form method="post" action="/staff/plugin.php?p=' . SCHICHT . '&amp;page=schichtuebergabe" style="margin:0">' . csrf_field() . '<input type="hidden" name="action" value="mark_all"><button class="btn" type="submit">✓ Gelesen</button></form></div></div>';
foreach (array_slice($notes, 0, 3) as $n) {
$html .= '<div style="margin-top:10px;font-size:.9rem;line-height:1.45"><strong>' . e((string) $n['author']) . '</strong>'
. ' <span style="color:#64748b;font-size:.8rem">' . e(format_dt((string) $n['created_at'])) . ((int) $n['is_important'] ? ' · wichtig' : '') . '</span><br>'
. nl2br(e(mb_strimwidth((string) $n['message'], 0, 400, '…'))) . '</div>';
}
if ($count > 3) $html .= '<div style="margin-top:8px;font-size:.85rem;color:#64748b">… und ' . ($count - 3) . ' weitere</div>';
$html .= '</div>';
}
// Eigene Notizen: nur auf der Übersicht, damit der Verfasser sieht, dass seine Notiz da ist und wer sie schon gelesen hat
if (in_array($activePage, ['reports', 'dashboard'], true)) {
foreach (schicht_own_recent($pdo, $staff) as $n) {
$readers = $n['readers'];
$html .= '<div class="schicht-banner schicht-banner-own" role="status" style="margin:0 0 16px;padding:12px 18px;border-radius:12px;border:1px solid #93c5fd;border-left-width:6px;background:#eff6ff;color:#16202a">'
. '<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">'
. '<strong style="font-size:.95rem">Ihre Notiz an die nächste Schicht' . ((int) $n['is_important'] ? ' (wichtig)' : '') . ' · ' . ($n['department'] ? 'Abteilung ' . e((string) $n['department']) : 'alle Mitarbeiter') . '</strong>'
. '<div style="display:flex;gap:8px;flex-wrap:wrap"><a class="btn secondary" href="/staff/plugin.php?p=' . SCHICHT . '&amp;page=schichtuebergabe">Alle Notizen</a>'
. '<form method="post" action="/staff/plugin.php?p=' . SCHICHT . '&amp;page=schichtuebergabe" style="margin:0">' . csrf_field() . '<input type="hidden" name="action" value="hide_own"><input type="hidden" name="id" value="' . (int) $n['id'] . '"><button class="btn" type="submit">Ausblenden</button></form></div></div>'
. '<div style="margin-top:8px;font-size:.9rem;line-height:1.45">' . nl2br(e(mb_strimwidth((string) $n['message'], 0, 400, '…'))) . '</div>'
. '<div style="margin-top:6px;font-size:.8rem;color:#64748b">' . e(format_dt((string) $n['created_at'])) . ' · ' . ($readers ? 'Gelesen von: ' . e(implode(', ', $readers)) : 'noch von niemandem gelesen') . '</div></div>';
}
}
return $html;
}