From 7f011424bba4877f6569e4555b2328e8fec0d488 Mon Sep 17 00:00:00 2001 From: data Date: Sun, 15 Feb 2026 20:39:53 +0100 Subject: [PATCH] Version 1.5: Dashboard-Widget und Browser-Benachrichtigungen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dashboard-Widget (Box) zeigt offene Zuordnungen auf der Dolibarr-Startseite - Browser-Benachrichtigungen: Automatische Push-Notifications wenn neue Zahlungseingänge per Cron-Import erkannt werden - AJAX-Endpoint checkpending.php für Notification-Polling (alle 5 Min.) - JavaScript läuft auf jeder Dolibarr-Seite und benachrichtigt bei neuen Buchungen mit Betrag und Anzahl - Klick auf Notification öffnet direkt die Zahlungsabgleich-Seite Co-Authored-By: Claude Opus 4.6 --- ajax/checkpending.php | 102 +++++++++++++++ core/boxes/box_bankimport_pending.php | 167 ++++++++++++++++++++++++ core/modules/modBankImport.class.php | 15 ++- js/bankimport_notify.js.php | 174 ++++++++++++++++++++++++++ langs/de_DE/bankimport.lang | 5 + langs/en_US/bankimport.lang | 5 + 6 files changed, 460 insertions(+), 8 deletions(-) create mode 100644 ajax/checkpending.php create mode 100644 core/boxes/box_bankimport_pending.php create mode 100644 js/bankimport_notify.js.php diff --git a/ajax/checkpending.php b/ajax/checkpending.php new file mode 100644 index 0000000..8ccb6b5 --- /dev/null +++ b/ajax/checkpending.php @@ -0,0 +1,102 @@ + + * + * 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. + */ + +/** + * AJAX endpoint to check for pending bank transaction matches + * Used by browser notification system + */ + +if (!defined('NOTOKENRENEWAL')) { + define('NOTOKENRENEWAL', '1'); +} +if (!defined('NOREQUIREMENU')) { + define('NOREQUIREMENU', '1'); +} +if (!defined('NOREQUIREHTML')) { + define('NOREQUIREHTML', '1'); +} +if (!defined('NOREQUIREAJAX')) { + define('NOREQUIREAJAX', '1'); +} + +// Load Dolibarr environment +$res = 0; +if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) { + $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php"; +} +$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; +$tmp2 = realpath(__FILE__); +$i = strlen($tmp) - 1; +$j = strlen($tmp2) - 1; +while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { + $i--; + $j--; +} +if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) { + $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; +} +if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) { + $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; +} +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res && file_exists("../../../main.inc.php")) { + $res = @include "../../../main.inc.php"; +} +if (!$res) { + die("Include of main fails"); +} + +header('Content-Type: application/json'); + +// Security check +if (!$user->hasRight('bankimport', 'read')) { + echo json_encode(array('error' => 'access_denied')); + exit; +} + +$bankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID'); +if (empty($bankAccountId)) { + echo json_encode(array('pending' => 0, 'incoming' => 0)); + exit; +} + +// Count new unmatched transactions (incoming payments = positive amount) +$sqlIncoming = "SELECT COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total"; +$sqlIncoming .= " FROM ".MAIN_DB_PREFIX."bankimport_transaction"; +$sqlIncoming .= " WHERE entity IN (".getEntity('banktransaction').")"; +$sqlIncoming .= " AND status = 0 AND amount > 0"; +$resIncoming = $db->query($sqlIncoming); +$incoming = 0; +$incomingTotal = 0; +if ($resIncoming) { + $obj = $db->fetch_object($resIncoming); + $incoming = (int) $obj->cnt; + $incomingTotal = (float) $obj->total; +} + +// Count all new transactions +$sqlAll = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."bankimport_transaction"; +$sqlAll .= " WHERE entity IN (".getEntity('banktransaction').")"; +$sqlAll .= " AND status = 0"; +$resAll = $db->query($sqlAll); +$pending = 0; +if ($resAll) { + $obj = $db->fetch_object($resAll); + $pending = (int) $obj->cnt; +} + +echo json_encode(array( + 'pending' => $pending, + 'incoming' => $incoming, + 'incoming_total' => $incomingTotal, +)); + +$db->close(); diff --git a/core/boxes/box_bankimport_pending.php b/core/boxes/box_bankimport_pending.php new file mode 100644 index 0000000..3152eb4 --- /dev/null +++ b/core/boxes/box_bankimport_pending.php @@ -0,0 +1,167 @@ + + * + * 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. + */ + +include_once DOL_DOCUMENT_ROOT.'/core/boxes/modules_boxes.php'; + +/** + * Dashboard widget showing pending bank transaction matches + */ +class box_bankimport_pending extends ModeleBoxes +{ + public $boxcode = "bankimport_pending"; + public $boximg = "fa-money-check-alt"; + public $boxlabel = "BoxBankImportPending"; + public $depends = array("bankimport"); + + /** + * Constructor + * + * @param DoliDB $db Database handler + * @param string $param More parameters + */ + public function __construct($db, $param = '') + { + global $user; + $this->db = $db; + $this->hidden = !$user->hasRight('bankimport', 'read'); + } + + /** + * Load data into info_box_contents array to show on dashboard + * + * @param int $max Maximum number of records to load + * @return void + */ + public function loadBox($max = 5) + { + global $user, $langs, $conf; + + $langs->loadLangs(array("bankimport@bankimport", "banks")); + + $this->max = $max; + + // Box header + $this->info_box_head = array( + 'text' => $langs->trans("BoxBankImportPending"), + 'sublink' => dol_buildpath('/bankimport/confirm.php', 1).'?mainmenu=bank&leftmenu=bankimport', + 'subtext' => $langs->trans("ReviewAndConfirm"), + 'subpicto' => 'payment', + ); + + $line = 0; + $bankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID'); + + if (empty($bankAccountId)) { + // No bank account configured + $this->info_box_contents[$line][] = array( + 'td' => 'class="center" colspan="4"', + 'text' => img_warning().' '.$langs->trans("NoBankAccountConfigured"), + 'url' => dol_buildpath('/bankimport/admin/setup.php', 1), + ); + return; + } + + // Count new (unmatched) transactions + $sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."bankimport_transaction"; + $sql .= " WHERE entity IN (".getEntity('banktransaction').")"; + $sql .= " AND status = 0"; + $resql = $this->db->query($sql); + $newCount = 0; + if ($resql) { + $obj = $this->db->fetch_object($resql); + $newCount = (int) $obj->cnt; + } + + if ($newCount > 0) { + // Summary line: X transactions pending + $this->info_box_contents[$line][] = array( + 'td' => 'class="left" colspan="3"', + 'text' => ''.$langs->trans("PendingPaymentMatches", $newCount).'', + 'asis' => 1, + ); + $this->info_box_contents[$line][] = array( + 'td' => 'class="right"', + 'text' => ''.$langs->trans("ReviewAndConfirm").'', + 'asis' => 1, + ); + $line++; + + // Show last few unmatched transactions + $sql2 = "SELECT t.rowid, t.date_trans, t.name, t.amount, t.currency"; + $sql2 .= " FROM ".MAIN_DB_PREFIX."bankimport_transaction as t"; + $sql2 .= " WHERE t.entity IN (".getEntity('banktransaction').")"; + $sql2 .= " AND t.status = 0 AND t.amount > 0"; + $sql2 .= " ORDER BY t.date_trans DESC"; + $sql2 .= $this->db->plimit($max, 0); + + $resql2 = $this->db->query($sql2); + if ($resql2) { + $num = $this->db->num_rows($resql2); + $i = 0; + while ($i < $num && $line < $max + 1) { + $obj2 = $this->db->fetch_object($resql2); + if (!$obj2) { + break; + } + + // Date + $this->info_box_contents[$line][] = array( + 'td' => 'class="nowraponall"', + 'text' => dol_print_date($this->db->jdate($obj2->date_trans), 'day'), + ); + + // Name + $this->info_box_contents[$line][] = array( + 'td' => 'class="tdoverflowmax150"', + 'text' => dol_trunc($obj2->name, 28), + 'url' => dol_buildpath('/bankimport/card.php', 1).'?id='.$obj2->rowid.'&mainmenu=bank&leftmenu=bankimport', + ); + + // Amount + $amountColor = $obj2->amount >= 0 ? 'color: green;' : 'color: red;'; + $amountPrefix = $obj2->amount >= 0 ? '+' : ''; + $this->info_box_contents[$line][] = array( + 'td' => 'class="right nowraponall"', + 'text' => ''.$amountPrefix.price($obj2->amount, 0, $langs, 1, -1, 2, $obj2->currency).'', + 'asis' => 1, + ); + + // Status badge + $this->info_box_contents[$line][] = array( + 'td' => 'class="right"', + 'text' => ''.$langs->trans("New").'', + 'asis' => 1, + ); + + $line++; + $i++; + } + } + } else { + // No pending transactions + $this->info_box_contents[$line][] = array( + 'td' => 'class="center opacitymedium" colspan="4"', + 'text' => $langs->trans("NoNewMatchesFound"), + ); + } + } + + /** + * Render the box + * + * @param array|null $head Optional head array + * @param array|null $contents Optional contents array + * @param int $nooutput No print, return output + * @return string + */ + public function showBox($head = null, $contents = null, $nooutput = 0) + { + return parent::showBox($this->info_box_head, $this->info_box_contents, $nooutput); + } +} diff --git a/core/modules/modBankImport.class.php b/core/modules/modBankImport.class.php index f7fb10a..c330b3b 100755 --- a/core/modules/modBankImport.class.php +++ b/core/modules/modBankImport.class.php @@ -76,7 +76,7 @@ class modBankImport extends DolibarrModules $this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@bankimport' // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' - $this->version = '1.4'; + $this->version = '1.5'; // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; @@ -115,7 +115,7 @@ class modBankImport extends DolibarrModules ), // Set this to relative path of js file if module must load a js on all pages 'js' => array( - // '/bankimport/js/bankimport.js.php', + '/bankimport/js/bankimport_notify.js.php', ), // Set here all hooks context managed by module. To find available hook context, make a "grep -r '>initHooks(' *" on source code. You can also set hook context to 'all' /* BEGIN MODULEBUILDER HOOKSCONTEXTS */ @@ -255,12 +255,11 @@ class modBankImport extends DolibarrModules // Add here list of php file(s) stored in bankimport/core/boxes that contains a class to show a widget. /* BEGIN MODULEBUILDER WIDGETS */ $this->boxes = array( - // 0 => array( - // 'file' => 'bankimportwidget1.php@bankimport', - // 'note' => 'Widget provided by BankImport', - // 'enabledbydefaulton' => 'Home', - // ), - // ... + 0 => array( + 'file' => 'box_bankimport_pending.php@bankimport', + 'note' => 'Pending bank transaction matches', + 'enabledbydefaulton' => 'Home', + ), ); /* END MODULEBUILDER WIDGETS */ diff --git a/js/bankimport_notify.js.php b/js/bankimport_notify.js.php new file mode 100644 index 0000000..75241a4 --- /dev/null +++ b/js/bankimport_notify.js.php @@ -0,0 +1,174 @@ + + * + * 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. + */ + +/** + * JavaScript for browser push notifications about incoming payments + * Loaded on every Dolibarr page via module_parts['js'] + */ + +// Define MIME type +if (!defined('NOTOKENRENEWAL')) { + define('NOTOKENRENEWAL', '1'); +} +if (!defined('NOREQUIREMENU')) { + define('NOREQUIREMENU', '1'); +} +if (!defined('NOREQUIREHTML')) { + define('NOREQUIREHTML', '1'); +} +if (!defined('NOREQUIREAJAX')) { + define('NOREQUIREAJAX', '1'); +} + +$res = 0; +if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) { + $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php"; +} +$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; +$tmp2 = realpath(__FILE__); +$i = strlen($tmp) - 1; +$j = strlen($tmp2) - 1; +while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { + $i--; + $j--; +} +if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) { + $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; +} +if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) { + $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; +} +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res && file_exists("../../../main.inc.php")) { + $res = @include "../../../main.inc.php"; +} + +header('Content-Type: application/javascript; charset=UTF-8'); +header('Cache-Control: max-age=3600'); + +if (!isModEnabled('bankimport') || empty($user->id) || !$user->hasRight('bankimport', 'read')) { + echo '/* bankimport: no access */'; + exit; +} + +$checkUrl = dol_buildpath('/bankimport/ajax/checkpending.php', 1); +$confirmUrl = dol_buildpath('/bankimport/confirm.php', 1).'?mainmenu=bank&leftmenu=bankimport'; +$checkInterval = 5 * 60 * 1000; // 5 Minuten +?> +(function() { + 'use strict'; + + var STORAGE_KEY = 'bankimport_last_pending'; + var CHECK_URL = ; + var CONFIRM_URL = ; + var CHECK_INTERVAL = ; + + // Erst nach Seitenload starten + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + + function init() { + // Berechtigung anfragen beim ersten Mal + if ('Notification' in window && Notification.permission === 'default') { + // Dezent um Berechtigung bitten - nicht sofort, sondern nach 10 Sekunden + setTimeout(function() { + Notification.requestPermission(); + }, 10000); + } + + // Sofort prüfen + checkPending(); + + // Regelmäßig prüfen + setInterval(checkPending, CHECK_INTERVAL); + } + + function checkPending() { + var xhr = new XMLHttpRequest(); + xhr.open('GET', CHECK_URL, true); + xhr.timeout = 15000; + + xhr.onload = function() { + if (xhr.status !== 200) return; + + try { + var data = JSON.parse(xhr.responseText); + } catch(e) { + return; + } + + var lastKnown = parseInt(localStorage.getItem(STORAGE_KEY) || '0', 10); + var currentPending = data.pending || 0; + var incoming = data.incoming || 0; + var incomingTotal = data.incoming_total || 0; + + // Neue Buchungen seit letztem Check? + if (currentPending > lastKnown && currentPending > 0) { + var newCount = currentPending - lastKnown; + + if (incoming > 0) { + showNotification( + 'Zahlungseingang', + incoming + ' Zahlungseingang' + (incoming > 1 ? 'e' : '') + ' (' + formatAmount(incomingTotal) + ' €)\nBestätigung erforderlich', + 'incoming' + ); + } else { + showNotification( + 'Bankimport', + newCount + ' neue Buchung' + (newCount > 1 ? 'en' : '') + ' warten auf Zuordnung', + 'pending' + ); + } + } + + // Aktuellen Stand merken + localStorage.setItem(STORAGE_KEY, currentPending.toString()); + }; + + xhr.onerror = function() {}; + xhr.send(); + } + + function showNotification(title, body, type) { + if (!('Notification' in window)) return; + if (Notification.permission !== 'granted') return; + + var icon = type === 'incoming' + ? '/theme/common/mime/money.png' + : '/theme/common/mime/doc.png'; + + var notification = new Notification(title, { + body: body, + icon: icon, + tag: 'bankimport-' + type, + requireInteraction: true + }); + + notification.onclick = function() { + window.focus(); + window.location.href = CONFIRM_URL; + notification.close(); + }; + + // Nach 30 Sekunden automatisch schließen + setTimeout(function() { + notification.close(); + }, 30000); + } + + function formatAmount(amount) { + return parseFloat(amount).toFixed(2).replace('.', ',').replace(/\B(?=(\d{3})+(?!\d))/g, '.'); + } + +})(); diff --git a/langs/de_DE/bankimport.lang b/langs/de_DE/bankimport.lang index be8e2c1..ccc4f16 100755 --- a/langs/de_DE/bankimport.lang +++ b/langs/de_DE/bankimport.lang @@ -307,3 +307,8 @@ Difference = Differenz BankEntry = Bank-Eintrag ReconciliationConfirmed = Zuordnung bestätigt und abgeglichen Confirm = Bestätigen + +# +# Dashboard-Widget +# +BoxBankImportPending = Bankimport - Offene Zuordnungen diff --git a/langs/en_US/bankimport.lang b/langs/en_US/bankimport.lang index f91b265..5ea01f1 100755 --- a/langs/en_US/bankimport.lang +++ b/langs/en_US/bankimport.lang @@ -203,3 +203,8 @@ Difference = Difference BankEntry = Bank Entry ReconciliationConfirmed = Match confirmed and reconciled Confirm = Confirm + +# +# Dashboard Widget +# +BoxBankImportPending = BankImport - Pending Matches