* * 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/actions_globalnotify.class.php * \ingroup globalnotify * \brief Hook class for displaying notifications in top bar */ require_once DOL_DOCUMENT_ROOT.'/core/class/commonhookactions.class.php'; dol_include_once('/globalnotify/class/globalnotify.class.php'); /** * Class ActionsGlobalNotify * Hook actions for GlobalNotify module */ class ActionsGlobalNotify extends CommonHookActions { /** * @var DoliDB Database handler */ public $db; /** * @var string Error message */ public $error = ''; /** * @var array Error messages */ public $errors = array(); /** * @var array Results */ public $results = array(); /** * @var string Returned string */ public $resprints = ''; /** * Constructor * * @param DoliDB $db Database handler */ public function __construct($db) { $this->db = $db; } /** * Hook to add floating notification widget * * @param array $parameters Parameters * @param object $object Object * @param string $action Action * @return int 0=OK, >0=KO */ public function printTopRightMenu($parameters, &$object, &$action) { global $conf, $user, $langs; if (!$user->admin) { return 0; // Only show to admins } $langs->load('globalnotify@globalnotify'); $notify = new GlobalNotify($this->db); $unreadNotifications = $notify->getAllNotifications($user->id, true); $readNotifications = $notify->getReadNotifications($user->id, 20); $unreadCount = count($unreadNotifications); $readCount = count($readNotifications); $totalCount = $unreadCount + $readCount; // Check for urgent notifications $hasUrgent = false; foreach ($unreadNotifications as $n) { if (in_array($n['type'], array('error', 'action'))) { $hasUrgent = true; break; } } // Build floating widget HTML $html = ''; // Floating button (always visible, bottom-left corner) $html .= '
'; // Draggable handle + toggle button $html .= '
'; $html .= ''; if ($unreadCount > 0) { $html .= ''.$unreadCount.''; } $html .= '
'; // Floating panel (expandable) $html .= ''; // panel $html .= '
'; // widget $this->resprints = $html; return 0; } /** * Render a single notification item * * @param array $notif Notification data * @param bool $isRead Is this a read notification * @return string HTML */ private function renderNotificationItem($notif, $isRead = false) { $typeClass = 'globalnotify-item globalnotify-item-'.$notif['type']; if ($isRead) { $typeClass .= ' globalnotify-item-read'; } $html = '
'; // Checkbox for quick action $html .= '
'; if ($isRead) { $html .= ''; } else { $html .= ''; } $html .= '
'; // Content area (clickable if has action) $hasAction = !empty($notif['action_url']); $contentClass = 'globalnotify-item-content'.($hasAction ? ' globalnotify-clickable' : ''); $onclick = $hasAction ? ' onclick="GlobalNotify.goToAction(\''.$notif['id'].'\', \''.dol_escape_htmltag($notif['action_url']).'\')"' : ''; $html .= '
'; // Icon $icon = 'info-circle'; if ($notif['type'] == 'error') $icon = 'exclamation-circle'; elseif ($notif['type'] == 'warning') $icon = 'exclamation-triangle'; elseif ($notif['type'] == 'success') $icon = 'check-circle'; elseif ($notif['type'] == 'action') $icon = 'hand-point-right'; $html .= '
'; $html .= '
'; $html .= '
'.dol_escape_htmltag($notif['title']).'
'; $html .= '
'.dol_escape_htmltag($notif['message']).'
'; $html .= '
'; $html .= ''.dol_escape_htmltag($notif['module']).''; $html .= ''.dol_print_date($notif['created'], 'dayhour').''; $html .= '
'; $html .= '
'; $html .= '
'; // content $html .= '
'; // item return $html; } /** * Hook called on every page load - collect notifications from modules * * @param array $parameters Parameters * @param object $object Object * @param string $action Action * @return int 0=OK */ public function addMoreActionsButtons($parameters, &$object, &$action) { global $conf, $user; if (!$user->admin) { return 0; } // Check ALL stuck cron jobs (generic check) $this->checkStuckCronJobs(); // Check BankImport cron status (module-specific) if (isModEnabled('bankimport')) { $this->checkBankImportStatus(); } // Check ImportZugferd cron status (module-specific) if (isModEnabled('importzugferd')) { $this->checkImportZugferdStatus(); } return 0; } /** * Check all cron jobs for stuck/hanging state * This is the generic check that works for all modules */ private function checkStuckCronJobs() { global $conf; // Only check once per minute (cache) $lastCheck = getDolGlobalInt('GLOBALNOTIFY_CRON_LASTCHECK'); if ($lastCheck > (time() - 60)) { return; } dolibarr_set_const($this->db, 'GLOBALNOTIFY_CRON_LASTCHECK', time(), 'chaine', 0, '', $conf->entity); // Find all stuck cron jobs (processing=1 for more than 30 minutes) $sql = "SELECT rowid, label, module_name, datelastrun, processing FROM ".MAIN_DB_PREFIX."cronjob WHERE processing = 1 AND status = 1 AND datelastrun < DATE_SUB(NOW(), INTERVAL 30 MINUTE)"; $resql = $this->db->query($sql); if (!$resql) { return; } $notify = new GlobalNotify($this->db); while ($obj = $this->db->fetch_object($resql)) { $module = !empty($obj->module_name) ? strtolower($obj->module_name) : 'cron'; $lastRun = $this->db->jdate($obj->datelastrun); $stuckMinutes = round((time() - $lastRun) / 60); // Check if we already have an active notification for this job $notifKey = 'cronjob_stuck_'.$obj->rowid; $existing = $notify->getModuleNotifications('cron'); $hasNotif = false; foreach ($existing as $n) { if (strpos($n['id'], $notifKey) !== false && !$n['read']) { $hasNotif = true; break; } } if (!$hasNotif) { $notify->addNotification( 'cron', GlobalNotify::TYPE_ERROR, 'Cron-Job hängt: '.$obj->label, "Der Job läuft seit {$stuckMinutes} Minuten ohne Antwort. Modul: {$module}", dol_buildpath('/cron/list.php', 1), 'Cron-Jobs anzeigen', 10, // highest priority 0 ); } } // Also check for jobs that haven't run when they should have // (datenextrun is in the past but job hasn't started) $sql = "SELECT rowid, label, module_name, datenextrun, datelastrun FROM ".MAIN_DB_PREFIX."cronjob WHERE processing = 0 AND status = 1 AND datenextrun < DATE_SUB(NOW(), INTERVAL 2 HOUR)"; $resql = $this->db->query($sql); if ($resql) { while ($obj = $this->db->fetch_object($resql)) { $module = !empty($obj->module_name) ? strtolower($obj->module_name) : 'cron'; $missedTime = $this->db->jdate($obj->datenextrun); $missedHours = round((time() - $missedTime) / 3600); $existing = $notify->getModuleNotifications('cron'); $hasNotif = false; foreach ($existing as $n) { if (strpos($n['message'], $obj->label) !== false && strpos($n['title'], 'verpasst') !== false && !$n['read']) { $hasNotif = true; break; } } if (!$hasNotif) { $notify->addNotification( 'cron', GlobalNotify::TYPE_WARNING, 'Cron-Job verpasst', "Job '{$obj->label}' hätte vor {$missedHours} Stunden laufen sollen. Prüfe Cron-Aufruf.", dol_buildpath('/cron/list.php', 1), 'Cron-Jobs prüfen', 7, 0 ); } } } } /** * Check BankImport module for issues */ private function checkBankImportStatus() { dol_include_once('/bankimport/class/bankimportcron.class.php'); if (!class_exists('BankImportCron')) { return; } $status = BankImportCron::getCronStatus(); $notify = new GlobalNotify($this->db); // Clear old notifications first $existing = $notify->getModuleNotifications('bankimport'); // Check for issues if ($status['paused']) { // Check if we already have this notification $hasNotif = false; foreach ($existing as $n) { if ($n['type'] == 'warning' && strpos($n['title'], 'pausiert') !== false && !$n['read']) { $hasNotif = true; break; } } if (!$hasNotif) { GlobalNotify::warning( 'bankimport', 'BankImport Cron pausiert', $status['pause_reason'], dol_buildpath('/bankimport/admin/cronmonitor.php', 1), 'Cron Monitor öffnen' ); } } if (!empty($status['notification'])) { $notifType = $status['notification']['type']; $messages = array( 'tan_required' => array('TAN erforderlich', 'Bank-Login erfordert TAN-Bestätigung'), 'login_error' => array('Login-Fehler', 'Bank-Login fehlgeschlagen'), 'fetch_error' => array('Abruf-Fehler', 'Kontoauszüge konnten nicht abgerufen werden'), 'session_expired' => array('Session abgelaufen', 'Bank-Session ist abgelaufen, neuer Login erforderlich'), ); if (isset($messages[$notifType])) { $hasNotif = false; foreach ($existing as $n) { if (strpos($n['title'], $messages[$notifType][0]) !== false && !$n['read']) { $hasNotif = true; break; } } if (!$hasNotif) { if ($notifType == 'tan_required') { GlobalNotify::actionRequired( 'bankimport', $messages[$notifType][0], $messages[$notifType][1], dol_buildpath('/bankimport/fetch.php', 1), 'TAN eingeben' ); } else { GlobalNotify::error( 'bankimport', $messages[$notifType][0], $messages[$notifType][1], dol_buildpath('/bankimport/admin/cronmonitor.php', 1), 'Details anzeigen' ); } } } } } /** * Check ImportZugferd module for issues */ private function checkImportZugferdStatus() { // Similar check for ImportZugferd can be added here // Check if cron job is stuck $sql = "SELECT processing, datelastrun FROM ".MAIN_DB_PREFIX."cronjob WHERE label = 'ImportZugferdScheduled' AND processing = 1"; $resql = $this->db->query($sql); if ($resql && $obj = $this->db->fetch_object($resql)) { $lastRun = $this->db->jdate($obj->datelastrun); // If running for more than 30 minutes, it's probably stuck if ($lastRun < (time() - 1800)) { $notify = new GlobalNotify($this->db); $existing = $notify->getModuleNotifications('importzugferd'); $hasNotif = false; foreach ($existing as $n) { if (strpos($n['title'], 'hängt') !== false && !$n['read']) { $hasNotif = true; break; } } if (!$hasNotif) { GlobalNotify::error( 'importzugferd', 'ImportZugferd Cron hängt', 'Der Cron-Job läuft seit '.dol_print_date($lastRun, 'dayhour').' und reagiert nicht mehr.', dol_buildpath('/importzugferd/admin/setup.php', 1), 'Einstellungen öffnen' ); } } } } }