From 5687b562fbda04cc09e40982b5f6845641d128fc Mon Sep 17 00:00:00 2001 From: Thanakorn S Date: Tue, 26 May 2026 13:26:57 +0700 Subject: [PATCH] system notification --- app/assets/utils/notify_node.php | 19 ++++++++ app/include_ending.php | 62 ++++++++++++++++++++++++ app/include_topbar.php | 81 ++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+) diff --git a/app/assets/utils/notify_node.php b/app/assets/utils/notify_node.php index 39a3fc2..6a3fd25 100644 --- a/app/assets/utils/notify_node.php +++ b/app/assets/utils/notify_node.php @@ -35,6 +35,25 @@ function notify_node(string $event, array $data, int $company_id): void curl_close($ch); } +/** + * emit_notification — push a real-time notification to all browser tabs in a company. + * + * The browser shows a toast immediately and adds the item to the bell dropdown. + * + * @param int $company_id Target company + * @param string $title Short heading, e.g. 'PO Approved' + * @param string $message Detail line, e.g. 'PO-2605-00042 has been approved.' + * @param string $type 'success' | 'info' | 'warning' | 'danger' (default 'info') + */ +function emit_notification(int $company_id, string $title, string $message, string $type = 'info'): void +{ + notify_node('notification', [ + 'title' => $title, + 'message' => $message, + 'type' => $type, + ], $company_id); +} + function gl_posted_payload(string $doc_type, int $id, string $action, array $lines): array { $has_revenue = false; diff --git a/app/include_ending.php b/app/include_ending.php index b920375..f381c29 100644 --- a/app/include_ending.php +++ b/app/include_ending.php @@ -29,7 +29,69 @@ window._socket.on('disconnect', function () { console.log('[socket] disconnected'); }); + + // ── Real-time notifications ─────────────────────────────────────────────── + // PHP emits { title, message, type } where type is 'success'|'info'|'warning'|'danger' + window._socket.on('notification', function (data) { + var title = data.title || ''; + var message = data.message || ''; + var type = data.type || 'info'; + show_toast(title, message, type); + if (typeof add_notification === 'function') { + add_notification(title, message, type); + } + }); })(); + +// ── show_toast(title, message, type) ───────────────────────────────────────── +// Renders a Bootstrap 5 toast in the top-right corner. +// type: 'success' | 'info' | 'warning' | 'danger' +function show_toast(title, message, type) { + type = type || 'info'; + + var icon_map = { + success: 'ti-circle-check text-success', + info: 'ti-info-circle text-info', + warning: 'ti-alert-triangle text-warning', + danger: 'ti-circle-x text-danger', + }; + var icon = icon_map[type] || icon_map.info; + + var container = document.getElementById('_toast_container'); + if (!container) { + container = document.createElement('div'); + container.id = '_toast_container'; + container.className = 'toast-container position-fixed top-0 end-0 p-3'; + container.style.zIndex = 9999; + document.body.appendChild(container); + } + + var id = '_toast_' + Date.now(); + var el = document.createElement('div'); + el.id = id; + el.className = 'toast align-items-center border-0 shadow-sm'; + el.setAttribute('role', 'alert'); + el.setAttribute('aria-live', 'assertive'); + el.innerHTML = [ + '
', + '
', + ' ', + '
', + (title ? '
' + title + '
' : ''), + (message ? '
' + message + '
' : ''), + '
', + '
', + ' ', + '
', + ].join(''); + + container.appendChild(el); + + var toast = new bootstrap.Toast(el, { delay: 5000 }); + toast.show(); + + el.addEventListener('hidden.bs.toast', function () { el.remove(); }); +} diff --git a/app/include_topbar.php b/app/include_topbar.php index c652a93..7940a4b 100644 --- a/app/include_topbar.php +++ b/app/include_topbar.php @@ -208,6 +208,25 @@ $_usage_full = $_usage_max_pct >= 100; + + +