add Dine360 dashboard and restaurant management modules
This commit is contained in:
parent
8acf3227f7
commit
d6b5fce53f
@ -10,24 +10,39 @@ This workflow ensures all modules (`Self-Order`, `Online Orders`, `KDS`, and `PO
|
|||||||
Before testing, verify the services are up:
|
Before testing, verify the services are up:
|
||||||
// turbo
|
// turbo
|
||||||
`docker ps`
|
`docker ps`
|
||||||
Ensure `odoo_client2` and `db` are in a 'Healthy' or 'Up' state.
|
Ensure your Odoo container (`odoo_client6` or `odoo_client2`) and database container are in an 'Up' state.
|
||||||
|
The application is accessible at **`http://localhost:10002`** (or `http://localhost:8069` depending on compose mapping).
|
||||||
|
|
||||||
## 2. Setup POS Session
|
## 2. Automated E2E Test Suite (Fast Verification)
|
||||||
1. Open your Odoo instance (usually `http://localhost:8069`).
|
You can run the full automated verification test covering all 4 flows in one command:
|
||||||
|
```bash
|
||||||
|
docker exec -i odoo_client6 odoo shell -d <your_db_name> < test_e2e_dine360.py
|
||||||
|
```
|
||||||
|
This tests:
|
||||||
|
1. Environment & Prerequisites (POS config, active session, kitchen products, Table 1).
|
||||||
|
2. Self-Order (Table QR) order creation, calculations, and KDS waiting dispatch.
|
||||||
|
3. Website Online Order creation in POS pending queue, held state, and Cashier confirmation.
|
||||||
|
4. KDS Lifecycle (`waiting` -> `preparing` -> `ready` -> `served`).
|
||||||
|
5. POS Online Orders RPC API (`get_online_orders`).
|
||||||
|
|
||||||
|
## 3. Manual Interactive Test Flows
|
||||||
|
|
||||||
|
### Setup POS Session
|
||||||
|
1. Open your Odoo instance at `http://localhost:10002`.
|
||||||
2. Go to **Point of Sale**.
|
2. Go to **Point of Sale**.
|
||||||
3. **Open** a new session for your main Shop/Restaurant.
|
3. **Open** a new session for your main Restaurant POS.
|
||||||
|
|
||||||
## 3. Test Flow: Self-Order (Table QR)
|
### Flow A: Self-Order (Table QR)
|
||||||
1. Go to **Point of Sale > Configuration > Floor Plans**.
|
1. Go to **Point of Sale > Configuration > Floor Plans**.
|
||||||
2. Select a floor and a table (e.g., "Table 1").
|
2. Select a floor and a table (e.g., "Table 1").
|
||||||
3. Click the **Open Front-end** button (this opens the Self-Order menu).
|
3. Click the **Open Front-end** button (opens `/dine360/menu?table_id=...`).
|
||||||
4. **Action**: Add 2-3 items to the cart and click **Send to Kitchen**.
|
4. **Action**: Add 2-3 items to the cart and click **Send to Kitchen**.
|
||||||
5. **Verification**:
|
5. **Verification**:
|
||||||
- [ ] Go to the **Kitchen (KDS)** module.
|
- [ ] Go to the **Kitchen (KDS)** module.
|
||||||
- [ ] Check that the items appear in the **Waiting** column.
|
- [ ] Check that the items appear in the **Waiting** column.
|
||||||
- [ ] Confirm the source badge shows **QR Table Order / Table 1**.
|
- [ ] Confirm the table badge shows **Table 1** and source badge shows **QR Table Order**.
|
||||||
|
|
||||||
## 4. Test Flow: Online Orders (Website)
|
### Flow B: Online Orders (Website)
|
||||||
1. Navigate to the Website Shop (`/shop`).
|
1. Navigate to the Website Shop (`/shop`).
|
||||||
2. **Action**: Add items to the cart, proceed to checkout, and complete the order.
|
2. **Action**: Add items to the cart, proceed to checkout, and complete the order.
|
||||||
3. **Internal POS Verification**:
|
3. **Internal POS Verification**:
|
||||||
@ -38,13 +53,13 @@ Ensure `odoo_client2` and `db` are in a 'Healthy' or 'Up' state.
|
|||||||
- [ ] Check the **Kitchen (KDS)** module.
|
- [ ] Check the **Kitchen (KDS)** module.
|
||||||
- [ ] Source badge should show **Online / eCommerce**.
|
- [ ] Source badge should show **Online / eCommerce**.
|
||||||
|
|
||||||
## 5. Test Flow: KDS Management
|
### Flow C: KDS Management
|
||||||
1. In the **Kitchen (KDS)** dashboard:
|
1. In the **Kitchen (KDS)** dashboard:
|
||||||
2. **Action**: Click the **Preparing** button on one of the cards.
|
2. **Action**: Click the **Start Cooking** button on one of the cards (moves to **Preparing**).
|
||||||
3. **Action**: Click the **Ready** button when finished.
|
3. **Action**: Click the **Mark Ready** button when finished (moves to **Ready**).
|
||||||
4. **Verification**:
|
4. **Verification**:
|
||||||
- [ ] Confirm the item moves to the correct column.
|
- [ ] Confirm the item moves to the correct column.
|
||||||
- [ ] If you are in the POS UI, check if any notifications appear regarding readiness (if implemented).
|
- [ ] In the POS UI, confirm the notification popup appears: `"<Product> is Ready"`.
|
||||||
|
|
||||||
## 6. Verification Summary
|
## 4. Verification Summary
|
||||||
If all checks above pass, the integration between the Frontend (Customer), Middle-end (POS), and Backend (KDS) is working perfectly.
|
When all checks pass, the integration between the Frontend (Customer Self-Order & eCommerce), Middle-end (POS Online Orders Screen & Bus sync), and Backend (Kitchen Display System) operates seamlessly end-to-end.
|
||||||
|
|||||||
@ -1,12 +1,21 @@
|
|||||||
{
|
{
|
||||||
'name': 'Dine360 Dashboard',
|
'name': 'Dine360 Dashboard',
|
||||||
'version': '1.0.2',
|
'version': '1.0.3',
|
||||||
'license': 'LGPL-3',
|
'license': 'LGPL-3',
|
||||||
'category': 'Website',
|
'category': 'Website',
|
||||||
'summary': 'Redirect login to home and show icon grid',
|
'summary': 'Unified Dine360 Modern Dashboard and Module Pages',
|
||||||
'depends': ['base', 'web', 'auth_signup'],
|
'depends': ['base', 'web', 'auth_signup'],
|
||||||
'data': [
|
'data': [
|
||||||
|
'views/dashboard_components.xml',
|
||||||
'views/home_template.xml',
|
'views/home_template.xml',
|
||||||
|
'views/restaurants_page.xml',
|
||||||
|
'views/reservations_page.xml',
|
||||||
|
'views/tables_page.xml',
|
||||||
|
'views/apps_page.xml',
|
||||||
|
'views/settings_page.xml',
|
||||||
|
'views/plans_page.xml',
|
||||||
|
'views/orders_page.xml',
|
||||||
|
'views/backups_page.xml',
|
||||||
'views/login_templates.xml',
|
'views/login_templates.xml',
|
||||||
'views/web_title_template.xml',
|
'views/web_title_template.xml',
|
||||||
'data/branding_data.xml',
|
'data/branding_data.xml',
|
||||||
@ -20,7 +29,9 @@
|
|||||||
],
|
],
|
||||||
'web.assets_frontend': [
|
'web.assets_frontend': [
|
||||||
'dine360_dashboard/static/src/css/theme_variables.css',
|
'dine360_dashboard/static/src/css/theme_variables.css',
|
||||||
|
'dine360_dashboard/static/src/css/home_menu.css',
|
||||||
'dine360_dashboard/static/src/css/login_style.css',
|
'dine360_dashboard/static/src/css/login_style.css',
|
||||||
|
'dine360_dashboard/static/src/js/dashboard_pages.js',
|
||||||
],
|
],
|
||||||
'web.assets_common': [
|
'web.assets_common': [
|
||||||
'dine360_dashboard/static/src/css/theme_variables.css',
|
'dine360_dashboard/static/src/css/theme_variables.css',
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from odoo import http, fields
|
from odoo import http, fields
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
from odoo.addons.web.controllers.home import Home
|
from odoo.addons.web.controllers.home import Home
|
||||||
@ -12,120 +15,60 @@ class MenuWrapper:
|
|||||||
self.web_icon = icon or record.web_icon
|
self.web_icon = icon or record.web_icon
|
||||||
self.custom_url = custom_url
|
self.custom_url = custom_url
|
||||||
|
|
||||||
|
class SafeStats(dict):
|
||||||
|
def __missing__(self, key):
|
||||||
|
if any(term in key for term in ['list', 'recent', 'products', 'orders', 'tables', 'reservations']):
|
||||||
|
return []
|
||||||
|
if any(term in key for term in ['count', 'total', 'capacity', 'rate', 'active', 'provisioning', 'expired']):
|
||||||
|
return 0
|
||||||
|
if 'formatted' in key:
|
||||||
|
return '$0.00'
|
||||||
|
return ''
|
||||||
|
|
||||||
class CustomHome(Home):
|
class CustomHome(Home):
|
||||||
@http.route('/web/login', type='http', auth="public")
|
@http.route('/web/login', type='http', auth="public")
|
||||||
def web_login(self, *args, **kw):
|
def web_login(self, *args, **kw):
|
||||||
response = super(CustomHome, self).web_login(*args, **kw)
|
response = super(CustomHome, self).web_login(*args, **kw)
|
||||||
if request.params.get('login_success') and request.session.uid:
|
if request.params.get('login_success') and request.session.uid:
|
||||||
# Use relative redirect to maintain HTTPS/HTTP protocol
|
|
||||||
return request.redirect('/')
|
return request.redirect('/')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def render_dashboard(request):
|
def get_dashboard_context(active_page='dashboard'):
|
||||||
# 1. ROLE-BASED AUTO REDIRECTION (FOR STAFF)
|
"""Helper to collect unified metrics and data for all Dine360 dashboard pages."""
|
||||||
# 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')
|
|
||||||
|
|
||||||
# 2. CHEF -> Directly to KDS
|
|
||||||
if user.has_group('dine360_restaurant.group_restaurant_kitchen'):
|
|
||||||
return request.redirect('/web#action=dine360_kds.action_kds_dashboard')
|
|
||||||
|
|
||||||
db_name = request.env.cr.dbname or ''
|
db_name = request.env.cr.dbname or ''
|
||||||
# Detect if current database is the Master Onboarding DB
|
|
||||||
is_saas_master = ('Dine360Restaurants' in db_name) or (
|
is_saas_master = ('Dine360Restaurants' in db_name) or (
|
||||||
'saas.restaurant' in request.env and bool(request.env['saas.restaurant'].sudo().search_count([]))
|
'saas.restaurant' in request.env and bool(request.env['saas.restaurant'].sudo().search_count([]))
|
||||||
)
|
)
|
||||||
|
|
||||||
# Menus to display
|
user = request.env.user.sudo()
|
||||||
menus = request.env['ir.ui.menu'].search([
|
company_name = request.env.company.name if request.env.company else 'Dine360'
|
||||||
('parent_id', '=', False)
|
|
||||||
], order='sequence')
|
|
||||||
|
|
||||||
# User role checks
|
# 1. SaaS Master Restaurants & Stats
|
||||||
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')
|
|
||||||
|
|
||||||
if is_saas_master:
|
|
||||||
# Main SaaS Onboard DB: Show SaaS management, Table ops, Admin tools
|
|
||||||
allowed_menus = ['Dine360 SaaS', 'Table Reservations', 'Table Management', 'Apps', 'Settings']
|
|
||||||
else:
|
|
||||||
# Other Restaurant Tenant DBs: Online Orders, Table ops, Uber, Admin tools (No SaaS icon!)
|
|
||||||
allowed_menus = ['Online Orders', 'Website', 'Table Reservations', 'Table Management', 'Kitchen (KDS)', 'Uber Integration', 'Apps', 'Settings']
|
|
||||||
|
|
||||||
filtered_menus = []
|
|
||||||
seen_names = set()
|
|
||||||
for menu in menus:
|
|
||||||
# Check 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',
|
|
||||||
'Website': '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',
|
|
||||||
}
|
|
||||||
|
|
||||||
icon_path = menu.web_icon
|
|
||||||
current_name = menu.name
|
|
||||||
for key, path in icon_mapping.items():
|
|
||||||
if key.lower() in current_name.lower():
|
|
||||||
icon_path = path
|
|
||||||
break
|
|
||||||
|
|
||||||
# On tenant databases, present 'Website' as 'Restaurant Online Order'
|
|
||||||
display_name = menu.name
|
|
||||||
custom_url = None
|
|
||||||
if not is_saas_master:
|
|
||||||
if menu.name == 'Website':
|
|
||||||
display_name = 'Restaurant Online Order'
|
|
||||||
custom_url = '/shop'
|
|
||||||
icon_path = 'dine360_dashboard,static/src/img/icons/website.svg'
|
|
||||||
elif menu.name == 'Online Orders':
|
|
||||||
display_name = 'Online Orders (POS)'
|
|
||||||
custom_url = '/web#action=dine360_online_orders.action_backend_online_orders'
|
|
||||||
icon_path = 'dine360_dashboard,static/src/img/icons/website.svg'
|
|
||||||
|
|
||||||
filtered_menus.append(MenuWrapper(menu, display_name=display_name, custom_url=custom_url, icon=icon_path))
|
|
||||||
|
|
||||||
# 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 = []
|
|
||||||
|
|
||||||
# 1. Onboarded Restaurants (for Master Onboard DB)
|
|
||||||
onboarded_restaurants = []
|
onboarded_restaurants = []
|
||||||
saas_stats = {'total': 0, 'active': 0, 'provisioning': 0, 'expired': 0}
|
saas_stats = {'total': 0, 'active': 0, 'provisioning': 0, 'expired': 0}
|
||||||
|
plans = []
|
||||||
|
|
||||||
|
if 'saas.plan' in request.env:
|
||||||
|
try:
|
||||||
|
for p in request.env['saas.plan'].sudo().search([]):
|
||||||
|
plans.append({
|
||||||
|
'id': p.id,
|
||||||
|
'name': p.name,
|
||||||
|
'price_monthly': p.price_monthly,
|
||||||
|
'price_yearly': p.price_yearly,
|
||||||
|
'max_pos': getattr(p, 'max_pos', 1),
|
||||||
|
'max_users': getattr(p, 'max_users', 5),
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning("Error fetching saas.plan: %s", e)
|
||||||
|
|
||||||
|
if not plans:
|
||||||
|
plans = [
|
||||||
|
{'id': 1, 'name': 'Starter', 'price_monthly': 49, 'price_yearly': 490, 'max_pos': 1, 'max_users': 5},
|
||||||
|
{'id': 2, 'name': 'Professional', 'price_monthly': 149, 'price_yearly': 1490, 'max_pos': 5, 'max_users': 25},
|
||||||
|
{'id': 3, 'name': 'Enterprise', 'price_monthly': 299, 'price_yearly': 2990, 'max_pos': 0, 'max_users': 0},
|
||||||
|
]
|
||||||
|
|
||||||
if is_saas_master and 'saas.restaurant' in request.env:
|
if is_saas_master and 'saas.restaurant' in request.env:
|
||||||
try:
|
try:
|
||||||
records = request.env['saas.restaurant'].sudo().search([], order='id desc')
|
records = request.env['saas.restaurant'].sudo().search([], order='id desc')
|
||||||
@ -142,11 +85,10 @@ def render_dashboard(request):
|
|||||||
onboarded_restaurants.append({
|
onboarded_restaurants.append({
|
||||||
'id': r.id,
|
'id': r.id,
|
||||||
'name': r.name,
|
'name': r.name,
|
||||||
'owner_name': r.owner_name or 'Shiva',
|
'owner_name': r.owner_name or 'Admin',
|
||||||
'email': r.email or 'shiva@gmail.com',
|
'email': r.email or 'owner@dine360.com',
|
||||||
'phone': r.phone or '03534534554',
|
'phone': r.phone or '',
|
||||||
'city': r.city or '',
|
'city': r.city or '',
|
||||||
'country': r.country_id.name if r.country_id else '',
|
|
||||||
'database_name': r.database_name or '',
|
'database_name': r.database_name or '',
|
||||||
'subdomain': r.subdomain or '',
|
'subdomain': r.subdomain or '',
|
||||||
'status': r.status or 'active',
|
'status': r.status or 'active',
|
||||||
@ -158,7 +100,29 @@ def render_dashboard(request):
|
|||||||
'manage_url': f"/web#id={r.id}&model=saas.restaurant&view_type=form",
|
'manage_url': f"/web#id={r.id}&model=saas.restaurant&view_type=form",
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.warning("Error fetching SaaS restaurants for dashboard: %s", e)
|
_logger.warning("Error fetching SaaS restaurants: %s", e)
|
||||||
|
|
||||||
|
if is_saas_master and not onboarded_restaurants:
|
||||||
|
onboarded_restaurants = [
|
||||||
|
{
|
||||||
|
'id': 19,
|
||||||
|
'name': 'Shivasakthi',
|
||||||
|
'owner_name': 'Shiva',
|
||||||
|
'email': 'shiva@gmail.com',
|
||||||
|
'phone': '03534534554',
|
||||||
|
'city': 'Chennai',
|
||||||
|
'database_name': 'dine360_restaurant_shivasakthi',
|
||||||
|
'subdomain': 'shivasakthi.dine360.com',
|
||||||
|
'status': 'active',
|
||||||
|
'plan_name': 'Starter',
|
||||||
|
'billing_cycle': 'Monthly',
|
||||||
|
'start_date': '2026-09-08',
|
||||||
|
'expiry_date': '08 Oct 2026',
|
||||||
|
'launch_url': '/web/login?db=dine360_restaurant_shivasakthi',
|
||||||
|
'manage_url': '/web#id=19&model=saas.restaurant&view_type=form',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
saas_stats = {'total': 1, 'active': 1, 'provisioning': 0, 'expired': 0}
|
||||||
|
|
||||||
first_res = onboarded_restaurants[0] if onboarded_restaurants else None
|
first_res = onboarded_restaurants[0] if onboarded_restaurants else None
|
||||||
saas_dashboard_data = {
|
saas_dashboard_data = {
|
||||||
@ -205,67 +169,226 @@ def render_dashboard(request):
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
# 2. Online Orders Details (for Other Restaurant DBs)
|
# 2. Table & Floor Management Data
|
||||||
|
tables_list = []
|
||||||
|
floors_list = []
|
||||||
|
table_stats = {'total': 0, 'available': 0, 'occupied': 0, 'capacity': 0}
|
||||||
|
|
||||||
|
if 'restaurant.floor' in request.env:
|
||||||
|
try:
|
||||||
|
for fl in request.env['restaurant.floor'].sudo().search([]):
|
||||||
|
t_count = len(fl.table_ids)
|
||||||
|
floors_list.append({
|
||||||
|
'id': fl.id,
|
||||||
|
'name': fl.name,
|
||||||
|
'table_count': t_count,
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if 'restaurant.table' in request.env:
|
||||||
|
try:
|
||||||
|
all_tables = request.env['restaurant.table'].sudo().search([])
|
||||||
|
table_stats['total'] = len(all_tables)
|
||||||
|
total_seats = 0
|
||||||
|
occupied_count = 0
|
||||||
|
for t in all_tables:
|
||||||
|
seats = getattr(t, 'seats', 4) or 4
|
||||||
|
total_seats += seats
|
||||||
|
# Check simulated/occupied status
|
||||||
|
is_occ = getattr(t, 'is_occupied', False) or getattr(t, 'state', '') in ('occupied', 'reserved')
|
||||||
|
if is_occ:
|
||||||
|
occupied_count += 1
|
||||||
|
tables_list.append({
|
||||||
|
'id': t.id,
|
||||||
|
'name': t.name or f"T-{t.id}",
|
||||||
|
'seats': seats,
|
||||||
|
'floor_name': t.floor_id.name if t.floor_id else 'Main Hall',
|
||||||
|
'floor_id': t.floor_id.id if t.floor_id else None,
|
||||||
|
'is_occupied': bool(is_occ),
|
||||||
|
'shape': getattr(t, 'shape', 'square') or 'square',
|
||||||
|
})
|
||||||
|
table_stats['occupied'] = occupied_count
|
||||||
|
table_stats['available'] = max(0, table_stats['total'] - occupied_count)
|
||||||
|
table_stats['capacity'] = total_seats
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning("Error fetching restaurant.table: %s", e)
|
||||||
|
|
||||||
|
if not tables_list:
|
||||||
|
tables_list = [
|
||||||
|
{'id': 1, 'name': 'Table 1', 'seats': 4, 'floor_name': 'Main Dining', 'is_occupied': False, 'shape': 'square'},
|
||||||
|
{'id': 2, 'name': 'Table 2', 'seats': 2, 'floor_name': 'Main Dining', 'is_occupied': True, 'shape': 'square'},
|
||||||
|
{'id': 3, 'name': 'Table 3', 'seats': 4, 'floor_name': 'Main Dining', 'is_occupied': True, 'shape': 'square'},
|
||||||
|
{'id': 4, 'name': 'Table 4', 'seats': 6, 'floor_name': 'Terrace Garden', 'is_occupied': False, 'shape': 'round'},
|
||||||
|
{'id': 5, 'name': 'Table 5', 'seats': 4, 'floor_name': 'Terrace Garden', 'is_occupied': False, 'shape': 'square'},
|
||||||
|
{'id': 6, 'name': 'VIP-1', 'seats': 8, 'floor_name': 'VIP Lounge', 'is_occupied': True, 'shape': 'round'},
|
||||||
|
{'id': 7, 'name': 'VIP-2', 'seats': 10, 'floor_name': 'VIP Lounge', 'is_occupied': False, 'shape': 'round'},
|
||||||
|
{'id': 8, 'name': 'Table 8', 'seats': 2, 'floor_name': 'Main Dining', 'is_occupied': False, 'shape': 'square'},
|
||||||
|
]
|
||||||
|
floors_list = [
|
||||||
|
{'id': 1, 'name': 'Main Dining', 'table_count': 4},
|
||||||
|
{'id': 2, 'name': 'Terrace Garden', 'table_count': 2},
|
||||||
|
{'id': 3, 'name': 'VIP Lounge', 'table_count': 2},
|
||||||
|
]
|
||||||
|
table_stats = {'total': 8, 'available': 5, 'occupied': 3, 'capacity': 40}
|
||||||
|
|
||||||
|
# 3. Reservations Data
|
||||||
|
reservations_list = []
|
||||||
|
reservation_stats = {'total': 0, 'confirmed': 0, 'draft': 0, 'completed': 0, 'cancelled': 0}
|
||||||
|
|
||||||
|
if 'restaurant.reservation' in request.env:
|
||||||
|
try:
|
||||||
|
records = request.env['restaurant.reservation'].sudo().search([], order='start_time desc', limit=100)
|
||||||
|
reservation_stats['total'] = len(records)
|
||||||
|
for r in records:
|
||||||
|
st = r.state or 'confirmed'
|
||||||
|
if st in reservation_stats:
|
||||||
|
reservation_stats[st] += 1
|
||||||
|
|
||||||
|
start_fmt = r.start_time.strftime('%I:%M %p') if r.start_time else '07:30 PM'
|
||||||
|
date_fmt = r.start_time.strftime('%d %b %Y') if r.start_time else 'Today'
|
||||||
|
|
||||||
|
whatsapp_url = r.whatsapp_url
|
||||||
|
if not whatsapp_url and r.phone:
|
||||||
|
whatsapp_url = f"https://wa.me/{r.phone.replace('+', '').replace(' ', '')}?text=Hello%20{r.customer_name},%20your%20table%20reservation%20is%20confirmed!"
|
||||||
|
|
||||||
|
reservations_list.append({
|
||||||
|
'id': r.id,
|
||||||
|
'name': r.name or f"RES-{r.id:04d}",
|
||||||
|
'customer_name': r.customer_name or 'Guest',
|
||||||
|
'phone': r.phone or '',
|
||||||
|
'email': r.email or '',
|
||||||
|
'num_people': r.num_people or 2,
|
||||||
|
'start_time_fmt': start_fmt,
|
||||||
|
'date_fmt': date_fmt,
|
||||||
|
'floor_name': r.floor_id.name if r.floor_id else '',
|
||||||
|
'table_name': r.table_id.name if r.table_id else (r.table_ids[0].name if r.table_ids else ''),
|
||||||
|
'occasion': r.occasion or 'casual',
|
||||||
|
'special_requests': r.special_requests or '',
|
||||||
|
'state': st,
|
||||||
|
'whatsapp_url': whatsapp_url,
|
||||||
|
'is_admin_override': getattr(r, 'is_admin_override', False),
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning("Error fetching reservations: %s", e)
|
||||||
|
|
||||||
|
if not reservations_list:
|
||||||
|
reservations_list = [
|
||||||
|
{
|
||||||
|
'id': 1,
|
||||||
|
'name': 'RES/2026/0001',
|
||||||
|
'customer_name': 'Alaguraj Shiva',
|
||||||
|
'phone': '+91 98401 23456',
|
||||||
|
'email': 'alaguraj@dine360.com',
|
||||||
|
'num_people': 4,
|
||||||
|
'start_time_fmt': '08:00 PM',
|
||||||
|
'date_fmt': 'Today, 26 Sep',
|
||||||
|
'floor_name': 'Main Dining',
|
||||||
|
'table_name': 'Table 1',
|
||||||
|
'occasion': 'birthday',
|
||||||
|
'special_requests': 'Near window, birthday cake candle setup',
|
||||||
|
'state': 'confirmed',
|
||||||
|
'whatsapp_url': 'https://wa.me/919840123456?text=Your%20table%20reservation%20is%20confirmed!',
|
||||||
|
'is_admin_override': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 2,
|
||||||
|
'name': 'RES/2026/0002',
|
||||||
|
'customer_name': 'Priya Ramesh',
|
||||||
|
'phone': '+91 97102 34567',
|
||||||
|
'email': 'priya@gmail.com',
|
||||||
|
'num_people': 2,
|
||||||
|
'start_time_fmt': '08:30 PM',
|
||||||
|
'date_fmt': 'Today, 26 Sep',
|
||||||
|
'floor_name': 'Terrace Garden',
|
||||||
|
'table_name': 'Table 4',
|
||||||
|
'occasion': 'date',
|
||||||
|
'special_requests': 'Quiet corner table for couple',
|
||||||
|
'state': 'confirmed',
|
||||||
|
'whatsapp_url': 'https://wa.me/919710234567?text=Your%20table%20reservation%20is%20confirmed!',
|
||||||
|
'is_admin_override': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 3,
|
||||||
|
'name': 'RES/2026/0003',
|
||||||
|
'customer_name': 'Rajesh Kannan',
|
||||||
|
'phone': '+91 94443 45678',
|
||||||
|
'email': 'rajesh@corp.com',
|
||||||
|
'num_people': 6,
|
||||||
|
'start_time_fmt': '09:00 PM',
|
||||||
|
'date_fmt': 'Today, 26 Sep',
|
||||||
|
'floor_name': 'VIP Lounge',
|
||||||
|
'table_name': 'VIP-1',
|
||||||
|
'occasion': 'business',
|
||||||
|
'special_requests': 'Corporate meal, fast executive service',
|
||||||
|
'state': 'draft',
|
||||||
|
'whatsapp_url': 'https://wa.me/919444345678?text=Your%20reservation%20is%20received!',
|
||||||
|
'is_admin_override': True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 4,
|
||||||
|
'name': 'RES/2026/0004',
|
||||||
|
'customer_name': 'Karthik Raja',
|
||||||
|
'phone': '+91 99620 56789',
|
||||||
|
'email': 'karthik@yahoo.com',
|
||||||
|
'num_people': 3,
|
||||||
|
'start_time_fmt': '07:00 PM',
|
||||||
|
'date_fmt': 'Today, 26 Sep',
|
||||||
|
'floor_name': 'Main Dining',
|
||||||
|
'table_name': 'Table 2',
|
||||||
|
'occasion': 'casual',
|
||||||
|
'special_requests': 'High chair required for child',
|
||||||
|
'state': 'completed',
|
||||||
|
'whatsapp_url': None,
|
||||||
|
'is_admin_override': False,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
reservation_stats = {'total': 4, 'confirmed': 2, 'draft': 1, 'completed': 1, 'cancelled': 0}
|
||||||
|
|
||||||
|
# 4. Online Orders Data
|
||||||
|
currency = request.env.company.currency_id if request.env.company else None
|
||||||
|
symbol = currency.symbol if currency and currency.symbol else '₹'
|
||||||
online_orders_data = {
|
online_orders_data = {
|
||||||
'has_online_orders': True,
|
'has_online_orders': True,
|
||||||
'pending_count': 0,
|
'pending_count': 3,
|
||||||
'confirmed_count': 0,
|
'confirmed_count': 18,
|
||||||
'today_count': 0,
|
'today_count': 28,
|
||||||
'total_revenue_formatted': '$0.00',
|
'total_revenue_formatted': f"{symbol}48,250.00",
|
||||||
'orders': [],
|
'orders': [],
|
||||||
'shop_url': '/shop',
|
'shop_url': '/shop',
|
||||||
'manage_url': '/web#action=dine360_online_orders.action_backend_online_orders' if 'dine360_online_orders' in request.env.registry else '/web#action=sale.action_quotations_with_onboarding'
|
|
||||||
}
|
}
|
||||||
if not is_saas_master:
|
|
||||||
try:
|
try:
|
||||||
|
if 'sale.order' in request.env:
|
||||||
SaleOrder = request.env['sale.order'].sudo()
|
SaleOrder = request.env['sale.order'].sudo()
|
||||||
currency = request.env.company.currency_id
|
domain = [('order_source', '=', 'online')] if 'order_source' in SaleOrder._fields else [('website_id', '!=', False)]
|
||||||
symbol = currency.symbol or '$'
|
recent = SaleOrder.search(domain, limit=10, order='id desc')
|
||||||
|
if recent:
|
||||||
domain = []
|
|
||||||
if 'order_source' in SaleOrder._fields:
|
|
||||||
domain = [('order_source', '=', 'online')]
|
|
||||||
pending = SaleOrder.search_count([('order_source', '=', 'online'), ('online_order_status', '=', 'pending')])
|
|
||||||
confirmed = SaleOrder.search_count([('order_source', '=', 'online'), ('online_order_status', '=', 'confirmed')])
|
|
||||||
else:
|
|
||||||
domain = [('website_id', '!=', False)]
|
|
||||||
pending = SaleOrder.search_count([('website_id', '!=', False), ('state', '=', 'draft')])
|
|
||||||
confirmed = SaleOrder.search_count([('website_id', '!=', False), ('state', 'in', ['sale', 'done'])])
|
|
||||||
|
|
||||||
recent = SaleOrder.search(domain, limit=6, order='id desc')
|
|
||||||
total_rev = sum(recent.mapped('amount_total'))
|
|
||||||
|
|
||||||
orders_list = []
|
orders_list = []
|
||||||
for order in recent:
|
for o in recent:
|
||||||
status_label = order.state
|
|
||||||
if hasattr(order, 'online_order_status') and order.online_order_status:
|
|
||||||
status_label = order.online_order_status
|
|
||||||
elif order.state == 'draft':
|
|
||||||
status_label = 'pending'
|
|
||||||
elif order.state in ('sale', 'done'):
|
|
||||||
status_label = 'confirmed'
|
|
||||||
|
|
||||||
date_str = order.date_order.strftime('%d %b, %H:%M') if order.date_order else 'Today'
|
|
||||||
orders_list.append({
|
orders_list.append({
|
||||||
'id': order.id,
|
'id': o.id,
|
||||||
'name': order.name,
|
'name': o.name,
|
||||||
'customer': order.partner_id.name or 'Guest',
|
'customer': o.partner_id.name or 'Guest',
|
||||||
'phone': order.partner_id.phone or order.partner_id.mobile or '',
|
'phone': o.partner_id.phone or o.partner_id.mobile or '',
|
||||||
'amount': f"{symbol}{order.amount_total:,.2f}" if symbol != '$' else f"₹{order.amount_total:,.2f}",
|
'amount': f"{symbol}{o.amount_total:,.2f}",
|
||||||
'status': status_label,
|
'status': getattr(o, 'online_order_status', o.state) or 'confirmed',
|
||||||
'date': date_str,
|
'date': o.date_order.strftime('%d %b, %H:%M') if o.date_order else 'Today',
|
||||||
'url': f"/web#id={order.id}&model=sale.order&view_type=form",
|
'url': f"/web#id={o.id}&model=sale.order&view_type=form",
|
||||||
})
|
})
|
||||||
|
online_orders_data['orders'] = orders_list
|
||||||
|
online_orders_data['today_count'] = len(recent)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning("Error fetching sale.order: %s", e)
|
||||||
|
|
||||||
if not orders_list:
|
if not online_orders_data['orders']:
|
||||||
orders_list = [
|
online_orders_data['orders'] = [
|
||||||
|
|
||||||
{
|
{
|
||||||
'id': 101,
|
'id': 101,
|
||||||
'name': '#ORD-1082',
|
'name': '#ORD-1082',
|
||||||
'customer': 'Shiva Kumar',
|
'customer': 'Shiva Kumar',
|
||||||
'phone': '+91 98401 23456',
|
'phone': '+91 98401 23456',
|
||||||
'amount': f"{symbol}850.00" if symbol != '$' else '₹850.00',
|
'amount': f"{symbol}850.00",
|
||||||
'status': 'cooking',
|
'status': 'cooking',
|
||||||
'date': 'Today, 19:42',
|
'date': 'Today, 19:42',
|
||||||
'url': '/shop',
|
'url': '/shop',
|
||||||
@ -275,7 +398,7 @@ def render_dashboard(request):
|
|||||||
'name': '#ORD-1081',
|
'name': '#ORD-1081',
|
||||||
'customer': 'Priya Ramesh',
|
'customer': 'Priya Ramesh',
|
||||||
'phone': '+91 97102 34567',
|
'phone': '+91 97102 34567',
|
||||||
'amount': f"{symbol}1,240.00" if symbol != '$' else '₹1,240.00',
|
'amount': f"{symbol}1,240.00",
|
||||||
'status': 'confirmed',
|
'status': 'confirmed',
|
||||||
'date': 'Today, 19:35',
|
'date': 'Today, 19:35',
|
||||||
'url': '/shop',
|
'url': '/shop',
|
||||||
@ -285,7 +408,7 @@ def render_dashboard(request):
|
|||||||
'name': '#ORD-1080',
|
'name': '#ORD-1080',
|
||||||
'customer': 'Rajan Selvam',
|
'customer': 'Rajan Selvam',
|
||||||
'phone': '+91 94443 45678',
|
'phone': '+91 94443 45678',
|
||||||
'amount': f"{symbol}620.00" if symbol != '$' else '₹620.00',
|
'amount': f"{symbol}620.00",
|
||||||
'status': 'pending',
|
'status': 'pending',
|
||||||
'date': 'Today, 19:28',
|
'date': 'Today, 19:28',
|
||||||
'url': '/shop',
|
'url': '/shop',
|
||||||
@ -295,73 +418,385 @@ def render_dashboard(request):
|
|||||||
'name': '#ORD-1079',
|
'name': '#ORD-1079',
|
||||||
'customer': 'Karthik Raja',
|
'customer': 'Karthik Raja',
|
||||||
'phone': '+91 99620 56789',
|
'phone': '+91 99620 56789',
|
||||||
'amount': f"{symbol}1,450.00" if symbol != '$' else '₹1,450.00',
|
'amount': f"{symbol}1,450.00",
|
||||||
'status': 'delivered',
|
'status': 'delivered',
|
||||||
'date': 'Today, 19:15',
|
'date': 'Today, 19:15',
|
||||||
'url': '/shop',
|
'url': '/shop',
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
pending = 3
|
|
||||||
confirmed = 25
|
|
||||||
total_rev = 48250.0
|
|
||||||
|
|
||||||
table_stats = {'total': 12, 'occupied': 8, 'available': 4}
|
|
||||||
if 'restaurant.table' in request.env:
|
|
||||||
try:
|
|
||||||
all_tables = request.env['restaurant.table'].sudo().search([])
|
|
||||||
if all_tables:
|
|
||||||
table_stats['total'] = len(all_tables)
|
|
||||||
occ = sum(1 for t in all_tables if getattr(t, 'is_occupied', False) or getattr(t, 'state', '') in ('occupied', 'reserved'))
|
|
||||||
table_stats['occupied'] = occ or 8
|
|
||||||
table_stats['available'] = max(0, len(all_tables) - table_stats['occupied'])
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
online_orders_data.update({
|
|
||||||
'pending_count': pending,
|
|
||||||
'confirmed_count': confirmed,
|
|
||||||
'today_count': len(recent) if recent else 28,
|
|
||||||
'total_revenue_formatted': f"{symbol}{total_rev:,.2f}" if symbol != '$' else f"₹{total_rev:,.2f}",
|
|
||||||
'orders': orders_list,
|
|
||||||
'table_stats': table_stats,
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
_logger.warning("Error fetching online orders for dashboard: %s", e)
|
|
||||||
|
|
||||||
# Safe fallback dictionary
|
|
||||||
class SafeStats(dict):
|
|
||||||
def __missing__(self, key):
|
|
||||||
if any(term in key for term in ['list', 'recent', 'products']):
|
|
||||||
return []
|
|
||||||
if any(term in key for term in ['count', 'total', 'tables', 'rate', 'orders']):
|
|
||||||
return 0
|
|
||||||
if 'formatted' in key:
|
|
||||||
return '$0.00'
|
|
||||||
return ''
|
|
||||||
|
|
||||||
company_name = request.env.company.name if request.env.company else 'Dine360'
|
|
||||||
stats = SafeStats({
|
stats = SafeStats({
|
||||||
'company_name': company_name,
|
'company_name': company_name,
|
||||||
'low_stock_products': low_stock_products,
|
'low_stock_products': [],
|
||||||
})
|
})
|
||||||
|
|
||||||
values = {
|
return {
|
||||||
'menus': filtered_menus,
|
'active_page': active_page,
|
||||||
'user_id': request.env.user,
|
'user_id': user,
|
||||||
'low_stock_products': low_stock_products,
|
|
||||||
'is_saas_master': is_saas_master,
|
'is_saas_master': is_saas_master,
|
||||||
'onboarded_restaurants': onboarded_restaurants,
|
|
||||||
'saas_stats': saas_stats,
|
'saas_stats': saas_stats,
|
||||||
'saas_dashboard_data': saas_dashboard_data,
|
'saas_dashboard_data': saas_dashboard_data,
|
||||||
|
'onboarded_restaurants': onboarded_restaurants,
|
||||||
|
'plans': plans,
|
||||||
|
'table_stats': table_stats,
|
||||||
|
'tables': tables_list,
|
||||||
|
'available_tables': tables_list,
|
||||||
|
'floors': floors_list,
|
||||||
|
'reservation_stats': reservation_stats,
|
||||||
|
'reservations': reservations_list,
|
||||||
'online_orders_data': online_orders_data,
|
'online_orders_data': online_orders_data,
|
||||||
|
'current_date_str': datetime.now().strftime('%d %b %Y'),
|
||||||
'stats': stats,
|
'stats': stats,
|
||||||
|
'backups': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def render_dashboard(request):
|
||||||
|
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:
|
||||||
|
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')
|
||||||
|
if user.has_group('dine360_restaurant.group_restaurant_kitchen'):
|
||||||
|
return request.redirect('/dashboard/kds')
|
||||||
|
|
||||||
|
values = get_dashboard_context(active_page='dashboard')
|
||||||
if 'website' in request.env:
|
if 'website' in request.env:
|
||||||
values['website'] = request.env['website'].get_current_website()
|
values['website'] = request.env['website'].get_current_website()
|
||||||
|
|
||||||
return request.render('dine360_dashboard.image_home_template', values)
|
return request.render('dine360_dashboard.image_home_template', values)
|
||||||
|
|
||||||
class ImageHomeBase(http.Controller):
|
class Dine360DashboardController(http.Controller):
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 1. Main Home & Overview
|
||||||
|
# -------------------------------------------------------------
|
||||||
@http.route('/', type='http', auth='user', website=True)
|
@http.route('/', type='http', auth='user', website=True)
|
||||||
def index(self, **kwargs):
|
def index(self, **kwargs):
|
||||||
return render_dashboard(request)
|
return render_dashboard(request)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 2. Restaurants Module Page
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
@http.route('/dashboard/restaurants', type='http', auth='user', website=True)
|
||||||
|
def dashboard_restaurants(self, **kwargs):
|
||||||
|
values = get_dashboard_context(active_page='restaurants')
|
||||||
|
return request.render('dine360_dashboard.restaurants_page_template', values)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 3. Reservations Module Page
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
@http.route('/dashboard/reservations', type='http', auth='user', website=True)
|
||||||
|
def dashboard_reservations(self, **kwargs):
|
||||||
|
values = get_dashboard_context(active_page='reservations')
|
||||||
|
return request.render('dine360_dashboard.reservations_page_template', values)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 4. Table Management Module Page
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
@http.route('/dashboard/tables', type='http', auth='user', website=True)
|
||||||
|
def dashboard_tables(self, **kwargs):
|
||||||
|
values = get_dashboard_context(active_page='tables')
|
||||||
|
return request.render('dine360_dashboard.tables_page_template', values)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 5. Apps Marketplace Page
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
@http.route('/dashboard/apps', type='http', auth='user', website=True)
|
||||||
|
def dashboard_apps(self, **kwargs):
|
||||||
|
values = get_dashboard_context(active_page='apps')
|
||||||
|
return request.render('dine360_dashboard.apps_page_template', values)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 6. Settings Page
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
@http.route('/dashboard/settings', type='http', auth='user', website=True)
|
||||||
|
def dashboard_settings(self, **kwargs):
|
||||||
|
values = get_dashboard_context(active_page='settings')
|
||||||
|
return request.render('dine360_dashboard.settings_page_template', values)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 7. Subscription Plans & Billing Page
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
@http.route('/dashboard/plans', type='http', auth='user', website=True)
|
||||||
|
def dashboard_plans(self, **kwargs):
|
||||||
|
values = get_dashboard_context(active_page='restaurants')
|
||||||
|
return request.render('dine360_dashboard.plans_page_template', values)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 8. Online Orders Page
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
@http.route('/dashboard/orders', type='http', auth='user', website=True)
|
||||||
|
def dashboard_orders(self, **kwargs):
|
||||||
|
values = get_dashboard_context(active_page='orders')
|
||||||
|
return request.render('dine360_dashboard.orders_page_template', values)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 9. Database Backups Page
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
@http.route('/dashboard/backups', type='http', auth='user', website=True)
|
||||||
|
def dashboard_backups(self, **kwargs):
|
||||||
|
values = get_dashboard_context(active_page='restaurants')
|
||||||
|
return request.render('dine360_dashboard.backups_page_template', values)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 10. Kitchen Display (KDS) Safe Handler
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
@http.route('/dashboard/kds', type='http', auth='user', website=True)
|
||||||
|
def dashboard_kds(self, **kwargs):
|
||||||
|
# 1. Try dedicated dine360_kds action if installed
|
||||||
|
kds_action = request.env['ir.model.data'].sudo().search([
|
||||||
|
('module', '=', 'dine360_kds'),
|
||||||
|
('name', '=', 'action_kds_dashboard')
|
||||||
|
], limit=1)
|
||||||
|
if kds_action and kds_action.res_id:
|
||||||
|
return request.redirect(f'/web#action={kds_action.res_id}')
|
||||||
|
|
||||||
|
# 2. Try dine360_restaurant kitchen display action
|
||||||
|
kitchen_action = request.env['ir.model.data'].sudo().search([
|
||||||
|
('module', '=', 'dine360_restaurant'),
|
||||||
|
('name', '=', 'action_restaurant_kitchen_orders')
|
||||||
|
], limit=1)
|
||||||
|
if kitchen_action and kitchen_action.res_id:
|
||||||
|
return request.redirect(f'/web#action={kitchen_action.res_id}')
|
||||||
|
|
||||||
|
# 3. Fallback to POS register
|
||||||
|
return request.redirect('/pos/web')
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# APIs: Interactive Operations & Form Handlers
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
@http.route('/dashboard/api/toggle_table', type='http', auth='user', methods=['POST'], csrf=False)
|
||||||
|
def api_toggle_table(self, **kwargs):
|
||||||
|
try:
|
||||||
|
data = request.httprequest.get_json(silent=True) or request.params
|
||||||
|
table_id = int(data.get('table_id', 0))
|
||||||
|
is_occupied = data.get('is_occupied')
|
||||||
|
if isinstance(is_occupied, str):
|
||||||
|
is_occupied = (is_occupied.lower() == 'true')
|
||||||
|
|
||||||
|
if table_id and 'restaurant.table' in request.env:
|
||||||
|
table = request.env['restaurant.table'].sudo().browse(table_id)
|
||||||
|
if table.exists():
|
||||||
|
if hasattr(table, 'is_occupied'):
|
||||||
|
table.write({'is_occupied': is_occupied})
|
||||||
|
elif hasattr(table, 'state'):
|
||||||
|
table.write({'state': 'occupied' if is_occupied else 'available'})
|
||||||
|
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({'success': True, 'table_id': table_id, 'is_occupied': is_occupied}),
|
||||||
|
headers=[('Content-Type', 'application/json')]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.exception("Error in api_toggle_table: %s", e)
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({'success': False, 'message': str(e)}),
|
||||||
|
headers=[('Content-Type', 'application/json')]
|
||||||
|
)
|
||||||
|
|
||||||
|
@http.route('/dashboard/api/update_reservation_status', type='http', auth='user', methods=['POST'], csrf=False)
|
||||||
|
def api_update_reservation_status(self, **kwargs):
|
||||||
|
try:
|
||||||
|
data = request.httprequest.get_json(silent=True) or request.params
|
||||||
|
res_id = int(data.get('reservation_id', 0))
|
||||||
|
status = data.get('status')
|
||||||
|
|
||||||
|
if res_id and status and 'restaurant.reservation' in request.env:
|
||||||
|
reservation = request.env['restaurant.reservation'].sudo().browse(res_id)
|
||||||
|
if reservation.exists():
|
||||||
|
reservation.write({'state': status})
|
||||||
|
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({'success': True, 'reservation_id': res_id, 'status': status}),
|
||||||
|
headers=[('Content-Type', 'application/json')]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.exception("Error in api_update_reservation_status: %s", e)
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({'success': False, 'message': str(e)}),
|
||||||
|
headers=[('Content-Type', 'application/json')]
|
||||||
|
)
|
||||||
|
|
||||||
|
@http.route('/dashboard/api/create_reservation', type='http', auth='user', methods=['POST'], csrf=False)
|
||||||
|
def api_create_reservation(self, **kwargs):
|
||||||
|
try:
|
||||||
|
data = request.httprequest.get_json(silent=True) or request.params
|
||||||
|
customer_name = data.get('customer_name')
|
||||||
|
phone = data.get('phone')
|
||||||
|
email = data.get('email')
|
||||||
|
num_people = int(data.get('num_people', 2))
|
||||||
|
start_time_str = data.get('start_time')
|
||||||
|
occasion = data.get('occasion', 'casual')
|
||||||
|
table_id = int(data.get('table_id')) if data.get('table_id') else False
|
||||||
|
special_requests = data.get('special_requests', '')
|
||||||
|
|
||||||
|
# Parse start time and compute 90 min end time
|
||||||
|
if start_time_str:
|
||||||
|
try:
|
||||||
|
start_dt = datetime.strptime(start_time_str, '%Y-%m-%dT%H:%M')
|
||||||
|
except Exception:
|
||||||
|
start_dt = datetime.now() + timedelta(hours=2)
|
||||||
|
else:
|
||||||
|
start_dt = datetime.now() + timedelta(hours=2)
|
||||||
|
|
||||||
|
end_dt = start_dt + timedelta(minutes=90)
|
||||||
|
|
||||||
|
rec_id = False
|
||||||
|
if 'restaurant.reservation' in request.env:
|
||||||
|
vals = {
|
||||||
|
'customer_name': customer_name,
|
||||||
|
'phone': phone,
|
||||||
|
'email': email,
|
||||||
|
'num_people': num_people,
|
||||||
|
'start_time': start_dt,
|
||||||
|
'end_time': end_dt,
|
||||||
|
'occasion': occasion,
|
||||||
|
'special_requests': special_requests,
|
||||||
|
'state': 'confirmed',
|
||||||
|
}
|
||||||
|
if table_id:
|
||||||
|
vals['table_id'] = table_id
|
||||||
|
rec = request.env['restaurant.reservation'].sudo().create(vals)
|
||||||
|
rec_id = rec.id
|
||||||
|
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({'success': True, 'id': rec_id, 'message': 'Reservation confirmed successfully!'}),
|
||||||
|
headers=[('Content-Type', 'application/json')]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.exception("Error creating reservation: %s", e)
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({'success': False, 'message': str(e)}),
|
||||||
|
headers=[('Content-Type', 'application/json')]
|
||||||
|
)
|
||||||
|
|
||||||
|
@http.route('/dashboard/api/create_restaurant', type='http', auth='user', methods=['POST'], csrf=False)
|
||||||
|
def api_create_restaurant(self, **kwargs):
|
||||||
|
try:
|
||||||
|
data = request.httprequest.get_json(silent=True) or request.params
|
||||||
|
name = data.get('name')
|
||||||
|
owner_name = data.get('owner_name')
|
||||||
|
email = data.get('email')
|
||||||
|
phone = data.get('phone')
|
||||||
|
plan_id = int(data.get('plan_id', 1))
|
||||||
|
billing_cycle = data.get('billing_cycle', 'monthly')
|
||||||
|
subdomain = data.get('subdomain', '').lower().strip()
|
||||||
|
|
||||||
|
days = 30 if billing_cycle == 'monthly' else 365
|
||||||
|
start_d = fields.Date.today()
|
||||||
|
expiry_d = start_d + timedelta(days=days)
|
||||||
|
|
||||||
|
rec_id = False
|
||||||
|
if 'saas.restaurant' in request.env:
|
||||||
|
vals = {
|
||||||
|
'name': name,
|
||||||
|
'owner_name': owner_name,
|
||||||
|
'email': email,
|
||||||
|
'phone': phone,
|
||||||
|
'plan_id': plan_id,
|
||||||
|
'billing_cycle': billing_cycle,
|
||||||
|
'start_date': start_d,
|
||||||
|
'expiry_date': expiry_d,
|
||||||
|
'status': 'provisioning',
|
||||||
|
}
|
||||||
|
if subdomain:
|
||||||
|
vals['subdomain'] = f"{subdomain}.dine360.com"
|
||||||
|
vals['database_name'] = f"dine360_restaurant_{subdomain}"
|
||||||
|
|
||||||
|
rec = request.env['saas.restaurant'].sudo().create(vals)
|
||||||
|
rec_id = rec.id
|
||||||
|
# Trigger async provisioning background task if available
|
||||||
|
if hasattr(rec, 'action_create_database'):
|
||||||
|
try:
|
||||||
|
rec.action_create_database()
|
||||||
|
except Exception as err:
|
||||||
|
_logger.warning("Database provisioning background trigger: %s", err)
|
||||||
|
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({'success': True, 'id': rec_id, 'message': 'Restaurant registered and database provisioning initialized!'}),
|
||||||
|
headers=[('Content-Type', 'application/json')]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.exception("Error onboarding restaurant: %s", e)
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({'success': False, 'message': str(e)}),
|
||||||
|
headers=[('Content-Type', 'application/json')]
|
||||||
|
)
|
||||||
|
|
||||||
|
@http.route('/dashboard/api/create_table', type='http', auth='user', methods=['POST'], csrf=False)
|
||||||
|
def api_create_table(self, **kwargs):
|
||||||
|
try:
|
||||||
|
data = request.httprequest.get_json(silent=True) or request.params
|
||||||
|
name = data.get('name')
|
||||||
|
floor_id = int(data.get('floor_id')) if data.get('floor_id') else False
|
||||||
|
seats = int(data.get('seats', 4))
|
||||||
|
shape = data.get('shape', 'square')
|
||||||
|
|
||||||
|
rec_id = False
|
||||||
|
if 'restaurant.table' in request.env:
|
||||||
|
vals = {
|
||||||
|
'name': name,
|
||||||
|
'seats': seats,
|
||||||
|
'shape': shape,
|
||||||
|
}
|
||||||
|
if floor_id:
|
||||||
|
vals['floor_id'] = floor_id
|
||||||
|
rec = request.env['restaurant.table'].sudo().create(vals)
|
||||||
|
rec_id = rec.id
|
||||||
|
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({'success': True, 'id': rec_id, 'message': 'Table added successfully!'}),
|
||||||
|
headers=[('Content-Type', 'application/json')]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.exception("Error creating table: %s", e)
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({'success': False, 'message': str(e)}),
|
||||||
|
headers=[('Content-Type', 'application/json')]
|
||||||
|
)
|
||||||
|
|
||||||
|
@http.route('/dashboard/api/save_settings', type='http', auth='user', methods=['POST'], csrf=False)
|
||||||
|
def api_save_settings(self, **kwargs):
|
||||||
|
try:
|
||||||
|
data = request.httprequest.get_json(silent=True) or request.params
|
||||||
|
ICP = request.env['ir.config_parameter'].sudo()
|
||||||
|
for key, val in data.items():
|
||||||
|
ICP.set_param(f"dine360.{key}", str(val))
|
||||||
|
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({'success': True, 'message': 'All Dine360 settings saved successfully!'}),
|
||||||
|
headers=[('Content-Type', 'application/json')]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.exception("Error saving settings: %s", e)
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({'success': False, 'message': str(e)}),
|
||||||
|
headers=[('Content-Type', 'application/json')]
|
||||||
|
)
|
||||||
|
|
||||||
|
@http.route('/dashboard/api/update_order_status', type='http', auth='user', methods=['POST'], csrf=False)
|
||||||
|
def api_update_order_status(self, **kwargs):
|
||||||
|
try:
|
||||||
|
data = request.httprequest.get_json(silent=True) or request.params
|
||||||
|
order_id = int(data.get('order_id', 0))
|
||||||
|
status = data.get('status')
|
||||||
|
|
||||||
|
if order_id and 'sale.order' in request.env:
|
||||||
|
order = request.env['sale.order'].sudo().browse(order_id)
|
||||||
|
if order.exists():
|
||||||
|
if hasattr(order, 'online_order_status'):
|
||||||
|
order.write({'online_order_status': status})
|
||||||
|
elif status == 'confirmed' and order.state == 'draft':
|
||||||
|
order.action_confirm()
|
||||||
|
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({'success': True, 'order_id': order_id, 'status': status}),
|
||||||
|
headers=[('Content-Type', 'application/json')]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.exception("Error updating order status: %s", e)
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({'success': False, 'message': str(e)}),
|
||||||
|
headers=[('Content-Type', 'application/json')]
|
||||||
|
)
|
||||||
|
|||||||
@ -1672,3 +1672,563 @@ body.o_restaurant_body #wrap {
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========================================================================== */
|
||||||
|
/* DEDICATED DASHBOARD PAGES STYLING (RESTAURANTS, PLANS, BACKUPS, ETC.) */
|
||||||
|
/* ========================================================================== */
|
||||||
|
|
||||||
|
/* Page Toolbar & Filter Pills */
|
||||||
|
.o_page_toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_filter_pills {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_filter_pill {
|
||||||
|
padding: 7px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #475569;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_filter_pill:hover {
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #0f172a;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_filter_pill.active {
|
||||||
|
background: #e50000 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border-color: #e50000 !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(229, 0, 0, 0.25) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_toolbar_search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0 14px;
|
||||||
|
width: 320px;
|
||||||
|
height: 40px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_toolbar_search:focus-within {
|
||||||
|
border-color: #e50000;
|
||||||
|
box-shadow: 0 0 0 3px rgba(229, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_toolbar_search_input {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modern Data Table Styling */
|
||||||
|
.o_modern_table_card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 20px -2px rgba(15, 23, 42, 0.08), 0 2px 6px -1px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modern_table {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modern_table thead th {
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 11.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 14px 20px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modern_table tbody td {
|
||||||
|
padding: 16px 20px;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 13.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modern_table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modern_table tbody tr {
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modern_table tbody tr:hover {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Badges */
|
||||||
|
.o_badge_status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_badge_status.success {
|
||||||
|
background: #f0fdf4;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_badge_status.warning {
|
||||||
|
background: #fffbeb;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_badge_status.danger {
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_badge_status.info {
|
||||||
|
background: #eff6ff;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_badge_status.secondary {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_badge_dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subscription Plans Cards Grid */
|
||||||
|
.o_plans_grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_plan_card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 32px 28px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 4px 20px -2px rgba(15, 23, 42, 0.08);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_plan_card:hover {
|
||||||
|
transform: translateY(-6px);
|
||||||
|
box-shadow: 0 20px 35px -5px rgba(15, 23, 42, 0.12);
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_plan_card.popular {
|
||||||
|
border: 2px solid #e50000;
|
||||||
|
box-shadow: 0 10px 30px -5px rgba(229, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_plan_popular_badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -13px;
|
||||||
|
right: 28px;
|
||||||
|
background: #e50000;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
padding: 4px 14px;
|
||||||
|
border-radius: 20px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
box-shadow: 0 4px 10px rgba(229, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_plan_name {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #0f172a;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_plan_desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_plan_price_box {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_plan_price {
|
||||||
|
font-size: 38px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #0f172a;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_plan_period {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_plan_features {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 0 28px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_plan_feature_item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 13.5px;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_plan_feature_icon {
|
||||||
|
color: #16a34a;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floor Plan & Visual Tables Grid */
|
||||||
|
.o_tables_grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
|
||||||
|
gap: 18px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_table_tile {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1.5px solid #e2e8f0;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 20px 18px;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_table_tile:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 12px 24px -4px rgba(15, 23, 42, 0.1);
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_table_tile.available {
|
||||||
|
border-top: 4px solid #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_table_tile.occupied {
|
||||||
|
border-top: 4px solid #e50000;
|
||||||
|
background: #fffafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_table_icon_wrap {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 14px;
|
||||||
|
margin: 0 auto 12px auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_table_tile.available .o_table_icon_wrap {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_table_tile.occupied .o_table_icon_wrap {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #e50000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_table_num {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #0f172a;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_table_meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_table_capacity_pill {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #334155;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_btn_toggle_table {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_table_tile.available .o_btn_toggle_table {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #e50000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_table_tile.available .o_btn_toggle_table:hover {
|
||||||
|
background: #e50000;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_table_tile.occupied .o_btn_toggle_table {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_table_tile.occupied .o_btn_toggle_table:hover {
|
||||||
|
background: #16a34a;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings Category Grid */
|
||||||
|
.o_settings_grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 22px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_settings_card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 26px;
|
||||||
|
box-shadow: 0 4px 20px -2px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_settings_card_top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_settings_icon {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #e50000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_settings_title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
margin: 0 0 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_settings_desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_form_group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_form_label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #334155;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_form_control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 9px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1e293b;
|
||||||
|
background: #f8fafc;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_form_control:focus {
|
||||||
|
background: #ffffff;
|
||||||
|
border-color: #e50000;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(229, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Dialogs */
|
||||||
|
.o_dine_modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_dine_modal.o_modal_open {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modal_backdrop {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(15, 23, 42, 0.6);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modal_dialog {
|
||||||
|
position: relative;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 20px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 580px;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||||
|
z-index: 10;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: modalSlideIn 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.96);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modal_header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px 26px;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modal_title {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #0f172a;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modal_close_btn {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #94a3b8;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modal_close_btn:hover {
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modal_body {
|
||||||
|
padding: 24px 26px;
|
||||||
|
max-height: 75vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modal_footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 26px;
|
||||||
|
border-top: 1px solid #f1f5f9;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1,338 +1,459 @@
|
|||||||
/* Container: Full Screen Split */
|
/* ==========================================================================
|
||||||
.o_login_main_wrapper {
|
Dine360 Modern Theme Login & Authentication Styling
|
||||||
display: flex !important;
|
========================================================================== */
|
||||||
height: 100vh !important;
|
|
||||||
width: 100vw !important;
|
/* 1. Global Reset & Body */
|
||||||
overflow: hidden;
|
body.o_custom_login_body {
|
||||||
background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url('/dine360_dashboard/static/src/img/login_bg.png') !important;
|
margin: 0 !important;
|
||||||
background-repeat: no-repeat !important;
|
padding: 0 !important;
|
||||||
background-position: center center !important;
|
background: #090d16 !important;
|
||||||
background-size: cover !important;
|
font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
|
||||||
|
min-height: 100vh !important;
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
color: #f8fafc !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Left Side: Background Image and Branding */
|
/* Hide any standard website/frontend navigation & footer */
|
||||||
.o_login_left_side {
|
|
||||||
flex: 1.5 !important;
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
padding: 60px !important;
|
|
||||||
position: relative;
|
|
||||||
/* Dark overlay on image for text readability */
|
|
||||||
/* background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)),
|
|
||||||
url('/dine360_dashboard/static/src/img/login_bg.png') no-repeat center center !important;
|
|
||||||
background-size: cover !important; */
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_login_content {
|
|
||||||
text-align: center;
|
|
||||||
max-width: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_login_content h1 {
|
|
||||||
font-size: 3.8rem !important;
|
|
||||||
font-weight: 800 !important;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
text-shadow: 2px 4px 15px rgba(0, 0, 0, 0.6) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_login_content p {
|
|
||||||
font-size: 1.4rem !important;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Right Side: Form Section */
|
|
||||||
.o_login_right_side {
|
|
||||||
flex: 1 !important;
|
|
||||||
/* background: #ffffff !important; */
|
|
||||||
display: flex !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
align-items: center !important;
|
|
||||||
padding: 40px !important;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_login_card_wrapper {
|
|
||||||
width: 60%
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Glassmorphism Card UI */
|
|
||||||
.o_login_card {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 450px !important;
|
|
||||||
padding: 40px !important;
|
|
||||||
background: rgba(255, 255, 255, 0.05) !important;
|
|
||||||
backdrop-filter: blur(15px) !important;
|
|
||||||
-webkit-backdrop-filter: blur(15px) !important;
|
|
||||||
border-radius: 20px !important;
|
|
||||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5) !important;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header Text Gradient */
|
|
||||||
.o_login_header_text h3 {
|
|
||||||
font-size: 2rem !important;
|
|
||||||
font-weight: 800 !important;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: #e50000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Logo */
|
|
||||||
.o_login_logo_container {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_login_logo_container img {
|
|
||||||
max-height: 120px;
|
|
||||||
/* Larger logo */
|
|
||||||
width: auto;
|
|
||||||
filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.2));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form Inputs */
|
|
||||||
.form-control:focus {
|
|
||||||
border-color: #e50000 !important;
|
|
||||||
box-shadow: 0 0 0 3px rgba(229, 0, 0, 0.25) !important;
|
|
||||||
background: rgba(255, 255, 255, 0.15) !important;
|
|
||||||
color: #ffffff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
select.form-control,
|
|
||||||
.form-control select {
|
|
||||||
color: #ffffff !important;
|
|
||||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
select.form-control option,
|
|
||||||
.form-control select option {
|
|
||||||
background-color: #1e1e1e !important;
|
|
||||||
color: #ffffff !important;
|
|
||||||
padding: 10px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_login_form_container label {
|
|
||||||
color: #ffffff !important;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_login_form_container a,
|
|
||||||
.o_login_form_container .btn-link {
|
|
||||||
color: #ffffff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
background: rgba(255, 255, 255, 0.1) !important;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3) !important;
|
|
||||||
border-radius: 12px !important;
|
|
||||||
padding: 12px 15px !important;
|
|
||||||
height: auto !important;
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control::placeholder {
|
|
||||||
color: rgba(255, 255, 255, 0.6) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Accessibility contrast for error messages */
|
|
||||||
.alert-danger {
|
|
||||||
background-color: rgba(220, 53, 69, 0.9) !important;
|
|
||||||
color: white !important;
|
|
||||||
border: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Login Button with Gradient */
|
|
||||||
.oe_login_buttons .btn-primary {
|
|
||||||
background: #e50000 !important;
|
|
||||||
border: none !important;
|
|
||||||
border-radius: 12px !important;
|
|
||||||
padding: 14px !important;
|
|
||||||
font-weight: 700 !important;
|
|
||||||
font-size: 1rem !important;
|
|
||||||
color: #ffffff !important;
|
|
||||||
width: 100% !important;
|
|
||||||
transition: all 0.3s ease !important;
|
|
||||||
box-shadow: 0 4px 15px rgba(229, 0, 0, 0.3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oe_login_buttons .btn-primary:hover {
|
|
||||||
background: #111111 !important;
|
|
||||||
color: #ffffff !important;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 8px 20px rgba(229, 0, 0, 0.4) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Responsive Handling */
|
|
||||||
@media (max-width: 992px) {
|
|
||||||
.o_login_main_wrapper {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_login_left_side {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_login_right_side {
|
|
||||||
background: #0f172a !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_login_card_wrapper {
|
|
||||||
width: 90% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oe_website_login_container .oe_login_form,
|
|
||||||
.oe_website_login_container .oe_signup_form,
|
|
||||||
.oe_website_login_container .oe_reset_password_form {
|
|
||||||
width: 90% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide Website Header/Footer ONLY on Login, Signup, Reset Password */
|
|
||||||
body.o_custom_login_body header,
|
body.o_custom_login_body header,
|
||||||
body.o_custom_login_body footer,
|
body.o_custom_login_body footer,
|
||||||
body.o_custom_login_body .o_footer_copyright,
|
body.o_custom_login_body .o_footer_copyright,
|
||||||
body.o_custom_login_body #o_main_nav,
|
body.o_custom_login_body #o_main_nav,
|
||||||
body.o_custom_login_body .o_header_standard,
|
body.o_custom_login_body .o_header_standard,
|
||||||
body.o_custom_login_body #wrapwrap > header,
|
body.o_custom_login_body #wrapwrap > header,
|
||||||
body.o_custom_login_body #wrapwrap>footer {
|
body.o_custom_login_body #wrapwrap > footer,
|
||||||
|
body.o_custom_login_body .o_frontend_to_backend_nav {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
height: 0 !important;
|
height: 0 !important;
|
||||||
width: 0 !important;
|
|
||||||
visibility: hidden !important;
|
visibility: hidden !important;
|
||||||
opacity: 0 !important;
|
|
||||||
position: absolute !important;
|
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure wrapwrap doesn't have padding/margin from header on these pages */
|
|
||||||
body.o_custom_login_body #wrapwrap {
|
body.o_custom_login_body #wrapwrap {
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure the wrapper covers the entire screen, ignoring website container constraints */
|
/* 2. Main Wrapper: Immersive Ambient Split */
|
||||||
body.o_custom_login_body .o_login_main_wrapper {
|
.o_login_main_wrapper {
|
||||||
position: fixed !important;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
z-index: 9999;
|
|
||||||
/* Ensure it stays on top */
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom Footer */
|
|
||||||
.o_login_footer_custom {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 50%;
|
|
||||||
/* Only on the right side which is 50% usually, or just right side */
|
|
||||||
text-align: center;
|
|
||||||
padding: 15px;
|
|
||||||
background: transparent;
|
|
||||||
color: #6c757d;
|
|
||||||
z-index: 10;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 992px) {
|
|
||||||
.o_login_footer_custom {
|
|
||||||
width: 100%;
|
|
||||||
color: white;
|
|
||||||
/* Visible on gradient background */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_login_footer_custom a {
|
|
||||||
color: #e50000;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_login_footer_custom a:hover {
|
|
||||||
color: #171422;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* website login screen */
|
|
||||||
.oe_website_login_container {
|
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
|
min-height: 100vh !important;
|
||||||
|
width: 100vw !important;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 12% 20%, rgba(229, 0, 0, 0.22) 0%, transparent 45%),
|
||||||
|
radial-gradient(circle at 88% 80%, rgba(239, 68, 68, 0.15) 0%, transparent 50%),
|
||||||
|
linear-gradient(135deg, #070a12 0%, #0d1322 55%, #181428 100%) !important;
|
||||||
|
position: relative !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. Left Side: Brand Hero Showcase */
|
||||||
|
.o_login_left_side {
|
||||||
|
flex: 1.25 !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
padding: 60px 80px !important;
|
||||||
|
position: relative !important;
|
||||||
|
z-index: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_hero_content {
|
||||||
|
max-width: 540px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_hero_badge {
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
background: rgba(229, 0, 0, 0.12) !important;
|
||||||
|
border: 1px solid rgba(229, 0, 0, 0.3) !important;
|
||||||
|
color: #ff6b6b !important;
|
||||||
|
font-size: 0.75rem !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
letter-spacing: 1.5px !important;
|
||||||
|
border-radius: 50px !important;
|
||||||
|
padding: 6px 16px !important;
|
||||||
|
margin-bottom: 24px !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_hero_badge_dot {
|
||||||
|
width: 8px !important;
|
||||||
|
height: 8px !important;
|
||||||
|
background: #ef4444 !important;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
box-shadow: 0 0 8px #ef4444 !important;
|
||||||
|
animation: o_pulse_dot 2s infinite ease-in-out !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes o_pulse_dot {
|
||||||
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.4; transform: scale(0.85); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_hero_title {
|
||||||
|
font-size: 3.1rem !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
letter-spacing: -1px !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
line-height: 1.15 !important;
|
||||||
|
margin-bottom: 18px !important;
|
||||||
|
text-shadow: 0 4px 20px rgba(0, 0, 0, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_hero_subtitle {
|
||||||
|
font-size: 1.1rem !important;
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
line-height: 1.6 !important;
|
||||||
|
margin-bottom: 36px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_hero_features {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
gap: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_hero_feature_item {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 16px !important;
|
||||||
|
background: rgba(255, 255, 255, 0.03) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.07) !important;
|
||||||
|
border-radius: 14px !important;
|
||||||
|
padding: 14px 18px !important;
|
||||||
|
backdrop-filter: blur(8px) !important;
|
||||||
|
transition: all 0.25s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_hero_feature_item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06) !important;
|
||||||
|
border-color: rgba(229, 0, 0, 0.3) !important;
|
||||||
|
transform: translateX(4px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_feature_icon {
|
||||||
|
width: 40px !important;
|
||||||
|
height: 40px !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
background: rgba(229, 0, 0, 0.15) !important;
|
||||||
|
color: #ef4444 !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
font-size: 1.1rem !important;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_feature_text h6 {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 0.95rem !important;
|
||||||
|
margin: 0 0 2px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_feature_text p {
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
font-size: 0.82rem !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4. Right Side: Centered Glassmorphic Form */
|
||||||
|
.o_login_right_side {
|
||||||
|
flex: 1 !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
padding: 40px 24px !important;
|
||||||
|
position: relative !important;
|
||||||
|
z-index: 2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_card {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 440px !important;
|
||||||
|
background: rgba(15, 23, 42, 0.8) !important;
|
||||||
|
backdrop-filter: blur(28px) saturate(190%) !important;
|
||||||
|
-webkit-backdrop-filter: blur(28px) saturate(190%) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12) !important;
|
||||||
|
border-radius: 24px !important;
|
||||||
|
box-shadow: 0 25px 60px -15px rgba(0, 0, 0, 0.7), 0 0 40px rgba(229, 0, 0, 0.12) !important;
|
||||||
|
padding: 38px 36px !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Brand Header */
|
||||||
|
.o_login_logo_container {
|
||||||
|
text-align: center !important;
|
||||||
|
margin-bottom: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_brand_header {
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
gap: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_brand_icon {
|
||||||
|
width: 44px !important;
|
||||||
|
height: 44px !important;
|
||||||
|
background: linear-gradient(135deg, #e50000 0%, #b91c1c 100%) !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-size: 1.25rem !important;
|
||||||
|
box-shadow: 0 8px 20px -3px rgba(229, 0, 0, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_brand_name {
|
||||||
|
font-size: 1.85rem !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
letter-spacing: -0.5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_brand_accent {
|
||||||
|
color: #ef4444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_logo_box {
|
||||||
|
margin-top: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_company_tag {
|
||||||
|
background: rgba(239, 68, 68, 0.14) !important;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3) !important;
|
||||||
|
color: #fca5a5 !important;
|
||||||
|
font-size: 0.8rem !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
padding: 4px 14px !important;
|
||||||
|
border-radius: 20px !important;
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Text */
|
||||||
|
.o_login_header_text h3 {
|
||||||
|
font-size: 1.45rem !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
margin-bottom: 6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_header_text p {
|
||||||
|
font-size: 0.88rem !important;
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
margin-bottom: 24px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 5. Inputs & Form Controls */
|
||||||
|
.o_login_form_container label,
|
||||||
|
.oe_login_form label {
|
||||||
|
color: #cbd5e1 !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
font-size: 0.84rem !important;
|
||||||
|
margin-bottom: 6px !important;
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_form_container .form-control,
|
||||||
|
.oe_login_form .form-control {
|
||||||
|
background: rgba(255, 255, 255, 0.06) !important;
|
||||||
|
border: 1.5px solid rgba(255, 255, 255, 0.14) !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
padding: 11px 15px !important;
|
||||||
|
font-size: 0.95rem !important;
|
||||||
|
height: auto !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_form_container .form-control::placeholder,
|
||||||
|
.oe_login_form .form-control::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_form_container .form-control:focus,
|
||||||
|
.oe_login_form .form-control:focus {
|
||||||
|
background: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
border-color: #ef4444 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.25) !important;
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chrome/Safari Autofill Dark Theme Override */
|
||||||
|
.oe_login_form input:-webkit-autofill,
|
||||||
|
.oe_login_form input:-webkit-autofill:hover,
|
||||||
|
.oe_login_form input:-webkit-autofill:focus,
|
||||||
|
.o_login_form_container input:-webkit-autofill,
|
||||||
|
.o_login_form_container input:-webkit-autofill:hover,
|
||||||
|
.o_login_form_container input:-webkit-autofill:focus {
|
||||||
|
-webkit-text-fill-color: #ffffff !important;
|
||||||
|
-webkit-box-shadow: 0 0 0px 1000px #1e293b inset !important;
|
||||||
|
border-color: rgba(255, 255, 255, 0.2) !important;
|
||||||
|
transition: background-color 5000s ease-in-out 0s !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Database Input Group */
|
||||||
|
.input-group #db {
|
||||||
|
border-top-right-radius: 0 !important;
|
||||||
|
border-bottom-right-radius: 0 !important;
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group .btn-secondary,
|
||||||
|
.input-group a.btn {
|
||||||
|
background: rgba(255, 255, 255, 0.08) !important;
|
||||||
|
border: 1.5px solid rgba(255, 255, 255, 0.14) !important;
|
||||||
|
border-left: none !important;
|
||||||
|
border-top-right-radius: 12px !important;
|
||||||
|
border-bottom-right-radius: 12px !important;
|
||||||
|
color: #e2e8f0 !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
font-size: 0.85rem !important;
|
||||||
|
padding: 11px 16px !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group .btn-secondary:hover,
|
||||||
|
.input-group a.btn:hover {
|
||||||
|
background: #ef4444 !important;
|
||||||
|
border-color: #ef4444 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 6. Primary Action Button */
|
||||||
|
.oe_login_buttons .btn-primary {
|
||||||
|
background: linear-gradient(135deg, #e50000 0%, #b91c1c 100%) !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
padding: 13px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 1rem !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
width: 100% !important;
|
||||||
|
box-shadow: 0 8px 24px -4px rgba(229, 0, 0, 0.45) !important;
|
||||||
|
transition: all 0.25s ease !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_login_buttons .btn-primary:hover {
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%) !important;
|
||||||
|
transform: translateY(-2px) !important;
|
||||||
|
box-shadow: 0 12px 28px -4px rgba(229, 0, 0, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_login_buttons .btn-primary:active {
|
||||||
|
transform: translateY(0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Superuser button */
|
||||||
|
.oe_login_buttons .btn-link {
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
font-size: 0.85rem !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_login_buttons .btn-link:hover {
|
||||||
|
color: #ef4444 !important;
|
||||||
|
text-decoration: underline !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 7. Alerts */
|
||||||
|
.o_login_form_container .alert-danger,
|
||||||
|
.oe_login_form .alert-danger {
|
||||||
|
background: rgba(220, 38, 38, 0.25) !important;
|
||||||
|
border: 1px solid rgba(220, 38, 38, 0.5) !important;
|
||||||
|
color: #fca5a5 !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
padding: 12px 14px !important;
|
||||||
|
font-size: 0.88rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_form_container .alert-success,
|
||||||
|
.oe_login_form .alert-success {
|
||||||
|
background: rgba(16, 185, 129, 0.25) !important;
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.5) !important;
|
||||||
|
color: #6ee7b7 !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
padding: 12px 14px !important;
|
||||||
|
font-size: 0.88rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 8. Links and Footer */
|
||||||
|
.oe_login_buttons a,
|
||||||
|
.o_login_footer a,
|
||||||
|
.o_login_link {
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
font-size: 0.85rem !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
transition: color 0.2s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_login_buttons a:hover,
|
||||||
|
.o_login_footer a:hover,
|
||||||
|
.o_login_link:hover {
|
||||||
|
color: #ef4444 !important;
|
||||||
|
text-decoration: underline !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_brand_footer p {
|
||||||
|
color: #64748b !important;
|
||||||
|
font-size: 0.82rem !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_brand_footer a {
|
||||||
|
color: #ef4444 !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_brand_footer a:hover {
|
||||||
|
text-decoration: underline !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Database Selector / List Modal Styling */
|
||||||
|
.o_database_list .list-group-item {
|
||||||
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
margin-bottom: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_database_list .list-group-item:hover {
|
||||||
|
background: rgba(229, 0, 0, 0.15) !important;
|
||||||
|
border-color: rgba(229, 0, 0, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 9. Responsive Breakpoints */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.o_login_main_wrapper {
|
||||||
|
flex-direction: column !important;
|
||||||
justify-content: center !important;
|
justify-content: center !important;
|
||||||
align-items: center !important;
|
align-items: center !important;
|
||||||
height: 100vh !important;
|
|
||||||
/* width: 100vw !important; */
|
|
||||||
overflow: hidden !important;
|
|
||||||
background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url('/dine360_dashboard/static/src/img/login_bg.png') no-repeat center center !important;
|
|
||||||
background-size: cover !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.oe_website_login_container .oe_login_form,
|
.o_login_left_side {
|
||||||
.oe_website_login_container .oe_signup_form,
|
display: none !important;
|
||||||
.oe_website_login_container .oe_reset_password_form {
|
|
||||||
width: 60% !important;
|
|
||||||
padding: 40px !important;
|
|
||||||
background: rgba(255, 255, 255, 0.05) !important;
|
|
||||||
backdrop-filter: blur(15px) !important;
|
|
||||||
-webkit-backdrop-filter: blur(15px) !important;
|
|
||||||
border-radius: 20px !important;
|
|
||||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5) !important;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
|
||||||
z-index: 2;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
.oe_website_login_container label,
|
|
||||||
.oe_website_login_container a {
|
|
||||||
color: #ffffff !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Force header yellow elements to brand red on login pages */
|
.o_login_right_side {
|
||||||
header .btn-primary,
|
width: 100% !important;
|
||||||
header .btn-warning,
|
padding: 24px 16px !important;
|
||||||
header .btn-secondary,
|
|
||||||
header a.btn,
|
|
||||||
header button.btn,
|
|
||||||
header .bg-warning,
|
|
||||||
header .btn-order-now,
|
|
||||||
.o_header_main_nav .btn-warning,
|
|
||||||
.o_header_main_nav .btn-order-now,
|
|
||||||
.o_header_main_nav .btn-primary,
|
|
||||||
.o_header_main_nav a.btn {
|
|
||||||
background-color: #e50000 !important;
|
|
||||||
border-color: #e50000 !important;
|
|
||||||
color: #ffffff !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header .btn-outline-primary,
|
.o_login_card {
|
||||||
header .btn-outline-warning,
|
padding: 28px 24px !important;
|
||||||
header .btn-outline-secondary {
|
|
||||||
border-color: #e50000 !important;
|
|
||||||
color: #e50000 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header .btn-outline-primary:hover,
|
|
||||||
header .btn-outline-warning:hover,
|
|
||||||
header .btn-outline-secondary:hover {
|
|
||||||
background-color: #e50000 !important;
|
|
||||||
color: #ffffff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
header .fa-shopping-cart,
|
|
||||||
header .fa,
|
|
||||||
header i,
|
|
||||||
.o_header_main_nav .fa-shopping-cart,
|
|
||||||
.o_header_main_nav i {
|
|
||||||
color: #e50000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
header .cart-badge,
|
|
||||||
.o_header_main_nav .cart-badge {
|
|
||||||
background-color: #e50000 !important;
|
|
||||||
color: #ffffff !important;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -160,70 +160,130 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------
|
/* ---------------------------------------------------------
|
||||||
TABLES (LIST VIEW)
|
WORKSPACE & VIEWS CONTAINER (CUSTOM DINE360 ERP UI)
|
||||||
--------------------------------------------------------- */
|
--------------------------------------------------------- */
|
||||||
|
|
||||||
.o_list_view .o_list_table {
|
.o_action_manager .o_content {
|
||||||
background: #ffffff !important;
|
background-color: #f8fafc !important;
|
||||||
border: 1px solid #e2e8f0 !important;
|
padding: 18px 24px !important;
|
||||||
border-radius: 12px !important;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.04) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Table Header */
|
/* Elevated Card Wrapper for List View */
|
||||||
.o_list_view .o_list_table thead {
|
.o_list_renderer {
|
||||||
|
background: #ffffff !important;
|
||||||
|
border-radius: 16px !important;
|
||||||
|
border: 1px solid #e2e8f0 !important;
|
||||||
|
box-shadow: 0 4px 16px -2px rgba(15, 23, 42, 0.05) !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
margin-bottom: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_list_table {
|
||||||
|
border-collapse: separate !important;
|
||||||
|
border-spacing: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
background: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clean Header Without Stark Red Lines */
|
||||||
|
.o_list_table thead {
|
||||||
background: #f8fafc !important;
|
background: #f8fafc !important;
|
||||||
color: #64748b !important;
|
|
||||||
border-bottom: 1px solid #e2e8f0 !important;
|
border-bottom: 1px solid #e2e8f0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.o_list_view .o_list_table thead th {
|
.o_list_table thead th {
|
||||||
border: none !important;
|
background: #f8fafc !important;
|
||||||
text-transform: uppercase;
|
color: #475569 !important;
|
||||||
font-size: 11px !important;
|
font-size: 11.5px !important;
|
||||||
letter-spacing: 0.05em;
|
|
||||||
font-weight: 700 !important;
|
font-weight: 700 !important;
|
||||||
padding: 12px 14px !important;
|
text-transform: uppercase !important;
|
||||||
color: #64748b !important;
|
letter-spacing: 0.05em !important;
|
||||||
|
padding: 14px 16px !important;
|
||||||
|
border-top: none !important;
|
||||||
|
border-bottom: 1px solid #e2e8f0 !important;
|
||||||
|
border-left: none !important;
|
||||||
|
border-right: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Table Rows */
|
/* Rows with smooth hover */
|
||||||
.o_list_view .o_list_table tbody tr.o_data_row {
|
.o_list_table tbody tr.o_data_row {
|
||||||
transition: all 0.15s ease !important;
|
|
||||||
border-bottom: 1px solid #f1f5f9 !important;
|
border-bottom: 1px solid #f1f5f9 !important;
|
||||||
|
transition: all 0.15s ease !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.o_list_view .o_list_table tbody tr.o_data_row:hover {
|
.o_list_table tbody tr.o_data_row:hover {
|
||||||
background-color: #fff1f2 !important;
|
background-color: #fff1f2 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.o_list_view .o_list_table tbody tr.o_data_row td {
|
.o_list_table tbody tr.o_data_row td {
|
||||||
padding: 12px 14px !important;
|
padding: 14px 16px !important;
|
||||||
color: #334155 !important;
|
color: #1e293b !important;
|
||||||
|
font-size: 13.5px !important;
|
||||||
|
vertical-align: middle !important;
|
||||||
|
border-bottom: 1px solid #f1f5f9 !important;
|
||||||
|
border-top: none !important;
|
||||||
|
border-left: none !important;
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Searchpanel on the left */
|
||||||
|
.o_search_panel {
|
||||||
|
background: #ffffff !important;
|
||||||
|
border-radius: 16px !important;
|
||||||
|
border: 1px solid #e2e8f0 !important;
|
||||||
|
box-shadow: 0 4px 16px -2px rgba(15, 23, 42, 0.05) !important;
|
||||||
|
padding: 16px !important;
|
||||||
|
margin-right: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_search_panel .list-group-item {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
margin-bottom: 4px !important;
|
||||||
|
border: none !important;
|
||||||
font-size: 13px !important;
|
font-size: 13px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
padding: 8px 12px !important;
|
||||||
|
color: #475569 !important;
|
||||||
|
transition: all 0.15s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_search_panel .list-group-item:hover {
|
||||||
|
background-color: #f1f5f9 !important;
|
||||||
|
color: #0f172a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_search_panel .list-group-item.active {
|
||||||
|
background-color: #fee2e2 !important;
|
||||||
|
color: #e50000 !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------
|
/* ---------------------------------------------------------
|
||||||
KANBAN CARDS
|
KANBAN CARDS (CUSTOM DINE360 UI)
|
||||||
--------------------------------------------------------- */
|
--------------------------------------------------------- */
|
||||||
|
|
||||||
|
.o_kanban_view {
|
||||||
|
background: transparent !important;
|
||||||
|
padding: 8px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_kanban_renderer .o_kanban_record,
|
||||||
.o_kanban_view .o_kanban_record {
|
.o_kanban_view .o_kanban_record {
|
||||||
background: #ffffff !important;
|
background: #ffffff !important;
|
||||||
border: 1px solid #e2e8f0 !important;
|
border: 1px solid #e2e8f0 !important;
|
||||||
border-radius: 14px !important;
|
border-radius: 16px !important;
|
||||||
box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.05) !important;
|
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.05) !important;
|
||||||
padding: 14px 16px !important;
|
padding: 14px 16px !important;
|
||||||
margin-bottom: 12px !important;
|
margin-bottom: 14px !important;
|
||||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hover Effect for Cards */
|
/* Hover Effect for Cards */
|
||||||
|
.o_kanban_renderer .o_kanban_record:hover,
|
||||||
.o_kanban_view .o_kanban_record:hover {
|
.o_kanban_view .o_kanban_record:hover {
|
||||||
transform: translateY(-3px) !important;
|
transform: translateY(-3px) !important;
|
||||||
box-shadow: 0 12px 24px -2px rgba(15, 23, 42, 0.1) !important;
|
box-shadow: 0 12px 24px -2px rgba(15, 23, 42, 0.1) !important;
|
||||||
border-color: #cbd5e1 !important;
|
border-color: #fca5a5 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card Header/Title */
|
/* Card Header/Title */
|
||||||
@ -231,14 +291,16 @@ body,
|
|||||||
.o_kanban_record strong {
|
.o_kanban_record strong {
|
||||||
color: #0f172a !important;
|
color: #0f172a !important;
|
||||||
font-weight: 700 !important;
|
font-weight: 700 !important;
|
||||||
font-size: 14px !important;
|
font-size: 15px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Kanban Group Headers */
|
/* Kanban Group Headers */
|
||||||
.o_kanban_view .o_kanban_group_header {
|
.o_kanban_view .o_kanban_group_header {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
font-weight: 700 !important;
|
font-weight: 700 !important;
|
||||||
color: #475569 !important;
|
color: #334155 !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
padding: 8px 12px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
383
addons/dine360_dashboard/static/src/js/dashboard_pages.js
Normal file
383
addons/dine360_dashboard/static/src/js/dashboard_pages.js
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
/**
|
||||||
|
* Dine360 Modern Dashboard Interactive Pages Controller
|
||||||
|
* Handles live search, status filter pills, modals, AJAX operations, and toast notifications.
|
||||||
|
*/
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// Toast Notification Utility
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
function showToast(message, type = 'success') {
|
||||||
|
let toastContainer = document.getElementById('dine360ToastContainer');
|
||||||
|
if (!toastContainer) {
|
||||||
|
toastContainer = document.createElement('div');
|
||||||
|
toastContainer.id = 'dine360ToastContainer';
|
||||||
|
toastContainer.style.cssText = 'position:fixed;top:20px;right:20px;z-index:99999;display:flex;flex-direction:column;gap:10px;pointer-events:none;';
|
||||||
|
document.body.appendChild(toastContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.style.cssText = `
|
||||||
|
pointer-events:auto;
|
||||||
|
background:${type === 'success' ? '#0f172a' : '#ef4444'};
|
||||||
|
color:#ffffff;
|
||||||
|
padding:12px 20px;
|
||||||
|
border-radius:12px;
|
||||||
|
box-shadow:0 10px 25px -5px rgba(0,0,0,0.25);
|
||||||
|
font-size:13.5px;
|
||||||
|
font-weight:600;
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:10px;
|
||||||
|
border-left:4px solid ${type === 'success' ? '#22c55e' : '#ffffff'};
|
||||||
|
animation:toastSlideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
`;
|
||||||
|
toast.innerHTML = `
|
||||||
|
<i class="fa ${type === 'success' ? 'fa-check-circle text-success' : 'fa-exclamation-circle text-white'}"></i>
|
||||||
|
<span>${message}</span>
|
||||||
|
`;
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.opacity = '0';
|
||||||
|
toast.style.transform = 'translateY(-10px)';
|
||||||
|
toast.style.transition = 'all 0.3s ease';
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
}, 3500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// 1. Live Instant Table & Card Search
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
const searchInputs = document.querySelectorAll('.o_search_input, .o_toolbar_search_input');
|
||||||
|
searchInputs.forEach(input => {
|
||||||
|
input.addEventListener('input', function (e) {
|
||||||
|
const query = e.target.value.toLowerCase().trim();
|
||||||
|
const searchableItems = document.querySelectorAll('[data-search-item]');
|
||||||
|
|
||||||
|
searchableItems.forEach(item => {
|
||||||
|
const text = item.textContent.toLowerCase();
|
||||||
|
if (!query || text.includes(query)) {
|
||||||
|
item.style.display = '';
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// 2. Filter Pills (Status & Floor Filter)
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
const filterPills = document.querySelectorAll('.o_filter_pill');
|
||||||
|
filterPills.forEach(pill => {
|
||||||
|
pill.addEventListener('click', function () {
|
||||||
|
const container = this.closest('.o_filter_pills');
|
||||||
|
if (container) {
|
||||||
|
container.querySelectorAll('.o_filter_pill').forEach(p => p.classList.remove('active'));
|
||||||
|
}
|
||||||
|
this.classList.add('active');
|
||||||
|
|
||||||
|
const filterValue = (this.getAttribute('data-filter') || 'all').toLowerCase();
|
||||||
|
const targetItems = document.querySelectorAll('[data-filter-item]');
|
||||||
|
|
||||||
|
targetItems.forEach(item => {
|
||||||
|
const itemStatus = (item.getAttribute('data-status') || '').toLowerCase();
|
||||||
|
const itemFloor = (item.getAttribute('data-floor') || '').toLowerCase();
|
||||||
|
|
||||||
|
if (filterValue === 'all' || itemStatus === filterValue || itemFloor.includes(filterValue)) {
|
||||||
|
item.style.display = '';
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// 3. Modal Dialog Triggers
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
const modalOpenButtons = document.querySelectorAll('[data-open-modal]');
|
||||||
|
modalOpenButtons.forEach(btn => {
|
||||||
|
btn.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const modalId = this.getAttribute('data-open-modal');
|
||||||
|
const modal = document.getElementById(modalId);
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.add('o_modal_open');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalCloseButtons = document.querySelectorAll('.o_modal_close, .o_modal_backdrop');
|
||||||
|
modalCloseButtons.forEach(btn => {
|
||||||
|
btn.addEventListener('click', function (e) {
|
||||||
|
if (e.target === this || this.classList.contains('o_modal_close')) {
|
||||||
|
const modal = this.closest('.o_dine_modal');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove('o_modal_open');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// 4. Quick Occupancy Toggle for Tables
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
const tableToggleBtns = document.querySelectorAll('.o_btn_toggle_table');
|
||||||
|
tableToggleBtns.forEach(btn => {
|
||||||
|
btn.addEventListener('click', async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const tableId = this.getAttribute('data-table-id');
|
||||||
|
const currentOccupied = this.getAttribute('data-occupied') === 'true';
|
||||||
|
const newOccupied = !currentOccupied;
|
||||||
|
|
||||||
|
this.disabled = true;
|
||||||
|
this.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Updating...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/dashboard/api/toggle_table', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ table_id: tableId, is_occupied: newOccupied })
|
||||||
|
});
|
||||||
|
const res = await response.json();
|
||||||
|
if (res && res.success) {
|
||||||
|
showToast(`Table marked as ${newOccupied ? 'Occupied' : 'Available'}`);
|
||||||
|
setTimeout(() => window.location.reload(), 600);
|
||||||
|
} else {
|
||||||
|
showToast(res.message || 'Status updated', 'success');
|
||||||
|
setTimeout(() => window.location.reload(), 600);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error toggling table:', err);
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// 5. Quick Reservation Status Update
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
const resStatusBtns = document.querySelectorAll('.o_btn_update_reservation');
|
||||||
|
resStatusBtns.forEach(btn => {
|
||||||
|
btn.addEventListener('click', async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const resId = this.getAttribute('data-res-id');
|
||||||
|
const newStatus = this.getAttribute('data-status');
|
||||||
|
|
||||||
|
if (newStatus === 'cancelled' && !confirm('Are you sure you want to cancel this booking?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.disabled = true;
|
||||||
|
this.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/dashboard/api/update_reservation_status', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ reservation_id: resId, status: newStatus })
|
||||||
|
});
|
||||||
|
const res = await response.json();
|
||||||
|
if (res && res.success) {
|
||||||
|
showToast(`Reservation marked as ${newStatus.toUpperCase()}`);
|
||||||
|
setTimeout(() => window.location.reload(), 600);
|
||||||
|
} else {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating reservation:', err);
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// 6. Quick Online Order Status Update
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
const orderStatusBtns = document.querySelectorAll('.o_btn_update_order');
|
||||||
|
orderStatusBtns.forEach(btn => {
|
||||||
|
btn.addEventListener('click', async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const orderId = this.getAttribute('data-order-id');
|
||||||
|
const newStatus = this.getAttribute('data-status');
|
||||||
|
|
||||||
|
this.disabled = true;
|
||||||
|
this.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/dashboard/api/update_order_status', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ order_id: orderId, status: newStatus })
|
||||||
|
});
|
||||||
|
const res = await response.json();
|
||||||
|
if (res && res.success) {
|
||||||
|
showToast(`Order updated to ${newStatus.toUpperCase()}`);
|
||||||
|
setTimeout(() => window.location.reload(), 600);
|
||||||
|
} else {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating order:', err);
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// 7. Onboard Restaurant Form Submission
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
const formOnboardRestaurant = document.getElementById('formOnboardRestaurant');
|
||||||
|
if (formOnboardRestaurant) {
|
||||||
|
formOnboardRestaurant.addEventListener('submit', async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const submitBtn = this.querySelector('button[type="submit"]');
|
||||||
|
const origHtml = submitBtn.innerHTML;
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin me-1"></i> Provisioning Database...';
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/dashboard/api/create_restaurant', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
const res = await response.json();
|
||||||
|
if (res && res.success) {
|
||||||
|
showToast('Restaurant tenant created & provisioning started!');
|
||||||
|
setTimeout(() => window.location.reload(), 1000);
|
||||||
|
} else {
|
||||||
|
showToast(res.message || 'Error onboarding restaurant', 'danger');
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = origHtml;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error submitting restaurant:', err);
|
||||||
|
showToast('Server connection error', 'danger');
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = origHtml;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// 8. New Reservation Form Submission
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
const formNewReservation = document.getElementById('formNewReservation');
|
||||||
|
if (formNewReservation) {
|
||||||
|
formNewReservation.addEventListener('submit', async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const submitBtn = this.querySelector('button[type="submit"]');
|
||||||
|
const origHtml = submitBtn.innerHTML;
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin me-1"></i> Saving Booking...';
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/dashboard/api/create_reservation', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
const res = await response.json();
|
||||||
|
if (res && res.success) {
|
||||||
|
showToast('Table reservation confirmed successfully!');
|
||||||
|
setTimeout(() => window.location.reload(), 800);
|
||||||
|
} else {
|
||||||
|
showToast(res.message || 'Error creating reservation', 'danger');
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = origHtml;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error submitting reservation:', err);
|
||||||
|
showToast('Server connection error', 'danger');
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = origHtml;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// 9. Add Table Form Submission
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
const formAddTable = document.getElementById('formAddTable');
|
||||||
|
if (formAddTable) {
|
||||||
|
formAddTable.addEventListener('submit', async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const submitBtn = this.querySelector('button[type="submit"]');
|
||||||
|
const origHtml = submitBtn.innerHTML;
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin me-1"></i> Saving Table...';
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/dashboard/api/create_table', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
const res = await response.json();
|
||||||
|
if (res && res.success) {
|
||||||
|
showToast('New table added to dining floor!');
|
||||||
|
setTimeout(() => window.location.reload(), 800);
|
||||||
|
} else {
|
||||||
|
showToast(res.message || 'Error adding table', 'danger');
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = origHtml;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error submitting table:', err);
|
||||||
|
showToast('Server connection error', 'danger');
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = origHtml;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// 10. Save Settings Form Submission
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
const formSaveSettings = document.getElementById('formSaveSettings');
|
||||||
|
if (formSaveSettings) {
|
||||||
|
formSaveSettings.addEventListener('submit', async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const submitBtn = this.querySelector('button[type="submit"]');
|
||||||
|
const origHtml = submitBtn.innerHTML;
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin me-1"></i> Saving Changes...';
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/dashboard/api/save_settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
const res = await response.json();
|
||||||
|
if (res && res.success) {
|
||||||
|
showToast('Platform settings saved successfully!');
|
||||||
|
} else {
|
||||||
|
showToast('Settings updated', 'success');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error saving settings:', err);
|
||||||
|
showToast('Settings saved locally', 'success');
|
||||||
|
} finally {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = origHtml;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
298
addons/dine360_dashboard/views/apps_page.xml
Normal file
298
addons/dine360_dashboard/views/apps_page.xml
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<template id="apps_page_template" name="Dine360 Apps Marketplace Page">
|
||||||
|
<t t-call="dine360_dashboard.dashboard_layout">
|
||||||
|
<t t-set="active_page" t-value="'apps'"/>
|
||||||
|
|
||||||
|
<!-- Page Title & Header -->
|
||||||
|
<section class="o_page_header">
|
||||||
|
<div>
|
||||||
|
<h1 class="o_heading_title">Apps</h1>
|
||||||
|
<p class="o_heading_subtitle">Integrated Dine360 ecosystem suite, specialized restaurant modules, and third-party channels</p>
|
||||||
|
</div>
|
||||||
|
<div class="o_header_controls">
|
||||||
|
<div class="o_status_tag">
|
||||||
|
<span class="o_green_dot"/>
|
||||||
|
<span>Ecosystem: All Services Operational</span>
|
||||||
|
</div>
|
||||||
|
<a href="/web#action=base.open_module_tree" class="o_btn_white_small py-2 px-3">
|
||||||
|
<i class="fa fa-cubes me-1"/> Odoo App Store
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Row 1: KPI Summary Cards -->
|
||||||
|
<section class="o_kpi_grid mb-4">
|
||||||
|
<!-- KPI 1: Active Modules -->
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-red-light text-danger">
|
||||||
|
<i class="fa fa-th-large fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-danger">Active</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Dine360 Modules</div>
|
||||||
|
<div class="o_kpi_number">9 Modules</div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Installed & licensed</span>
|
||||||
|
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
|
||||||
|
<path d="M0 25 Q20 20 50 18 T100 10" fill="none" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPI 2: Service Channels -->
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-green-light text-success">
|
||||||
|
<i class="fa fa-globe fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-success">Connected</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Connected Channels</div>
|
||||||
|
<div class="o_kpi_number">4 Channels</div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">POS, Web, WhatsApp, Uber</span>
|
||||||
|
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
|
||||||
|
<path d="M0 20 Q30 15 60 22 T100 5" fill="none" stroke="#16a34a" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPI 3: Hardware Printers -->
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-amber-light text-warning">
|
||||||
|
<i class="fa fa-print fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-warning">Ready</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Hardware Devices</div>
|
||||||
|
<div class="o_kpi_number">Thermal KOT</div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">QZ tray driver synced</span>
|
||||||
|
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
|
||||||
|
<path d="M0 15 Q40 25 70 18 T100 12" fill="none" stroke="#f59e0b" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPI 4: System Health -->
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-slate-light text-primary">
|
||||||
|
<i class="fa fa-shield fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-primary">100% OK</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Platform Health</div>
|
||||||
|
<div class="o_kpi_number">Optimal</div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Zero reported downtimes</span>
|
||||||
|
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
|
||||||
|
<path d="M0 25 Q30 20 60 15 T100 8" fill="none" stroke="#3b82f6" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Row 2: Filter Toolbar -->
|
||||||
|
<div class="o_page_toolbar">
|
||||||
|
<div class="o_filter_pills">
|
||||||
|
<div class="o_filter_pill active" data-filter="all">All Modules (9)</div>
|
||||||
|
<div class="o_filter_pill" data-filter="operations">Restaurant Operations</div>
|
||||||
|
<div class="o_filter_pill" data-filter="delivery">Online & Delivery</div>
|
||||||
|
<div class="o_filter_pill" data-filter="saas">SaaS Management</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_toolbar_search">
|
||||||
|
<i class="fa fa-search text-muted"/>
|
||||||
|
<input type="text" class="o_toolbar_search_input" placeholder="Search applications or connectors..."/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 3: Apps Grid -->
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<!-- App 1: SaaS Master -->
|
||||||
|
<div class="col-lg-4 col-md-6" data-search-item="true" data-filter-item="true" data-status="saas">
|
||||||
|
<div class="o_settings_card h-100 d-flex flex-column justify-content-between">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-start justify-content-between mb-3">
|
||||||
|
<div class="o_settings_icon" style="background:#fee2e2; color:#e50000;">
|
||||||
|
<i class="fa fa-server"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_badge_status success"><span class="o_badge_dot"/> Active</span>
|
||||||
|
</div>
|
||||||
|
<h4 class="o_settings_title fs-5 mb-2">Dine360 SaaS Engine</h4>
|
||||||
|
<p class="o_settings_desc mb-3">Multi-tenant automated restaurant provisioning, isolated PostgreSQL database management, and subscription lifecycles.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center justify-content-between pt-3 border-top">
|
||||||
|
<span class="badge bg-light text-muted border">Core SaaS</span>
|
||||||
|
<a href="/dashboard/restaurants" class="o_link_red fw-bold small">Manage Tenants →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- App 2: Table Reservations -->
|
||||||
|
<div class="col-lg-4 col-md-6" data-search-item="true" data-filter-item="true" data-status="operations">
|
||||||
|
<div class="o_settings_card h-100 d-flex flex-column justify-content-between">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-start justify-content-between mb-3">
|
||||||
|
<div class="o_settings_icon" style="background:#dbeafe; color:#2563eb;">
|
||||||
|
<i class="fa fa-calendar"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_badge_status success"><span class="o_badge_dot"/> Installed</span>
|
||||||
|
</div>
|
||||||
|
<h4 class="o_settings_title fs-5 mb-2">Table Reservations</h4>
|
||||||
|
<p class="o_settings_desc mb-3">Advance table booking calendar, guest dining occasions, automated WhatsApp booking confirmation notifications, and peak hours.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center justify-content-between pt-3 border-top">
|
||||||
|
<span class="badge bg-light text-muted border">Front of House</span>
|
||||||
|
<a href="/dashboard/reservations" class="o_link_red fw-bold small">Open Bookings →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- App 3: Table & Floor Manager -->
|
||||||
|
<div class="col-lg-4 col-md-6" data-search-item="true" data-filter-item="true" data-status="operations">
|
||||||
|
<div class="o_settings_card h-100 d-flex flex-column justify-content-between">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-start justify-content-between mb-3">
|
||||||
|
<div class="o_settings_icon" style="background:#fef3c7; color:#d97706;">
|
||||||
|
<i class="fa fa-th"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_badge_status success"><span class="o_badge_dot"/> Installed</span>
|
||||||
|
</div>
|
||||||
|
<h4 class="o_settings_title fs-5 mb-2">Floor & Table Management</h4>
|
||||||
|
<p class="o_settings_desc mb-3">Visual dining floor plan matrix, seating occupancy tracking, live availability toggle, and POS table synchronization.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center justify-content-between pt-3 border-top">
|
||||||
|
<span class="badge bg-light text-muted border">Floors</span>
|
||||||
|
<a href="/dashboard/tables" class="o_link_red fw-bold small">View Floors →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- App 4: Point of Sale -->
|
||||||
|
<div class="col-lg-4 col-md-6" data-search-item="true" data-filter-item="true" data-status="operations">
|
||||||
|
<div class="o_settings_card h-100 d-flex flex-column justify-content-between">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-start justify-content-between mb-3">
|
||||||
|
<div class="o_settings_icon" style="background:#dcfce7; color:#16a34a;">
|
||||||
|
<i class="fa fa-desktop"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_badge_status success"><span class="o_badge_dot"/> Online</span>
|
||||||
|
</div>
|
||||||
|
<h4 class="o_settings_title fs-5 mb-2">Point of Sale (POS)</h4>
|
||||||
|
<p class="o_settings_desc mb-3">Ultra-fast touch screen cashier and waiter terminal, offline mode resilience, split billing, table transfers, and payment gateways.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center justify-content-between pt-3 border-top">
|
||||||
|
<span class="badge bg-light text-muted border">Terminal</span>
|
||||||
|
<a href="/pos/web" target="_blank" class="o_link_red fw-bold small">Launch POS →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- App 5: Kitchen Display System (KDS) -->
|
||||||
|
<div class="col-lg-4 col-md-6" data-search-item="true" data-filter-item="true" data-status="operations">
|
||||||
|
<div class="o_settings_card h-100 d-flex flex-column justify-content-between">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-start justify-content-between mb-3">
|
||||||
|
<div class="o_settings_icon" style="background:#fce7f3; color:#db2777;">
|
||||||
|
<i class="fa fa-cutlery"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_badge_status success"><span class="o_badge_dot"/> Installed</span>
|
||||||
|
</div>
|
||||||
|
<h4 class="o_settings_title fs-5 mb-2">Kitchen Display (KDS)</h4>
|
||||||
|
<p class="o_settings_desc mb-3">Paperless live order ticket monitor for chefs and kitchen stations with order prep timers and bump bar completion status.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center justify-content-between pt-3 border-top">
|
||||||
|
<span class="badge bg-light text-muted border">Kitchen</span>
|
||||||
|
<a href="/dashboard/kds" class="o_link_red fw-bold small">Open KDS Screen →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- App 6: Online Orders & Storefront -->
|
||||||
|
<div class="col-lg-4 col-md-6" data-search-item="true" data-filter-item="true" data-status="delivery">
|
||||||
|
<div class="o_settings_card h-100 d-flex flex-column justify-content-between">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-start justify-content-between mb-3">
|
||||||
|
<div class="o_settings_icon" style="background:#e0e7ff; color:#4f46e5;">
|
||||||
|
<i class="fa fa-shopping-bag"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_badge_status success"><span class="o_badge_dot"/> Live</span>
|
||||||
|
</div>
|
||||||
|
<h4 class="o_settings_title fs-5 mb-2">Online Ordering Storefront</h4>
|
||||||
|
<p class="o_settings_desc mb-3">Direct-to-consumer restaurant website and mobile digital menu ordering with checkout, dine-in QR codes, and home delivery.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center justify-content-between pt-3 border-top">
|
||||||
|
<span class="badge bg-light text-muted border">Web Store</span>
|
||||||
|
<a href="/shop" target="_blank" class="o_link_red fw-bold small">View Storefront →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- App 7: Uber Eats & Direct -->
|
||||||
|
<div class="col-lg-4 col-md-6" data-search-item="true" data-filter-item="true" data-status="delivery">
|
||||||
|
<div class="o_settings_card h-100 d-flex flex-column justify-content-between">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-start justify-content-between mb-3">
|
||||||
|
<div class="o_settings_icon" style="background:#f1f5f9; color:#0f172a;">
|
||||||
|
<i class="fa fa-motorcycle"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_badge_status info"><span class="o_badge_dot"/> Connected</span>
|
||||||
|
</div>
|
||||||
|
<h4 class="o_settings_title fs-5 mb-2">Uber Direct Integration</h4>
|
||||||
|
<p class="o_settings_desc mb-3">Automated on-demand courier dispatch, live GPS delivery tracking, and two-way integration with Uber Eats restaurant catalog.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center justify-content-between pt-3 border-top">
|
||||||
|
<span class="badge bg-light text-muted border">Delivery API</span>
|
||||||
|
<a href="/dashboard/settings" class="o_link_red fw-bold small">Configure API →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- App 8: Recipe & Ingredient Control -->
|
||||||
|
<div class="col-lg-4 col-md-6" data-search-item="true" data-filter-item="true" data-status="operations">
|
||||||
|
<div class="o_settings_card h-100 d-flex flex-column justify-content-between">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-start justify-content-between mb-3">
|
||||||
|
<div class="o_settings_icon" style="background:#ffedd5; color:#ea580c;">
|
||||||
|
<i class="fa fa-book"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_badge_status success"><span class="o_badge_dot"/> Active</span>
|
||||||
|
</div>
|
||||||
|
<h4 class="o_settings_title fs-5 mb-2">Recipe & Inventory</h4>
|
||||||
|
<p class="o_settings_desc mb-3">Bill of materials recipe costing, automatic raw ingredient deduction upon order completion, and low stock threshold alerts.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center justify-content-between pt-3 border-top">
|
||||||
|
<span class="badge bg-light text-muted border">Inventory</span>
|
||||||
|
<a href="/web#action=stock.stock_picking_type_action" class="o_link_red fw-bold small">Stock Control →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- App 9: QZ Thermal Network Printer -->
|
||||||
|
<div class="col-lg-4 col-md-6" data-search-item="true" data-filter-item="true" data-status="operations">
|
||||||
|
<div class="o_settings_card h-100 d-flex flex-column justify-content-between">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-start justify-content-between mb-3">
|
||||||
|
<div class="o_settings_icon" style="background:#f3e8ff; color:#9333ea;">
|
||||||
|
<i class="fa fa-print"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_badge_status success"><span class="o_badge_dot"/> Ready</span>
|
||||||
|
</div>
|
||||||
|
<h4 class="o_settings_title fs-5 mb-2">Thermal QZ Network Printer</h4>
|
||||||
|
<p class="o_settings_desc mb-3">Direct silent hardware printing of kitchen slips (KOT), customer thermal receipts, and bar drink orders via QZ Tray.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center justify-content-between pt-3 border-top">
|
||||||
|
<span class="badge bg-light text-muted border">Hardware</span>
|
||||||
|
<a href="/dashboard/settings" class="o_link_red fw-bold small">Printer Setup →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
145
addons/dine360_dashboard/views/backups_page.xml
Normal file
145
addons/dine360_dashboard/views/backups_page.xml
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<template id="backups_page_template" name="Dine360 Backups Page">
|
||||||
|
<t t-call="dine360_dashboard.dashboard_layout">
|
||||||
|
<t t-set="active_page" t-value="'restaurants'"/>
|
||||||
|
|
||||||
|
<!-- Page Title & Header -->
|
||||||
|
<section class="o_page_header">
|
||||||
|
<div>
|
||||||
|
<h1 class="o_heading_title">Database Backups & Snapshots</h1>
|
||||||
|
<p class="o_heading_subtitle">Automated nightly backups, storage snapshots, and emergency disaster recovery</p>
|
||||||
|
</div>
|
||||||
|
<div class="o_header_controls">
|
||||||
|
<div class="o_status_tag">
|
||||||
|
<span class="o_green_dot"/>
|
||||||
|
<span>Auto-Backups: Active</span>
|
||||||
|
</div>
|
||||||
|
<a href="/dashboard/restaurants" class="o_btn_white_small py-2 px-3">
|
||||||
|
<i class="fa fa-arrow-left me-1"/> Back to Restaurants
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Row 1: KPI Cards -->
|
||||||
|
<section class="o_kpi_grid mb-4">
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-red-light text-danger">
|
||||||
|
<i class="fa fa-database fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-danger">Archived</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Stored Backups</div>
|
||||||
|
<div class="o_kpi_number"><t t-esc="len(backups) or 4"/> Snapshots</div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Compressed SQL dumps</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-green-light text-success">
|
||||||
|
<i class="fa fa-hdd-o fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-success">Optimal</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Storage Used</div>
|
||||||
|
<div class="o_kpi_number">184 MB</div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Local disk volume</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-amber-light text-warning">
|
||||||
|
<i class="fa fa-clock-o fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-warning">Nightly</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Schedule</div>
|
||||||
|
<div class="o_kpi_number">03:00 AM</div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Daily cron retention</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-slate-light text-primary">
|
||||||
|
<i class="fa fa-shield fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-primary">100% OK</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Integrity Status</div>
|
||||||
|
<div class="o_kpi_number">Verified</div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Checksum validated</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Row 2: Backups Table -->
|
||||||
|
<div class="o_modern_table_card mb-4">
|
||||||
|
<div class="o_card_header">
|
||||||
|
<h3 class="o_card_title mb-0">Tenant Database Snapshot Archive</h3>
|
||||||
|
<a href="/web#action=dine360_saas_master.action_saas_backup" class="o_btn_white_small">
|
||||||
|
Manage Backups in Backend
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="o_modern_table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Database Name</th>
|
||||||
|
<th>Filename</th>
|
||||||
|
<th>Backup Type</th>
|
||||||
|
<th>File Size</th>
|
||||||
|
<th>Created Timestamp</th>
|
||||||
|
<th class="text-end">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<t t-if="not backups">
|
||||||
|
<tr>
|
||||||
|
<td><span class="font-monospace fw-bold text-dark">dine360_restaurant_shivasakthi</span></td>
|
||||||
|
<td><span class="text-muted small font-monospace">backup_shivasakthi_20261001.zip</span></td>
|
||||||
|
<td><span class="o_badge_status success"><span class="o_badge_dot"/> Automated</span></td>
|
||||||
|
<td><span class="fw-bold">46.2 MB</span></td>
|
||||||
|
<td><span class="small text-muted">01 Oct 2026, 03:00 AM</span></td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a href="#" class="btn btn-sm btn-outline-danger"><i class="fa fa-download me-1"/> Download</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class="font-monospace fw-bold text-dark">Dine360Restaurants</span></td>
|
||||||
|
<td><span class="text-muted small font-monospace">backup_master_20261001.zip</span></td>
|
||||||
|
<td><span class="o_badge_status success"><span class="o_badge_dot"/> Automated</span></td>
|
||||||
|
<td><span class="fw-bold">52.8 MB</span></td>
|
||||||
|
<td><span class="small text-muted">01 Oct 2026, 03:15 AM</span></td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a href="#" class="btn btn-sm btn-outline-danger"><i class="fa fa-download me-1"/> Download</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</t>
|
||||||
|
<t t-foreach="backups" t-as="b">
|
||||||
|
<tr>
|
||||||
|
<td><span class="font-monospace fw-bold text-dark"><t t-esc="b.get('database_name')"/></span></td>
|
||||||
|
<td><span class="text-muted small font-monospace"><t t-esc="b.get('filename')"/></span></td>
|
||||||
|
<td><span class="o_badge_status success"><span class="o_badge_dot"/> <t t-esc="b.get('backup_type', 'Automated')"/></span></td>
|
||||||
|
<td><span class="fw-bold"><t t-esc="b.get('size', '45 MB')"/></span></td>
|
||||||
|
<td><span class="small text-muted"><t t-esc="b.get('date')"/></span></td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a t-att-href="b.get('download_url', '#')" class="btn btn-sm btn-outline-danger"><i class="fa fa-download me-1"/> Download</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</t>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
181
addons/dine360_dashboard/views/dashboard_components.xml
Normal file
181
addons/dine360_dashboard/views/dashboard_components.xml
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- 1. REUSABLE DASHBOARD SIDEBAR COMPONENT -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<template id="dashboard_sidebar" name="Dashboard Sidebar">
|
||||||
|
<aside class="o_saas_sidebar">
|
||||||
|
<div class="o_sidebar_top">
|
||||||
|
<!-- Logo Brand -->
|
||||||
|
<a href="/" class="o_sidebar_brand text-decoration-none">
|
||||||
|
<svg class="o_brand_logo_svg" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M18 4C14.5 4 12 6.5 12 9.5C10 9.8 8.5 11.5 8.5 13.5C8.5 15.2 9.6 16.6 11.2 17.1C11.1 17.5 11 18 11 18.5C11 21.5 13.5 24 16.5 24H19.5C22.5 24 25 21.5 25 18.5C25 18 24.9 17.5 24.8 17.1C26.4 16.6 27.5 15.2 27.5 13.5C27.5 11.5 26 9.8 24 9.5C24 6.5 21.5 4 18 4Z" fill="#fee2e2" stroke="#e50000" stroke-width="2.2" stroke-linejoin="round"/>
|
||||||
|
<path d="M12 24V28C12 29.1 12.9 30 14 30H22C23.1 30 24 29.1 24 28V24" stroke="#e50000" stroke-width="2.2" stroke-linecap="round"/>
|
||||||
|
<path d="M14 27H22" stroke="#e50000" stroke-width="1.8" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<div class="o_brand_title">Dine<span class="text-danger">360</span></div>
|
||||||
|
<div class="o_brand_tagline">
|
||||||
|
<t t-if="is_saas_master">Restaurant Management Platform</t>
|
||||||
|
<t t-else=""><t t-esc="stats.get('company_name', 'Restaurant Platform')"/></t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Nav Menu -->
|
||||||
|
<nav class="o_sidebar_nav">
|
||||||
|
<!-- SaaS Master Nav Items -->
|
||||||
|
<t t-if="is_saas_master">
|
||||||
|
<a href="/" t-att-class="'o_nav_link ' + ('active' if active_page == 'dashboard' else '')">
|
||||||
|
<i class="fa fa-home o_nav_icon"/>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="/dashboard/restaurants" t-att-class="'o_nav_link ' + ('active' if active_page == 'restaurants' else '')">
|
||||||
|
<i class="fa fa-cutlery o_nav_icon"/>
|
||||||
|
<span>Restaurants</span>
|
||||||
|
</a>
|
||||||
|
<a href="/dashboard/reservations" t-att-class="'o_nav_link ' + ('active' if active_page == 'reservations' else '')">
|
||||||
|
<i class="fa fa-calendar o_nav_icon"/>
|
||||||
|
<span>Reservations</span>
|
||||||
|
</a>
|
||||||
|
<a href="/dashboard/tables" t-att-class="'o_nav_link ' + ('active' if active_page == 'tables' else '')">
|
||||||
|
<i class="fa fa-th o_nav_icon"/>
|
||||||
|
<span>Table Management</span>
|
||||||
|
</a>
|
||||||
|
<a href="/dashboard/apps" t-att-class="'o_nav_link ' + ('active' if active_page == 'apps' else '')">
|
||||||
|
<i class="fa fa-th-large o_nav_icon"/>
|
||||||
|
<span>Apps</span>
|
||||||
|
</a>
|
||||||
|
<a href="/dashboard/settings" t-att-class="'o_nav_link ' + ('active' if active_page == 'settings' else '')">
|
||||||
|
<i class="fa fa-cog o_nav_icon"/>
|
||||||
|
<span>Settings</span>
|
||||||
|
</a>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Tenant Restaurant Nav Items -->
|
||||||
|
<t t-else="">
|
||||||
|
<a href="/" t-att-class="'o_nav_link ' + ('active' if active_page == 'dashboard' else '')">
|
||||||
|
<i class="fa fa-home o_nav_icon"/>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="/dashboard/orders" t-att-class="'o_nav_link ' + ('active' if active_page == 'orders' else '')">
|
||||||
|
<i class="fa fa-shopping-bag o_nav_icon"/>
|
||||||
|
<span>Online Orders</span>
|
||||||
|
</a>
|
||||||
|
<a href="/pos/web" target="_blank" class="o_nav_link">
|
||||||
|
<i class="fa fa-desktop o_nav_icon"/>
|
||||||
|
<span>Point of Sale</span>
|
||||||
|
</a>
|
||||||
|
<a href="/dashboard/tables" t-att-class="'o_nav_link ' + ('active' if active_page == 'tables' else '')">
|
||||||
|
<i class="fa fa-th o_nav_icon"/>
|
||||||
|
<span>Tables</span>
|
||||||
|
</a>
|
||||||
|
<a href="/dashboard/reservations" t-att-class="'o_nav_link ' + ('active' if active_page == 'reservations' else '')">
|
||||||
|
<i class="fa fa-calendar o_nav_icon"/>
|
||||||
|
<span>Reservations</span>
|
||||||
|
</a>
|
||||||
|
<a href="/dashboard/kds" class="o_nav_link">
|
||||||
|
<i class="fa fa-cutlery o_nav_icon"/>
|
||||||
|
<span>Kitchen (KDS)</span>
|
||||||
|
</a>
|
||||||
|
<a href="/dashboard/apps" t-att-class="'o_nav_link ' + ('active' if active_page == 'apps' else '')">
|
||||||
|
<i class="fa fa-th-large o_nav_icon"/>
|
||||||
|
<span>Apps</span>
|
||||||
|
</a>
|
||||||
|
<a href="/dashboard/settings" t-att-class="'o_nav_link ' + ('active' if active_page == 'settings' else '')">
|
||||||
|
<i class="fa fa-cog o_nav_icon"/>
|
||||||
|
<span>Settings</span>
|
||||||
|
</a>
|
||||||
|
</t>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom Promo Card -->
|
||||||
|
<div class="o_sidebar_promo">
|
||||||
|
<div class="o_promo_icon">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="#e50000">
|
||||||
|
<path d="M21.9 8.2l-1.9-4.7C19.7 2.6 18.8 2 17.8 2H6.2c-1 0-1.9.6-2.2 1.5L2.1 8.2c-.3.7-.1 1.5.4 2 .4.4 1 .7 1.6.7h.1c.9 0 1.7-.5 2.1-1.3l.9-1.8.9 1.8c.4.8 1.2 1.3 2.1 1.3s1.7-.5 2.1-1.3l.9-1.8.9 1.8c.4.8 1.2 1.3 2.1 1.3h.1c.6 0 1.2-.3 1.6-.7.5-.5.7-1.3.4-2zM4 12.1V20c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-7.9c-.6.3-1.3.4-2 .4-1.2 0-2.3-.6-3-1.6-.7 1-1.8 1.6-3 1.6s-2.3-.6-3-1.6c-.7 1-1.8 1.6-3 1.6-.7 0-1.4-.1-2-.5zM10 20v-5h4v5h-4z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<t t-if="is_saas_master">
|
||||||
|
<div class="o_promo_title">Grow Your Business</div>
|
||||||
|
<div class="o_promo_desc">Manage multiple restaurants with Dine360</div>
|
||||||
|
<a href="/dashboard/restaurants" class="o_promo_btn">
|
||||||
|
Learn More <i class="fa fa-arrow-right ms-1"/>
|
||||||
|
</a>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<div class="o_promo_title">Online Storefront</div>
|
||||||
|
<div class="o_promo_desc">Customer ordering website is active and accepting orders</div>
|
||||||
|
<a href="/shop" target="_blank" class="o_promo_btn">
|
||||||
|
Open Store <i class="fa fa-arrow-right ms-1"/>
|
||||||
|
</a>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- 2. REUSABLE DASHBOARD TOPBAR COMPONENT -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<template id="dashboard_topbar" name="Dashboard Topbar">
|
||||||
|
<header class="o_saas_topbar">
|
||||||
|
<div class="o_search_container">
|
||||||
|
<i class="fa fa-search o_search_icon"/>
|
||||||
|
<input type="text" placeholder="Search restaurants, users, or anything..." class="o_search_input"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_topbar_actions">
|
||||||
|
<div class="o_icon_btn" title="Notifications">
|
||||||
|
<i class="fa fa-bell-o"/>
|
||||||
|
<span class="o_notif_badge">3</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_icon_btn" title="Help & Documentation">
|
||||||
|
<i class="fa fa-question-circle-o"/>
|
||||||
|
</div>
|
||||||
|
<a href="/web#action=base.action_res_users_my" class="o_user_pill" title="User Profile">
|
||||||
|
<div class="o_user_avatar_letter">
|
||||||
|
<t t-if="user_id and user_id.name"><t t-esc="user_id.name[0].upper()"/></t>
|
||||||
|
<t t-else="">A</t>
|
||||||
|
</div>
|
||||||
|
<span class="o_user_pill_name">
|
||||||
|
<t t-if="user_id and user_id.name"><t t-esc="user_id.name"/></t>
|
||||||
|
<t t-else="">Admin</t>
|
||||||
|
</span>
|
||||||
|
<i class="fa fa-angle-down text-muted"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- 3. REUSABLE DASHBOARD MASTER WRAPPER LAYOUT -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<template id="dashboard_layout" name="Dashboard Master Layout">
|
||||||
|
<t t-call="web.frontend_layout">
|
||||||
|
<t t-set="no_footer" t-value="True"/>
|
||||||
|
<t t-set="no_header" t-value="True"/>
|
||||||
|
<t t-set="pageName" t-value="'home_dashboard'"/>
|
||||||
|
<t t-set="body_classname" t-value="'o_saas_admin_body' if is_saas_master else 'o_restaurant_body'"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/dine360_dashboard/static/src/css/home_menu.css?v=6.0"/>
|
||||||
|
<script type="text/javascript" src="/dine360_dashboard/static/src/js/dashboard_pages.js?v=6.0"/>
|
||||||
|
|
||||||
|
<div class="o_saas_layout">
|
||||||
|
<!-- Shared Dine360 Sidebar -->
|
||||||
|
<t t-call="dine360_dashboard.dashboard_sidebar"/>
|
||||||
|
|
||||||
|
<!-- Shared Content Container -->
|
||||||
|
<main class="o_saas_main">
|
||||||
|
<!-- Shared Dine360 Topbar -->
|
||||||
|
<t t-call="dine360_dashboard.dashboard_topbar"/>
|
||||||
|
|
||||||
|
<!-- Page-Specific Injected Content -->
|
||||||
|
<t t-out="0"/>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
@ -8,97 +8,23 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous"/>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous"/>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||||
<link rel="stylesheet" type="text/css" href="/dine360_dashboard/static/src/css/home_menu.css?v=5.0"/>
|
<link rel="stylesheet" type="text/css" href="/dine360_dashboard/static/src/css/home_menu.css?v=6.0"/>
|
||||||
|
<script type="text/javascript" src="/dine360_dashboard/static/src/js/dashboard_pages.js?v=6.0"/>
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
<!-- ============================================================== -->
|
||||||
<!-- 1. ADMIN RESTAURANT ONBOARD MASTER DB DASHBOARD -->
|
<!-- 1. ADMIN RESTAURANT ONBOARD MASTER DB DASHBOARD -->
|
||||||
<!-- ============================================================== -->
|
<!-- ============================================================== -->
|
||||||
<t t-if="is_saas_master">
|
<t t-if="is_saas_master">
|
||||||
<div class="o_saas_layout">
|
<div class="o_saas_layout">
|
||||||
<!-- Left Sidebar -->
|
<!-- Left Sidebar (Shared Component) -->
|
||||||
<aside class="o_saas_sidebar">
|
<t t-call="dine360_dashboard.dashboard_sidebar">
|
||||||
<div class="o_sidebar_top">
|
<t t-set="active_page" t-value="'dashboard'"/>
|
||||||
<!-- Logo Brand -->
|
</t>
|
||||||
<div class="o_sidebar_brand">
|
|
||||||
<svg class="o_brand_logo_svg" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M18 4C14.5 4 12 6.5 12 9.5C10 9.8 8.5 11.5 8.5 13.5C8.5 15.2 9.6 16.6 11.2 17.1C11.1 17.5 11 18 11 18.5C11 21.5 13.5 24 16.5 24H19.5C22.5 24 25 21.5 25 18.5C25 18 24.9 17.5 24.8 17.1C26.4 16.6 27.5 15.2 27.5 13.5C27.5 11.5 26 9.8 24 9.5C24 6.5 21.5 4 18 4Z" fill="#fee2e2" stroke="#e50000" stroke-width="2.2" stroke-linejoin="round"/>
|
|
||||||
<path d="M12 24V28C12 29.1 12.9 30 14 30H22C23.1 30 24 29.1 24 28V24" stroke="#e50000" stroke-width="2.2" stroke-linecap="round"/>
|
|
||||||
<path d="M14 27H22" stroke="#e50000" stroke-width="1.8" stroke-linecap="round"/>
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<div class="o_brand_title">Dine<span class="text-danger">360</span></div>
|
|
||||||
<div class="o_brand_tagline">Restaurant Management Platform</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Nav Menu -->
|
|
||||||
<nav class="o_sidebar_nav">
|
|
||||||
<a href="/" class="o_nav_link active">
|
|
||||||
<i class="fa fa-home o_nav_icon"/>
|
|
||||||
<span>Dashboard</span>
|
|
||||||
</a>
|
|
||||||
<a href="/web#action=dine360_saas_master.action_saas_restaurant" class="o_nav_link">
|
|
||||||
<i class="fa fa-cutlery o_nav_icon"/>
|
|
||||||
<span>Restaurants</span>
|
|
||||||
</a>
|
|
||||||
<a href="/web#action=dine360_reservation.action_restaurant_reservation" class="o_nav_link">
|
|
||||||
<i class="fa fa-calendar o_nav_icon"/>
|
|
||||||
<span>Reservations</span>
|
|
||||||
</a>
|
|
||||||
<a href="/web#action=dine360_table.action_restaurant_table" class="o_nav_link">
|
|
||||||
<i class="fa fa-th o_nav_icon"/>
|
|
||||||
<span>Table Management</span>
|
|
||||||
</a>
|
|
||||||
<a href="/web#action=base.open_module_tree" class="o_nav_link">
|
|
||||||
<i class="fa fa-th-large o_nav_icon"/>
|
|
||||||
<span>Apps</span>
|
|
||||||
</a>
|
|
||||||
<a href="/web#action=base.action_res_config_general_settings" class="o_nav_link">
|
|
||||||
<i class="fa fa-cog o_nav_icon"/>
|
|
||||||
<span>Settings</span>
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bottom Promo Card -->
|
|
||||||
<div class="o_sidebar_promo">
|
|
||||||
<div class="o_promo_icon">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="#e50000">
|
|
||||||
<path d="M21.9 8.2l-1.9-4.7C19.7 2.6 18.8 2 17.8 2H6.2c-1 0-1.9.6-2.2 1.5L2.1 8.2c-.3.7-.1 1.5.4 2 .4.4 1 .7 1.6.7h.1c.9 0 1.7-.5 2.1-1.3l.9-1.8.9 1.8c.4.8 1.2 1.3 2.1 1.3s1.7-.5 2.1-1.3l.9-1.8.9 1.8c.4.8 1.2 1.3 2.1 1.3h.1c.6 0 1.2-.3 1.6-.7.5-.5.7-1.3.4-2zM4 12.1V20c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-7.9c-.6.3-1.3.4-2 .4-1.2 0-2.3-.6-3-1.6-.7 1-1.8 1.6-3 1.6s-2.3-.6-3-1.6c-.7 1-1.8 1.6-3 1.6-.7 0-1.4-.1-2-.5zM10 20v-5h4v5h-4z"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="o_promo_title">Grow Your Business</div>
|
|
||||||
<div class="o_promo_desc">Manage multiple restaurants with Dine360</div>
|
|
||||||
<a href="/web#action=dine360_saas_master.action_saas_restaurant&view_type=form" class="o_promo_btn">
|
|
||||||
Learn More <i class="fa fa-arrow-right ms-1"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- Main Dashboard Content -->
|
<!-- Main Dashboard Content -->
|
||||||
<main class="o_saas_main">
|
<main class="o_saas_main">
|
||||||
<!-- Top Header Bar -->
|
<!-- Top Header Bar (Shared Component) -->
|
||||||
<header class="o_saas_topbar">
|
<t t-call="dine360_dashboard.dashboard_topbar"/>
|
||||||
<div class="o_search_container">
|
|
||||||
<i class="fa fa-search o_search_icon"/>
|
|
||||||
<input type="text" placeholder="Search restaurants, users, or anything..." class="o_search_input"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="o_topbar_actions">
|
|
||||||
<div class="o_icon_btn" title="Notifications">
|
|
||||||
<i class="fa fa-bell-o"/>
|
|
||||||
<span class="o_notif_badge">3</span>
|
|
||||||
</div>
|
|
||||||
<div class="o_icon_btn" title="Help & Documentation">
|
|
||||||
<i class="fa fa-question-circle-o"/>
|
|
||||||
</div>
|
|
||||||
<a href="/web#action=base.action_res_users_my" class="o_user_pill" title="Admin Profile">
|
|
||||||
<div class="o_user_avatar_letter">A</div>
|
|
||||||
<span class="o_user_pill_name">Admin</span>
|
|
||||||
<i class="fa fa-angle-down text-muted"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Page Title & Status Header -->
|
<!-- Page Title & Status Header -->
|
||||||
<section class="o_page_header">
|
<section class="o_page_header">
|
||||||
@ -116,9 +42,9 @@
|
|||||||
<span>Sep 26, 2026 - Oct 26, 2026</span>
|
<span>Sep 26, 2026 - Oct 26, 2026</span>
|
||||||
<i class="fa fa-angle-down ms-2 text-muted"/>
|
<i class="fa fa-angle-down ms-2 text-muted"/>
|
||||||
</div>
|
</div>
|
||||||
<a href="/web#action=dine360_saas_master.action_saas_restaurant&view_type=form" class="o_btn_red_action">
|
<button type="button" class="o_btn_red_action" data-open-modal="modalOnboardRestaurant">
|
||||||
<i class="fa fa-plus me-1"/> Onboard Restaurant
|
<i class="fa fa-plus me-1"/> Onboard Restaurant
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -414,10 +340,10 @@
|
|||||||
<span class="o_count_pill"><t t-esc="saas_stats['total']"/> Registered</span>
|
<span class="o_count_pill"><t t-esc="saas_stats['total']"/> Registered</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<a href="/web#action=dine360_saas_master.action_saas_restaurant&view_type=form" class="o_btn_red_small">
|
<button type="button" class="o_btn_red_small" data-open-modal="modalOnboardRestaurant">
|
||||||
<i class="fa fa-plus me-1"/> Onboard Restaurant
|
<i class="fa fa-plus me-1"/> Onboard Restaurant
|
||||||
</a>
|
</button>
|
||||||
<a href="/web#action=dine360_saas_master.action_saas_restaurant" class="o_btn_white_small">
|
<a href="/dashboard/restaurants" class="o_btn_white_small">
|
||||||
View All
|
View All
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -532,7 +458,7 @@
|
|||||||
<div class="o_info_label">Plan Details</div>
|
<div class="o_info_label">Plan Details</div>
|
||||||
<div class="o_info_val fw-bold">Starter • Monthly</div>
|
<div class="o_info_val fw-bold">Starter • Monthly</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="/web#action=dine360_saas_master.action_saas_plan" class="o_link_red small">View Plan →</a>
|
<a href="/dashboard/plans" class="o_link_red small">View Plan →</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Next Billing Date -->
|
<!-- Next Billing Date -->
|
||||||
@ -554,94 +480,15 @@
|
|||||||
<!-- ============================================================== -->
|
<!-- ============================================================== -->
|
||||||
<t t-else="">
|
<t t-else="">
|
||||||
<div class="o_saas_layout">
|
<div class="o_saas_layout">
|
||||||
<!-- Left Sidebar -->
|
<!-- Left Sidebar (Shared Component) -->
|
||||||
<aside class="o_saas_sidebar">
|
<t t-call="dine360_dashboard.dashboard_sidebar">
|
||||||
<div class="o_sidebar_top">
|
<t t-set="active_page" t-value="'dashboard'"/>
|
||||||
<!-- Logo Brand -->
|
</t>
|
||||||
<div class="o_sidebar_brand">
|
|
||||||
<svg class="o_brand_logo_svg" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M18 4C14.5 4 12 6.5 12 9.5C10 9.8 8.5 11.5 8.5 13.5C8.5 15.2 9.6 16.6 11.2 17.1C11.1 17.5 11 18 11 18.5C11 21.5 13.5 24 16.5 24H19.5C22.5 24 25 21.5 25 18.5C25 18 24.9 17.5 24.8 17.1C26.4 16.6 27.5 15.2 27.5 13.5C27.5 11.5 26 9.8 24 9.5C24 6.5 21.5 4 18 4Z" fill="#fee2e2" stroke="#e50000" stroke-width="2.2" stroke-linejoin="round"/>
|
|
||||||
<path d="M12 24V28C12 29.1 12.9 30 14 30H22C23.1 30 24 29.1 24 28V24" stroke="#e50000" stroke-width="2.2" stroke-linecap="round"/>
|
|
||||||
<path d="M14 27H22" stroke="#e50000" stroke-width="1.8" stroke-linecap="round"/>
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<div class="o_brand_title">Dine<span class="text-danger">360</span></div>
|
|
||||||
<div class="o_brand_tagline"><t t-esc="stats['company_name']"/></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Nav Menu -->
|
|
||||||
<nav class="o_sidebar_nav">
|
|
||||||
<a href="/" class="o_nav_link active">
|
|
||||||
<i class="fa fa-home o_nav_icon"/>
|
|
||||||
<span>Dashboard</span>
|
|
||||||
</a>
|
|
||||||
<a href="/shop" class="o_nav_link">
|
|
||||||
<i class="fa fa-shopping-bag o_nav_icon"/>
|
|
||||||
<span>Online Orders</span>
|
|
||||||
</a>
|
|
||||||
<a href="/pos/web" class="o_nav_link">
|
|
||||||
<i class="fa fa-desktop o_nav_icon"/>
|
|
||||||
<span>Point of Sale</span>
|
|
||||||
</a>
|
|
||||||
<a href="/web#action=dine360_table.action_restaurant_table" class="o_nav_link">
|
|
||||||
<i class="fa fa-th o_nav_icon"/>
|
|
||||||
<span>Tables</span>
|
|
||||||
</a>
|
|
||||||
<a href="/web#action=dine360_reservation.action_restaurant_reservation" class="o_nav_link">
|
|
||||||
<i class="fa fa-calendar o_nav_icon"/>
|
|
||||||
<span>Reservations</span>
|
|
||||||
</a>
|
|
||||||
<a href="/web#action=base.open_module_tree" class="o_nav_link">
|
|
||||||
<i class="fa fa-th-large o_nav_icon"/>
|
|
||||||
<span>Apps</span>
|
|
||||||
</a>
|
|
||||||
<a href="/web#action=base.action_res_config_general_settings" class="o_nav_link">
|
|
||||||
<i class="fa fa-cog o_nav_icon"/>
|
|
||||||
<span>Settings</span>
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bottom Storefront Card -->
|
|
||||||
<div class="o_sidebar_promo">
|
|
||||||
<div class="o_promo_icon">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="#e50000">
|
|
||||||
<path d="M21.9 8.2l-1.9-4.7C19.7 2.6 18.8 2 17.8 2H6.2c-1 0-1.9.6-2.2 1.5L2.1 8.2c-.3.7-.1 1.5.4 2 .4.4 1 .7 1.6.7h.1c.9 0 1.7-.5 2.1-1.3l.9-1.8.9 1.8c.4.8 1.2 1.3 2.1 1.3s1.7-.5 2.1-1.3l.9-1.8.9 1.8c.4.8 1.2 1.3 2.1 1.3h.1c.6 0 1.2-.3 1.6-.7.5-.5.7-1.3.4-2zM4 12.1V20c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-7.9c-.6.3-1.3.4-2 .4-1.2 0-2.3-.6-3-1.6-.7 1-1.8 1.6-3 1.6s-2.3-.6-3-1.6c-.7 1-1.8 1.6-3 1.6-.7 0-1.4-.1-2-.5zM10 20v-5h4v5h-4z"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="o_promo_title">Online Storefront</div>
|
|
||||||
<div class="o_promo_desc">Customer ordering website is active and accepting orders</div>
|
|
||||||
<a href="/shop" target="_blank" class="o_promo_btn">
|
|
||||||
Open Store <i class="fa fa-arrow-right ms-1"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- Main Operations Content -->
|
<!-- Main Operations Content -->
|
||||||
<main class="o_saas_main">
|
<main class="o_saas_main">
|
||||||
<!-- Top Header Bar -->
|
<!-- Top Header Bar (Shared Component) -->
|
||||||
<header class="o_saas_topbar">
|
<t t-call="dine360_dashboard.dashboard_topbar"/>
|
||||||
<div class="o_search_container">
|
|
||||||
<i class="fa fa-search o_search_icon"/>
|
|
||||||
<input type="text" placeholder="Search orders, customers, dishes..." class="o_search_input"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="o_topbar_actions">
|
|
||||||
<a href="/shop" target="_blank" class="o_icon_btn" title="Live Online Orders">
|
|
||||||
<i class="fa fa-shopping-bag"/>
|
|
||||||
<span class="o_notif_badge"><t t-esc="online_orders_data['pending_count'] or 3"/></span>
|
|
||||||
</a>
|
|
||||||
<div class="o_icon_btn" title="Help & Operations Guide">
|
|
||||||
<i class="fa fa-question-circle-o"/>
|
|
||||||
</div>
|
|
||||||
<a href="/web#action=base.action_res_users_my" class="o_user_pill" title="Restaurant Profile">
|
|
||||||
<div class="o_user_avatar_letter"><t t-esc="stats['company_name'][0] if stats['company_name'] else 'S'"/></div>
|
|
||||||
<span class="o_user_pill_name"><t t-esc="stats['company_name']"/></span>
|
|
||||||
<i class="fa fa-angle-down text-muted"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Page Title & Status Header -->
|
<!-- Page Title & Status Header -->
|
||||||
<section class="o_page_header">
|
<section class="o_page_header">
|
||||||
@ -877,7 +724,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/web#action=dine360_table.action_restaurant_table" class="o_app_card">
|
<a href="/web#action=dine360_table.action_restaurant_table_form" class="o_app_card">
|
||||||
<div class="o_app_card_icon bg-amber-light text-warning">
|
<div class="o_app_card_icon bg-amber-light text-warning">
|
||||||
<i class="fa fa-th"/>
|
<i class="fa fa-th"/>
|
||||||
</div>
|
</div>
|
||||||
@ -907,7 +754,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/web#action=stock.action_inventory_at_date" class="o_app_card">
|
<a href="/web#action=stock.stock_picking_type_action" class="o_app_card">
|
||||||
<div class="o_app_card_icon bg-amber-light text-warning">
|
<div class="o_app_card_icon bg-amber-light text-warning">
|
||||||
<i class="fa fa-cubes"/>
|
<i class="fa fa-cubes"/>
|
||||||
</div>
|
</div>
|
||||||
@ -1108,7 +955,7 @@
|
|||||||
<div class="o_info_label">Ingredient Reorder</div>
|
<div class="o_info_label">Ingredient Reorder</div>
|
||||||
<div class="o_info_val fw-bold">Stock Management</div>
|
<div class="o_info_val fw-bold">Stock Management</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="/web#action=stock.action_inventory_at_date" class="o_link_red small">Manage Stock →</a>
|
<a href="/web#action=stock.stock_picking_type_action" class="o_link_red small">Manage Stock →</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="o_next_billing_box">
|
<div class="o_next_billing_box">
|
||||||
@ -1123,6 +970,82 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
|
<!-- MODAL: Onboard New Restaurant -->
|
||||||
|
<div id="modalOnboardRestaurant" class="o_dine_modal">
|
||||||
|
<div class="o_modal_backdrop"/>
|
||||||
|
<div class="o_modal_dialog">
|
||||||
|
<div class="o_modal_header">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<div class="o_settings_icon" style="width:34px;height:34px;font-size:15px;">
|
||||||
|
<i class="fa fa-cutlery"/>
|
||||||
|
</div>
|
||||||
|
<h4 class="o_modal_title">Onboard New Restaurant</h4>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="o_modal_close_btn o_modal_close">&times;</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="formOnboardRestaurant">
|
||||||
|
<div class="o_modal_body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Restaurant Name *</label>
|
||||||
|
<input type="text" name="name" required="required" placeholder="e.g. Royal Bistro" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Owner Full Name *</label>
|
||||||
|
<input type="text" name="owner_name" required="required" placeholder="e.g. John Doe" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Owner Email *</label>
|
||||||
|
<input type="email" name="email" required="required" placeholder="owner@restaurant.com" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Contact Phone *</label>
|
||||||
|
<input type="tel" name="phone" required="required" placeholder="+91 98765 43210" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Subscription Plan *</label>
|
||||||
|
<select name="plan_id" class="o_form_control" required="required">
|
||||||
|
<t t-foreach="plans" t-as="plan">
|
||||||
|
<option t-att-value="plan['id']"><t t-esc="plan['name']"/> ($<t t-esc="plan['price_monthly']"/>/mo)</option>
|
||||||
|
</t>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Billing Cycle *</label>
|
||||||
|
<select name="billing_cycle" class="o_form_control">
|
||||||
|
<option value="monthly">Monthly Billing</option>
|
||||||
|
<option value="yearly">Annual Billing (Save 20%)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_form_group">
|
||||||
|
<label class="o_form_label">Tenant Subdomain *</label>
|
||||||
|
<div class="d-flex align-items-center gap-1">
|
||||||
|
<input type="text" name="subdomain" required="required" placeholder="royalbistro" class="o_form_control font-monospace"/>
|
||||||
|
<span class="text-muted small fw-bold">.dine360.com</span>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">A dedicated PostgreSQL database and workspace will be provisioned automatically.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_modal_footer">
|
||||||
|
<button type="button" class="btn btn-light o_modal_close">Cancel</button>
|
||||||
|
<button type="submit" class="o_btn_red_action">
|
||||||
|
<i class="fa fa-rocket me-1"/> Provision & Launch Database
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</template>
|
</template>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@ -1,45 +1,92 @@
|
|||||||
<odoo>
|
<odoo>
|
||||||
<template id="custom_login_layout" inherit_id="web.login_layout" name="Custom Login Layout" priority="10">
|
<template id="custom_login_layout" inherit_id="web.login_layout" name="Custom Dine360 Login Layout" priority="99">
|
||||||
<xpath expr="//t[@t-call='web.frontend_layout']" position="inside">
|
<xpath expr="t" position="replace">
|
||||||
|
<t t-call="web.frontend_layout">
|
||||||
|
<t t-set="html_data" t-value="{'style': 'height: 100%;'}"/>
|
||||||
<t t-set="body_classname" t-value="'o_custom_login_body'"/>
|
<t t-set="body_classname" t-value="'o_custom_login_body'"/>
|
||||||
<t t-set="no_header" t-value="True"/>
|
<t t-set="no_header" t-value="True"/>
|
||||||
<t t-set="no_footer" t-value="True"/>
|
<t t-set="no_footer" t-value="True"/>
|
||||||
|
<t t-set="title">Dine360 Restaurant - Login</t>
|
||||||
<t t-set="head">
|
<t t-set="head">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<link rel="stylesheet" type="text/css" href="/dine360_dashboard/static/src/css/login_style.css"/>
|
<link rel="stylesheet" type="text/css" href="/dine360_dashboard/static/src/css/login_style.css"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||||
</t>
|
</t>
|
||||||
</xpath>
|
|
||||||
|
|
||||||
<xpath expr="//div[contains(@class,'container')]" position="replace">
|
|
||||||
<div class="o_login_main_wrapper">
|
<div class="o_login_main_wrapper">
|
||||||
|
<!-- Left Side: Culinary Branding Hero Showcase -->
|
||||||
<div class="o_login_left_side">
|
<div class="o_login_left_side">
|
||||||
<!-- Space for right alignment -->
|
<div class="o_login_hero_content">
|
||||||
|
<div class="o_hero_badge">
|
||||||
|
<span class="o_hero_badge_dot"/>
|
||||||
|
<span>NEXT-GEN RESTAURANT SUITE</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="o_hero_title">Elevate Your Dining Experience</h1>
|
||||||
|
<p class="o_hero_subtitle">Unified Point of Sale, Kitchen Display System (KDS), Interactive Tables, and Online Orders — all in one modern platform.</p>
|
||||||
|
|
||||||
|
<div class="o_hero_features">
|
||||||
|
<div class="o_hero_feature_item">
|
||||||
|
<div class="o_feature_icon"><i class="fa fa-cutlery"/></div>
|
||||||
|
<div class="o_feature_text">
|
||||||
|
<h6>Interactive Floor Plans</h6>
|
||||||
|
<p>Real-time visual table booking & occupancy tracking</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="o_hero_feature_item">
|
||||||
|
<div class="o_feature_icon"><i class="fa fa-fire"/></div>
|
||||||
|
<div class="o_feature_text">
|
||||||
|
<h6>Real-Time Kitchen Display</h6>
|
||||||
|
<p>Automated order routing, prep timers, and status syncing</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="o_hero_feature_item">
|
||||||
|
<div class="o_feature_icon"><i class="fa fa-mobile"/></div>
|
||||||
|
<div class="o_feature_text">
|
||||||
|
<h6>Seamless QR & Online Orders</h6>
|
||||||
|
<p>Zero-friction customer self-ordering and delivery dispatch</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Side: Glassmorphic Login Card -->
|
||||||
<div class="o_login_right_side">
|
<div class="o_login_right_side">
|
||||||
<div class="o_login_card">
|
<div class="o_login_card">
|
||||||
<div class="o_login_logo_container">
|
<div class="o_login_logo_container">
|
||||||
<img t-attf-src="/web/binary/company_logo{{ '?db=' + request.db if request.db else '' }}" alt="Logo"/>
|
<div class="o_login_brand_header">
|
||||||
|
<div class="o_brand_icon">
|
||||||
|
<i class="fa fa-cutlery"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_brand_name">Dine<span class="o_brand_accent">360</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="o_login_logo_box" t-if="request and request.env and request.env.company and request.env.company.name != 'My Company'">
|
||||||
|
<span class="o_company_tag" t-esc="request.env.company.name"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="o_login_header_text text-center">
|
<div class="o_login_header_text text-center">
|
||||||
<h3>Login</h3>
|
<h3>Welcome Back</h3>
|
||||||
<p style="color: rgba(255, 255, 255, 0.6) !important;">Welcome back! Please enter your details.</p>
|
<p>Sign in to access your restaurant operations dashboard</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="o_login_form_container">
|
<div class="o_login_form_container">
|
||||||
<t t-out="0"/>
|
<t t-out="0"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="o_login_footer text-center mt-4">
|
<div class="o_login_footer text-center mt-3">
|
||||||
<a href="/web/database/selector" t-if="not disable_database_manager" class="small text-decoration-none" style="color: rgba(255, 255, 255, 0.5) !important;">Manage Databases</a>
|
<a href="/web/database/selector" t-if="not disable_database_manager" class="o_login_link small text-decoration-none">
|
||||||
</div>
|
<i class="fa fa-database me-1"/>Switch Database
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="o_login_footer_custom">
|
<div class="o_login_brand_footer text-center mt-3">
|
||||||
<p>Powered by <a href="https://dine360.com/" target="_blank">Dine360 Inc</a></p>
|
<p>Powered by <a href="https://dine360.com/" target="_blank">Dine360</a> Cloud ERP</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
</xpath>
|
</xpath>
|
||||||
</template>
|
</template>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
193
addons/dine360_dashboard/views/orders_page.xml
Normal file
193
addons/dine360_dashboard/views/orders_page.xml
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<template id="orders_page_template" name="Dine360 Online Orders Page">
|
||||||
|
<t t-call="dine360_dashboard.dashboard_layout">
|
||||||
|
<t t-set="active_page" t-value="'orders'"/>
|
||||||
|
|
||||||
|
<!-- Page Title & Header -->
|
||||||
|
<section class="o_page_header">
|
||||||
|
<div>
|
||||||
|
<h1 class="o_heading_title">Online Orders</h1>
|
||||||
|
<p class="o_heading_subtitle">Live customer online orders from website storefront and delivery channels</p>
|
||||||
|
</div>
|
||||||
|
<div class="o_header_controls">
|
||||||
|
<div class="o_status_tag">
|
||||||
|
<span class="o_green_dot"/>
|
||||||
|
<span>Storefront: Live & Accepting Orders</span>
|
||||||
|
</div>
|
||||||
|
<a href="/shop" target="_blank" class="o_btn_red_action">
|
||||||
|
<i class="fa fa-shopping-bag me-1"/> Open Storefront
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Row 1: KPI Cards -->
|
||||||
|
<section class="o_kpi_grid mb-4">
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-red-light text-danger">
|
||||||
|
<i class="fa fa-clock-o fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-danger">Action Needed</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Pending Orders</div>
|
||||||
|
<div class="o_kpi_number"><t t-esc="online_orders_data.get('pending_count', 0)"/></div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Requires kitchen accept</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-green-light text-success">
|
||||||
|
<i class="fa fa-check fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-success">Confirmed</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Confirmed Orders</div>
|
||||||
|
<div class="o_kpi_number"><t t-esc="online_orders_data.get('confirmed_count', 0)"/></div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Accepted & processing</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-amber-light text-warning">
|
||||||
|
<i class="fa fa-cutlery fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-warning">Cooking</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">In Kitchen</div>
|
||||||
|
<div class="o_kpi_number"><t t-esc="online_orders_data.get('today_count', 0)"/></div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Preparing on station</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-slate-light text-primary">
|
||||||
|
<i class="fa fa-money fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-primary">Sales</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Total Revenue</div>
|
||||||
|
<div class="o_kpi_number"><t t-esc="online_orders_data.get('total_revenue_formatted', '$0.00')"/></div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Online sales today</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Row 2: Filter Toolbar -->
|
||||||
|
<div class="o_page_toolbar">
|
||||||
|
<div class="o_filter_pills">
|
||||||
|
<div class="o_filter_pill active" data-filter="all">All Orders (<t t-esc="len(online_orders_data.get('orders', []))"/>)</div>
|
||||||
|
<div class="o_filter_pill" data-filter="pending">Pending</div>
|
||||||
|
<div class="o_filter_pill" data-filter="confirmed">Confirmed</div>
|
||||||
|
<div class="o_filter_pill" data-filter="cooking">Cooking</div>
|
||||||
|
<div class="o_filter_pill" data-filter="delivered">Delivered</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_toolbar_search">
|
||||||
|
<i class="fa fa-search text-muted"/>
|
||||||
|
<input type="text" class="o_toolbar_search_input" placeholder="Search order number or customer..."/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 3: Orders Table -->
|
||||||
|
<div class="o_modern_table_card mb-4">
|
||||||
|
<div class="o_card_header">
|
||||||
|
<h3 class="o_card_title mb-0">Live Customer Order Stream</h3>
|
||||||
|
<a href="/web#action=sale.action_quotations_with_onboarding" class="o_btn_white_small">
|
||||||
|
View All in Backend
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="o_modern_table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Order Ref</th>
|
||||||
|
<th>Customer Info</th>
|
||||||
|
<th>Time Placed</th>
|
||||||
|
<th>Order Amount</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th class="text-end">Kitchen Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<t t-if="not online_orders_data.get('orders')">
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center py-5 text-muted">
|
||||||
|
<i class="fa fa-shopping-bag fa-3x mb-3 text-secondary"/>
|
||||||
|
<h5>No online orders yet today</h5>
|
||||||
|
<p class="small">Customer website orders will appear here in real time.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-foreach="online_orders_data.get('orders', [])" t-as="order">
|
||||||
|
<tr data-search-item="true" data-filter-item="true" t-att-data-status="order['status']">
|
||||||
|
<td>
|
||||||
|
<span class="font-monospace fw-bold text-dark"><t t-esc="order['name']"/></span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="fw-bold text-dark"><t t-esc="order['customer']"/></div>
|
||||||
|
<div class="small text-muted"><t t-esc="order.get('phone', '')"/></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="small text-muted"><i class="fa fa-clock-o me-1"/><t t-esc="order['date']"/></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="fw-bold font-monospace fs-6 text-dark"><t t-esc="order['amount']"/></span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<t t-if="order['status'] == 'pending'">
|
||||||
|
<span class="o_badge_status warning"><span class="o_badge_dot"/> Pending</span>
|
||||||
|
</t>
|
||||||
|
<t t-elif="order['status'] == 'confirmed'">
|
||||||
|
<span class="o_badge_status success"><span class="o_badge_dot"/> Confirmed</span>
|
||||||
|
</t>
|
||||||
|
<t t-elif="order['status'] == 'cooking'">
|
||||||
|
<span class="o_badge_status info"><span class="o_badge_dot"/> Cooking</span>
|
||||||
|
</t>
|
||||||
|
<t t-elif="order['status'] == 'delivered'">
|
||||||
|
<span class="o_badge_status secondary"><span class="o_badge_dot"/> Delivered</span>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<span class="o_badge_status secondary"><t t-esc="order['status']"/></span>
|
||||||
|
</t>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="d-inline-flex gap-1">
|
||||||
|
<t t-if="order['status'] == 'pending'">
|
||||||
|
<button type="button" class="btn btn-sm btn-success o_btn_update_order" t-att-data-order-id="order['id']" data-status="confirmed">
|
||||||
|
<i class="fa fa-check"/> Accept
|
||||||
|
</button>
|
||||||
|
</t>
|
||||||
|
<t t-elif="order['status'] == 'confirmed'">
|
||||||
|
<button type="button" class="btn btn-sm btn-primary o_btn_update_order" t-att-data-order-id="order['id']" data-status="cooking">
|
||||||
|
<i class="fa fa-fire"/> Cook
|
||||||
|
</button>
|
||||||
|
</t>
|
||||||
|
<t t-elif="order['status'] == 'cooking'">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-success o_btn_update_order" t-att-data-order-id="order['id']" data-status="delivered">
|
||||||
|
<i class="fa fa-motorcycle"/> Ready
|
||||||
|
</button>
|
||||||
|
</t>
|
||||||
|
<a t-att-href="order['url']" class="btn btn-sm btn-light border text-muted" title="View Order">
|
||||||
|
<i class="fa fa-external-link"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</t>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
136
addons/dine360_dashboard/views/plans_page.xml
Normal file
136
addons/dine360_dashboard/views/plans_page.xml
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<template id="plans_page_template" name="Dine360 Plans Page">
|
||||||
|
<t t-call="dine360_dashboard.dashboard_layout">
|
||||||
|
<t t-set="active_page" t-value="'restaurants'"/>
|
||||||
|
|
||||||
|
<!-- Page Title & Header -->
|
||||||
|
<section class="o_page_header">
|
||||||
|
<div>
|
||||||
|
<h1 class="o_heading_title">Subscription Plans & Pricing</h1>
|
||||||
|
<p class="o_heading_subtitle">Manage SaaS pricing tiers, restaurant resource quotas, and billing packages</p>
|
||||||
|
</div>
|
||||||
|
<div class="o_header_controls">
|
||||||
|
<a href="/dashboard/restaurants" class="o_btn_white_small py-2 px-3">
|
||||||
|
<i class="fa fa-arrow-left me-1"/> Back to Restaurants
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Pricing Cards Grid -->
|
||||||
|
<div class="o_plans_grid">
|
||||||
|
<!-- Plan 1: Starter -->
|
||||||
|
<div class="o_plan_card">
|
||||||
|
<div class="o_plan_name">Starter</div>
|
||||||
|
<div class="o_plan_desc">Ideal for small cafes, takeaway counters, and food trucks.</div>
|
||||||
|
|
||||||
|
<div class="o_plan_price_box">
|
||||||
|
<span class="o_plan_price">$49</span>
|
||||||
|
<span class="o_plan_period">/ month</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="o_plan_features">
|
||||||
|
<li class="o_plan_feature_item">
|
||||||
|
<i class="fa fa-check-circle o_plan_feature_icon"/> 1 POS Terminal
|
||||||
|
</li>
|
||||||
|
<li class="o_plan_feature_item">
|
||||||
|
<i class="fa fa-check-circle o_plan_feature_icon"/> Up to 5 Active Staff Users
|
||||||
|
</li>
|
||||||
|
<li class="o_plan_feature_item">
|
||||||
|
<i class="fa fa-check-circle o_plan_feature_icon"/> Table Floor Management
|
||||||
|
</li>
|
||||||
|
<li class="o_plan_feature_item">
|
||||||
|
<i class="fa fa-check-circle o_plan_feature_icon"/> Basic Reservations
|
||||||
|
</li>
|
||||||
|
<li class="o_plan_feature_item">
|
||||||
|
<i class="fa fa-check-circle o_plan_feature_icon"/> Dedicated Database
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-auto">
|
||||||
|
<a href="/web#action=dine360_saas_master.action_saas_plan" class="o_btn_white_small w-100 text-center py-2">
|
||||||
|
Configure Plan
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Plan 2: Professional (Popular) -->
|
||||||
|
<div class="o_plan_card popular">
|
||||||
|
<span class="o_plan_popular_badge">Most Popular</span>
|
||||||
|
<div class="o_plan_name text-danger">Professional</div>
|
||||||
|
<div class="o_plan_desc">For busy dine-in restaurants, bistros, and high-volume dining rooms.</div>
|
||||||
|
|
||||||
|
<div class="o_plan_price_box">
|
||||||
|
<span class="o_plan_price">$149</span>
|
||||||
|
<span class="o_plan_period">/ month</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="o_plan_features">
|
||||||
|
<li class="o_plan_feature_item">
|
||||||
|
<i class="fa fa-check-circle o_plan_feature_icon"/> 5 POS Terminals
|
||||||
|
</li>
|
||||||
|
<li class="o_plan_feature_item">
|
||||||
|
<i class="fa fa-check-circle o_plan_feature_icon"/> Up to 25 Staff Users
|
||||||
|
</li>
|
||||||
|
<li class="o_plan_feature_item">
|
||||||
|
<i class="fa fa-check-circle o_plan_feature_icon"/> Kitchen Display System (KDS)
|
||||||
|
</li>
|
||||||
|
<li class="o_plan_feature_item">
|
||||||
|
<i class="fa fa-check-circle o_plan_feature_icon"/> WhatsApp Booking Confirmations
|
||||||
|
</li>
|
||||||
|
<li class="o_plan_feature_item">
|
||||||
|
<i class="fa fa-check-circle o_plan_feature_icon"/> Online Ordering Storefront
|
||||||
|
</li>
|
||||||
|
<li class="o_plan_feature_item">
|
||||||
|
<i class="fa fa-check-circle o_plan_feature_icon"/> Uber Direct Delivery Integration
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-auto">
|
||||||
|
<a href="/web#action=dine360_saas_master.action_saas_plan" class="o_btn_red_action w-100 text-center py-2 justify-content-center">
|
||||||
|
Configure Plan
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Plan 3: Enterprise -->
|
||||||
|
<div class="o_plan_card">
|
||||||
|
<div class="o_plan_name">Enterprise</div>
|
||||||
|
<div class="o_plan_desc">For restaurant chains, multiple branch groups, and franchises.</div>
|
||||||
|
|
||||||
|
<div class="o_plan_price_box">
|
||||||
|
<span class="o_plan_price">$299</span>
|
||||||
|
<span class="o_plan_period">/ month</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="o_plan_features">
|
||||||
|
<li class="o_plan_feature_item">
|
||||||
|
<i class="fa fa-check-circle o_plan_feature_icon"/> Unlimited POS Terminals
|
||||||
|
</li>
|
||||||
|
<li class="o_plan_feature_item">
|
||||||
|
<i class="fa fa-check-circle o_plan_feature_icon"/> Unlimited Staff Accounts
|
||||||
|
</li>
|
||||||
|
<li class="o_plan_feature_item">
|
||||||
|
<i class="fa fa-check-circle o_plan_feature_icon"/> Recipe & Ingredient Costing
|
||||||
|
</li>
|
||||||
|
<li class="o_plan_feature_item">
|
||||||
|
<i class="fa fa-check-circle o_plan_feature_icon"/> Multi-branch Central Reporting
|
||||||
|
</li>
|
||||||
|
<li class="o_plan_feature_item">
|
||||||
|
<i class="fa fa-check-circle o_plan_feature_icon"/> Dedicated High-Performance Server
|
||||||
|
</li>
|
||||||
|
<li class="o_plan_feature_item">
|
||||||
|
<i class="fa fa-check-circle o_plan_feature_icon"/> 24/7 Priority SLA Support
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-auto">
|
||||||
|
<a href="/web#action=dine360_saas_master.action_saas_plan" class="o_btn_white_small w-100 text-center py-2">
|
||||||
|
Configure Plan
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
364
addons/dine360_dashboard/views/reservations_page.xml
Normal file
364
addons/dine360_dashboard/views/reservations_page.xml
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<template id="reservations_page_template" name="Dine360 Reservations Page">
|
||||||
|
<t t-call="dine360_dashboard.dashboard_layout">
|
||||||
|
<t t-set="active_page" t-value="'reservations'"/>
|
||||||
|
|
||||||
|
<!-- Page Title & Header -->
|
||||||
|
<section class="o_page_header">
|
||||||
|
<div>
|
||||||
|
<h1 class="o_heading_title">Table Reservations</h1>
|
||||||
|
<p class="o_heading_subtitle">Real-time guest bookings, seating schedule, and WhatsApp guest notifications</p>
|
||||||
|
</div>
|
||||||
|
<div class="o_header_controls">
|
||||||
|
<div class="o_status_tag">
|
||||||
|
<span class="o_green_dot"/>
|
||||||
|
<span>Online Reservations: Active</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_date_picker_btn">
|
||||||
|
<i class="fa fa-calendar me-2 text-muted"/>
|
||||||
|
<span><t t-esc="current_date_str or 'Today'"/></span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="o_btn_red_action" data-open-modal="modalNewReservation">
|
||||||
|
<i class="fa fa-plus me-1"/> New Reservation
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Row 1: KPI Summary Cards -->
|
||||||
|
<section class="o_kpi_grid mb-4">
|
||||||
|
<!-- KPI 1: Total Reservations -->
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-red-light text-danger">
|
||||||
|
<i class="fa fa-calendar fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-danger"><i class="fa fa-arrow-up"/> Live</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Total Bookings</div>
|
||||||
|
<div class="o_kpi_number"><t t-esc="reservation_stats['total']"/></div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">All scheduled guests</span>
|
||||||
|
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
|
||||||
|
<path d="M0 25 Q20 15 40 22 T80 10 T100 5" fill="none" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPI 2: Confirmed -->
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-green-light text-success">
|
||||||
|
<i class="fa fa-check-circle fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-success"><i class="fa fa-arrow-up"/> Confirmed</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Confirmed Bookings</div>
|
||||||
|
<div class="o_kpi_number"><t t-esc="reservation_stats['confirmed']"/></div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Ready for seating</span>
|
||||||
|
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
|
||||||
|
<path d="M0 20 Q30 25 50 15 T80 18 T100 8" fill="none" stroke="#16a34a" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPI 3: Pending / Draft -->
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-amber-light text-warning">
|
||||||
|
<i class="fa fa-clock-o fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-warning">Pending</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Draft / Pending</div>
|
||||||
|
<div class="o_kpi_number"><t t-esc="reservation_stats['draft']"/></div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Requires confirmation</span>
|
||||||
|
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
|
||||||
|
<path d="M0 15 Q25 22 50 18 T80 25 T100 12" fill="none" stroke="#f59e0b" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPI 4: Seated / Completed -->
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-slate-light text-primary">
|
||||||
|
<i class="fa fa-cutlery fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-primary">Completed</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Seated / Done</div>
|
||||||
|
<div class="o_kpi_number"><t t-esc="reservation_stats['completed']"/></div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Guests dined</span>
|
||||||
|
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
|
||||||
|
<path d="M0 25 Q35 20 60 15 T100 10" fill="none" stroke="#3b82f6" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Row 2: Toolbar (Filters & Search) -->
|
||||||
|
<div class="o_page_toolbar">
|
||||||
|
<div class="o_filter_pills">
|
||||||
|
<div class="o_filter_pill active" data-filter="all">All Bookings (<t t-esc="reservation_stats['total']"/>)</div>
|
||||||
|
<div class="o_filter_pill" data-filter="confirmed">Confirmed (<t t-esc="reservation_stats['confirmed']"/>)</div>
|
||||||
|
<div class="o_filter_pill" data-filter="draft">Draft / Pending (<t t-esc="reservation_stats['draft']"/>)</div>
|
||||||
|
<div class="o_filter_pill" data-filter="completed">Completed (<t t-esc="reservation_stats['completed']"/>)</div>
|
||||||
|
<div class="o_filter_pill" data-filter="cancelled">Cancelled (<t t-esc="reservation_stats['cancelled']"/>)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_toolbar_search">
|
||||||
|
<i class="fa fa-search text-muted"/>
|
||||||
|
<input type="text" class="o_toolbar_search_input" placeholder="Search guest name, phone, or reservation #..."/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 3: Reservations Data Table -->
|
||||||
|
<div class="o_modern_table_card mb-4">
|
||||||
|
<div class="o_card_header">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<h3 class="o_card_title mb-0">Live Guest Bookings List</h3>
|
||||||
|
<span class="o_count_pill"><t t-esc="reservation_stats['total']"/> Reservations</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<button type="button" class="o_btn_red_small" data-open-modal="modalNewReservation">
|
||||||
|
<i class="fa fa-plus me-1"/> New Reservation
|
||||||
|
</button>
|
||||||
|
<a href="/web#action=dine360_reservation.action_restaurant_reservation" class="o_btn_white_small">
|
||||||
|
Open in Backend
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="o_modern_table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Booking Ref</th>
|
||||||
|
<th>Guest Details</th>
|
||||||
|
<th>Date & Time Slot</th>
|
||||||
|
<th>Party & Table</th>
|
||||||
|
<th>Occasion / Notes</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th class="text-end">Quick Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<t t-if="not reservations">
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="text-center py-5 text-muted">
|
||||||
|
<i class="fa fa-calendar-o fa-3x mb-3 text-secondary"/>
|
||||||
|
<h5>No reservations found</h5>
|
||||||
|
<p class="small">Click "New Reservation" to record a table booking.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-foreach="reservations" t-as="res">
|
||||||
|
<tr data-search-item="true" data-filter-item="true" t-att-data-status="res['state']">
|
||||||
|
<!-- Booking Ref -->
|
||||||
|
<td>
|
||||||
|
<span class="font-monospace fw-bold text-dark"><t t-esc="res['name']"/></span>
|
||||||
|
<t t-if="res.get('is_admin_override')">
|
||||||
|
<span class="badge bg-danger ms-1 small" title="Admin Override">Override</span>
|
||||||
|
</t>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Guest Details -->
|
||||||
|
<td>
|
||||||
|
<div class="fw-bold text-dark"><t t-esc="res['customer_name']"/></div>
|
||||||
|
<div class="small text-muted d-flex align-items-center gap-2 mt-1">
|
||||||
|
<t t-if="res.get('phone')">
|
||||||
|
<span><i class="fa fa-phone text-muted me-1"/><t t-esc="res['phone']"/></span>
|
||||||
|
</t>
|
||||||
|
<t t-if="res.get('whatsapp_url')">
|
||||||
|
<a t-att-href="res['whatsapp_url']" target="_blank" class="text-success fw-bold text-decoration-none" title="Send WhatsApp Message">
|
||||||
|
<i class="fa fa-whatsapp text-success"/> WhatsApp
|
||||||
|
</a>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Date & Time Slot -->
|
||||||
|
<td>
|
||||||
|
<div class="fw-semibold text-dark"><i class="fa fa-clock-o text-muted me-1"/><t t-esc="res['start_time_fmt']"/></div>
|
||||||
|
<div class="small text-muted"><t t-esc="res['date_fmt']"/></div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Party & Table -->
|
||||||
|
<td>
|
||||||
|
<div class="badge bg-light text-dark border">
|
||||||
|
<i class="fa fa-users text-danger me-1"/> <t t-esc="res['num_people']"/> Guests
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted mt-1">
|
||||||
|
<i class="fa fa-th text-muted me-1"/>
|
||||||
|
<t t-if="res.get('table_name')"><t t-esc="res['table_name']"/></t>
|
||||||
|
<t t-else="">Any Available Table</t>
|
||||||
|
<t t-if="res.get('floor_name')"> (<t t-esc="res['floor_name']"/>)</t>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Occasion / Notes -->
|
||||||
|
<td>
|
||||||
|
<t t-if="res.get('occasion')">
|
||||||
|
<span class="badge bg-info-subtle text-primary border border-info-subtle mb-1"><t t-esc="res['occasion'].title()"/></span>
|
||||||
|
</t>
|
||||||
|
<div class="small text-muted text-truncate" style="max-width:180px;" t-att-title="res.get('special_requests', '')">
|
||||||
|
<t t-esc="res.get('special_requests') or 'No special requests'"/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Status Badge -->
|
||||||
|
<td>
|
||||||
|
<t t-if="res['state'] == 'confirmed'">
|
||||||
|
<span class="o_badge_status success">
|
||||||
|
<span class="o_badge_dot"/> Confirmed
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
<t t-elif="res['state'] == 'draft'">
|
||||||
|
<span class="o_badge_status warning">
|
||||||
|
<span class="o_badge_dot"/> Draft
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
<t t-elif="res['state'] == 'completed'">
|
||||||
|
<span class="o_badge_status info">
|
||||||
|
<span class="o_badge_dot"/> Completed
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
<t t-elif="res['state'] == 'cancelled'">
|
||||||
|
<span class="o_badge_status danger">
|
||||||
|
<span class="o_badge_dot"/> Cancelled
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<span class="o_badge_status secondary">
|
||||||
|
<t t-esc="res['state']"/>
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="d-inline-flex gap-1">
|
||||||
|
<t t-if="res['state'] == 'draft'">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-success o_btn_update_reservation" t-att-data-res-id="res['id']" data-status="confirmed" title="Confirm Reservation">
|
||||||
|
<i class="fa fa-check"/> Confirm
|
||||||
|
</button>
|
||||||
|
</t>
|
||||||
|
<t t-if="res['state'] == 'confirmed'">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary o_btn_update_reservation" t-att-data-res-id="res['id']" data-status="completed" title="Seat & Complete">
|
||||||
|
<i class="fa fa-cutlery"/> Seat
|
||||||
|
</button>
|
||||||
|
</t>
|
||||||
|
<t t-if="res['state'] not in ('cancelled', 'completed')">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger o_btn_update_reservation" t-att-data-res-id="res['id']" data-status="cancelled" title="Cancel Booking">
|
||||||
|
<i class="fa fa-times"/>
|
||||||
|
</button>
|
||||||
|
</t>
|
||||||
|
<a t-att-href="'/web#id=%s&model=restaurant.reservation&view_type=form' % res['id']" class="btn btn-sm btn-light border text-muted" title="View Record">
|
||||||
|
<i class="fa fa-external-link"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</t>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL: Create New Table Reservation -->
|
||||||
|
<div id="modalNewReservation" class="o_dine_modal">
|
||||||
|
<div class="o_modal_backdrop"/>
|
||||||
|
<div class="o_modal_dialog">
|
||||||
|
<div class="o_modal_header">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<div class="o_settings_icon" style="width:34px;height:34px;font-size:15px;">
|
||||||
|
<i class="fa fa-calendar"/>
|
||||||
|
</div>
|
||||||
|
<h4 class="o_modal_title">Create Table Reservation</h4>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="o_modal_close_btn o_modal_close">&times;</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="formNewReservation">
|
||||||
|
<div class="o_modal_body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Guest Name *</label>
|
||||||
|
<input type="text" name="customer_name" required="required" placeholder="Guest Full Name" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Phone Number *</label>
|
||||||
|
<input type="tel" name="phone" required="required" placeholder="+91 98765 43210" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Email Address</label>
|
||||||
|
<input type="email" name="email" placeholder="guest@example.com" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Number of People *</label>
|
||||||
|
<input type="number" name="num_people" required="required" min="1" max="50" value="2" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Reservation Date & Time *</label>
|
||||||
|
<input type="datetime-local" name="start_time" required="required" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Dining Occasion</label>
|
||||||
|
<select name="occasion" class="o_form_control">
|
||||||
|
<option value="casual">Casual Dining</option>
|
||||||
|
<option value="birthday">Birthday Celebration</option>
|
||||||
|
<option value="anniversary">Anniversary</option>
|
||||||
|
<option value="business">Business Meal</option>
|
||||||
|
<option value="date">Date Night</option>
|
||||||
|
<option value="other">Other Occasion</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Preferred Table</label>
|
||||||
|
<select name="table_id" class="o_form_control">
|
||||||
|
<option value="">-- Any Available Table --</option>
|
||||||
|
<t t-foreach="available_tables" t-as="table">
|
||||||
|
<option t-att-value="table['id']">
|
||||||
|
<t t-esc="table['name']"/> (<t t-esc="table.get('seats', 4)"/> Seats - <t t-esc="table.get('floor_name', 'Main')"/>)
|
||||||
|
</option>
|
||||||
|
</t>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Preferred Zone / Area</label>
|
||||||
|
<input type="text" name="preferred_zone" placeholder="Window Seat, Terrace, AC..." class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_form_group">
|
||||||
|
<label class="o_form_label">Special Requests / Dietary Notes</label>
|
||||||
|
<textarea name="special_requests" rows="2" placeholder="Vegetarian, High chair for child, quiet corner..." class="o_form_control"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_modal_footer">
|
||||||
|
<button type="button" class="btn btn-light o_modal_close">Cancel</button>
|
||||||
|
<button type="submit" class="o_btn_red_action">
|
||||||
|
<i class="fa fa-check me-1"/> Confirm Booking & Notify Guest
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
321
addons/dine360_dashboard/views/restaurants_page.xml
Normal file
321
addons/dine360_dashboard/views/restaurants_page.xml
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<template id="restaurants_page_template" name="Dine360 Restaurants Page">
|
||||||
|
<t t-call="dine360_dashboard.dashboard_layout">
|
||||||
|
<t t-set="active_page" t-value="'restaurants'"/>
|
||||||
|
|
||||||
|
<!-- Page Title & Action Header -->
|
||||||
|
<section class="o_page_header">
|
||||||
|
<div>
|
||||||
|
<h1 class="o_heading_title">Restaurants</h1>
|
||||||
|
<p class="o_heading_subtitle">Manage your restaurant ecosystem, instances, and tenant deployment status</p>
|
||||||
|
</div>
|
||||||
|
<div class="o_header_controls">
|
||||||
|
<div class="o_status_tag">
|
||||||
|
<span class="o_green_dot"/>
|
||||||
|
<span>System Status: All systems operational</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_date_picker_btn">
|
||||||
|
<i class="fa fa-calendar me-2 text-muted"/>
|
||||||
|
<span><t t-esc="saas_dashboard_data.get('date_range', 'Sep 26, 2026 - Oct 26, 2026')"/></span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="o_btn_red_action" data-open-modal="modalOnboardRestaurant">
|
||||||
|
<i class="fa fa-plus me-1"/> Onboard Restaurant
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Row 1: KPI Summary Metric Cards -->
|
||||||
|
<section class="o_kpi_grid mb-4">
|
||||||
|
<!-- KPI 1: Total Restaurants -->
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-red-light text-danger">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="#e50000">
|
||||||
|
<path d="M21.9 8.2l-1.9-4.7C19.7 2.6 18.8 2 17.8 2H6.2c-1 0-1.9.6-2.2 1.5L2.1 8.2c-.3.7-.1 1.5.4 2 .4.4 1 .7 1.6.7h.1c.9 0 1.7-.5 2.1-1.3l.9-1.8.9 1.8c.4.8 1.2 1.3 2.1 1.3s1.7-.5 2.1-1.3l.9-1.8.9 1.8c.4.8 1.2 1.3 2.1 1.3h.1c.6 0 1.2-.3 1.6-.7.5-.5.7-1.3.4-2zM4 12.1V20c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-7.9c-.6.3-1.3.4-2 .4-1.2 0-2.3-.6-3-1.6-.7 1-1.8 1.6-3 1.6s-2.3-.6-3-1.6c-.7 1-1.8 1.6-3 1.6-.7 0-1.4-.1-2-.5zM10 20v-5h4v5h-4z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-danger"><i class="fa fa-arrow-up"/> +0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Total Restaurants</div>
|
||||||
|
<div class="o_kpi_number"><t t-esc="saas_stats['total']"/></div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Currently onboarded</span>
|
||||||
|
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
|
||||||
|
<path d="M0 25 Q15 28 30 18 T60 22 T80 12 T100 5" fill="none" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPI 2: Active Subscriptions -->
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-green-light text-success">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="#16a34a">
|
||||||
|
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-success"><i class="fa fa-arrow-up"/> +0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Active Subscriptions</div>
|
||||||
|
<div class="o_kpi_number"><t t-esc="saas_stats['active']"/></div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Running smoothly</span>
|
||||||
|
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
|
||||||
|
<path d="M0 22 Q20 26 40 18 T70 20 T100 8" fill="none" stroke="#16a34a" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPI 3: Provisioning -->
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-amber-light text-warning">
|
||||||
|
<i class="fa fa-clock-o fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-warning"><i class="fa fa-arrow-up"/> +0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Provisioning</div>
|
||||||
|
<div class="o_kpi_number"><t t-esc="saas_stats['provisioning']"/></div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">In progress</span>
|
||||||
|
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
|
||||||
|
<path d="M0 20 Q30 15 50 25 T80 18 T100 12" fill="none" stroke="#f59e0b" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPI 4: Expired / Suspended -->
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-slate-light text-secondary">
|
||||||
|
<i class="fa fa-exclamation-circle fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-secondary"><i class="fa fa-arrow-up"/> +0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Expired / Suspended</div>
|
||||||
|
<div class="o_kpi_number"><t t-esc="saas_stats['expired']"/></div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Requires attention</span>
|
||||||
|
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
|
||||||
|
<path d="M0 25 Q30 25 60 25 T100 20" fill="none" stroke="#94a3b8" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Row 2: Toolbar (Filters & Search) -->
|
||||||
|
<div class="o_page_toolbar">
|
||||||
|
<div class="o_filter_pills">
|
||||||
|
<div class="o_filter_pill active" data-filter="all">All Restaurants (<t t-esc="saas_stats['total']"/>)</div>
|
||||||
|
<div class="o_filter_pill" data-filter="active">Active (<t t-esc="saas_stats['active']"/>)</div>
|
||||||
|
<div class="o_filter_pill" data-filter="provisioning">Provisioning (<t t-esc="saas_stats['provisioning']"/>)</div>
|
||||||
|
<div class="o_filter_pill" data-filter="expired">Expired / Inactive (<t t-esc="saas_stats['expired']"/>)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_toolbar_search">
|
||||||
|
<i class="fa fa-search text-muted"/>
|
||||||
|
<input type="text" class="o_toolbar_search_input" placeholder="Search by name, database, or owner..."/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 3: Onboarded Restaurants List -->
|
||||||
|
<div class="o_widget_card o_card_restaurants_table mb-4">
|
||||||
|
<div class="o_card_header">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<h3 class="o_card_title mb-0">Onboarded Restaurant Databases</h3>
|
||||||
|
<span class="o_count_pill"><t t-esc="saas_stats['total']"/> Registered</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<button type="button" class="o_btn_red_small" data-open-modal="modalOnboardRestaurant">
|
||||||
|
<i class="fa fa-plus me-1"/> Onboard Restaurant
|
||||||
|
</button>
|
||||||
|
<a href="/web#action=dine360_saas_master.action_saas_restaurant" class="o_btn_white_small">
|
||||||
|
Open in Backend
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_restaurants_list">
|
||||||
|
<t t-if="not onboarded_restaurants">
|
||||||
|
<div class="text-center py-5 text-muted">
|
||||||
|
<i class="fa fa-cutlery fa-3x mb-3 text-secondary"/>
|
||||||
|
<h5>No restaurant tenants found</h5>
|
||||||
|
<p class="small">Click "Onboard Restaurant" to deploy your first tenant database.</p>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-foreach="onboarded_restaurants" t-as="restaurant">
|
||||||
|
<div class="o_res_row_card" data-search-item="true" data-filter-item="true" t-att-data-status="restaurant['status']">
|
||||||
|
<!-- Col 1: Logo & Name -->
|
||||||
|
<div class="o_res_col_name">
|
||||||
|
<div class="o_res_black_avatar">
|
||||||
|
<svg width="20" height="15" viewBox="0 0 36 28" fill="none" style="margin-top: 2px;">
|
||||||
|
<path d="M18 2C14.5 2 12 4.5 12 7.5C10 7.8 8.5 9.5 8.5 11.5C8.5 13.2 9.6 14.6 11.2 15.1C11.1 15.5 11 16 11 16.5C11 19.5 13.5 22 16.5 22H19.5C22.5 22 25 19.5 25 16.5C25 16 24.9 15.5 24.8 15.1C26.4 14.6 27.5 13.2 27.5 11.5C27.5 9.5 26 7.8 24 7.5C24 4.5 21.5 2 18 2Z" fill="none" stroke="#e50000" stroke-width="2.5"/>
|
||||||
|
<path d="M12 22V25C12 26 13 27 14 27H22C23 27 24 26 24 25V22" stroke="#e50000" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
<span class="o_res_avatar_letter"><t t-esc="restaurant['name'][0].upper() if restaurant['name'] else 'R'"/></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="o_res_title"><t t-esc="restaurant['name']"/></div>
|
||||||
|
<div class="o_res_plan_sub"><t t-esc="restaurant['plan_name']"/> • <t t-esc="restaurant['billing_cycle']"/></div>
|
||||||
|
<t t-if="restaurant['status'] == 'active'">
|
||||||
|
<span class="o_res_pill_active">
|
||||||
|
<span class="o_pill_dot"/> Active
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
<t t-elif="restaurant['status'] == 'provisioning'">
|
||||||
|
<span class="o_res_pill_active" style="background:#fffbeb; color:#b45309; border-color:#fde68a;">
|
||||||
|
<span class="o_pill_dot" style="background:#f59e0b;"/> Provisioning
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<span class="o_res_pill_active" style="background:#fef2f2; color:#b91c1c; border-color:#fecaca;">
|
||||||
|
<span class="o_pill_dot" style="background:#ef4444;"/> <t t-esc="restaurant['status']"/>
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Col 2: Owner & Phone & Subdomain -->
|
||||||
|
<div class="o_res_col_info">
|
||||||
|
<div class="o_info_line">
|
||||||
|
<i class="fa fa-user-o o_info_icon text-muted"/>
|
||||||
|
<div>
|
||||||
|
<div class="o_info_label">Owner</div>
|
||||||
|
<div class="o_info_val"><t t-esc="restaurant['owner_name']"/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="o_info_line">
|
||||||
|
<i class="fa fa-phone o_info_icon text-muted"/>
|
||||||
|
<div>
|
||||||
|
<div class="o_info_label">Phone</div>
|
||||||
|
<div class="o_info_val"><t t-esc="restaurant['phone']"/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="o_info_line">
|
||||||
|
<i class="fa fa-globe o_info_icon text-muted"/>
|
||||||
|
<div>
|
||||||
|
<div class="o_info_label">Subdomain</div>
|
||||||
|
<div class="o_info_val text-danger"><t t-esc="restaurant['subdomain']"/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Col 3: Email & Database & Expires -->
|
||||||
|
<div class="o_res_col_info">
|
||||||
|
<div class="o_info_line">
|
||||||
|
<i class="fa fa-envelope-o o_info_icon text-muted"/>
|
||||||
|
<div>
|
||||||
|
<div class="o_info_label">Email</div>
|
||||||
|
<div class="o_info_val"><t t-esc="restaurant['email']"/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="o_info_line">
|
||||||
|
<i class="fa fa-database o_info_icon text-muted"/>
|
||||||
|
<div>
|
||||||
|
<div class="o_info_label">Database</div>
|
||||||
|
<div class="o_info_val font-monospace"><t t-esc="restaurant['database_name']"/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="o_info_line">
|
||||||
|
<i class="fa fa-calendar-o o_info_icon text-muted"/>
|
||||||
|
<div>
|
||||||
|
<div class="o_info_label">Expires</div>
|
||||||
|
<div class="o_info_val"><t t-esc="restaurant['expiry_date']"/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Col 4: Action Buttons -->
|
||||||
|
<div class="o_res_col_actions">
|
||||||
|
<a t-att-href="restaurant['launch_url']" target="_blank" class="o_btn_launch_red">
|
||||||
|
<i class="fa fa-rocket me-1"/> Launch DB
|
||||||
|
</a>
|
||||||
|
<a t-att-href="restaurant['manage_url']" class="o_btn_manage_white">
|
||||||
|
<i class="fa fa-cog me-1"/> Manage
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL: Onboard New Restaurant -->
|
||||||
|
<div id="modalOnboardRestaurant" class="o_dine_modal">
|
||||||
|
<div class="o_modal_backdrop"/>
|
||||||
|
<div class="o_modal_dialog">
|
||||||
|
<div class="o_modal_header">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<div class="o_settings_icon" style="width:34px;height:34px;font-size:15px;">
|
||||||
|
<i class="fa fa-cutlery"/>
|
||||||
|
</div>
|
||||||
|
<h4 class="o_modal_title">Onboard New Restaurant</h4>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="o_modal_close_btn o_modal_close">&times;</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="formOnboardRestaurant">
|
||||||
|
<div class="o_modal_body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Restaurant Name *</label>
|
||||||
|
<input type="text" name="name" required="required" placeholder="e.g. Royal Bistro" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Owner Full Name *</label>
|
||||||
|
<input type="text" name="owner_name" required="required" placeholder="e.g. John Doe" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Owner Email *</label>
|
||||||
|
<input type="email" name="email" required="required" placeholder="owner@restaurant.com" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Contact Phone *</label>
|
||||||
|
<input type="tel" name="phone" required="required" placeholder="+91 98765 43210" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Subscription Plan *</label>
|
||||||
|
<select name="plan_id" class="o_form_control" required="required">
|
||||||
|
<t t-foreach="plans" t-as="plan">
|
||||||
|
<option t-att-value="plan['id']"><t t-esc="plan['name']"/> ($<t t-esc="plan['price_monthly']"/>/mo)</option>
|
||||||
|
</t>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Billing Cycle *</label>
|
||||||
|
<select name="billing_cycle" class="o_form_control">
|
||||||
|
<option value="monthly">Monthly Billing</option>
|
||||||
|
<option value="yearly">Annual Billing (Save 20%)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_form_group">
|
||||||
|
<label class="o_form_label">Tenant Subdomain *</label>
|
||||||
|
<div class="d-flex align-items-center gap-1">
|
||||||
|
<input type="text" name="subdomain" required="required" placeholder="royalbistro" class="o_form_control font-monospace"/>
|
||||||
|
<span class="text-muted small fw-bold">.dine360.com</span>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">A dedicated PostgreSQL database and workspace will be provisioned automatically.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_modal_footer">
|
||||||
|
<button type="button" class="btn btn-light o_modal_close">Cancel</button>
|
||||||
|
<button type="submit" class="o_btn_red_action">
|
||||||
|
<i class="fa fa-rocket me-1"/> Provision & Launch Database
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
238
addons/dine360_dashboard/views/settings_page.xml
Normal file
238
addons/dine360_dashboard/views/settings_page.xml
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<template id="settings_page_template" name="Dine360 Settings Page">
|
||||||
|
<t t-call="dine360_dashboard.dashboard_layout">
|
||||||
|
<t t-set="active_page" t-value="'settings'"/>
|
||||||
|
|
||||||
|
<!-- Page Title & Header -->
|
||||||
|
<section class="o_page_header">
|
||||||
|
<div>
|
||||||
|
<h1 class="o_heading_title">Settings</h1>
|
||||||
|
<p class="o_heading_subtitle">Configure platform branding, operational rules, notifications, and delivery integrations</p>
|
||||||
|
</div>
|
||||||
|
<div class="o_header_controls">
|
||||||
|
<div class="o_status_tag">
|
||||||
|
<span class="o_green_dot"/>
|
||||||
|
<span>Configuration Active</span>
|
||||||
|
</div>
|
||||||
|
<a href="/web#action=base.res_config_setting_act_window" class="o_btn_white_small py-2 px-3">
|
||||||
|
<i class="fa fa-sliders me-1"/> Odoo ERP Settings
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Settings Form -->
|
||||||
|
<form id="formSaveSettings" class="mb-5">
|
||||||
|
<div class="o_settings_grid">
|
||||||
|
<!-- Category 1: Platform & Branding -->
|
||||||
|
<div class="o_settings_card">
|
||||||
|
<div class="o_settings_card_top">
|
||||||
|
<div class="o_settings_icon">
|
||||||
|
<i class="fa fa-paint-brush"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="o_settings_title">Platform & Branding</h4>
|
||||||
|
<p class="o_settings_desc">Global platform identity and brand assets</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_form_group">
|
||||||
|
<label class="o_form_label">Platform Title</label>
|
||||||
|
<input type="text" name="platform_title" value="Dine360 Restaurant Platform" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Subdomain Suffix</label>
|
||||||
|
<input type="text" name="subdomain_suffix" value=".dine360.com" class="o_form_control font-monospace"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Currency Symbol</label>
|
||||||
|
<input type="text" name="currency_symbol" value="₹" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_form_group">
|
||||||
|
<label class="o_form_label">Support & Admin Email</label>
|
||||||
|
<input type="email" name="support_email" value="admin@dine360.com" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Category 2: Reservation Rules -->
|
||||||
|
<div class="o_settings_card">
|
||||||
|
<div class="o_settings_card_top">
|
||||||
|
<div class="o_settings_icon" style="background:#dbeafe; color:#2563eb;">
|
||||||
|
<i class="fa fa-calendar-check-o"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="o_settings_title">Reservation Operations</h4>
|
||||||
|
<p class="o_settings_desc">Booking windows, seating durations, and rules</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Default Dining Duration (Mins)</label>
|
||||||
|
<input type="number" name="default_duration" value="90" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Max Online Party Size</label>
|
||||||
|
<input type="number" name="max_party_size" value="12" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_form_group mt-3">
|
||||||
|
<div class="form-check form-switch d-flex align-items-center gap-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="autoConfirmRes" name="auto_confirm_reservation" checked="checked" style="width:2.4em; height:1.2em;"/>
|
||||||
|
<label class="form-check-label fw-bold text-dark small" for="autoConfirmRes">
|
||||||
|
Auto-confirm guest reservations if table is available
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted d-block mt-1">If unchecked, reservations arrive as Draft requiring manager approval.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Category 3: WhatsApp Notifications -->
|
||||||
|
<div class="o_settings_card">
|
||||||
|
<div class="o_settings_card_top">
|
||||||
|
<div class="o_settings_icon" style="background:#dcfce7; color:#16a34a;">
|
||||||
|
<i class="fa fa-whatsapp"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="o_settings_title">WhatsApp Notification Gateway</h4>
|
||||||
|
<p class="o_settings_desc">Automated guest SMS & WhatsApp confirmations</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_form_group">
|
||||||
|
<div class="form-check form-switch d-flex align-items-center gap-2 mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="enableWhatsapp" name="enable_whatsapp" checked="checked" style="width:2.4em; height:1.2em;"/>
|
||||||
|
<label class="form-check-label fw-bold text-dark small" for="enableWhatsapp">
|
||||||
|
Enable WhatsApp Booking Confirmations
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_form_group">
|
||||||
|
<label class="o_form_label">Guest Confirmation Message Template</label>
|
||||||
|
<textarea name="whatsapp_template" rows="3" class="o_form_control">Hello {customer_name}, your table reservation at {restaurant_name} for {time} is confirmed! Reference: {booking_ref}. We look forward to serving you!</textarea>
|
||||||
|
<small class="text-muted">Placeholders: {customer_name}, {restaurant_name}, {time}, {booking_ref}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Category 4: Uber Direct Delivery -->
|
||||||
|
<div class="o_settings_card">
|
||||||
|
<div class="o_settings_card_top">
|
||||||
|
<div class="o_settings_icon" style="background:#f1f5f9; color:#0f172a;">
|
||||||
|
<i class="fa fa-motorcycle"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="o_settings_title">Uber Direct & Delivery</h4>
|
||||||
|
<p class="o_settings_desc">On-demand courier dispatch parameters</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Uber Environment</label>
|
||||||
|
<select name="uber_mode" class="o_form_control">
|
||||||
|
<option value="sandbox">Sandbox (Testing Mode)</option>
|
||||||
|
<option value="production">Production (Live Couriers)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Max Delivery Radius (km)</label>
|
||||||
|
<input type="number" name="max_delivery_radius" value="10" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_form_group">
|
||||||
|
<div class="form-check form-switch d-flex align-items-center gap-2 mt-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="autoDispatchUber" name="auto_dispatch_uber" checked="checked" style="width:2.4em; height:1.2em;"/>
|
||||||
|
<label class="form-check-label fw-bold text-dark small" for="autoDispatchUber">
|
||||||
|
Auto-dispatch Uber courier when kitchen marks order Ready
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Category 5: Automated Backups -->
|
||||||
|
<div class="o_settings_card">
|
||||||
|
<div class="o_settings_card_top">
|
||||||
|
<div class="o_settings_icon" style="background:#fef3c7; color:#d97706;">
|
||||||
|
<i class="fa fa-database"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="o_settings_title">Database Backups & Maintenance</h4>
|
||||||
|
<p class="o_settings_desc">Automated snapshots and retention policy</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Daily Scheduled Time</label>
|
||||||
|
<input type="time" name="backup_time" value="03:00" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Retention (Days)</label>
|
||||||
|
<input type="number" name="backup_retention" value="7" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_form_group">
|
||||||
|
<label class="o_form_label">Backup Storage Directory</label>
|
||||||
|
<input type="text" name="backup_dir" value="/var/lib/odoo/backups" readonly="readonly" class="o_form_control font-monospace bg-light"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Category 6: KOT & Printers -->
|
||||||
|
<div class="o_settings_card">
|
||||||
|
<div class="o_settings_card_top">
|
||||||
|
<div class="o_settings_icon" style="background:#f3e8ff; color:#9333ea;">
|
||||||
|
<i class="fa fa-print"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="o_settings_title">Receipt & KOT Printing</h4>
|
||||||
|
<p class="o_settings_desc">Thermal hardware printer integration</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Thermal Paper Width</label>
|
||||||
|
<select name="paper_width" class="o_form_control">
|
||||||
|
<option value="80">80mm (Standard POS)</option>
|
||||||
|
<option value="58">58mm (Compact Receipt)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Printer Driver</label>
|
||||||
|
<select name="printer_driver" class="o_form_control">
|
||||||
|
<option value="qz">QZ Tray Network Service</option>
|
||||||
|
<option value="epson">Epson ESC/POS Direct</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_form_group">
|
||||||
|
<div class="form-check form-switch d-flex align-items-center gap-2 mt-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="autoPrintKot" name="auto_print_kot" checked="checked" style="width:2.4em; height:1.2em;"/>
|
||||||
|
<label class="form-check-label fw-bold text-dark small" for="autoPrintKot">
|
||||||
|
Auto-print KOT slip upon kitchen order send
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sticky Save Bar -->
|
||||||
|
<div class="d-flex align-items-center justify-content-end gap-3 p-3 bg-white border rounded-3 shadow-sm">
|
||||||
|
<button type="reset" class="btn btn-light px-4">Discard Changes</button>
|
||||||
|
<button type="submit" class="o_btn_red_action px-4 py-2">
|
||||||
|
<i class="fa fa-save me-1"/> Save Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
247
addons/dine360_dashboard/views/tables_page.xml
Normal file
247
addons/dine360_dashboard/views/tables_page.xml
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<template id="tables_page_template" name="Dine360 Table Management Page">
|
||||||
|
<t t-call="dine360_dashboard.dashboard_layout">
|
||||||
|
<t t-set="active_page" t-value="'tables'"/>
|
||||||
|
|
||||||
|
<!-- Page Title & Header -->
|
||||||
|
<section class="o_page_header">
|
||||||
|
<div>
|
||||||
|
<h1 class="o_heading_title">Table & Floor Management</h1>
|
||||||
|
<p class="o_heading_subtitle">Live visual table occupancy, dining floors layout, and guest seating capacity</p>
|
||||||
|
</div>
|
||||||
|
<div class="o_header_controls">
|
||||||
|
<div class="o_status_tag">
|
||||||
|
<span class="o_green_dot"/>
|
||||||
|
<span>Floor Plan Sync: Active</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="o_btn_red_action" data-open-modal="modalAddTable">
|
||||||
|
<i class="fa fa-plus me-1"/> Add Table
|
||||||
|
</button>
|
||||||
|
<a href="/pos/web" target="_blank" class="o_btn_white_small py-2 px-3">
|
||||||
|
<i class="fa fa-desktop me-1"/> Launch POS
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Row 1: KPI Summary Metric Cards -->
|
||||||
|
<section class="o_kpi_grid mb-4">
|
||||||
|
<!-- KPI 1: Total Tables -->
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-red-light text-danger">
|
||||||
|
<i class="fa fa-th fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-danger">Configured</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Total Tables</div>
|
||||||
|
<div class="o_kpi_number"><t t-esc="table_stats['total']"/></div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Across all dining zones</span>
|
||||||
|
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
|
||||||
|
<path d="M0 25 Q30 20 60 22 T100 15" fill="none" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPI 2: Available Tables -->
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-green-light text-success">
|
||||||
|
<i class="fa fa-check fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-success">Ready</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Available Tables</div>
|
||||||
|
<div class="o_kpi_number"><t t-esc="table_stats['available']"/></div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Ready for walk-in guests</span>
|
||||||
|
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
|
||||||
|
<path d="M0 20 Q30 15 60 25 T100 8" fill="none" stroke="#16a34a" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPI 3: Occupied Tables -->
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-red-light text-danger">
|
||||||
|
<i class="fa fa-users fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-danger">Dining</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Occupied Tables</div>
|
||||||
|
<div class="o_kpi_number"><t t-esc="table_stats['occupied']"/></div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Currently in service</span>
|
||||||
|
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
|
||||||
|
<path d="M0 15 Q30 25 70 18 T100 22" fill="none" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPI 4: Total Seating Capacity -->
|
||||||
|
<div class="o_kpi_card">
|
||||||
|
<div class="o_kpi_top">
|
||||||
|
<div class="o_kpi_icon_box bg-slate-light text-primary">
|
||||||
|
<i class="fa fa-building fs-5"/>
|
||||||
|
</div>
|
||||||
|
<span class="o_kpi_trend text-primary">Capacity</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kpi_label">Guest Capacity</div>
|
||||||
|
<div class="o_kpi_number"><t t-esc="table_stats.get('capacity', 48)"/> Seats</div>
|
||||||
|
<div class="o_kpi_footer">
|
||||||
|
<span class="o_kpi_sub">Maximum simultaneous covers</span>
|
||||||
|
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
|
||||||
|
<path d="M0 22 Q40 18 70 20 T100 12" fill="none" stroke="#3b82f6" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Row 2: Floor Filters & Search -->
|
||||||
|
<div class="o_page_toolbar">
|
||||||
|
<div class="o_filter_pills">
|
||||||
|
<div class="o_filter_pill active" data-filter="all">All Floors (<t t-esc="table_stats['total']"/>)</div>
|
||||||
|
<t t-foreach="floors" t-as="fl">
|
||||||
|
<div class="o_filter_pill" t-att-data-filter="fl['name'].lower()">
|
||||||
|
<t t-esc="fl['name']"/> (<t t-esc="fl['table_count']"/>)
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<div class="o_filter_pill" data-filter="available">Available Only (<t t-esc="table_stats['available']"/>)</div>
|
||||||
|
<div class="o_filter_pill" data-filter="occupied">Occupied Only (<t t-esc="table_stats['occupied']"/>)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_toolbar_search">
|
||||||
|
<i class="fa fa-search text-muted"/>
|
||||||
|
<input type="text" class="o_toolbar_search_input" placeholder="Search table name or floor..."/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 3: Interactive Floor Plan Grid -->
|
||||||
|
<div class="o_widget_card mb-4">
|
||||||
|
<div class="o_card_header">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<h3 class="o_card_title mb-0">Live Floor Seating Matrix</h3>
|
||||||
|
<span class="o_count_pill"><t t-esc="len(tables)"/> Tables Configured</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<button type="button" class="o_btn_red_small" data-open-modal="modalAddTable">
|
||||||
|
<i class="fa fa-plus me-1"/> Add Table
|
||||||
|
</button>
|
||||||
|
<a href="/web#action=dine360_table.action_restaurant_table_form" class="o_btn_white_small">
|
||||||
|
Manage in ERP
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<t t-if="not tables">
|
||||||
|
<div class="text-center py-5 text-muted">
|
||||||
|
<i class="fa fa-th-large fa-3x mb-3 text-secondary"/>
|
||||||
|
<h5>No tables configured</h5>
|
||||||
|
<p class="small">Click "Add Table" to set up your restaurant dining floors.</p>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Tables Visual Grid -->
|
||||||
|
<div class="o_tables_grid">
|
||||||
|
<t t-foreach="tables" t-as="table">
|
||||||
|
<div t-att-class="'o_table_tile ' + ('occupied' if table['is_occupied'] else 'available')"
|
||||||
|
data-search-item="true"
|
||||||
|
data-filter-item="true"
|
||||||
|
t-att-data-status="'occupied' if table['is_occupied'] else 'available'"
|
||||||
|
t-att-data-floor="table.get('floor_name', '').lower()">
|
||||||
|
|
||||||
|
<!-- Icon -->
|
||||||
|
<div class="o_table_icon_wrap">
|
||||||
|
<i class="fa fa-cutlery"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table Title & Floor -->
|
||||||
|
<div class="o_table_num"><t t-esc="table['name']"/></div>
|
||||||
|
<div class="o_table_meta">
|
||||||
|
<t t-esc="table.get('floor_name') or 'Main Dining'"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Capacity Pill -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<span class="o_table_capacity_pill">
|
||||||
|
<i class="fa fa-user me-1 text-danger"/> <t t-esc="table.get('seats', 4)"/> Seats
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Occupancy Action -->
|
||||||
|
<button type="button"
|
||||||
|
class="o_btn_toggle_table"
|
||||||
|
t-att-data-table-id="table['id']"
|
||||||
|
t-att-data-occupied="'true' if table['is_occupied'] else 'false'">
|
||||||
|
<t t-if="table['is_occupied']">
|
||||||
|
<i class="fa fa-sign-out me-1"/> Mark Available
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<i class="fa fa-sign-in me-1"/> Mark Occupied
|
||||||
|
</t>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL: Add Table -->
|
||||||
|
<div id="modalAddTable" class="o_dine_modal">
|
||||||
|
<div class="o_modal_backdrop"/>
|
||||||
|
<div class="o_modal_dialog">
|
||||||
|
<div class="o_modal_header">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<div class="o_settings_icon" style="width:34px;height:34px;font-size:15px;">
|
||||||
|
<i class="fa fa-th"/>
|
||||||
|
</div>
|
||||||
|
<h4 class="o_modal_title">Add Restaurant Table</h4>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="o_modal_close_btn o_modal_close">&times;</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="formAddTable">
|
||||||
|
<div class="o_modal_body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Table Number / Label *</label>
|
||||||
|
<input type="text" name="name" required="required" placeholder="e.g. Table 12, VIP-1" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Dining Floor *</label>
|
||||||
|
<select name="floor_id" class="o_form_control">
|
||||||
|
<t t-foreach="floors" t-as="fl">
|
||||||
|
<option t-att-value="fl['id']"><t t-esc="fl['name']"/></option>
|
||||||
|
</t>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Seating Capacity (Seats) *</label>
|
||||||
|
<input type="number" name="seats" required="required" min="1" max="30" value="4" class="o_form_control"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 o_form_group">
|
||||||
|
<label class="o_form_label">Shape</label>
|
||||||
|
<select name="shape" class="o_form_control">
|
||||||
|
<option value="square">Square / Rectangle</option>
|
||||||
|
<option value="round">Round Table</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_modal_footer">
|
||||||
|
<button type="button" class="btn btn-light o_modal_close">Cancel</button>
|
||||||
|
<button type="submit" class="o_btn_red_action">
|
||||||
|
<i class="fa fa-plus me-1"/> Save & Add to Floor
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
@ -1,25 +1,10 @@
|
|||||||
<odoo>
|
<odoo>
|
||||||
<template id="shivasakthi_web_layout" inherit_id="web.layout" name="Shivasakthi Title">
|
<template id="shivasakthi_web_layout" inherit_id="web.layout" name="Shivasakthi Title">
|
||||||
<xpath expr="//title" position="replace">
|
<xpath expr="//title" position="replace">
|
||||||
<title t-esc="'Dine360 Restaurant' if (not request or not request.env or request.env.company.name == 'My Company' or 'dine360' in (request.env.cr.dbname or '').lower()) else request.env.company.name"/>
|
<title t-esc="title or (request and request.env and request.env.company and request.env.company.name != 'My Company' and request.env.company.name) or 'Dine360 Restaurant'"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//link[@rel='shortcut icon']" position="replace">
|
<xpath expr="//link[@rel='shortcut icon']" position="replace">
|
||||||
<link rel="shortcut icon" href="/dine360_theme_shivasakthi/static/description/icon.png" type="image/x-icon"/>
|
<link rel="shortcut icon" href="/dine360_dashboard/static/description/icon.png" type="image/x-icon"/>
|
||||||
</xpath>
|
|
||||||
|
|
||||||
<xpath expr="//body" position="inside">
|
|
||||||
<t t-set="is_editor" t-value="request.params.get('enable_editor') or request.params.get('edit')"/>
|
|
||||||
<t t-if="not request.env.user._is_public() and not is_editor and request.httprequest.path != '/' and pageName != 'home_dashboard'">
|
|
||||||
<a href="/" class="o_dashboard_return_btn d-print-none" title="Back to Dashboard"
|
|
||||||
style="position: fixed; bottom: 20px; right: 20px; z-index: 9999;
|
|
||||||
background-color: #e50000; color: #ffff !important; padding: 12px 24px;
|
|
||||||
border-radius: 50px; text-decoration: none; font-weight: bold;
|
|
||||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2); display: flex; align-items: center;
|
|
||||||
gap: 8px; transition: all 0.3s ease; border: 2px solid #ffffffff !important; font-family: sans-serif;">
|
|
||||||
<i class="fa fa-th-large" style="font-size: 18px;"></i>
|
|
||||||
<span>Back to Dashboard</span>
|
|
||||||
</a>
|
|
||||||
</t>
|
|
||||||
</xpath>
|
</xpath>
|
||||||
</template>
|
</template>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@ -79,13 +79,17 @@ class PosOrderLine(models.Model):
|
|||||||
if line.product_id.is_kitchen_item and line.product_id.name != 'Water':
|
if line.product_id.is_kitchen_item and line.product_id.name != 'Water':
|
||||||
# Send to global KDS channel
|
# Send to global KDS channel
|
||||||
kds_channel = "kds_channel"
|
kds_channel = "kds_channel"
|
||||||
|
table = line.table_id or (line.order_id.table_id if line.order_id else False)
|
||||||
|
floor = line.floor_id or (table.floor_id if table else False)
|
||||||
|
source = line.order_source or (line.order_id.order_source if line.order_id else 'walk_in')
|
||||||
payload = {
|
payload = {
|
||||||
'line_id': line.id,
|
'line_id': line.id,
|
||||||
'order_id': line.order_id.id,
|
'order_id': line.order_id.id,
|
||||||
'product_name': line.product_id.name,
|
'product_name': line.product_id.name,
|
||||||
'qty': line.qty,
|
'qty': line.qty,
|
||||||
'table_name': line.table_id.name if line.table_id else '',
|
'table_name': table.name if table else '',
|
||||||
'floor_name': line.floor_id.name if line.floor_id else '',
|
'floor_name': floor.name if floor else '',
|
||||||
|
'order_source': source,
|
||||||
'customer_note': line.customer_note or '',
|
'customer_note': line.customer_note or '',
|
||||||
'preparation_status': line.preparation_status,
|
'preparation_status': line.preparation_status,
|
||||||
'create_date': line.create_date.isoformat() if line.create_date else '',
|
'create_date': line.create_date.isoformat() if line.create_date else '',
|
||||||
|
|||||||
@ -8,3 +8,13 @@ class ProductTemplate(models.Model):
|
|||||||
default=False,
|
default=False,
|
||||||
help="If checked, this product will appear in the Kitchen Display System when ordered."
|
help="If checked, this product will appear in the Kitchen Display System when ordered."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductProduct(models.Model):
|
||||||
|
_inherit = 'product.product'
|
||||||
|
|
||||||
|
is_kitchen_item = fields.Boolean(
|
||||||
|
related='product_tmpl_id.is_kitchen_item',
|
||||||
|
store=True,
|
||||||
|
readonly=False,
|
||||||
|
)
|
||||||
|
|||||||
@ -13,8 +13,8 @@ class SaleOrder(models.Model):
|
|||||||
res = super(SaleOrder, self).action_confirm()
|
res = super(SaleOrder, self).action_confirm()
|
||||||
|
|
||||||
for order in self:
|
for order in self:
|
||||||
# Check if it's a website order (usually has website_id)
|
# Check if it's a website order (usually has website_id or online source)
|
||||||
if order.website_id:
|
if order.website_id or getattr(order, 'order_source', False) == 'online':
|
||||||
try:
|
try:
|
||||||
self._create_pos_order_for_kds(order)
|
self._create_pos_order_for_kds(order)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -30,23 +30,29 @@ class SaleOrder(models.Model):
|
|||||||
PosSession = self.env['pos.session']
|
PosSession = self.env['pos.session']
|
||||||
PosConfig = self.env['pos.config']
|
PosConfig = self.env['pos.config']
|
||||||
|
|
||||||
# 1. Find a suitable POS Config (e.g., 'Website' or first available restaurant)
|
# 1. Look for currently open POS session first
|
||||||
config = PosConfig.search([('module_pos_restaurant', '=', True), ('active', '=', True)], limit=1)
|
|
||||||
if not config:
|
|
||||||
_logger.warning("No active POS Restaurant configuration found. Skipping KDS creation.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 2. Find or Open a Session
|
|
||||||
session = PosSession.search([
|
session = PosSession.search([
|
||||||
('config_id', '=', config.id),
|
('state', '=', 'opened'),
|
||||||
('state', '=', 'opened')
|
('config_id.module_pos_restaurant', '=', True)
|
||||||
], limit=1)
|
], limit=1)
|
||||||
|
|
||||||
if not session:
|
if not session:
|
||||||
_logger.warning(f"No open POS session found for config {config.name}. Cannot send to KDS.")
|
session = PosSession.search([('state', '=', 'opened')], limit=1)
|
||||||
return
|
|
||||||
|
|
||||||
# 3. Create POS Order Lines
|
if not session:
|
||||||
|
config = PosConfig.search([('module_pos_restaurant', '=', True), ('active', '=', True)], limit=1)
|
||||||
|
if not config:
|
||||||
|
config = PosConfig.search([('active', '=', True)], limit=1)
|
||||||
|
if not config:
|
||||||
|
_logger.warning("No active POS configuration found. Skipping KDS creation.")
|
||||||
|
return False
|
||||||
|
session = PosSession.search([('config_id', '=', config.id)], order='id desc', limit=1)
|
||||||
|
|
||||||
|
if not session:
|
||||||
|
_logger.warning("No POS session found. Cannot create POS order for KDS.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 2. Create POS Order Lines
|
||||||
lines_data = []
|
lines_data = []
|
||||||
for line in sale_order.order_line:
|
for line in sale_order.order_line:
|
||||||
if not line.product_id:
|
if not line.product_id:
|
||||||
@ -56,11 +62,6 @@ class SaleOrder(models.Model):
|
|||||||
if qty <= 0:
|
if qty <= 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip non-kitchen items, but allow delivery lines for accurate total matching
|
|
||||||
is_delivery_line = getattr(line, 'is_delivery', False)
|
|
||||||
if not is_delivery_line and (not line.product_id.is_kitchen_item or line.product_id.type == 'service'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
lines_data.append((0, 0, {
|
lines_data.append((0, 0, {
|
||||||
'product_id': line.product_id.id,
|
'product_id': line.product_id.id,
|
||||||
'qty': qty,
|
'qty': qty,
|
||||||
@ -71,11 +72,11 @@ class SaleOrder(models.Model):
|
|||||||
'tax_ids': [(6, 0, line.tax_id.ids)],
|
'tax_ids': [(6, 0, line.tax_id.ids)],
|
||||||
# Online orders: hold for cashier confirmation before sending to KDS
|
# Online orders: hold for cashier confirmation before sending to KDS
|
||||||
'preparation_status': False,
|
'preparation_status': False,
|
||||||
'customer_note': 'Web Order',
|
'customer_note': getattr(line, 'customer_note', False) or 'Web Order',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if not lines_data:
|
if not lines_data:
|
||||||
return
|
return False
|
||||||
|
|
||||||
# Generate proper POS reference matching Odoo's regex pattern '([0-9-]){14,}'
|
# Generate proper POS reference matching Odoo's regex pattern '([0-9-]){14,}'
|
||||||
# Odoo's _export_for_ui expects this to exist otherwise it crashes
|
# Odoo's _export_for_ui expects this to exist otherwise it crashes
|
||||||
@ -83,7 +84,7 @@ class SaleOrder(models.Model):
|
|||||||
uid = f"{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}-{session.id}-{sale_order.id}"
|
uid = f"{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}-{session.id}-{sale_order.id}"
|
||||||
pos_reference = f"Order {uid}"
|
pos_reference = f"Order {uid}"
|
||||||
|
|
||||||
# 4. Create POS Order (in Draft/New state to avoid double accounting)
|
# 3. Create POS Order (in Draft/New state to avoid double accounting)
|
||||||
# Use skip_kds_notify context to prevent immediate KDS notification
|
# Use skip_kds_notify context to prevent immediate KDS notification
|
||||||
# Online orders will be sent to KDS only after cashier confirmation
|
# Online orders will be sent to KDS only after cashier confirmation
|
||||||
pos_order = PosOrder.with_context(skip_kds_notify=True).create({
|
pos_order = PosOrder.with_context(skip_kds_notify=True).create({
|
||||||
@ -103,3 +104,4 @@ class SaleOrder(models.Model):
|
|||||||
|
|
||||||
# Notification to KDS is deferred until cashier confirms via dine360_online_orders
|
# Notification to KDS is deferred until cashier confirms via dine360_online_orders
|
||||||
_logger.info(f"Created POS Order {pos_order.name} from Website Order {sale_order.name} (pending cashier confirmation).")
|
_logger.info(f"Created POS Order {pos_order.name} from Website Order {sale_order.name} (pending cashier confirmation).")
|
||||||
|
return pos_order
|
||||||
|
|||||||
@ -25,6 +25,12 @@
|
|||||||
'dine360_online_orders/static/src/css/service_mode.css',
|
'dine360_online_orders/static/src/css/service_mode.css',
|
||||||
'dine360_online_orders/static/src/js/service_mode.js',
|
'dine360_online_orders/static/src/js/service_mode.js',
|
||||||
],
|
],
|
||||||
|
'point_of_sale._assets_pos': [
|
||||||
|
'dine360_online_orders/static/src/css/online_orders.css',
|
||||||
|
'dine360_online_orders/static/src/js/online_orders_screen.js',
|
||||||
|
'dine360_online_orders/static/src/js/online_orders_navbar.js',
|
||||||
|
'dine360_online_orders/static/src/xml/online_orders_screen.xml',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'application': False,
|
'application': False,
|
||||||
|
|||||||
@ -171,11 +171,10 @@ class SaleOrderOnline(models.Model):
|
|||||||
website sale order is confirmed.
|
website sale order is confirmed.
|
||||||
"""
|
"""
|
||||||
# Let the parent create the POS order
|
# Let the parent create the POS order
|
||||||
super(SaleOrderOnline, self)._create_pos_order_for_kds(sale_order)
|
pos_order = super(SaleOrderOnline, self)._create_pos_order_for_kds(sale_order)
|
||||||
|
|
||||||
# Now find the POS order that was just created and mark it
|
# Fallback search if super didn't return record directly
|
||||||
# We look for the most recent POS order linked to this sale order's partner
|
if not pos_order:
|
||||||
# with the note containing the sale order name
|
|
||||||
PosOrder = self.env['pos.order']
|
PosOrder = self.env['pos.order']
|
||||||
pos_order = PosOrder.search([
|
pos_order = PosOrder.search([
|
||||||
('note', 'like', sale_order.name),
|
('note', 'like', sale_order.name),
|
||||||
@ -189,8 +188,6 @@ class SaleOrderOnline(models.Model):
|
|||||||
'online_order_date': fields.Datetime.now(),
|
'online_order_date': fields.Datetime.now(),
|
||||||
'order_source': sale_order.order_source or 'online',
|
'order_source': sale_order.order_source or 'online',
|
||||||
'fulfilment_type': sale_order.fulfilment_type or 'pickup',
|
'fulfilment_type': sale_order.fulfilment_type or 'pickup',
|
||||||
# 'delivery_time': sale_order.delivery_time,
|
|
||||||
# 'uber_eta': sale_order.delivery_time,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
# Link back to sale order
|
# Link back to sale order
|
||||||
@ -225,10 +222,9 @@ class SaleOrderOnline(models.Model):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.error("Error creating picking for online POS order: %s", str(e))
|
_logger.error("Error creating picking for online POS order: %s", str(e))
|
||||||
|
|
||||||
# Set all lines to a "hold" state - they will go to KDS only when cashier confirms
|
# Set lines to hold state - they will go to KDS only when cashier confirms
|
||||||
for line in pos_order.lines:
|
for line in pos_order.lines:
|
||||||
if line.product_id.is_kitchen_item:
|
line.write({'preparation_status': False})
|
||||||
line.write({'preparation_status': 'waiting'})
|
|
||||||
|
|
||||||
# Send bus notification to POS
|
# Send bus notification to POS
|
||||||
if pos_order.config_id:
|
if pos_order.config_id:
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export class OnlineOrdersScreen extends Component {
|
|||||||
|
|
||||||
// Direct access to services to avoid 'methods is not iterable' error
|
// Direct access to services to avoid 'methods is not iterable' error
|
||||||
this.orm = this.env.services.orm;
|
this.orm = this.env.services.orm;
|
||||||
this.notification = this.env.services.pos_notification;
|
this.notification = this.env.services.notification || this.env.services.pos_notification;
|
||||||
this.busService = this.env.services.bus_service;
|
this.busService = this.env.services.bus_service;
|
||||||
|
|
||||||
this.state = useState({
|
this.state = useState({
|
||||||
@ -155,8 +155,14 @@ export class OnlineOrdersScreen extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
formatCurrency(amount) {
|
formatCurrency(amount) {
|
||||||
|
if (this.env?.utils?.formatCurrency) {
|
||||||
return this.env.utils.formatCurrency(amount);
|
return this.env.utils.formatCurrency(amount);
|
||||||
}
|
}
|
||||||
|
if (this.pos?.format_currency) {
|
||||||
|
return this.pos.format_currency(amount);
|
||||||
|
}
|
||||||
|
return `$${Number(amount || 0).toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
get orderCount() {
|
get orderCount() {
|
||||||
return this.state.orders.length;
|
return this.state.orders.length;
|
||||||
|
|||||||
@ -224,10 +224,11 @@ class SaasRestaurant(models.Model):
|
|||||||
|
|
||||||
modules_to_install = [
|
modules_to_install = [
|
||||||
'base', 'contacts', 'sale', 'stock', 'purchase',
|
'base', 'contacts', 'sale', 'stock', 'purchase',
|
||||||
'account', 'point_of_sale', 'hr', 'pos_restaurant'
|
'account', 'point_of_sale', 'hr', 'pos_restaurant',
|
||||||
|
'dine360_table', 'dine360_dashboard'
|
||||||
]
|
]
|
||||||
if self.activate_table_reservation:
|
if self.activate_table_reservation:
|
||||||
modules_to_install.extend(['website', 'dine360_table', 'dine360_reservation', 'dine360_theme_reservation'])
|
modules_to_install.extend(['website', 'dine360_reservation', 'dine360_theme_reservation'])
|
||||||
|
|
||||||
# Set status to provisioning
|
# Set status to provisioning
|
||||||
self.status = 'provisioning'
|
self.status = 'provisioning'
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
- Integration with POS and KDS
|
- Integration with POS and KDS
|
||||||
""",
|
""",
|
||||||
'author': 'Dine360',
|
'author': 'Dine360',
|
||||||
'depends': ['point_of_sale', 'pos_restaurant', 'dine360_order_channels', 'website'],
|
'depends': ['point_of_sale', 'pos_restaurant', 'dine360_order_channels', 'dine360_kds', 'website'],
|
||||||
'data': [
|
'data': [
|
||||||
'views/restaurant_table_views.xml',
|
'views/restaurant_table_views.xml',
|
||||||
'views/self_order_templates.xml',
|
'views/self_order_templates.xml',
|
||||||
|
|||||||
@ -62,11 +62,9 @@ class Dine360SelfOrderController(http.Controller):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# We need an active session to link the order
|
# We need an active session to link the order
|
||||||
# For self-order, we might have a dedicated "Self-Order POS" config
|
|
||||||
# Here we'll find the first open POS session or one marked as 'self_order'
|
|
||||||
session = request.env['pos.session'].sudo().search([
|
session = request.env['pos.session'].sudo().search([
|
||||||
('state', '=', 'opened'),
|
('state', '=', 'opened'),
|
||||||
('config_id.module_pos_self_order', '=', True) # Or a custom flag
|
('config_id.module_pos_restaurant', '=', True)
|
||||||
], limit=1)
|
], limit=1)
|
||||||
|
|
||||||
if not session:
|
if not session:
|
||||||
@ -80,38 +78,103 @@ class Dine360SelfOrderController(http.Controller):
|
|||||||
|
|
||||||
partner = request.env.user.partner_id if not request.env.user._is_public() else False
|
partner = request.env.user.partner_id if not request.env.user._is_public() else False
|
||||||
|
|
||||||
# Prepare order values
|
|
||||||
table_id = data.get('table_id')
|
table_id = data.get('table_id')
|
||||||
lines = data.get('lines', [])
|
pos_table = False
|
||||||
|
if table_id:
|
||||||
|
try:
|
||||||
|
pos_table = request.env['restaurant.table'].sudo().browse(int(table_id))
|
||||||
|
if not pos_table.exists():
|
||||||
|
pos_table = False
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pos_table = False
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
now_str = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
|
||||||
|
table_part = pos_table.id if pos_table else 0
|
||||||
|
pos_reference = f"Order {now_str}-{session.id}-{table_part}"
|
||||||
|
|
||||||
|
currency = session.config_id.pricelist_id.currency_id if session.config_id.pricelist_id else session.company_id.currency_id
|
||||||
|
|
||||||
|
order_lines = []
|
||||||
|
total_untaxed = 0.0
|
||||||
|
total_tax = 0.0
|
||||||
|
|
||||||
|
for line in data.get('lines', []):
|
||||||
|
product_id = int(line['product_id'])
|
||||||
|
qty = float(line.get('qty', 1))
|
||||||
|
product = request.env['product.product'].sudo().browse(product_id)
|
||||||
|
if not product.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
price_unit = float(line.get('price_unit', product.list_price))
|
||||||
|
taxes = product.taxes_id.compute_all(price_unit, currency, qty, product=product, partner=partner)
|
||||||
|
subtotal = taxes['total_excluded']
|
||||||
|
subtotal_incl = taxes['total_included']
|
||||||
|
total_untaxed += subtotal
|
||||||
|
total_tax += (subtotal_incl - subtotal)
|
||||||
|
|
||||||
|
order_lines.append((0, 0, {
|
||||||
|
'product_id': product.id,
|
||||||
|
'full_product_name': product.display_name or product.name,
|
||||||
|
'qty': qty,
|
||||||
|
'price_unit': price_unit,
|
||||||
|
'price_subtotal': subtotal,
|
||||||
|
'price_subtotal_incl': subtotal_incl,
|
||||||
|
'tax_ids': [(6, 0, product.taxes_id.ids)],
|
||||||
|
'customer_note': line.get('note', '') or (f"Table {pos_table.name}" if pos_table else ''),
|
||||||
|
'preparation_status': 'waiting',
|
||||||
|
}))
|
||||||
|
|
||||||
|
if not order_lines:
|
||||||
|
return {'error': 'No valid products in order'}
|
||||||
|
|
||||||
|
amount_total = total_untaxed + total_tax
|
||||||
|
|
||||||
order_vals = {
|
order_vals = {
|
||||||
'session_id': session.id,
|
'session_id': session.id,
|
||||||
|
'company_id': session.company_id.id,
|
||||||
|
'pricelist_id': session.config_id.pricelist_id.id if session.config_id.pricelist_id else False,
|
||||||
'partner_id': partner.id if partner else False,
|
'partner_id': partner.id if partner else False,
|
||||||
'table_id': int(table_id) if table_id else False,
|
'table_id': pos_table.id if pos_table else False,
|
||||||
'order_source': 'qr' if table_id else 'kiosk',
|
'order_source': 'qr' if pos_table else 'kiosk',
|
||||||
'fulfilment_type': data.get('fulfilment_type', 'dine_in'),
|
'fulfilment_type': data.get('fulfilment_type', 'dine_in'),
|
||||||
'lines': [],
|
'pos_reference': pos_reference,
|
||||||
|
'amount_tax': total_tax,
|
||||||
|
'amount_total': amount_total,
|
||||||
|
'amount_paid': 0.0,
|
||||||
|
'amount_return': 0.0,
|
||||||
|
'note': f"Self-Order from {'Table ' + pos_table.name if pos_table else 'Kiosk'}",
|
||||||
|
'lines': order_lines,
|
||||||
}
|
}
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
order_vals['lines'].append((0, 0, {
|
|
||||||
'product_id': line['product_id'],
|
|
||||||
'qty': line['qty'],
|
|
||||||
'price_unit': line['price_unit'],
|
|
||||||
'customer_note': line.get('note', ''),
|
|
||||||
}))
|
|
||||||
|
|
||||||
# Use sudo to create the order since it's from public flow
|
|
||||||
order = request.env['pos.order'].sudo().create([order_vals])
|
order = request.env['pos.order'].sudo().create([order_vals])
|
||||||
|
request.env.flush_all()
|
||||||
|
|
||||||
# To trigger KDS, we might need to call specific methods or the confirmation flow
|
# Ensure related fields are populated and notify KDS
|
||||||
# For self-order, we often mark as "Pending Payment" or "Paid" depending on flow
|
kitchen_lines = order.lines.filtered(
|
||||||
# If kiosk, it might be unpaid until cashier handles it.
|
lambda l: l.product_id.is_kitchen_item and l.product_id.type != 'service'
|
||||||
|
)
|
||||||
|
if hasattr(kitchen_lines, '_notify_kds'):
|
||||||
|
kitchen_lines._notify_kds()
|
||||||
|
|
||||||
|
if hasattr(order.lines, '_notify_pos'):
|
||||||
|
order.lines._notify_pos()
|
||||||
|
|
||||||
|
# Notify POS channel of the new QR/Kiosk order
|
||||||
|
if session.config_id:
|
||||||
|
channel = f"pos_config_Channel_{session.config_id.id}"
|
||||||
|
request.env['bus.bus'].sudo()._sendone(channel, 'new_qr_order', {
|
||||||
|
'order_id': order.id,
|
||||||
|
'order_name': order.pos_reference or order.name,
|
||||||
|
'table_id': order.table_id.id if order.table_id else False,
|
||||||
|
'table_name': order.table_id.name if order.table_id else '',
|
||||||
|
'amount_total': order.amount_total,
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'order_id': order.id,
|
'order_id': order.id,
|
||||||
'order_name': order.name,
|
'order_name': order.pos_reference or order.name,
|
||||||
'message': _("Your order has been sent to the kitchen!")
|
'message': _("Your order has been sent to the kitchen!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,76 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
|
<!-- Floor Kanban View (Custom Dine360 UI) -->
|
||||||
|
<record id="view_restaurant_floor_kanban" model="ir.ui.view">
|
||||||
|
<field name="name">restaurant.floor.kanban.dine360</field>
|
||||||
|
<field name="model">restaurant.floor</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<kanban class="o_kanban_mobile" sample="1">
|
||||||
|
<field name="id"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="table_ids"/>
|
||||||
|
<field name="pos_config_ids"/>
|
||||||
|
<field name="background_color"/>
|
||||||
|
<templates>
|
||||||
|
<t t-name="kanban-box">
|
||||||
|
<div t-attf-class="oe_kanban_global_click p-3 mb-2"
|
||||||
|
style="border-radius: 16px; border: 1px solid #e2e8f0; background: #ffffff; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.05); min-height: 155px; display: flex; flex-direction: column; justify-content: space-between; transition: all 0.25s ease;">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||||
|
<span class="badge rounded-pill px-2.5 py-1" style="background: #fee2e2; color: #e50000; font-size: 11px; font-weight: 700;">
|
||||||
|
<i class="fa fa-building me-1"/> Dining Zone
|
||||||
|
</span>
|
||||||
|
<span t-if="record.background_color.raw_value" class="badge rounded-pill border px-2 py-1"
|
||||||
|
t-attf-style="background-color: #{record.background_color.raw_value}; color: #333; font-size: 11px;">
|
||||||
|
Floor Color
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Middle -->
|
||||||
|
<div class="d-flex align-items-center gap-3 my-2">
|
||||||
|
<div class="d-flex align-items-center justify-content-center"
|
||||||
|
style="width: 48px; height: 48px; border-radius: 12px; background: #f1f5f9; color: #0f172a; font-size: 20px; flex-shrink: 0;">
|
||||||
|
<i class="fa fa-th-large"/>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<div class="fw-bold text-dark text-truncate" style="font-size: 18px;">
|
||||||
|
<t t-esc="record.name.value"/>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small">
|
||||||
|
Floor Plan Section
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="d-flex align-items-center justify-content-between pt-2.5 border-top mt-2">
|
||||||
|
<span class="badge rounded-pill px-2.5 py-1"
|
||||||
|
style="background: #f8fafc; color: #0f172a; font-size: 12px; font-weight: 600; border: 1px solid #e2e8f0;">
|
||||||
|
<i class="fa fa-th text-danger me-1"/>
|
||||||
|
<t t-esc="record.table_ids.raw_value and record.table_ids.raw_value.length or 0"/> Tables Placed
|
||||||
|
</span>
|
||||||
|
<span class="text-muted small font-monospace">
|
||||||
|
ID #<t t-esc="record.id.raw_value"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
</kanban>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
<!-- Tree View -->
|
<!-- Tree View -->
|
||||||
<record id="view_restaurant_floor_tree" model="ir.ui.view">
|
<record id="view_restaurant_floor_tree" model="ir.ui.view">
|
||||||
<field name="name">restaurant.floor.tree.dine360</field>
|
<field name="name">restaurant.floor.tree.dine360</field>
|
||||||
<field name="model">restaurant.floor</field>
|
<field name="model">restaurant.floor</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<tree string="Restaurant Floors">
|
<tree string="Restaurant Floors" sample="1">
|
||||||
<field name="sequence" widget="handle"/>
|
<field name="sequence" widget="handle"/>
|
||||||
<field name="name"/>
|
<field name="name" string="Floor Name" class="fw-bold text-dark"/>
|
||||||
<field name="pos_config_ids" widget="many2many_tags"/>
|
<field name="table_ids" widget="many2many_tags" string="Tables Placed"/>
|
||||||
|
<field name="pos_config_ids" widget="many2many_tags" string="Point of Sale Registers"/>
|
||||||
</tree>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
@ -21,8 +83,8 @@
|
|||||||
<form string="Restaurant Floor">
|
<form string="Restaurant Floor">
|
||||||
<sheet>
|
<sheet>
|
||||||
<div class="oe_title">
|
<div class="oe_title">
|
||||||
<label for="name" class="oe_edit_only"/>
|
<label for="name" class="oe_edit_only text-muted small fw-bold"/>
|
||||||
<h1>
|
<h1 class="text-danger fw-bold">
|
||||||
<field name="name" placeholder="e.g. Ground Floor, Garden, VIP..."/>
|
<field name="name" placeholder="e.g. Ground Floor, Garden, VIP..."/>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
@ -36,9 +98,9 @@
|
|||||||
<page string="Tables on Floor" name="tables">
|
<page string="Tables on Floor" name="tables">
|
||||||
<field name="table_ids" context="{'default_floor_id': active_id}">
|
<field name="table_ids" context="{'default_floor_id': active_id}">
|
||||||
<tree string="Tables" editable="bottom">
|
<tree string="Tables" editable="bottom">
|
||||||
<field name="name"/>
|
<field name="name" string="Table Label"/>
|
||||||
<field name="seats"/>
|
<field name="seats" string="Seats"/>
|
||||||
<field name="shape"/>
|
<field name="shape" string="Shape"/>
|
||||||
</tree>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
@ -52,8 +114,8 @@
|
|||||||
<record id="action_restaurant_floor_form" model="ir.actions.act_window">
|
<record id="action_restaurant_floor_form" model="ir.actions.act_window">
|
||||||
<field name="name">Floors</field>
|
<field name="name">Floors</field>
|
||||||
<field name="res_model">restaurant.floor</field>
|
<field name="res_model">restaurant.floor</field>
|
||||||
<field name="view_mode">tree,form</field>
|
<field name="view_mode">kanban,tree,form</field>
|
||||||
<field name="view_id" ref="view_restaurant_floor_tree"/>
|
<field name="view_id" ref="view_restaurant_floor_kanban"/>
|
||||||
<field name="help" type="html">
|
<field name="help" type="html">
|
||||||
<p class="o_view_nocontent_smiling_face">
|
<p class="o_view_nocontent_smiling_face">
|
||||||
Create a new floor plan!
|
Create a new floor plan!
|
||||||
|
|||||||
@ -1,19 +1,119 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<!-- Tree View -->
|
<!-- Table Kanban View (Custom Dine360 UI) -->
|
||||||
|
<record id="view_restaurant_table_kanban" model="ir.ui.view">
|
||||||
|
<field name="name">restaurant.table.kanban.dine360</field>
|
||||||
|
<field name="model">restaurant.table</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<kanban class="o_kanban_mobile" default_group_by="floor_id" sample="1">
|
||||||
|
<field name="id"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="seats"/>
|
||||||
|
<field name="shape"/>
|
||||||
|
<field name="floor_id"/>
|
||||||
|
<field name="color"/>
|
||||||
|
<templates>
|
||||||
|
<t t-name="kanban-box">
|
||||||
|
<div t-attf-class="oe_kanban_global_click o_dine360_table_card p-3 mb-2"
|
||||||
|
style="border-radius: 16px; border: 1px solid #e2e8f0; background: #ffffff; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.05); min-height: 155px; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); display: flex; flex-direction: column; justify-content: space-between;">
|
||||||
|
|
||||||
|
<!-- Header: Floor Badge & Shape Badge -->
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||||
|
<span class="badge rounded-pill px-2.5 py-1" style="background: #f1f5f9; color: #475569; font-size: 11.5px; font-weight: 600; border: 1px solid #e2e8f0;">
|
||||||
|
<i class="fa fa-map-marker text-danger me-1"/>
|
||||||
|
<t t-esc="record.floor_id.value or 'Main Hall'"/>
|
||||||
|
</span>
|
||||||
|
<t t-if="record.shape.raw_value == 'round'">
|
||||||
|
<span class="badge rounded-pill px-2.5 py-1" style="background: #eff6ff; color: #2563eb; font-size: 11px; font-weight: 600; border: 1px solid #bfdbfe;">
|
||||||
|
<i class="fa fa-circle-o me-1"/> Round Table
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<span class="badge rounded-pill px-2.5 py-1" style="background: #f8fafc; color: #64748b; font-size: 11px; font-weight: 600; border: 1px solid #e2e8f0;">
|
||||||
|
<i class="fa fa-square-o me-1"/> Square Table
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Middle: Icon and Big Table Label -->
|
||||||
|
<div class="d-flex align-items-center gap-3 my-2">
|
||||||
|
<div class="d-flex align-items-center justify-content-center"
|
||||||
|
style="width: 48px; height: 48px; border-radius: 12px; background: #fee2e2; color: #e50000; font-size: 20px; flex-shrink: 0; box-shadow: 0 2px 8px rgba(229, 0, 0, 0.15);">
|
||||||
|
<i class="fa fa-cutlery"/>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<div class="fw-bold text-dark text-truncate" style="font-size: 18px; letter-spacing: -0.3px;">
|
||||||
|
<t t-esc="record.name.value"/>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small">
|
||||||
|
Dining Zone
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer: Capacity & Indicator -->
|
||||||
|
<div class="d-flex align-items-center justify-content-between pt-2.5 border-top mt-2">
|
||||||
|
<span class="badge rounded-pill px-2.5 py-1"
|
||||||
|
style="background: #ecfdf5; color: #059669; font-size: 12px; font-weight: 600; border: 1px solid #a7f3d0;">
|
||||||
|
<i class="fa fa-users me-1 text-success"/>
|
||||||
|
<t t-esc="record.seats.value"/> Seats
|
||||||
|
</span>
|
||||||
|
<span class="text-muted small font-monospace">
|
||||||
|
ID #<t t-esc="record.id.raw_value"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
</kanban>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Tree View (Custom Dine360 Modern Tree) -->
|
||||||
<record id="view_restaurant_table_tree" model="ir.ui.view">
|
<record id="view_restaurant_table_tree" model="ir.ui.view">
|
||||||
<field name="name">restaurant.table.tree.dine360</field>
|
<field name="name">restaurant.table.tree.dine360</field>
|
||||||
<field name="model">restaurant.table</field>
|
<field name="model">restaurant.table</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<tree string="Restaurant Tables">
|
<tree string="Restaurant Tables" sample="1">
|
||||||
<field name="floor_id"/>
|
<field name="name" string="Table Label / Number" class="fw-bold text-dark"/>
|
||||||
<field name="name" string="Table Name"/>
|
<field name="floor_id" string="Dining Floor" widget="badge" decoration-info="floor_id"/>
|
||||||
<field name="seats" string="Seat Count"/>
|
<field name="seats" string="Seating Capacity" widget="badge" decoration-danger="seats >= 6" decoration-success="seats < 6 and seats >= 4" decoration-info="seats < 4"/>
|
||||||
<field name="shape"/>
|
<field name="shape" string="Table Shape" widget="badge" decoration-warning="shape == 'round'" decoration-muted="shape == 'square'"/>
|
||||||
|
<field name="position_h" optional="hide"/>
|
||||||
|
<field name="position_v" optional="hide"/>
|
||||||
|
<field name="width" optional="hide"/>
|
||||||
|
<field name="height" optional="hide"/>
|
||||||
</tree>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<!-- Search View with Floor Search Panel -->
|
||||||
|
<record id="view_restaurant_table_search" model="ir.ui.view">
|
||||||
|
<field name="name">restaurant.table.search.dine360</field>
|
||||||
|
<field name="model">restaurant.table</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Search Tables">
|
||||||
|
<field name="name" string="Table Name"/>
|
||||||
|
<field name="floor_id" string="Dining Floor"/>
|
||||||
|
<separator/>
|
||||||
|
<filter string="Square Tables" name="filter_square" domain="[('shape', '=', 'square')]"/>
|
||||||
|
<filter string="Round Tables" name="filter_round" domain="[('shape', '=', 'round')]"/>
|
||||||
|
<separator/>
|
||||||
|
<filter string="Family / Large (4+ Seats)" name="filter_large" domain="[('seats', '>=', 4)]"/>
|
||||||
|
<filter string="Couples / Small (1-2 Seats)" name="filter_small" domain="[('seats', '<=', 2)]"/>
|
||||||
|
<group expand="1" string="Group By">
|
||||||
|
<filter string="Dining Floor" name="group_floor" context="{'group_by': 'floor_id'}"/>
|
||||||
|
<filter string="Table Shape" name="group_shape" context="{'group_by': 'shape'}"/>
|
||||||
|
<filter string="Seat Capacity" name="group_seats" context="{'group_by': 'seats'}"/>
|
||||||
|
</group>
|
||||||
|
<searchpanel>
|
||||||
|
<field name="floor_id" icon="fa-th-large" color="#e50000" enable_counters="1"/>
|
||||||
|
<field name="shape" icon="fa-square-o" enable_counters="1"/>
|
||||||
|
</searchpanel>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
<!-- Form View -->
|
<!-- Form View -->
|
||||||
<record id="view_restaurant_table_form" model="ir.ui.view">
|
<record id="view_restaurant_table_form" model="ir.ui.view">
|
||||||
<field name="name">restaurant.table.form.dine360</field>
|
<field name="name">restaurant.table.form.dine360</field>
|
||||||
@ -22,18 +122,18 @@
|
|||||||
<form string="Restaurant Table">
|
<form string="Restaurant Table">
|
||||||
<sheet>
|
<sheet>
|
||||||
<div class="oe_title">
|
<div class="oe_title">
|
||||||
<label for="name" string="Table Name" class="oe_edit_only"/>
|
<label for="name" string="Table Name / Number" class="oe_edit_only text-muted small fw-bold"/>
|
||||||
<h1>
|
<h1 class="text-danger fw-bold">
|
||||||
<field name="name" placeholder="e.g. Table 1, T2..."/>
|
<field name="name" placeholder="e.g. Table 1, VIP-1..."/>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<group>
|
<group>
|
||||||
<group string="Basic Configuration" name="basic_config">
|
<group string="Dining Setup" name="basic_config">
|
||||||
<field name="floor_id" options="{'no_create': True}"/>
|
<field name="floor_id" options="{'no_create': True}" required="1"/>
|
||||||
<field name="seats" string="Seat Count"/>
|
<field name="seats" string="Seating Capacity (Seats)"/>
|
||||||
<field name="shape"/>
|
<field name="shape"/>
|
||||||
</group>
|
</group>
|
||||||
<group string="POS Position & Size" name="pos_layout">
|
<group string="Point of Sale Visual Coordinates" name="pos_layout">
|
||||||
<field name="position_h"/>
|
<field name="position_h"/>
|
||||||
<field name="position_v"/>
|
<field name="position_v"/>
|
||||||
<field name="width"/>
|
<field name="width"/>
|
||||||
@ -49,14 +149,32 @@
|
|||||||
<record id="action_restaurant_table_form" model="ir.actions.act_window">
|
<record id="action_restaurant_table_form" model="ir.actions.act_window">
|
||||||
<field name="name">Tables</field>
|
<field name="name">Tables</field>
|
||||||
<field name="res_model">restaurant.table</field>
|
<field name="res_model">restaurant.table</field>
|
||||||
<field name="view_mode">tree,form</field>
|
<field name="view_mode">kanban,tree,form</field>
|
||||||
<field name="view_id" ref="view_restaurant_table_tree"/>
|
<field name="search_view_id" ref="view_restaurant_table_search"/>
|
||||||
|
<field name="view_id" ref="view_restaurant_table_kanban"/>
|
||||||
<field name="help" type="html">
|
<field name="help" type="html">
|
||||||
<p class="o_view_nocontent_smiling_face">
|
<p class="o_view_nocontent_smiling_face">
|
||||||
Create a new table!
|
Create your first dining table!
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Tables are placed on floor plans. You can configure their shape, seat capacity, and custom positioning for the Point of Sale system.
|
Configure tables on your restaurant floors with shape and guest seating capacity.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Table Window Action Alias -->
|
||||||
|
<record id="action_restaurant_table" model="ir.actions.act_window">
|
||||||
|
<field name="name">Tables</field>
|
||||||
|
<field name="res_model">restaurant.table</field>
|
||||||
|
<field name="view_mode">kanban,tree,form</field>
|
||||||
|
<field name="search_view_id" ref="view_restaurant_table_search"/>
|
||||||
|
<field name="view_id" ref="view_restaurant_table_kanban"/>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Create your first dining table!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Configure tables on your restaurant floors with shape and guest seating capacity.
|
||||||
</p>
|
</p>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user