diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/point_of_sale/static/src/js/Chrome.js | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (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.js | 454 |
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; +}); |
