Messenger-artiges Benachrichtigungssystem für Dolibarr: - Schwebendes FAB (Floating Action Button) unten links - Ausklappbares Panel mit allen Benachrichtigungen - Historie der gelesenen Nachrichten - Direktes Abhaken per Checkbox - Click-to-Navigate für Aktionen - Pulsierender Button bei dringenden Nachrichten - Draggable Panel-Header - Automatische Erkennung hängender Cron-Jobs - API für andere Module: GlobalNotify::error(), ::warning(), etc. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
372 lines
9.9 KiB
PHP
372 lines
9.9 KiB
PHP
<?php
|
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*/
|
|
|
|
/**
|
|
* \file globalnotify/class/globalnotify.class.php
|
|
* \ingroup globalnotify
|
|
* \brief Global notification manager class
|
|
*/
|
|
|
|
/**
|
|
* Class GlobalNotify
|
|
* Manages global notifications from all modules
|
|
*/
|
|
class GlobalNotify
|
|
{
|
|
/**
|
|
* @var DoliDB Database handler
|
|
*/
|
|
public $db;
|
|
|
|
/**
|
|
* Notification types with styling
|
|
*/
|
|
const TYPE_ERROR = 'error';
|
|
const TYPE_WARNING = 'warning';
|
|
const TYPE_INFO = 'info';
|
|
const TYPE_SUCCESS = 'success';
|
|
const TYPE_ACTION = 'action'; // Requires user action
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param DoliDB $db Database handler
|
|
*/
|
|
public function __construct($db)
|
|
{
|
|
$this->db = $db;
|
|
}
|
|
|
|
/**
|
|
* Add a notification
|
|
*
|
|
* @param string $module Module name (e.g., 'bankimport', 'importzugferd')
|
|
* @param string $type Notification type (error, warning, info, success, action)
|
|
* @param string $title Short title
|
|
* @param string $message Detailed message
|
|
* @param string $actionUrl URL for action button (optional)
|
|
* @param string $actionLabel Label for action button (optional)
|
|
* @param int $priority Priority 1-10 (10 = highest)
|
|
* @param int $userId Specific user ID or 0 for all admins
|
|
* @return int Notification ID or -1 on error
|
|
*/
|
|
public function addNotification($module, $type, $title, $message, $actionUrl = '', $actionLabel = '', $priority = 5, $userId = 0)
|
|
{
|
|
global $conf;
|
|
|
|
// Store in llx_const as JSON array
|
|
$key = 'GLOBALNOTIFY_'.strtoupper($module);
|
|
|
|
$notification = array(
|
|
'id' => uniqid($module.'_'),
|
|
'module' => $module,
|
|
'type' => $type,
|
|
'title' => $title,
|
|
'message' => $message,
|
|
'action_url' => $actionUrl,
|
|
'action_label' => $actionLabel,
|
|
'priority' => $priority,
|
|
'user_id' => $userId,
|
|
'created' => time(),
|
|
'read' => false
|
|
);
|
|
|
|
// Get existing notifications for this module
|
|
$existing = $this->getModuleNotifications($module);
|
|
$existing[] = $notification;
|
|
|
|
// Keep only last 50 notifications per module
|
|
if (count($existing) > 50) {
|
|
$existing = array_slice($existing, -50);
|
|
}
|
|
|
|
$result = dolibarr_set_const($this->db, $key, json_encode($existing), 'chaine', 0, '', $conf->entity);
|
|
|
|
return $result > 0 ? $notification['id'] : -1;
|
|
}
|
|
|
|
/**
|
|
* Get notifications for a specific module
|
|
*
|
|
* @param string $module Module name
|
|
* @return array Notifications
|
|
*/
|
|
public function getModuleNotifications($module)
|
|
{
|
|
$key = 'GLOBALNOTIFY_'.strtoupper($module);
|
|
$data = getDolGlobalString($key);
|
|
|
|
if (empty($data)) {
|
|
return array();
|
|
}
|
|
|
|
$notifications = json_decode($data, true);
|
|
return is_array($notifications) ? $notifications : array();
|
|
}
|
|
|
|
/**
|
|
* Get all active notifications for current user
|
|
*
|
|
* @param int $userId User ID
|
|
* @param bool $unreadOnly Only return unread notifications
|
|
* @return array All notifications sorted by priority and date
|
|
*/
|
|
public function getAllNotifications($userId = 0, $unreadOnly = true)
|
|
{
|
|
global $user;
|
|
|
|
if ($userId == 0) {
|
|
$userId = $user->id;
|
|
}
|
|
|
|
$allNotifications = array();
|
|
|
|
// Get all notification constants (exclude internal settings)
|
|
$sql = "SELECT name, value FROM ".MAIN_DB_PREFIX."const WHERE name LIKE 'GLOBALNOTIFY_%' AND name NOT LIKE 'GLOBALNOTIFY_CRON%' AND value != '' AND value LIKE '[%'";
|
|
$resql = $this->db->query($sql);
|
|
|
|
if ($resql) {
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
$notifications = json_decode($obj->value, true);
|
|
if (is_array($notifications)) {
|
|
foreach ($notifications as $notif) {
|
|
// Filter by user if specified in notification
|
|
if (!empty($notif['user_id']) && $notif['user_id'] != $userId) {
|
|
continue;
|
|
}
|
|
// Filter by read status
|
|
if ($unreadOnly && !empty($notif['read'])) {
|
|
continue;
|
|
}
|
|
$allNotifications[] = $notif;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort by priority (desc) and date (desc)
|
|
usort($allNotifications, function($a, $b) {
|
|
if ($a['priority'] != $b['priority']) {
|
|
return $b['priority'] - $a['priority'];
|
|
}
|
|
return $b['created'] - $a['created'];
|
|
});
|
|
|
|
return $allNotifications;
|
|
}
|
|
|
|
/**
|
|
* Get read notifications for history display
|
|
*
|
|
* @param int $userId User ID
|
|
* @param int $limit Maximum number to return
|
|
* @return array Read notifications sorted by date (newest first)
|
|
*/
|
|
public function getReadNotifications($userId = 0, $limit = 20)
|
|
{
|
|
global $user;
|
|
|
|
if ($userId == 0) {
|
|
$userId = $user->id;
|
|
}
|
|
|
|
$readNotifications = array();
|
|
|
|
// Get all notification constants (exclude internal settings)
|
|
$sql = "SELECT name, value FROM ".MAIN_DB_PREFIX."const WHERE name LIKE 'GLOBALNOTIFY_%' AND name NOT LIKE 'GLOBALNOTIFY_CRON%' AND value != '' AND value LIKE '[%'";
|
|
$resql = $this->db->query($sql);
|
|
|
|
if ($resql) {
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
$notifications = json_decode($obj->value, true);
|
|
if (is_array($notifications)) {
|
|
foreach ($notifications as $notif) {
|
|
// Filter by user if specified
|
|
if (!empty($notif['user_id']) && $notif['user_id'] != $userId) {
|
|
continue;
|
|
}
|
|
// Only read notifications
|
|
if (!empty($notif['read'])) {
|
|
$readNotifications[] = $notif;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort by date (newest first)
|
|
usort($readNotifications, function($a, $b) {
|
|
return $b['created'] - $a['created'];
|
|
});
|
|
|
|
// Limit results
|
|
return array_slice($readNotifications, 0, $limit);
|
|
}
|
|
|
|
/**
|
|
* Mark notification as read
|
|
*
|
|
* @param string $notificationId Notification ID
|
|
* @return bool Success
|
|
*/
|
|
public function markAsRead($notificationId)
|
|
{
|
|
global $conf;
|
|
|
|
// Find which module this notification belongs to
|
|
$sql = "SELECT name, value FROM ".MAIN_DB_PREFIX."const WHERE name LIKE 'GLOBALNOTIFY_%' AND value LIKE '%".addslashes($notificationId)."%'";
|
|
$resql = $this->db->query($sql);
|
|
|
|
if ($resql && $obj = $this->db->fetch_object($resql)) {
|
|
$notifications = json_decode($obj->value, true);
|
|
if (is_array($notifications)) {
|
|
foreach ($notifications as &$notif) {
|
|
if ($notif['id'] == $notificationId) {
|
|
$notif['read'] = true;
|
|
break;
|
|
}
|
|
}
|
|
dolibarr_set_const($this->db, $obj->name, json_encode($notifications), 'chaine', 0, '', $conf->entity);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Mark all notifications as read
|
|
*
|
|
* @param string $module Optional: only for specific module
|
|
* @return int Number of notifications marked as read
|
|
*/
|
|
public function markAllAsRead($module = '')
|
|
{
|
|
global $conf;
|
|
|
|
$count = 0;
|
|
$sql = "SELECT name, value FROM ".MAIN_DB_PREFIX."const WHERE name LIKE 'GLOBALNOTIFY_%'";
|
|
if (!empty($module)) {
|
|
$sql .= " AND name = 'GLOBALNOTIFY_".strtoupper($module)."'";
|
|
}
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
$notifications = json_decode($obj->value, true);
|
|
if (is_array($notifications)) {
|
|
$changed = false;
|
|
foreach ($notifications as &$notif) {
|
|
if (empty($notif['read'])) {
|
|
$notif['read'] = true;
|
|
$changed = true;
|
|
$count++;
|
|
}
|
|
}
|
|
if ($changed) {
|
|
dolibarr_set_const($this->db, $obj->name, json_encode($notifications), 'chaine', 0, '', $conf->entity);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* Delete a notification
|
|
*
|
|
* @param string $notificationId Notification ID
|
|
* @return bool Success
|
|
*/
|
|
public function deleteNotification($notificationId)
|
|
{
|
|
global $conf;
|
|
|
|
$sql = "SELECT name, value FROM ".MAIN_DB_PREFIX."const WHERE name LIKE 'GLOBALNOTIFY_%' AND value LIKE '%".addslashes($notificationId)."%'";
|
|
$resql = $this->db->query($sql);
|
|
|
|
if ($resql && $obj = $this->db->fetch_object($resql)) {
|
|
$notifications = json_decode($obj->value, true);
|
|
if (is_array($notifications)) {
|
|
$notifications = array_filter($notifications, function($n) use ($notificationId) {
|
|
return $n['id'] != $notificationId;
|
|
});
|
|
dolibarr_set_const($this->db, $obj->name, json_encode(array_values($notifications)), 'chaine', 0, '', $conf->entity);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Clear all notifications for a module
|
|
*
|
|
* @param string $module Module name
|
|
* @return bool Success
|
|
*/
|
|
public function clearModuleNotifications($module)
|
|
{
|
|
global $conf;
|
|
|
|
$key = 'GLOBALNOTIFY_'.strtoupper($module);
|
|
return dolibarr_del_const($this->db, $key, $conf->entity) >= 0;
|
|
}
|
|
|
|
/**
|
|
* Get unread count for badge display
|
|
*
|
|
* @return int Number of unread notifications
|
|
*/
|
|
public function getUnreadCount()
|
|
{
|
|
return count($this->getAllNotifications(0, true));
|
|
}
|
|
|
|
/**
|
|
* Helper: Add error notification
|
|
*/
|
|
public static function error($module, $title, $message, $actionUrl = '', $actionLabel = '')
|
|
{
|
|
global $db;
|
|
$notify = new self($db);
|
|
return $notify->addNotification($module, self::TYPE_ERROR, $title, $message, $actionUrl, $actionLabel, 10);
|
|
}
|
|
|
|
/**
|
|
* Helper: Add warning notification
|
|
*/
|
|
public static function warning($module, $title, $message, $actionUrl = '', $actionLabel = '')
|
|
{
|
|
global $db;
|
|
$notify = new self($db);
|
|
return $notify->addNotification($module, self::TYPE_WARNING, $title, $message, $actionUrl, $actionLabel, 7);
|
|
}
|
|
|
|
/**
|
|
* Helper: Add info notification
|
|
*/
|
|
public static function info($module, $title, $message, $actionUrl = '', $actionLabel = '')
|
|
{
|
|
global $db;
|
|
$notify = new self($db);
|
|
return $notify->addNotification($module, self::TYPE_INFO, $title, $message, $actionUrl, $actionLabel, 3);
|
|
}
|
|
|
|
/**
|
|
* Helper: Add action required notification
|
|
*/
|
|
public static function actionRequired($module, $title, $message, $actionUrl, $actionLabel = 'Aktion erforderlich')
|
|
{
|
|
global $db;
|
|
$notify = new self($db);
|
|
return $notify->addNotification($module, self::TYPE_ACTION, $title, $message, $actionUrl, $actionLabel, 9);
|
|
}
|
|
}
|