system notification
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 = [
|
||||
'<div class="d-flex">',
|
||||
' <div class="toast-body d-flex align-items-start gap-2">',
|
||||
' <i class="ti ' + icon + ' fs-5 mt-1 flex-shrink-0"></i>',
|
||||
' <div>',
|
||||
(title ? '<div class="fw-semibold lh-sm">' + title + '</div>' : ''),
|
||||
(message ? '<div class="small text-muted">' + message + '</div>' : ''),
|
||||
' </div>',
|
||||
' </div>',
|
||||
' <button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast"></button>',
|
||||
'</div>',
|
||||
].join('');
|
||||
|
||||
container.appendChild(el);
|
||||
|
||||
var toast = new bootstrap.Toast(el, { delay: 5000 });
|
||||
toast.show();
|
||||
|
||||
el.addEventListener('hidden.bs.toast', function () { el.remove(); });
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Stock rows modal — used by: order/order.php, order/return.php, po/po.php, po/supplier_returns.php (via show_stock_rows() in custom.js) -->
|
||||
|
||||
@@ -208,6 +208,25 @@ $_usage_full = $_usage_max_pct >= 100;
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Bell notifications -->
|
||||
<li class="dropdown">
|
||||
<a class="position-relative btn-icon btn-sm btn-light btn rounded-circle" href="#" role="button"
|
||||
data-bs-toggle="dropdown" aria-expanded="false" onclick="clear_notif_badge()">
|
||||
<i class="ti ti-bell" style="font-size:18px;"></i>
|
||||
<span id="notif_badge" class="badge bg-danger position-absolute top-0 start-100 translate-middle rounded-pill d-none"
|
||||
style="font-size:9px;min-width:16px;padding:2px 4px;"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end p-0" style="min-width:300px;">
|
||||
<div class="px-3 py-2 border-bottom d-flex align-items-center justify-content-between">
|
||||
<span class="fw-semibold small">Notifications</span>
|
||||
<a href="javascript:;" class="small text-muted" onclick="clear_all_notifs()">Clear all</a>
|
||||
</div>
|
||||
<div id="notif_list" style="max-height:360px;overflow-y:auto;">
|
||||
<div class="text-center text-muted small py-4">No notifications</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- User dropdown -->
|
||||
<li class="ms-3 dropdown">
|
||||
<a class="position-relative btn-icon btn-sm btn-primary btn rounded-circle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
@@ -258,6 +277,68 @@ var prop_limit = '<?php echo $prop['limit']; ?>';
|
||||
var user_role = document.getElementById('session-context')?.dataset.role || 'viewer';
|
||||
var company_id = parseInt(document.getElementById('session-context')?.dataset.companyId || '0', 10);
|
||||
|
||||
// ── Bell notifications ────────────────────────────────────────────────────────
|
||||
var _notif_items = [];
|
||||
var _notif_unread = 0;
|
||||
|
||||
var _notif_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',
|
||||
};
|
||||
|
||||
function add_notification(title, message, type) {
|
||||
var time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
_notif_items.unshift({ title: title, message: message, type: type || 'info', time: time });
|
||||
if (_notif_items.length > 20) _notif_items.pop();
|
||||
_notif_unread++;
|
||||
_render_notif_badge();
|
||||
_render_notif_list();
|
||||
}
|
||||
|
||||
function clear_notif_badge() {
|
||||
_notif_unread = 0;
|
||||
_render_notif_badge();
|
||||
}
|
||||
|
||||
function clear_all_notifs() {
|
||||
_notif_items = [];
|
||||
_notif_unread = 0;
|
||||
_render_notif_badge();
|
||||
_render_notif_list();
|
||||
}
|
||||
|
||||
function _render_notif_badge() {
|
||||
var badge = document.getElementById('notif_badge');
|
||||
if (!badge) return;
|
||||
if (_notif_unread > 0) {
|
||||
badge.textContent = _notif_unread > 99 ? '99+' : _notif_unread;
|
||||
badge.classList.remove('d-none');
|
||||
} else {
|
||||
badge.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
function _render_notif_list() {
|
||||
var el = document.getElementById('notif_list');
|
||||
if (!el) return;
|
||||
if (_notif_items.length === 0) {
|
||||
el.innerHTML = '<div class="text-center text-muted small py-4">No notifications</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = _notif_items.map(function(n) {
|
||||
var icon = _notif_icon_map[n.type] || _notif_icon_map.info;
|
||||
return '<div class="d-flex align-items-start gap-2 px-3 py-2 border-bottom">' +
|
||||
'<i class="ti ' + icon + ' fs-5 flex-shrink-0 mt-1"></i>' +
|
||||
'<div class="flex-grow-1 overflow-hidden">' +
|
||||
(n.title ? '<div class="fw-semibold small text-truncate">' + n.title + '</div>' : '') +
|
||||
(n.message ? '<div class="small text-muted text-truncate">' + n.message + '</div>' : '') +
|
||||
'<div class="small text-muted opacity-75 mt-1">' + n.time + '</div>' +
|
||||
'</div></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── Log out ───────────────────────────────────────────────────────────────────
|
||||
function log_out() {
|
||||
return ajax_request({
|
||||
|
||||
Reference in New Issue
Block a user