summaryrefslogtreecommitdiff
path: root/addons/point_of_sale/static/src/js/Chrome.js
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/point_of_sale/static/src/js/Chrome.js
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/point_of_sale/static/src/js/Chrome.js')
-rw-r--r--addons/point_of_sale/static/src/js/Chrome.js454
1 files changed, 454 insertions, 0 deletions
diff --git a/addons/point_of_sale/static/src/js/Chrome.js b/addons/point_of_sale/static/src/js/Chrome.js
new file mode 100644
index 00000000..63f5c363
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Chrome.js
@@ -0,0 +1,454 @@
+odoo.define('point_of_sale.Chrome', function(require) {
+ 'use strict';
+
+ const { useState, useRef, useContext } = owl.hooks;
+ const { debounce } = owl.utils;
+ const { loadCSS } = require('web.ajax');
+ const { useListener } = require('web.custom_hooks');
+ const { CrashManager } = require('web.CrashManager');
+ const { BarcodeEvents } = require('barcodes.BarcodeEvents');
+ const PosComponent = require('point_of_sale.PosComponent');
+ const NumberBuffer = require('point_of_sale.NumberBuffer');
+ const PopupControllerMixin = require('point_of_sale.PopupControllerMixin');
+ const Registries = require('point_of_sale.Registries');
+ const IndependentToOrderScreen = require('point_of_sale.IndependentToOrderScreen');
+ const contexts = require('point_of_sale.PosContext');
+
+ // This is kind of a trick.
+ // We get a reference to the whole exports so that
+ // when we create an instance of one of the classes,
+ // we instantiate the extended one.
+ const models = require('point_of_sale.models');
+
+ /**
+ * Chrome is the root component of the PoS App.
+ */
+ class Chrome extends PopupControllerMixin(PosComponent) {
+ constructor() {
+ super(...arguments);
+ useListener('show-main-screen', this.__showScreen);
+ useListener('toggle-debug-widget', debounce(this._toggleDebugWidget, 100));
+ useListener('show-temp-screen', this.__showTempScreen);
+ useListener('close-temp-screen', this.__closeTempScreen);
+ useListener('close-pos', this._closePos);
+ useListener('loading-skip-callback', () => this._loadingSkipCallback());
+ useListener('play-sound', this._onPlaySound);
+ useListener('set-sync-status', this._onSetSyncStatus);
+ NumberBuffer.activate();
+
+ this.chromeContext = useContext(contexts.chrome);
+
+ this.state = useState({
+ uiState: 'LOADING', // 'LOADING' | 'READY' | 'CLOSING'
+ debugWidgetIsShown: true,
+ hasBigScrollBars: false,
+ sound: { src: null },
+ });
+
+ this.loading = useState({
+ message: 'Loading',
+ skipButtonIsShown: false,
+ });
+
+ this.mainScreen = useState({ name: null, component: null });
+ this.mainScreenProps = {};
+
+ this.tempScreen = useState({ isShown: false, name: null, component: null });
+ this.tempScreenProps = {};
+
+ this.progressbar = useRef('progressbar');
+
+ this.previous_touch_y_coordinate = -1;
+ }
+
+ // OVERLOADED METHODS //
+
+ mounted() {
+ // remove default webclient handlers that induce click delay
+ $(document).off();
+ $(window).off();
+ $('html').off();
+ $('body').off();
+ // The above lines removed the bindings, but we really need them for the barcode
+ BarcodeEvents.start();
+ }
+ willUnmount() {
+ BarcodeEvents.stop();
+ }
+ destroy() {
+ super.destroy(...arguments);
+ this.env.pos.destroy();
+ }
+ catchError(error) {
+ console.error(error);
+ }
+
+ // GETTERS //
+
+ get clientScreenButtonIsShown() {
+ return (
+ this.env.pos.config.use_proxy && this.env.pos.config.iface_customer_facing_display
+ );
+ }
+ /**
+ * Startup screen can be based on pos config so the startup screen
+ * is only determined after pos data is completely loaded.
+ *
+ * NOTE: Wait for pos data to be completed before calling this getter.
+ */
+ get startScreen() {
+ if (this.state.uiState !== 'READY') {
+ console.warn(
+ `Accessing startScreen of Chrome component before 'state.uiState' to be 'READY' is not recommended.`
+ );
+ }
+ return { name: 'ProductScreen' };
+ }
+
+ // CONTROL METHODS //
+
+ /**
+ * Call this function after the Chrome component is mounted.
+ * This will load pos and assign it to the environment.
+ */
+ async start() {
+ try {
+ // Instead of passing chrome to the instantiation the PosModel,
+ // we inject functions needed by pos.
+ // This way, we somehow decoupled Chrome from PosModel.
+ // We can then test PosModel independently from Chrome by supplying
+ // mocked version of these default attributes.
+ const posModelDefaultAttributes = {
+ env: this.env,
+ rpc: this.rpc.bind(this),
+ session: this.env.session,
+ do_action: this.props.webClient.do_action.bind(this.props.webClient),
+ setLoadingMessage: this.setLoadingMessage.bind(this),
+ showLoadingSkip: this.showLoadingSkip.bind(this),
+ setLoadingProgress: this.setLoadingProgress.bind(this),
+ };
+ this.env.pos = new models.PosModel(posModelDefaultAttributes);
+ await this.env.pos.ready;
+ this._buildChrome();
+ this._closeOtherTabs();
+ this.env.pos.set(
+ 'selectedCategoryId',
+ this.env.pos.config.iface_start_categ_id
+ ? this.env.pos.config.iface_start_categ_id[0]
+ : 0
+ );
+ this.state.uiState = 'READY';
+ this.env.pos.on('change:selectedOrder', this._showSavedScreen, this);
+ this._showStartScreen();
+ if (_.isEmpty(this.env.pos.db.product_by_category_id)) {
+ this._loadDemoData();
+ }
+ setTimeout(() => {
+ // push order in the background, no need to await
+ this.env.pos.push_orders();
+ // Allow using the app even if not all the images are loaded.
+ // Basically, preload the images in the background.
+ this._preloadImages();
+ });
+ } catch (error) {
+ let title = 'Unknown Error',
+ body;
+
+ if (error.message && [100, 200, 404, -32098].includes(error.message.code)) {
+ // this is the signature of rpc error
+ if (error.message.code === -32098) {
+ title = 'Network Failure (XmlHttpRequestError)';
+ body =
+ 'The Point of Sale could not be loaded due to a network problem.\n' +
+ 'Please check your internet connection.';
+ } else if (error.message.code === 200) {
+ title = error.message.data.message || this.env._t('Server Error');
+ body =
+ error.message.data.debug ||
+ this.env._t(
+ 'The server encountered an error while receiving your order.'
+ );
+ }
+ } else if (error instanceof Error) {
+ title = error.message;
+ body = error.stack;
+ }
+
+ await this.showPopup('ErrorTracebackPopup', {
+ title,
+ body,
+ exitButtonIsShown: true,
+ });
+ }
+ }
+
+ // EVENT HANDLERS //
+
+ _showStartScreen() {
+ const { name, props } = this.startScreen;
+ this.showScreen(name, props);
+ }
+ /**
+ * Show the screen saved in the order when the `selectedOrder` of pos is changed.
+ * @param {models.PosModel} pos
+ * @param {models.Order} newSelectedOrder
+ */
+ _showSavedScreen(pos, newSelectedOrder) {
+ const { name, props } = this._getSavedScreen(newSelectedOrder);
+ this.showScreen(name, props);
+ }
+ _getSavedScreen(order) {
+ return order.get_screen_data();
+ }
+ __showTempScreen(event) {
+ const { name, props, resolve } = event.detail;
+ this.tempScreen.isShown = true;
+ this.tempScreen.name = name;
+ this.tempScreen.component = this.constructor.components[name];
+ this.tempScreenProps = Object.assign({}, props, { resolve });
+ }
+ __closeTempScreen() {
+ this.tempScreen.isShown = false;
+ }
+ __showScreen({ detail: { name, props = {} } }) {
+ const component = this.constructor.components[name];
+ // 1. Set the information of the screen to display.
+ this.mainScreen.name = name;
+ this.mainScreen.component = component;
+ this.mainScreenProps = props;
+
+ // 2. Set some options
+ this.chromeContext.showOrderSelector = !component.hideOrderSelector;
+
+ // 3. Save the screen to the order.
+ // - This screen is shown when the order is selected.
+ if (!(component.prototype instanceof IndependentToOrderScreen) && name !== "ReprintReceiptScreen") {
+ this._setScreenData(name, props);
+ }
+ }
+ /**
+ * Set the latest screen to the current order. This is done so that
+ * when the order is selected again, the ui returns to the latest screen
+ * saved in the order.
+ *
+ * @param {string} name Screen name
+ * @param {Object} props props for the Screen component
+ */
+ _setScreenData(name, props) {
+ const order = this.env.pos.get_order();
+ if (order) {
+ order.set_screen_data({ name, props });
+ }
+ }
+ async _closePos() {
+ // If pos is not properly loaded, we just go back to /web without
+ // doing anything in the order data.
+ if (!this.env.pos || this.env.pos.db.get_orders().length === 0) {
+ window.location = '/web#action=point_of_sale.action_client_pos_menu';
+ }
+
+ if (this.env.pos.db.get_orders().length) {
+ // If there are orders in the db left unsynced, we try to sync.
+ // If sync successful, close without asking.
+ // Otherwise, ask again saying that some orders are not yet synced.
+ try {
+ await this.env.pos.push_orders();
+ window.location = '/web#action=point_of_sale.action_client_pos_menu';
+ } catch (error) {
+ console.warn(error);
+ const reason = this.env.pos.get('failed')
+ ? this.env._t(
+ 'Some orders could not be submitted to ' +
+ 'the server due to configuration errors. ' +
+ 'You can exit the Point of Sale, but do ' +
+ 'not close the session before the issue ' +
+ 'has been resolved.'
+ )
+ : this.env._t(
+ 'Some orders could not be submitted to ' +
+ 'the server due to internet connection issues. ' +
+ 'You can exit the Point of Sale, but do ' +
+ 'not close the session before the issue ' +
+ 'has been resolved.'
+ );
+ const { confirmed } = await this.showPopup('ConfirmPopup', {
+ title: this.env._t('Offline Orders'),
+ body: reason,
+ });
+ if (confirmed) {
+ this.state.uiState = 'CLOSING';
+ this.loading.skipButtonIsShown = false;
+ this.setLoadingMessage(this.env._t('Closing ...'));
+ window.location = '/web#action=point_of_sale.action_client_pos_menu';
+ }
+ }
+ }
+ }
+ _toggleDebugWidget() {
+ this.state.debugWidgetIsShown = !this.state.debugWidgetIsShown;
+ }
+ _onPlaySound({ detail: name }) {
+ let src;
+ if (name === 'error') {
+ src = "/point_of_sale/static/src/sounds/error.wav";
+ } else if (name === 'bell') {
+ src = "/point_of_sale/static/src/sounds/bell.wav";
+ }
+ this.state.sound.src = src;
+ }
+ _onSetSyncStatus({ detail: { status, pending }}) {
+ this.env.pos.set('synch', { status, pending });
+ }
+
+ // TO PASS AS PARAMETERS //
+
+ setLoadingProgress(fac) {
+ if (this.progressbar.el) {
+ this.progressbar.el.style.width = `${Math.floor(fac * 100)}%`;
+ }
+ }
+ setLoadingMessage(msg, progress) {
+ this.loading.message = msg;
+ if (typeof progress !== 'undefined') {
+ this.setLoadingProgress(progress);
+ }
+ }
+ /**
+ * Show Skip button in the loading screen and allow to assign callback
+ * when the button is pressed.
+ *
+ * @param {Function} callback function to call when Skip button is pressed.
+ */
+ showLoadingSkip(callback) {
+ if (callback) {
+ this.loading.skipButtonIsShown = true;
+ this._loadingSkipCallback = callback;
+ }
+ }
+
+ get isTicketScreenShown() {
+ return this.mainScreen.name === 'TicketScreen';
+ }
+
+ // MISC METHODS //
+
+ async _loadDemoData() {
+ const { confirmed } = await this.showPopup('ConfirmPopup', {
+ title: this.env._t('You do not have any products'),
+ body: this.env._t(
+ 'Would you like to load demo data?'
+ ),
+ });
+ if (confirmed) {
+ await this.rpc({
+ 'route': '/pos/load_onboarding_data',
+ });
+ this.env.pos.load_server_data();
+ }
+ }
+
+ _preloadImages() {
+ for (let product of this.env.pos.db.get_product_by_category(0)) {
+ const image = new Image();
+ image.src = `/web/image?model=product.product&field=image_128&id=${product.id}&write_date=${product.write_date}&unique=1`;
+ }
+ for (let category of Object.values(this.env.pos.db.category_by_id)) {
+ if (category.id == 0) continue;
+ const image = new Image();
+ image.src = `/web/image?model=pos.category&field=image_128&id=${category.id}&write_date=${category.write_date}&unique=1`;
+ }
+ const staticImages = ['backspace.png', 'bc-arrow-big.png'];
+ for (let imageName of staticImages) {
+ const image = new Image();
+ image.src = `/point_of_sale/static/src/img/${imageName}`;
+ }
+ }
+
+ _buildChrome() {
+ if ($.browser.chrome) {
+ var chrome_version = $.browser.version.split('.')[0];
+ if (parseInt(chrome_version, 10) >= 50) {
+ loadCSS('/point_of_sale/static/src/css/chrome50.css');
+ }
+ }
+
+ if (this.env.pos.config.iface_big_scrollbars) {
+ this.state.hasBigScrollBars = true;
+ }
+
+ this._disableBackspaceBack();
+ this._replaceCrashmanager();
+ }
+ // replaces the error handling of the existing crashmanager which
+ // uses jquery dialog to display the error, to use the pos popup
+ // instead
+ _replaceCrashmanager() {
+ var self = this;
+ CrashManager.include({
+ show_warning: function (error) {
+ if (self.env.pos) {
+ // self == this component
+ self.showPopup('ErrorPopup', {
+ title: error.data.title.toString(),
+ body: error.data.message,
+ });
+ } else {
+ // this == CrashManager instance
+ this._super(error);
+ }
+ },
+ show_error: function (error) {
+ if (self.env.pos) {
+ // self == this component
+ self.showPopup('ErrorTracebackPopup', {
+ title: error.type,
+ body: error.message + '\n' + error.data.debug + '\n',
+ });
+ } else {
+ // this == CrashManager instance
+ this._super(error);
+ }
+ },
+ });
+ }
+ // prevent backspace from performing a 'back' navigation
+ _disableBackspaceBack() {
+ $(document).on('keydown', function (e) {
+ if (e.which === 8 && !$(e.target).is('input, textarea')) {
+ e.preventDefault();
+ }
+ });
+ }
+ _closeOtherTabs() {
+ localStorage['message'] = '';
+ localStorage['message'] = JSON.stringify({
+ message: 'close_tabs',
+ session: this.env.pos.pos_session.id,
+ });
+
+ window.addEventListener(
+ 'storage',
+ (event) => {
+ if (event.key === 'message' && event.newValue) {
+ const msg = JSON.parse(event.newValue);
+ if (
+ msg.message === 'close_tabs' &&
+ msg.session == this.env.pos.pos_session.id
+ ) {
+ console.info(
+ 'POS / Session opened in another window. EXITING POS'
+ );
+ this._closePos();
+ }
+ }
+ },
+ false
+ );
+ }
+ }
+ Chrome.template = 'Chrome';
+
+ Registries.Component.add(Chrome);
+
+ return Chrome;
+});