From c82f392e152479ee734f89a17685b1b2771c372b Mon Sep 17 00:00:00 2001 From: Alaguraj0361 Date: Tue, 23 Jun 2026 20:38:41 +0530 Subject: [PATCH] Add Dine360 Dashboard Website module with custom controllers and views - Created __init__.py and __manifest__.py for the new dine360_dashboard_website module. - Implemented custom controllers to handle website routing and dashboard rendering. - Added views for website logo and shop template to enhance the user interface. - Updated dine360_reservation and dine360_restaurant modules to include dine360_table as a dependency. - Enhanced restaurant table management with new views and actions for floors and tables. - Updated docker-compose to change the exposed port for the Odoo service. --- addons/Dine360_Shivasakthi/__manifest__.py | 1 + addons/dine360_dashboard/__manifest__.py | 6 +- addons/dine360_dashboard/controllers/main.py | 227 ++++----- addons/dine360_dashboard_website/__init__.py | 2 + .../dine360_dashboard_website/__manifest__.py | 21 + .../controllers/__init__.py | 2 + .../controllers/main.py | 45 ++ .../views/shop_template.xml | 10 + .../views/website_logo.xml | 14 + addons/dine360_reservation/__manifest__.py | 2 +- .../views/restaurant_table_views.xml | 18 + addons/dine360_restaurant/__manifest__.py | 2 +- addons/dine360_self_order/controllers/main.py | 14 +- .../static/src/js/self_order.js | 461 +++++++++--------- .../views/self_order_templates.xml | 12 +- addons/dine360_table/__init__.py | 1 + addons/dine360_table/__manifest__.py | 22 + .../dine360_table/static/description/icon.png | Bin 0 -> 403707 bytes addons/dine360_table/views/menu_items.xml | 21 + .../views/restaurant_floor_views.xml | 66 +++ .../views/restaurant_table_views.xml | 63 +++ docker-compose.yml | 2 +- 22 files changed, 633 insertions(+), 379 deletions(-) create mode 100644 addons/dine360_dashboard_website/__init__.py create mode 100644 addons/dine360_dashboard_website/__manifest__.py create mode 100644 addons/dine360_dashboard_website/controllers/__init__.py create mode 100644 addons/dine360_dashboard_website/controllers/main.py create mode 100644 addons/dine360_dashboard_website/views/shop_template.xml create mode 100644 addons/dine360_dashboard_website/views/website_logo.xml create mode 100644 addons/dine360_table/__init__.py create mode 100644 addons/dine360_table/__manifest__.py create mode 100644 addons/dine360_table/static/description/icon.png create mode 100644 addons/dine360_table/views/menu_items.xml create mode 100644 addons/dine360_table/views/restaurant_floor_views.xml create mode 100644 addons/dine360_table/views/restaurant_table_views.xml diff --git a/addons/Dine360_Shivasakthi/__manifest__.py b/addons/Dine360_Shivasakthi/__manifest__.py index caed817..a2e0df7 100644 --- a/addons/Dine360_Shivasakthi/__manifest__.py +++ b/addons/Dine360_Shivasakthi/__manifest__.py @@ -12,6 +12,7 @@ 'dine360_theme_shivasakthi', 'dine360_kds', 'dine360_reservation', + 'dine360_table', 'dine360_uber', 'dine360_recipe', 'dine360_self_order', diff --git a/addons/dine360_dashboard/__manifest__.py b/addons/dine360_dashboard/__manifest__.py index 010530f..a46f01a 100644 --- a/addons/dine360_dashboard/__manifest__.py +++ b/addons/dine360_dashboard/__manifest__.py @@ -4,13 +4,11 @@ 'license': 'LGPL-3', 'category': 'Website', 'summary': 'Redirect login to home and show icon grid', - 'depends': ['base', 'web', 'auth_signup', 'website', 'website_sale'], + 'depends': ['base', 'web', 'auth_signup'], 'data': [ 'views/home_template.xml', 'views/login_templates.xml', 'views/web_title_template.xml', - 'views/website_logo.xml', - 'views/shop_template.xml', 'data/branding_data.xml', ], 'assets': { @@ -23,8 +21,6 @@ 'web.assets_frontend': [ 'dine360_dashboard/static/src/css/theme_variables.css', 'dine360_dashboard/static/src/css/login_style.css', - 'dine360_dashboard/static/src/css/website_style.css', - 'dine360_dashboard/static/src/css/shop_style.css', ], 'web.assets_common': [ 'dine360_dashboard/static/src/css/theme_variables.css', diff --git a/addons/dine360_dashboard/controllers/main.py b/addons/dine360_dashboard/controllers/main.py index 3511074..81e3334 100644 --- a/addons/dine360_dashboard/controllers/main.py +++ b/addons/dine360_dashboard/controllers/main.py @@ -1,9 +1,10 @@ +# -*- coding: utf-8 -*- from odoo import http from odoo.http import request from odoo.addons.web.controllers.home import Home class CustomHome(Home): - @http.route('/web/login', type='http', auth="public", website=True) + @http.route('/web/login', type='http', auth="public") def web_login(self, *args, **kw): response = super(CustomHome, self).web_login(*args, **kw) if request.params.get('login_success') and request.session.uid: @@ -11,137 +12,109 @@ class CustomHome(Home): return request.redirect('/') return response -from odoo.addons.website.controllers.main import Website +def render_dashboard(request): + # 1. ROLE-BASED AUTO REDIRECTION (FOR STAFF) + # Skip the dashboard/website entirely for Chefs and Waiters + user = request.env.user.sudo() + is_admin = user.has_group('base.group_system') or \ + user.has_group('dine360_restaurant.group_restaurant_admin') + + if not is_admin: + # 1. WAITER / CASHIER -> Priority goes to POS + if user.has_group('dine360_restaurant.group_restaurant_waiter') or \ + user.has_group('dine360_restaurant.group_restaurant_cashier'): + return request.redirect('/web#action=point_of_sale.action_client_pos_menu') -class ImageHome(Website): - @http.route('/', type='http', auth='public', website=True, sitemap=True) - def index(self, **kwargs): - # ----------------------------------------------------------- - # SUPER SAFE EDITOR & IFRAME DETECTION - # ----------------------------------------------------------- - path = request.httprequest.path - params = request.params - headers = request.httprequest.headers - referer = headers.get('Referer', '') - fetch_dest = headers.get('Sec-Fetch-Dest', '') + # 2. CHEF -> Directly to KDS + if user.has_group('dine360_restaurant.group_restaurant_kitchen'): + return request.redirect('/web#action=dine360_kds.action_kds_dashboard') + + # Remove sudo() to respect Odoo's standard menu group restrictions + menus = request.env['ir.ui.menu'].search([ + ('parent_id', '=', False) + ], order='sequence') + + # User role checks + try: + is_admin = request.env.user.has_group('base.group_system') or \ + request.env.user.has_group('dine360_restaurant.group_restaurant_admin') + except Exception: + is_admin = request.env.user.has_group('base.group_system') + + # User requested to hide all standard apps and POS/KDS. + # Only allow specific menus based on user request + admin tools. + allowed_menus = ['Online Orders', 'Website', 'Table Reservations', 'Table Management', 'Uber Integration', 'Apps', 'Settings', 'Dine360 SaaS'] + + # Hide 'Website' icon from the parent level Dine360Restaurants database + if request.env.cr.dbname and 'Dine360Restaurants' in request.env.cr.dbname: + if 'Website' in allowed_menus: + allowed_menus.remove('Website') + + # Hide website/online orders if website module is not installed in current db + if 'website' not in request.env: + if 'Website' in allowed_menus: + allowed_menus.remove('Website') + if 'Online Orders' in allowed_menus: + allowed_menus.remove('Online Orders') + + filtered_menus = [] + seen_names = set() + for menu in menus: + # Match strictly against allowed menus + if menu.name not in allowed_menus and menu.name != 'Table Reservation': + continue + + # Hide "Apps" and "Settings" for non-admins + if menu.name in ['Apps', 'Settings'] and not is_admin: + continue + + # De-duplicate by name + if menu.name in seen_names: + continue + seen_names.add(menu.name) + + # Dynamic Icon Override (Dine360 Branding) + icon_mapping = { + 'Apps': 'dine360_dashboard,static/src/img/icons/apps.svg', + 'Settings': 'dine360_dashboard,static/src/img/icons/settings.svg', + 'Table Reservation': 'dine360_dashboard,static/src/img/icons/table_reservation.svg', + 'Table Reservations': 'dine360_dashboard,static/src/img/icons/table_reservation.svg', + 'Uber Integration': 'dine360_dashboard,static/src/img/icons/uber_integration.svg', + 'Online Orders': 'dine360_dashboard,static/src/img/icons/website.svg', + 'Dine360 SaaS': 'dine360_dashboard,static/src/img/icons/saas.svg', + 'SaaS': 'dine360_dashboard,static/src/img/icons/saas.svg', + } - # 1. If not logged in, always show standard homepage - if not request.session.uid: - return super(ImageHome, self).index(**kwargs) + # Find the best match in the mapping + current_name = menu.name + for key, icon_path in icon_mapping.items(): + if key.lower() in current_name.lower(): + menu.web_icon = icon_path + break - # 2. ROLE-BASED AUTO REDIRECTION (FOR STAFF) - # Skip the dashboard/website entirely for Chefs and Waiters - user = request.env.user.sudo() - is_admin = user.has_group('base.group_system') or \ - user.has_group('dine360_restaurant.group_restaurant_admin') - - if not is_admin: - # 1. WAITER / CASHIER -> Priority goes to POS - if user.has_group('dine360_restaurant.group_restaurant_waiter') or \ - user.has_group('dine360_restaurant.group_restaurant_cashier'): - return request.redirect('/web#action=point_of_sale.action_client_pos_menu') + filtered_menus.append(menu) - # 2. CHEF -> Directly to KDS - if user.has_group('dine360_restaurant.group_restaurant_kitchen'): - return request.redirect('/web#action=dine360_kds.action_kds_dashboard') - - # 3. SUPER SAFE EDITOR & IFRAME DETECTION - path = request.httprequest.path - params = request.params - headers = request.httprequest.headers - referer = headers.get('Referer', '') - fetch_dest = headers.get('Sec-Fetch-Dest', '') - - # Check for ANY editor or backend signal - editor_params = ['enable_editor', 'edit', 'path', 'website_id', 'frontend_edit', 'model', 'id'] - is_editor_request = any(p in params for p in editor_params) - is_from_backend = any(m in referer for m in ['/website/force', 'enable_editor']) - - # if it looks like Odoo internal business, return the real website - if fetch_dest == 'iframe' or is_editor_request or is_from_backend: - return super(ImageHome, self).index(**kwargs) - - if path != '/': - return super(ImageHome, self).index(**kwargs) - - # Remove sudo() to respect Odoo's standard menu group restrictions - menus = request.env['ir.ui.menu'].search([ - ('parent_id', '=', False) - ], order='sequence') - - # User role checks - try: - is_admin = request.env.user.has_group('base.group_system') or \ - request.env.user.has_group('dine360_restaurant.group_restaurant_admin') - is_kitchen = request.env.user.has_group('dine360_restaurant.group_restaurant_kitchen') - except Exception: - is_admin = request.env.user.has_group('base.group_system') - is_kitchen = False - - # User requested to hide all standard apps and POS/KDS. - # Only allow specific menus based on user request + admin tools. - allowed_menus = ['Online Orders', 'Website', 'Table Reservations', 'Uber Integration', 'Apps', 'Settings', 'Dine360 SaaS'] - - # Hide 'Website' icon from the parent level Dine360Restaurants database - if request.env.cr.dbname and 'Dine360Restaurants' in request.env.cr.dbname: - if 'Website' in allowed_menus: - allowed_menus.remove('Website') - - filtered_menus = [] - seen_names = set() - for menu in menus: - # Match strictly against allowed menus - if menu.name not in allowed_menus and menu.name != 'Table Reservation': - continue - - # Hide "Apps" and "Settings" for non-admins - if menu.name in ['Apps', 'Settings'] and not is_admin: - continue - - # De-duplicate by name - if menu.name in seen_names: - continue - seen_names.add(menu.name) - - # Dynamic Icon Override (Dine360 Branding) - icon_mapping = { - 'Apps': 'dine360_dashboard,static/src/img/icons/apps.svg', - 'Settings': 'dine360_dashboard,static/src/img/icons/settings.svg', - 'Table Reservation': 'dine360_dashboard,static/src/img/icons/table_reservation.svg', - 'Table Reservations': 'dine360_dashboard,static/src/img/icons/table_reservation.svg', - 'Uber Integration': 'dine360_dashboard,static/src/img/icons/uber_integration.svg', - 'Online Orders': 'dine360_dashboard,static/src/img/icons/website.svg', - 'Dine360 SaaS': 'dine360_dashboard,static/src/img/icons/saas.svg', - 'SaaS': 'dine360_dashboard,static/src/img/icons/saas.svg', - } - - # Find the best match in the mapping - current_name = menu.name - for key, icon_path in icon_mapping.items(): - if key.lower() in current_name.lower(): - menu.web_icon = icon_path - break - - filtered_menus.append(menu) - - # Low Stock Alerts (Ingredients) + # Low Stock Alerts (Ingredients) + low_stock_products = [] + try: + ProductTemplate = request.env['product.template'].sudo() + if hasattr(ProductTemplate, 'get_low_stock_products'): + low_stock_products = ProductTemplate.get_low_stock_products(limit=5) + except Exception: low_stock_products = [] - try: - ProductTemplate = request.env['product.template'].sudo() - if hasattr(ProductTemplate, 'get_low_stock_products'): - low_stock_products = ProductTemplate.get_low_stock_products(limit=5) - except Exception: - low_stock_products = [] - return request.render('dine360_dashboard.image_home_template', { - 'menus': filtered_menus, - 'user_id': request.env.user, - 'low_stock_products': low_stock_products - }) + values = { + 'menus': filtered_menus, + 'user_id': request.env.user, + 'low_stock_products': low_stock_products + } + if 'website' in request.env: + values['website'] = request.env['website'].get_current_website() + return request.render('dine360_dashboard.image_home_template', values) - - @http.route('/home', type='http', auth="public", website=True, sitemap=True) - def website_home(self, **kw): - # Explicit route for standard Website Homepage - return request.render('website.homepage') +class ImageHomeBase(http.Controller): + @http.route('/', type='http', auth='user', website=True) + def index(self, **kwargs): + # Render the dashboard for logged in users when website module is not installed + return render_dashboard(request) diff --git a/addons/dine360_dashboard_website/__init__.py b/addons/dine360_dashboard_website/__init__.py new file mode 100644 index 0000000..153a9e3 --- /dev/null +++ b/addons/dine360_dashboard_website/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import controllers diff --git a/addons/dine360_dashboard_website/__manifest__.py b/addons/dine360_dashboard_website/__manifest__.py new file mode 100644 index 0000000..f34d296 --- /dev/null +++ b/addons/dine360_dashboard_website/__manifest__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Dine360 Dashboard Website Glue', + 'version': '1.0.0', + 'category': 'Website', + 'summary': 'Glue module for Dine360 Dashboard Website features', + 'depends': ['dine360_dashboard', 'website', 'website_sale'], + 'data': [ + 'views/website_logo.xml', + 'views/shop_template.xml', + ], + 'assets': { + 'web.assets_frontend': [ + 'dine360_dashboard/static/src/css/website_style.css', + 'dine360_dashboard/static/src/css/shop_style.css', + ], + }, + 'auto_install': True, + 'installable': True, + 'license': 'LGPL-3', +} diff --git a/addons/dine360_dashboard_website/controllers/__init__.py b/addons/dine360_dashboard_website/controllers/__init__.py new file mode 100644 index 0000000..757b12a --- /dev/null +++ b/addons/dine360_dashboard_website/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import main diff --git a/addons/dine360_dashboard_website/controllers/main.py b/addons/dine360_dashboard_website/controllers/main.py new file mode 100644 index 0000000..ca9d8a0 --- /dev/null +++ b/addons/dine360_dashboard_website/controllers/main.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +from odoo import http +from odoo.http import request +from odoo.addons.web.controllers.home import Home +from odoo.addons.website.controllers.main import Website +from odoo.addons.dine360_dashboard.controllers.main import render_dashboard + +class CustomHomeWebsite(Home): + @http.route('/web/login', type='http', auth="public", website=True) + def web_login(self, *args, **kw): + response = super(CustomHomeWebsite, self).web_login(*args, **kw) + if request.params.get('login_success') and request.session.uid: + return request.redirect('/') + return response + +class ImageHomeWebsite(Website): + @http.route('/', type='http', auth='public', website=True, sitemap=True) + def index(self, **kwargs): + # 1. If not logged in, show website homepage + if not request.session.uid: + return super(ImageHomeWebsite, self).index(**kwargs) + + # 2. If logged in, check for editor / iframe detection + params = request.params + headers = request.httprequest.headers + referer = headers.get('Referer', '') + fetch_dest = headers.get('Sec-Fetch-Dest', '') + + editor_params = ['enable_editor', 'edit', 'path', 'website_id', 'frontend_edit', 'model', 'id'] + is_editor_request = any(p in params for p in editor_params) + is_from_backend = any(m in referer for m in ['/website/force', 'enable_editor']) + + if fetch_dest == 'iframe' or is_editor_request or is_from_backend: + return super(ImageHomeWebsite, self).index(**kwargs) + + if request.httprequest.path != '/': + return super(ImageHomeWebsite, self).index(**kwargs) + + # Render the dashboard + return render_dashboard(request) + + @http.route('/home', type='http', auth="public", website=True, sitemap=True) + def website_home(self, **kw): + # Explicit route for standard Website Homepage + return request.render('website.homepage') diff --git a/addons/dine360_dashboard_website/views/shop_template.xml b/addons/dine360_dashboard_website/views/shop_template.xml new file mode 100644 index 0000000..e207bd6 --- /dev/null +++ b/addons/dine360_dashboard_website/views/shop_template.xml @@ -0,0 +1,10 @@ + + + + diff --git a/addons/dine360_dashboard_website/views/website_logo.xml b/addons/dine360_dashboard_website/views/website_logo.xml new file mode 100644 index 0000000..db046c7 --- /dev/null +++ b/addons/dine360_dashboard_website/views/website_logo.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/addons/dine360_reservation/__manifest__.py b/addons/dine360_reservation/__manifest__.py index a1e9425..3af7b7e 100644 --- a/addons/dine360_reservation/__manifest__.py +++ b/addons/dine360_reservation/__manifest__.py @@ -11,7 +11,7 @@ - WhatsApp/SMS notification hooks """, 'author': 'Dine360', - 'depends': ['base', 'website', 'pos_restaurant'], + 'depends': ['base', 'website', 'pos_restaurant', 'dine360_table'], 'data': [ 'security/ir.model.access.csv', 'data/reservation_sequence.xml', diff --git a/addons/dine360_reservation/views/restaurant_table_views.xml b/addons/dine360_reservation/views/restaurant_table_views.xml index 9505ee7..9e6162a 100644 --- a/addons/dine360_reservation/views/restaurant_table_views.xml +++ b/addons/dine360_reservation/views/restaurant_table_views.xml @@ -48,4 +48,22 @@

+ + + + restaurant.table.form.inherit.dine360.table + restaurant.table + + + + + + + + + + + + + diff --git a/addons/dine360_restaurant/__manifest__.py b/addons/dine360_restaurant/__manifest__.py index 46c63c4..db083d9 100644 --- a/addons/dine360_restaurant/__manifest__.py +++ b/addons/dine360_restaurant/__manifest__.py @@ -13,7 +13,7 @@ - Store Keeper """, 'author': 'Dine360', - 'depends': ['point_of_sale', 'pos_restaurant', 'purchase', 'stock', 'website_sale'], + 'depends': ['point_of_sale', 'pos_restaurant', 'purchase', 'stock'], 'data': [ 'security/restaurant_security.xml', 'security/ir.model.access.csv', diff --git a/addons/dine360_self_order/controllers/main.py b/addons/dine360_self_order/controllers/main.py index 1ffd792..0a5432c 100644 --- a/addons/dine360_self_order/controllers/main.py +++ b/addons/dine360_self_order/controllers/main.py @@ -12,13 +12,19 @@ class Dine360SelfOrderController(http.Controller): def self_order_menu(self, **kwargs): """Displays the self-order menu for a specific table/kiosk""" table_id = kwargs.get('table_id') - table = False + pos_table = False if table_id: - table = request.env['restaurant.table'].sudo().browse(int(table_id)) + try: + pos_table_id = int(table_id) + pos_table = request.env['restaurant.table'].sudo().browse(pos_table_id) + if not pos_table.exists(): + pos_table = False + except (ValueError, TypeError): + pos_table = False values = { - 'table': table, - 'floor': table.floor_id if table else False, + 'pos_table': pos_table, + 'pos_floor': pos_table.floor_id if pos_table else False, } return request.render('dine360_self_order.self_order_menu_template', values) diff --git a/addons/dine360_self_order/static/src/js/self_order.js b/addons/dine360_self_order/static/src/js/self_order.js index 3130f3c..760d342 100644 --- a/addons/dine360_self_order/static/src/js/self_order.js +++ b/addons/dine360_self_order/static/src/js/self_order.js @@ -1,259 +1,252 @@ -/** @odoo-module */ +/** @odoo-module **/ +import publicWidget from "@web/legacy/js/public/public_widget"; import { jsonrpc } from "@web/core/network/rpc_service"; -// We'll use a standard self-invoking function style since it's a public web module -(function () { - 'use strict'; +publicWidget.registry.Dine360SelfOrder = publicWidget.Widget.extend({ + selector: '#self_order_app', + events: { + 'input #product_search': '_onSearchInput', + 'click #view_cart_btn': '_onViewCartClick', + 'click #submit_order_btn': '_onSubmitOrderClick', + 'click [data-category]': '_onCategoryClick', + 'click .add-to-cart-btn': '_onAddToCartClick', + 'click .increase-qty': '_onIncreaseQtyClick', + 'click .decrease-qty': '_onDecreaseQtyClick', + 'click .service-select': '_onServiceSelectClick', + }, - document.addEventListener('DOMContentLoaded', async () => { - const app = document.querySelector('#self_order_app'); - if (!app) return; - - const dataEl = document.querySelector('#self_order_data'); - const config = { - tableId: dataEl.dataset.tableId, - tableName: dataEl.dataset.tableName, - }; - - const state = { + init: function () { + this._super.apply(this, arguments); + this.config = {}; + this.state = { products: [], cart: [], activeCategory: 'all', searchTerm: '', }; + }, - // --- UI Elements --- - const productList = document.querySelector('#product_list'); - const categoryFilter = document.querySelector('#category_filter'); - const cartCount = document.querySelector('#cart_count'); - const cartTotal = document.querySelector('#cart_total'); - const footerCart = document.querySelector('#footer_cart'); - const loadingOverlay = document.querySelector('#loading_overlay'); - const contentArea = document.querySelector('#self_order_content'); + start: async function () { + await this._super.apply(this, arguments); + const dataEl = this.el.querySelector('#self_order_data'); + if (dataEl) { + this.config.tableId = dataEl.dataset.tableId; + this.config.tableName = dataEl.dataset.tableName; + } - // --- Init --- try { const products = await jsonrpc('/dine360/self_order/products', {}); - state.products = products; - renderCategories(); - renderProducts(); - loadingOverlay.classList.add('d-none'); - contentArea.classList.remove('d-none'); + this.state.products = products; + this._renderCategories(); + this._renderProducts(); + this.$('#loading_overlay').addClass('d-none'); + this.$('#self_order_content').removeClass('d-none'); } catch (e) { console.error("Failed to load products", e); alert("Error connecting to server. Please try again later."); } + }, - // --- Functions --- - function renderCategories() { - const categories = ['all', ...new Set(state.products.map(p => p.pos_categ_name))]; - categoryFilter.innerHTML = categories.map(cat => ` - - `).join(''); + _renderCategories: function () { + const categories = ['all', ...new Set(this.state.products.map(p => p.pos_categ_name))]; + const html = categories.map(cat => ` + + `).join(''); + this.$('#category_filter').html(html); + }, - categoryFilter.querySelectorAll('button').forEach(btn => { - btn.onclick = () => { - state.activeCategory = btn.dataset.category; - renderCategories(); - renderProducts(); - }; - }); + _renderProducts: function () { + let filtered = this.state.products; + if (this.state.activeCategory !== 'all') { + filtered = filtered.filter(p => p.pos_categ_name === this.state.activeCategory); + } + if (this.state.searchTerm) { + const term = this.state.searchTerm.toLowerCase(); + filtered = filtered.filter(p => p.display_name.toLowerCase().includes(term)); } - function renderProducts() { - let filtered = state.products; - if (state.activeCategory !== 'all') { - filtered = filtered.filter(p => p.pos_categ_name === state.activeCategory); - } - if (state.searchTerm) { - const term = state.searchTerm.toLowerCase(); - filtered = filtered.filter(p => p.display_name.toLowerCase().includes(term)); - } - - productList.innerHTML = filtered.map(p => ` -
-
-
- ${p.display_name} -
- -
-
-
-
${p.display_name}
-
- $${p.list_price.toFixed(2)} - - - -
-
-
-
- `).join(''); - - productList.querySelectorAll('.add-to-cart-btn').forEach(btn => { - btn.onclick = (ev) => { - ev.stopPropagation(); - const card = btn.closest('.product-card'); - const productId = parseInt(card.dataset.id); - addToCart(productId); - }; - }); - } - - function addToCart(productId) { - const product = state.products.find(p => p.id === productId); - const existing = state.cart.find(item => item.product_id === productId); - if (existing) { - existing.qty++; - } else { - state.cart.push({ - product_id: product.id, - display_name: product.display_name, - price_unit: product.list_price, - qty: 1 - }); - } - updateCartUI(); - } - - function updateCartUI() { - const count = state.cart.reduce((acc, item) => acc + item.qty, 0); - const total = state.cart.reduce((acc, item) => acc + (item.qty * item.price_unit), 0); - - cartCount.textContent = count; - cartTotal.textContent = `$${total.toFixed(2)}`; - - if (count > 0) { - footerCart.classList.remove('d-none'); - setTimeout(() => footerCart.style.transform = 'translateY(0)', 10); - } else { - footerCart.style.transform = 'translateY(100%)'; - setTimeout(() => footerCart.classList.add('d-none'), 300); - } - } - - // --- Search --- - document.querySelector('#product_search').oninput = (ev) => { - state.searchTerm = ev.target.value; - renderProducts(); - }; - - // --- Cart Modal --- - const cartModal = new bootstrap.Modal(document.getElementById('cart_modal')); - document.querySelector('#view_cart_btn').onclick = () => { - renderCartModal(); - cartModal.show(); - }; - - function renderCartModal() { - const list = document.querySelector('#cart_items_list'); - const total = state.cart.reduce((acc, item) => acc + (item.qty * item.price_unit), 0); - - list.innerHTML = state.cart.map((item, index) => ` -
-
-
${item.display_name}
-
$${(item.qty * item.price_unit).toFixed(2)}
-
-
- - ${item.qty} - -
-
- `).join(''); - - document.querySelector('#checkout_subtotal').textContent = `$${total.toFixed(2)}`; - document.querySelector('#checkout_total').textContent = `$${total.toFixed(2)}`; - - list.querySelectorAll('.increase-qty').forEach(btn => { - btn.onclick = () => { - state.cart[btn.dataset.index].qty++; - renderCartModal(); - updateCartUI(); - }; - }); - list.querySelectorAll('.decrease-qty').forEach(btn => { - btn.onclick = () => { - const index = btn.dataset.index; - state.cart[index].qty--; - if (state.cart[index].qty <= 0) { - state.cart.splice(index, 1); - } - if (state.cart.length === 0) cartModal.hide(); - renderCartModal(); - updateCartUI(); - }; - }); - } - - // --- Submit Order --- - document.querySelector('#submit_order_btn').onclick = async () => { - const btn = document.querySelector('#submit_order_btn'); - const originalText = btn.innerHTML; - - btn.disabled = true; - btn.innerHTML = 'Sending...'; - - const fulfilmentType = document.querySelector('input[name="fulfilment"]:checked')?.value || 'dine_in'; - - const orderData = { - table_id: config.tableId, - fulfilment_type: fulfilmentType, - lines: state.cart - }; - - try { - const response = await jsonrpc('/dine360/self_order/submit_order', { order_data: orderData }); - if (response.success) { - cartModal.hide(); - state.cart = []; - updateCartUI(); - - // Success View - document.querySelector('#self_order_content').innerHTML = ` -
-
-
- -
-
-

Order Received!

-

${response.message}

-
-
Order Number
-
${response.order_name}
-
-
- `; - } else { - alert(response.error || "Order submission failed"); - } - } catch (e) { - console.error(e); - alert("Connection lost. Please check your internet."); - } finally { - btn.disabled = false; - btn.innerHTML = originalText; - } + +
+
${p.display_name}
+
+ $${p.list_price.toFixed(2)} + ${p.is_kitchen_item ? '' : ''} +
+
+ + + `).join(''); + this.$('#product_list').html(html); + }, + + _onCategoryClick: function (ev) { + this.state.activeCategory = $(ev.currentTarget).data('category'); + this._renderCategories(); + this._renderProducts(); + }, + + _onAddToCartClick: function (ev) { + ev.stopPropagation(); + const card = $(ev.currentTarget).closest('.product-card'); + const productId = parseInt(card.data('id')); + const product = this.state.products.find(p => p.id === productId); + const existing = this.state.cart.find(item => item.product_id === productId); + if (existing) { + existing.qty++; + } else { + this.state.cart.push({ + product_id: product.id, + display_name: product.display_name, + price_unit: product.list_price, + qty: 1 + }); + } + this._updateCartUI(); + }, + + _updateCartUI: function () { + const count = this.state.cart.reduce((acc, item) => acc + item.qty, 0); + const total = this.state.cart.reduce((acc, item) => acc + (item.qty * item.price_unit), 0); + + this.$('#cart_count').text(count); + this.$('#cart_total').text(`$${total.toFixed(2)}`); + + const $footerCart = this.$('#footer_cart'); + if (count > 0) { + $footerCart.removeClass('d-none'); + setTimeout(() => $footerCart.css('transform', 'translateY(0)'), 10); + } else { + $footerCart.css('transform', 'translateY(100%)'); + setTimeout(() => $footerCart.addClass('d-none'), 300); + } + }, + + _onSearchInput: function (ev) { + this.state.searchTerm = ev.target.value; + this._renderProducts(); + }, + + _onViewCartClick: function () { + this._renderCartModal(); + if (!this.cartModal) { + this.cartModal = new bootstrap.Modal(this.el.querySelector('#cart_modal')); + } + this.cartModal.show(); + }, + + _renderCartModal: function () { + const total = this.state.cart.reduce((acc, item) => acc + (item.qty * item.price_unit), 0); + + const html = this.state.cart.map((item, index) => ` +
+
+
${item.display_name}
+
$${(item.qty * item.price_unit).toFixed(2)}
+
+
+ + ${item.qty} + +
+
+ `).join(''); + + this.$('#cart_items_list').html(html); + this.$('#checkout_subtotal').text(`$${total.toFixed(2)}`); + this.$('#checkout_total').text(`$${total.toFixed(2)}`); + }, + + _onIncreaseQtyClick: function (ev) { + const index = $(ev.currentTarget).data('index'); + this.state.cart[index].qty++; + this._renderCartModal(); + this._updateCartUI(); + }, + + _onDecreaseQtyClick: function (ev) { + const index = $(ev.currentTarget).data('index'); + this.state.cart[index].qty--; + if (this.state.cart[index].qty <= 0) { + this.state.cart.splice(index, 1); + } + if (this.state.cart.length === 0 && this.cartModal) { + this.cartModal.hide(); + } + this._renderCartModal(); + this._updateCartUI(); + }, + + _onServiceSelectClick: function (ev) { + const $label = $(ev.currentTarget); + this.$('.service-select').removeClass('active border-warning bg-warning-light'); + $label.addClass('active border-warning bg-warning-light'); + }, + + _onSubmitOrderClick: async function () { + const $btn = this.$('#submit_order_btn'); + const originalText = $btn.html(); + + $btn.prop('disabled', true); + $btn.html('Sending...'); + + const fulfilmentType = this.$('input[name="fulfilment"]:checked').val() || 'dine_in'; + + const orderData = { + table_id: this.config.tableId, + fulfilment_type: fulfilmentType, + lines: this.state.cart }; - // Service Type Toggle - document.querySelectorAll('.service-select').forEach(label => { - label.onclick = () => { - document.querySelectorAll('.service-select').forEach(l => l.classList.remove('active', 'border-warning', 'bg-warning-light')); - label.classList.add('active', 'border-warning', 'bg-warning-light'); - }; - }); - }); -})(); + try { + const response = await jsonrpc('/dine360/self_order/submit_order', { order_data: orderData }); + if (response.success) { + if (this.cartModal) this.cartModal.hide(); + this.state.cart = []; + this._updateCartUI(); + + // Success View + this.$('#self_order_content').html(` +
+
+
+ +
+
+

Order Received!

+

${response.message}

+
+
Order Number
+
${response.order_name}
+
+ +
+ `); + } else { + alert(response.error || "Order submission failed"); + } + } catch (e) { + console.error(e); + alert("Connection lost. Please check your internet."); + } finally { + $btn.prop('disabled', false); + $btn.html(originalText); + } + } +}); + +return publicWidget.registry.Dine360SelfOrder; diff --git a/addons/dine360_self_order/views/self_order_templates.xml b/addons/dine360_self_order/views/self_order_templates.xml index 7441191..68e7560 100644 --- a/addons/dine360_self_order/views/self_order_templates.xml +++ b/addons/dine360_self_order/views/self_order_templates.xml @@ -5,9 +5,9 @@
+ t-att-data-table-id="pos_table.id if pos_table else ''" + t-att-data-floor-id="pos_floor.id if pos_floor else ''" + t-att-data-table-name="pos_table.display_name if pos_table else ''"/>