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