feat: implement multi-device sync hardening with periodic auto-refresh and table-open synchronization
This commit is contained in:
parent
881c4af73a
commit
93ac66fa5b
@ -17,7 +17,7 @@ Features
|
|||||||
3. Portrait Display Mode: Single-pane responsive layout for mobile POS terminals.
|
3. Portrait Display Mode: Single-pane responsive layout for mobile POS terminals.
|
||||||
""",
|
""",
|
||||||
'author': "Suherdy Yacob",
|
'author': "Suherdy Yacob",
|
||||||
'depends': ['point_of_sale'],
|
'depends': ['point_of_sale', 'pos_restaurant'],
|
||||||
'data': [
|
'data': [
|
||||||
'views/res_config_settings_views.xml',
|
'views/res_config_settings_views.xml',
|
||||||
],
|
],
|
||||||
@ -33,6 +33,8 @@ Features
|
|||||||
'pos_ui_optimization/static/src/app/screens/product_screen/order_summary_patch.xml',
|
'pos_ui_optimization/static/src/app/screens/product_screen/order_summary_patch.xml',
|
||||||
# Safe multi-device Release table guard
|
# Safe multi-device Release table guard
|
||||||
'pos_ui_optimization/static/src/app/screens/product_screen/order_summary/safe_release_patch.js',
|
'pos_ui_optimization/static/src/app/screens/product_screen/order_summary/safe_release_patch.js',
|
||||||
|
# Multi-device sync hardening (table-open sync, 30s auto-refresh, visibility refresh)
|
||||||
|
'pos_ui_optimization/static/src/app/screens/floor_screen/floor_screen_sync_patch.js',
|
||||||
# Payment screen patches
|
# Payment screen patches
|
||||||
'pos_ui_optimization/static/src/app/screens/payment_screen/payment_screen_patch.xml',
|
'pos_ui_optimization/static/src/app/screens/payment_screen/payment_screen_patch.xml',
|
||||||
# Ticket screen patches
|
# Ticket screen patches
|
||||||
|
|||||||
212
static/src/app/screens/floor_screen/floor_screen_sync_patch.js
Normal file
212
static/src/app/screens/floor_screen/floor_screen_sync_patch.js
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* pos_ui_optimization — Multi-Device Sync Hardening
|
||||||
|
* ==================================================
|
||||||
|
*
|
||||||
|
* This patch adds three complementary sync strategies that work on top of
|
||||||
|
* Odoo's existing WebSocket-based DevicesSynchronisation to ensure order
|
||||||
|
* data stays fresh on all devices in a multi-tablet restaurant setup.
|
||||||
|
*
|
||||||
|
* === Android System WebView Compatibility Notes ===
|
||||||
|
*
|
||||||
|
* This file is intentionally written to be compatible with Android System
|
||||||
|
* WebView versions used by the Odoo Android app (Chrome/WebView 80+):
|
||||||
|
*
|
||||||
|
* requestIdleCallback:
|
||||||
|
* ✅ Supported in Android WebView 47+ (all modern devices).
|
||||||
|
* We have a setTimeout fallback for any older environment.
|
||||||
|
*
|
||||||
|
* visibilitychange:
|
||||||
|
* ⚠️ Reliable in Chrome browser, but in Android WebView the event only
|
||||||
|
* fires when the WebView loses DOM focus — NOT when the entire Android
|
||||||
|
* app is backgrounded by the OS, unless the native app explicitly calls
|
||||||
|
* WebView.onPause()/onResume(). The Odoo Android app does implement this
|
||||||
|
* correctly, so visibilitychange works. As a safety net, we also register
|
||||||
|
* a focus/blur fallback that works regardless of native app lifecycle.
|
||||||
|
*
|
||||||
|
* Optional chaining (?.) and nullish coalescing (??):
|
||||||
|
* ✅ Supported in Chrome 80+ / Android WebView from early 2020.
|
||||||
|
* All active Odoo Android installs run Chrome 80+. No changes needed.
|
||||||
|
*
|
||||||
|
* Numeric separators (30_000):
|
||||||
|
* ⚠️ Not supported in WebView < 75. Replaced with plain 30000.
|
||||||
|
*
|
||||||
|
* Strategy 1 — Sync-on-table-open:
|
||||||
|
* Pulls fresh data from the server before the FloorScreen opens a table so
|
||||||
|
* cashiers always see up-to-date orders (not stale local state).
|
||||||
|
*
|
||||||
|
* Strategy 2 — Periodic auto-refresh (30 s):
|
||||||
|
* A background timer fires every 30 seconds ONLY when safe:
|
||||||
|
* • The device is on the FloorScreen (never during order editing).
|
||||||
|
* • No input is focused (user is not typing a quantity/note).
|
||||||
|
* • No concurrent refresh is already running.
|
||||||
|
* • Uses requestIdleCallback so the browser picks a CPU-idle moment,
|
||||||
|
* preventing dropped frames or UI stutter.
|
||||||
|
*
|
||||||
|
* Strategy 3 — Visibility + focus/blur refresh:
|
||||||
|
* Triggers a refresh when:
|
||||||
|
* • document.visibilitychange fires (Chrome browser, Odoo Android app).
|
||||||
|
* • window 'focus' fires (fallback for Android WebView when the OS
|
||||||
|
* resumes the app but visibilitychange did not fire).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FloorScreen } from "@pos_restaurant/app/screens/floor_screen/floor_screen";
|
||||||
|
import { PosStore } from "@point_of_sale/app/services/pos_store";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
import { onMounted, onWillUnmount } from "@odoo/owl";
|
||||||
|
|
||||||
|
// Auto-refresh interval (avoid numeric separators for WebView < 75 compat)
|
||||||
|
const AUTO_REFRESH_INTERVAL_MS = 30000;
|
||||||
|
|
||||||
|
// ─── PosStore patch ───────────────────────────────────────────────────────────
|
||||||
|
patch(PosStore.prototype, {
|
||||||
|
/**
|
||||||
|
* Start all background sync strategies. Called from FloorScreen.onMounted.
|
||||||
|
* Guards against double-initialisation (e.g. if FloorScreen unmounts/remounts).
|
||||||
|
*/
|
||||||
|
setupSyncRefresh() {
|
||||||
|
if (this._syncRefreshInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._syncRefreshInitialized = true;
|
||||||
|
this._refreshInProgress = false;
|
||||||
|
|
||||||
|
// Strategy 2: periodic timer
|
||||||
|
this._autoRefreshInterval = setInterval(() => {
|
||||||
|
this._tryAutoRefresh();
|
||||||
|
}, AUTO_REFRESH_INTERVAL_MS);
|
||||||
|
|
||||||
|
// Strategy 3a: Page Visibility API
|
||||||
|
// Works in Chrome browser and in the Odoo Android app (which calls
|
||||||
|
// WebView.onPause/onResume bridging visibility state correctly).
|
||||||
|
this._onVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
this._tryAutoRefresh();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("visibilitychange", this._onVisibilityChange);
|
||||||
|
|
||||||
|
// Strategy 3b: window focus fallback for Android WebView environments
|
||||||
|
// where visibilitychange may not fire when the OS resumes the app.
|
||||||
|
// Deduplicated with _refreshInProgress so it never double-triggers.
|
||||||
|
this._onWindowFocus = () => {
|
||||||
|
// Add a short delay: Android sometimes fires focus before the
|
||||||
|
// WebView has fully re-rendered after coming back to foreground.
|
||||||
|
setTimeout(() => {
|
||||||
|
this._tryAutoRefresh();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
window.addEventListener("focus", this._onWindowFocus);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all timers and event listeners. Called from FloorScreen.onWillUnmount.
|
||||||
|
*/
|
||||||
|
destroySyncRefresh() {
|
||||||
|
if (this._autoRefreshInterval) {
|
||||||
|
clearInterval(this._autoRefreshInterval);
|
||||||
|
this._autoRefreshInterval = null;
|
||||||
|
}
|
||||||
|
if (this._onVisibilityChange) {
|
||||||
|
document.removeEventListener("visibilitychange", this._onVisibilityChange);
|
||||||
|
this._onVisibilityChange = null;
|
||||||
|
}
|
||||||
|
if (this._onWindowFocus) {
|
||||||
|
window.removeEventListener("focus", this._onWindowFocus);
|
||||||
|
this._onWindowFocus = null;
|
||||||
|
}
|
||||||
|
this._syncRefreshInitialized = false;
|
||||||
|
this._refreshInProgress = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central guard method — decides whether it is safe to run readDataFromServer()
|
||||||
|
* and schedules it via requestIdleCallback (or setTimeout(0) as fallback).
|
||||||
|
*
|
||||||
|
* Guards:
|
||||||
|
* 1. No concurrent refresh already running.
|
||||||
|
* 2. Skip if the user is actively typing in an input/textarea.
|
||||||
|
* 3. Only run while on the FloorScreen (never during order editing).
|
||||||
|
*/
|
||||||
|
_tryAutoRefresh() {
|
||||||
|
// Guard 1
|
||||||
|
if (this._refreshInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard 2: skip while the user is typing
|
||||||
|
const activeEl = document.activeElement;
|
||||||
|
const activeTag = activeEl ? activeEl.tagName : "";
|
||||||
|
if (activeTag === "INPUT" || activeTag === "TEXTAREA") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard 3: only on FloorScreen
|
||||||
|
const mainScreen = this.mainScreen;
|
||||||
|
const component = mainScreen ? mainScreen.component : null;
|
||||||
|
const screenName = component ? component.name : null;
|
||||||
|
if (screenName && screenName !== "FloorScreen") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const doRefresh = async () => {
|
||||||
|
this._refreshInProgress = true;
|
||||||
|
try {
|
||||||
|
if (this.deviceSync) {
|
||||||
|
await this.deviceSync.readDataFromServer();
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
// Non-blocking — if server is unreachable, just skip silently
|
||||||
|
} finally {
|
||||||
|
this._refreshInProgress = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// requestIdleCallback: lets the browser pick a CPU-idle moment between
|
||||||
|
// animation frames, preventing dropped frames or visible stutter.
|
||||||
|
// Supported in Android WebView 47+ (all modern devices).
|
||||||
|
// Falls back to setTimeout(fn, 0) which at minimum yields the call
|
||||||
|
// stack and runs after the current paint cycle.
|
||||||
|
if (typeof requestIdleCallback === "function") {
|
||||||
|
requestIdleCallback(() => doRefresh(), { timeout: 5000 });
|
||||||
|
} else {
|
||||||
|
setTimeout(() => doRefresh(), 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── FloorScreen patch ────────────────────────────────────────────────────────
|
||||||
|
patch(FloorScreen.prototype, {
|
||||||
|
setup() {
|
||||||
|
super.setup(...arguments);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
this.pos.setupSyncRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
onWillUnmount(() => {
|
||||||
|
this.pos.destroySyncRefresh();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strategy 1: Sync-on-table-open.
|
||||||
|
*
|
||||||
|
* Pulls the latest order state from the server before navigating into a
|
||||||
|
* table's order view. Non-blocking on failure — the cashier can always
|
||||||
|
* open the table even if the server is temporarily unreachable.
|
||||||
|
*/
|
||||||
|
async onClickTable(table, ev) {
|
||||||
|
if (!this.pos.isEditMode && !this.pos.isOrderTransferMode && !table.parent_id) {
|
||||||
|
try {
|
||||||
|
if (this.pos.deviceSync) {
|
||||||
|
await this.pos.deviceSync.readDataFromServer();
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
// Non-blocking: proceed with local data if server unreachable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onClickTable(table, ev);
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user