diff --git a/app/assets/utils/notify_node.php b/app/assets/utils/notify_node.php
index 6a3fd25..4092cf8 100644
--- a/app/assets/utils/notify_node.php
+++ b/app/assets/utils/notify_node.php
@@ -9,15 +9,15 @@
* @param array $data Payload sent to the browser
* @param int $company_id Scopes the broadcast to one company's room
*/
-function notify_node(string $event, array $data, int $company_id): void
+function notify_node(string $event, array $data, int $company_id, string $target = '', int $user_id = 0): void
{
if ($company_id <= 0) return;
- $body = json_encode([
- 'event' => $event,
- 'data' => $data,
- 'company_id' => $company_id,
- ]);
+ $payload = ['event' => $event, 'data' => $data, 'company_id' => $company_id];
+ if ($target) $payload['target'] = $target;
+ if ($user_id) $payload['user_id'] = $user_id;
+
+ $body = json_encode($payload);
$ch = curl_init(defined('NODE_EMIT_URL') ? NODE_EMIT_URL : 'http://127.0.0.1:3000/emit');
curl_setopt_array($ch, [
@@ -45,6 +45,7 @@ function notify_node(string $event, array $data, int $company_id): void
* @param string $message Detail line, e.g. 'PO-2605-00042 has been approved.'
* @param string $type 'success' | 'info' | 'warning' | 'danger' (default 'info')
*/
+// Company-wide notification — all users in the company see the toast.
function emit_notification(int $company_id, string $title, string $message, string $type = 'info'): void
{
notify_node('notification', [
@@ -54,6 +55,16 @@ function emit_notification(int $company_id, string $title, string $message, stri
], $company_id);
}
+// Document notification — the acting user + all admins/owners see the toast. Other staff/viewers do not.
+function emit_notification_user(int $company_id, int $user_id, string $title, string $message, string $type = 'info'): void
+{
+ notify_node('notification', [
+ 'title' => $title,
+ 'message' => $message,
+ 'type' => $type,
+ ], $company_id, 'user', $user_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 f381c29..49f50c8 100644
--- a/app/include_ending.php
+++ b/app/include_ending.php
@@ -17,7 +17,7 @@
if (typeof io === 'undefined') return;
window._socket = io('', {
- query: { company_id: company_id },
+ query: { company_id: company_id, user_id: user_id, role: user_role },
reconnection: true,
reconnectionDelay: 2000,
});
diff --git a/app/include_topbar.php b/app/include_topbar.php
index 7940a4b..371477d 100644
--- a/app/include_topbar.php
+++ b/app/include_topbar.php
@@ -128,6 +128,7 @@ $_usage_full = $_usage_max_pct >= 100;
@@ -276,6 +277,7 @@ var server_url = '';
var prop_limit = '';
var user_role = document.getElementById('session-context')?.dataset.role || 'viewer';
var company_id = parseInt(document.getElementById('session-context')?.dataset.companyId || '0', 10);
+var user_id = parseInt(document.getElementById('session-context')?.dataset.userId || '0', 10);
// ── Bell notifications ────────────────────────────────────────────────────────
var _notif_items = [];
diff --git a/nodejs/server.js b/nodejs/server.js
index 2852e07..58e2566 100644
--- a/nodejs/server.js
+++ b/nodejs/server.js
@@ -22,15 +22,26 @@ app.post('/emit', (req, res) => {
return res.status(403).json({ ok: false, message: 'Forbidden' });
}
- const { event, data, company_id } = req.body;
+ const { event, data, company_id, target, user_id } = req.body;
if (!event || !company_id) {
return res.status(400).json({ ok: false, message: 'event and company_id required' });
}
- // Broadcast only to the room for this company
- io.to(`company_${company_id}`).emit(event, data);
+ if (target === 'user' && user_id) {
+ // Notify the acting user on all their tabs + all admins (excluding the acting user to avoid duplicates)
+ io.to(`user_${user_id}`).emit(event, data);
+ io.to(`admin_${company_id}`).except(`user_${user_id}`).emit(event, data);
+ console.log(`[emit] company=${company_id} user=${user_id} event=${event}`, data);
+ } else if (target === 'admin') {
+ // Admins and owners only
+ io.to(`admin_${company_id}`).emit(event, data);
+ console.log(`[emit] company=${company_id} admin-only event=${event}`, data);
+ } else {
+ // Company-wide — stock events, GL events, scheduler alerts
+ io.to(`company_${company_id}`).emit(event, data);
+ console.log(`[emit] company=${company_id} event=${event}`, data);
+ }
- console.log(`[emit] company=${company_id} event=${event}`, data);
res.json({ ok: true });
});
@@ -50,6 +61,8 @@ app.get('/health', (req, res) => {
//
io.on('connection', (socket) => {
const company_id = socket.handshake.query.company_id;
+ const user_id = socket.handshake.query.user_id;
+ const role = socket.handshake.query.role || 'viewer';
if (!company_id) {
socket.disconnect();
@@ -57,7 +70,16 @@ io.on('connection', (socket) => {
}
socket.join(`company_${company_id}`);
- console.log(`[connect] socket=${socket.id} company=${company_id}`);
+
+ if (user_id) {
+ socket.join(`user_${user_id}`);
+ }
+
+ if (role === 'admin' || role === 'owner') {
+ socket.join(`admin_${company_id}`);
+ }
+
+ console.log(`[connect] socket=${socket.id} company=${company_id} user=${user_id} role=${role}`);
socket.on('disconnect', () => {
console.log(`[disconnect] socket=${socket.id}`);