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 */ 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 = '

Wichtige Schichtübergabe

'; 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 .= '

' . e($who) . ' ' . e(format_dt((string) $n['created_at'])) . ($readers !== null ? ' · von ' . $readers . ' gelesen' : '') . '
' . nl2br(e(mb_strimwidth((string) $n['message'], 0, 300, '…'))) . '

'; } $html .= 'Alle Notizen öffnen
'; return $html; } /** Ausgeblendete eigene Notizen (je Mitarbeiter, nur die Bestätigung auf der Übersicht). @return list */ 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 .= '
' . '
' . '' . ($important ? '⚠ ' : '') . 'Schichtübergabe: ' . $count . ($count === 1 ? ' neue Notiz' : ' neue Notizen') . ($important ? ' (' . $important . ' wichtig)' : '') . '' . '
Öffnen' . '
' . csrf_field() . '
'; foreach (array_slice($notes, 0, 3) as $n) { $html .= '
' . e((string) $n['author']) . '' . ' ' . e(format_dt((string) $n['created_at'])) . ((int) $n['is_important'] ? ' · wichtig' : '') . '
' . nl2br(e(mb_strimwidth((string) $n['message'], 0, 400, '…'))) . '
'; } if ($count > 3) $html .= '
… und ' . ($count - 3) . ' weitere
'; $html .= '
'; } // 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 .= '
' . '
' . 'Ihre Notiz an die nächste Schicht' . ((int) $n['is_important'] ? ' (wichtig)' : '') . ' · ' . ($n['department'] ? 'Abteilung ' . e((string) $n['department']) : 'alle Mitarbeiter') . '' . '
Alle Notizen' . '
' . csrf_field() . '
' . '
' . nl2br(e(mb_strimwidth((string) $n['message'], 0, 400, '…'))) . '
' . '
' . e(format_dt((string) $n['created_at'])) . ' · ' . ($readers ? 'Gelesen von: ' . e(implode(', ', $readers)) : 'noch von niemandem gelesen') . '
'; } } return $html; }