NODE JS introduction: socket polling
This commit is contained in:
+105
-28
@@ -180,6 +180,15 @@
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
||||
<style>
|
||||
/* Brief yellow flash when a card updates silently via WebSocket */
|
||||
@keyframes card-flash {
|
||||
0% { box-shadow: 0 0 0 3px rgba(250,204,21,0.8); }
|
||||
100% { box-shadow: none; }
|
||||
}
|
||||
.card-flash { animation: card-flash 1s ease-out; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
var trend_chart_instance = null;
|
||||
|
||||
@@ -207,26 +216,33 @@
|
||||
return Number(n).toLocaleString(undefined, {minimumFractionDigits:2, maximumFractionDigits:2});
|
||||
}
|
||||
|
||||
function show_overlays() {
|
||||
$('#overlay_revenue, #overlay_expense, #overlay_net, #overlay_journals, #overlay_lock, #overlay_trend, #overlay_source, #overlay_recent').show();
|
||||
// Flash a card briefly to signal a silent update happened
|
||||
function flash_card(el) {
|
||||
if (!el) return;
|
||||
el.classList.remove('card-flash');
|
||||
void el.offsetWidth; // force reflow so animation restarts
|
||||
el.classList.add('card-flash');
|
||||
setTimeout(function() { el.classList.remove('card-flash'); }, 1000);
|
||||
}
|
||||
|
||||
function set_dashboard_loading() {
|
||||
show_overlays();
|
||||
['stat_revenue', 'stat_expense', 'stat_net', 'stat_journals'].forEach(function(id) {
|
||||
document.getElementById(id).textContent = '—';
|
||||
});
|
||||
document.getElementById('posting_window_banner').textContent = 'Loading...';
|
||||
document.getElementById('by_source_list').innerHTML =
|
||||
'<div class="text-center text-muted small py-4">Loading...</div>';
|
||||
document.getElementById('recent_tbody').innerHTML =
|
||||
'<tr><td colspan="7" class="text-center text-muted py-4 small">Loading...</td></tr>';
|
||||
}
|
||||
// ── Individual load functions ─────────────────────────────────────────────
|
||||
// Each accepts a `silent` flag:
|
||||
// false (default) — show overlay spinner, blank value while loading
|
||||
// true — keep existing value visible, flash card on update
|
||||
|
||||
function load_dashboard() {
|
||||
set_dashboard_loading();
|
||||
// load_pl(silent, flash_revenue, flash_expense)
|
||||
// One DB query — but only flash cards that actually changed.
|
||||
// flash_revenue / flash_expense default to true when not passed.
|
||||
function load_pl(silent, flash_revenue, flash_expense) {
|
||||
if (flash_revenue === undefined) flash_revenue = true;
|
||||
if (flash_expense === undefined) flash_expense = true;
|
||||
|
||||
// Revenue, Expenses, Net Profit
|
||||
if (!silent) {
|
||||
$('#overlay_revenue, #overlay_expense, #overlay_net').show();
|
||||
['stat_revenue', 'stat_expense', 'stat_net'].forEach(function(id) {
|
||||
document.getElementById(id).textContent = '—';
|
||||
});
|
||||
}
|
||||
ajax_request({
|
||||
url: server_url + 'ac_dashboard/api/engine/pl.php',
|
||||
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, action: 'read',
|
||||
@@ -252,48 +268,89 @@
|
||||
if (sub) sub.className = net >= 0 ? 'text-success mb-0 small' : 'text-danger mb-0 small';
|
||||
}
|
||||
$('#overlay_net').hide();
|
||||
|
||||
// Flash only cards that actually changed
|
||||
if (silent) {
|
||||
if (flash_revenue) flash_card(document.getElementById('stat_revenue').closest('.card'));
|
||||
if (flash_expense) flash_card(document.getElementById('stat_expense').closest('.card'));
|
||||
flash_card(net_el.closest('.card')); // net profit always updates if either changed
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Journal Entries count
|
||||
function load_journals(silent) {
|
||||
if (!silent) {
|
||||
$('#overlay_journals').show();
|
||||
document.getElementById('stat_journals').textContent = '—';
|
||||
}
|
||||
ajax_request({
|
||||
url: server_url + 'ac_dashboard/api/engine/journals.php',
|
||||
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, action: 'read',
|
||||
onSuccess: function(res) {
|
||||
document.getElementById('stat_journals').textContent = res.output.journal_count;
|
||||
$('#overlay_journals').hide();
|
||||
if (silent) flash_card(document.getElementById('stat_journals').closest('.card'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Posting window banner
|
||||
function load_posting_window() {
|
||||
$('#overlay_lock').show();
|
||||
ajax_request({
|
||||
url: server_url + 'ac_dashboard/api/engine/posting_window.php',
|
||||
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, action: 'read',
|
||||
onSuccess: function(res) { render_posting_window(res.output); }
|
||||
});
|
||||
}
|
||||
|
||||
// Revenue vs Expenses trend chart
|
||||
function load_trend(silent) {
|
||||
if (!silent) $('#overlay_trend').show();
|
||||
ajax_request({
|
||||
url: server_url + 'ac_dashboard/api/engine/trend.php',
|
||||
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, action: 'read',
|
||||
onSuccess: function(res) { render_trend(res.output.trend); }
|
||||
onSuccess: function(res) { render_trend(res.output.trend, silent); }
|
||||
});
|
||||
}
|
||||
|
||||
// Entries by type breakdown
|
||||
function load_by_source(silent) {
|
||||
if (!silent) {
|
||||
$('#overlay_source').show();
|
||||
document.getElementById('by_source_list').innerHTML =
|
||||
'<div class="text-center text-muted small py-4">Loading...</div>';
|
||||
}
|
||||
ajax_request({
|
||||
url: server_url + 'ac_dashboard/api/engine/by_source.php',
|
||||
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, action: 'read',
|
||||
onSuccess: function(res) { render_by_source(res.output.by_source); }
|
||||
onSuccess: function(res) { render_by_source(res.output.by_source, silent); }
|
||||
});
|
||||
}
|
||||
|
||||
// Recent journal entries table
|
||||
function load_recent(silent) {
|
||||
if (!silent) {
|
||||
$('#overlay_recent').show();
|
||||
document.getElementById('recent_tbody').innerHTML =
|
||||
'<tr><td colspan="7" class="text-center text-muted py-4 small">Loading...</td></tr>';
|
||||
}
|
||||
ajax_request({
|
||||
url: server_url + 'ac_dashboard/api/engine/recent.php',
|
||||
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, action: 'read',
|
||||
onSuccess: function(res) { render_recent(res.output.recent); }
|
||||
onSuccess: function(res) { render_recent(res.output.recent, silent); }
|
||||
});
|
||||
}
|
||||
|
||||
// ── Full dashboard load (first load) ─────────────────────────────────────
|
||||
function load_dashboard() {
|
||||
load_pl(false);
|
||||
load_journals(false);
|
||||
load_posting_window();
|
||||
load_trend(false);
|
||||
load_by_source(false);
|
||||
load_recent(false);
|
||||
}
|
||||
|
||||
// ── Render functions ──────────────────────────────────────────────────────
|
||||
|
||||
function render_posting_window(window_data) {
|
||||
var el = document.getElementById('posting_window_banner');
|
||||
var from = window_data.open_from || '';
|
||||
@@ -308,8 +365,7 @@
|
||||
$('#overlay_lock').hide();
|
||||
}
|
||||
|
||||
function render_trend(trend) {
|
||||
// Build exactly 6 month labels (last 6 months ending today), fill zeros for missing
|
||||
function render_trend(trend, silent) {
|
||||
var labels = [], revenues = [], expenses = [];
|
||||
var idx = {};
|
||||
trend.forEach(function(r){ idx[r.period] = r; });
|
||||
@@ -345,9 +401,10 @@
|
||||
}
|
||||
});
|
||||
$('#overlay_trend').hide();
|
||||
if (silent) flash_card(document.getElementById('trend_chart').closest('.card'));
|
||||
}
|
||||
|
||||
function render_by_source(rows) {
|
||||
function render_by_source(rows, silent) {
|
||||
var el = document.getElementById('by_source_list');
|
||||
if (!rows || rows.length === 0) {
|
||||
el.innerHTML = '<div class="text-center text-muted small py-4">No entries found.</div>';
|
||||
@@ -371,9 +428,10 @@
|
||||
});
|
||||
el.innerHTML = html;
|
||||
$('#overlay_source').hide();
|
||||
if (silent) flash_card(el.closest('.card'));
|
||||
}
|
||||
|
||||
function render_recent(rows) {
|
||||
function render_recent(rows, silent) {
|
||||
var tbody = document.getElementById('recent_tbody');
|
||||
if (!rows || rows.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-4 small">No journal entries found.</td></tr>';
|
||||
@@ -397,9 +455,28 @@
|
||||
});
|
||||
tbody.innerHTML = html;
|
||||
$('#overlay_recent').hide();
|
||||
if (silent) flash_card(tbody.closest('.card'));
|
||||
}
|
||||
|
||||
// ── Initial load ──────────────────────────────────────────────────────────
|
||||
load_dashboard();
|
||||
|
||||
// ── Real-time: targeted silent updates per event payload ──────────────────
|
||||
if (window._socket) {
|
||||
window._socket.on('gl_posted', function(data) {
|
||||
console.log('[dashboard] gl_posted', data);
|
||||
|
||||
if (data.has_revenue || data.has_expense) {
|
||||
load_pl(true, data.has_revenue, data.has_expense); // only affected side flashes
|
||||
load_trend(true); // trend = revenue vs expense chart, only relevant when those change
|
||||
}
|
||||
|
||||
// Always update regardless of which accounts were hit
|
||||
load_journals(true);
|
||||
load_by_source(true);
|
||||
load_recent(true);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user