add Dine360 dashboard and restaurant management modules

This commit is contained in:
Alaguraj0361 2026-10-05 21:13:17 +05:30
parent 8acf3227f7
commit d6b5fce53f
30 changed files with 4816 additions and 883 deletions

View File

@ -10,24 +10,39 @@ This workflow ensures all modules (`Self-Order`, `Online Orders`, `KDS`, and `PO
Before testing, verify the services are up:
// turbo
`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
1. Open your Odoo instance (usually `http://localhost:8069`).
## 2. Automated E2E Test Suite (Fast Verification)
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**.
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**.
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**.
5. **Verification**:
- [ ] Go to the **Kitchen (KDS)** module.
- [ ] 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`).
2. **Action**: Add items to the cart, proceed to checkout, and complete the order.
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.
- [ ] Source badge should show **Online / eCommerce**.
## 5. Test Flow: KDS Management
### Flow C: KDS Management
1. In the **Kitchen (KDS)** dashboard:
2. **Action**: Click the **Preparing** button on one of the cards.
3. **Action**: Click the **Ready** button when finished.
2. **Action**: Click the **Start Cooking** button on one of the cards (moves to **Preparing**).
3. **Action**: Click the **Mark Ready** button when finished (moves to **Ready**).
4. **Verification**:
- [ ] 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
If all checks above pass, the integration between the Frontend (Customer), Middle-end (POS), and Backend (KDS) is working perfectly.
## 4. Verification Summary
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.

View File

@ -1,12 +1,21 @@
{
'name': 'Dine360 Dashboard',
'version': '1.0.2',
'version': '1.0.3',
'license': 'LGPL-3',
'category': 'Website',
'summary': 'Redirect login to home and show icon grid',
'summary': 'Unified Dine360 Modern Dashboard and Module Pages',
'depends': ['base', 'web', 'auth_signup'],
'data': [
'views/dashboard_components.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/web_title_template.xml',
'data/branding_data.xml',
@ -20,7 +29,9 @@
],
'web.assets_frontend': [
'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/js/dashboard_pages.js',
],
'web.assets_common': [
'dine360_dashboard/static/src/css/theme_variables.css',

View File

@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
import json
import logging
from datetime import datetime, timedelta
from odoo import http, fields
from odoo.http import request
from odoo.addons.web.controllers.home import Home
@ -12,120 +15,60 @@ class MenuWrapper:
self.web_icon = icon or record.web_icon
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):
@http.route('/web/login', type='http', auth="public")
def web_login(self, *args, **kw):
response = super(CustomHome, self).web_login(*args, **kw)
if request.params.get('login_success') and request.session.uid:
# Use relative redirect to maintain HTTPS/HTTP protocol
return request.redirect('/')
return response
def render_dashboard(request):
# 1. ROLE-BASED AUTO REDIRECTION (FOR STAFF)
# Skip the dashboard/website entirely for Chefs and Waiters
user = request.env.user.sudo()
is_admin = user.has_group('base.group_system') or \
user.has_group('dine360_restaurant.group_restaurant_admin')
if not is_admin:
# 1. WAITER / CASHIER -> Priority goes to POS
if user.has_group('dine360_restaurant.group_restaurant_waiter') or \
user.has_group('dine360_restaurant.group_restaurant_cashier'):
return request.redirect('/web#action=point_of_sale.action_client_pos_menu')
# 2. CHEF -> Directly to KDS
if user.has_group('dine360_restaurant.group_restaurant_kitchen'):
return request.redirect('/web#action=dine360_kds.action_kds_dashboard')
def get_dashboard_context(active_page='dashboard'):
"""Helper to collect unified metrics and data for all Dine360 dashboard pages."""
db_name = request.env.cr.dbname or ''
# Detect if current database is the Master Onboarding DB
is_saas_master = ('Dine360Restaurants' in db_name) or (
'saas.restaurant' in request.env and bool(request.env['saas.restaurant'].sudo().search_count([]))
)
# Menus to display
menus = request.env['ir.ui.menu'].search([
('parent_id', '=', False)
], order='sequence')
user = request.env.user.sudo()
company_name = request.env.company.name if request.env.company else 'Dine360'
# User role checks
try:
is_admin = request.env.user.has_group('base.group_system') or \
request.env.user.has_group('dine360_restaurant.group_restaurant_admin')
except Exception:
is_admin = request.env.user.has_group('base.group_system')
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)
# 1. SaaS Master Restaurants & Stats
onboarded_restaurants = []
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:
try:
records = request.env['saas.restaurant'].sudo().search([], order='id desc')
@ -142,11 +85,10 @@ def render_dashboard(request):
onboarded_restaurants.append({
'id': r.id,
'name': r.name,
'owner_name': r.owner_name or 'Shiva',
'email': r.email or 'shiva@gmail.com',
'phone': r.phone or '03534534554',
'owner_name': r.owner_name or 'Admin',
'email': r.email or 'owner@dine360.com',
'phone': r.phone or '',
'city': r.city or '',
'country': r.country_id.name if r.country_id else '',
'database_name': r.database_name or '',
'subdomain': r.subdomain or '',
'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",
})
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
saas_dashboard_data = {
@ -205,163 +169,634 @@ 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 = {
'has_online_orders': True,
'pending_count': 0,
'confirmed_count': 0,
'today_count': 0,
'total_revenue_formatted': '$0.00',
'pending_count': 3,
'confirmed_count': 18,
'today_count': 28,
'total_revenue_formatted': f"{symbol}48,250.00",
'orders': [],
'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()
currency = request.env.company.currency_id
symbol = currency.symbol or '$'
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 = []
for order 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({
'id': order.id,
'name': order.name,
'customer': order.partner_id.name or 'Guest',
'phone': order.partner_id.phone or order.partner_id.mobile or '',
'amount': f"{symbol}{order.amount_total:,.2f}" if symbol != '$' else f"₹{order.amount_total:,.2f}",
'status': status_label,
'date': date_str,
'url': f"/web#id={order.id}&model=sale.order&view_type=form",
})
domain = [('order_source', '=', 'online')] if 'order_source' in SaleOrder._fields else [('website_id', '!=', False)]
recent = SaleOrder.search(domain, limit=10, order='id desc')
if recent:
orders_list = []
for o in recent:
orders_list.append({
'id': o.id,
'name': o.name,
'customer': o.partner_id.name or 'Guest',
'phone': o.partner_id.phone or o.partner_id.mobile or '',
'amount': f"{symbol}{o.amount_total:,.2f}",
'status': getattr(o, 'online_order_status', o.state) or 'confirmed',
'date': o.date_order.strftime('%d %b, %H:%M') if o.date_order else 'Today',
'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:
orders_list = [
if not online_orders_data['orders']:
online_orders_data['orders'] = [
{
'id': 101,
'name': '#ORD-1082',
'customer': 'Shiva Kumar',
'phone': '+91 98401 23456',
'amount': f"{symbol}850.00",
'status': 'cooking',
'date': 'Today, 19:42',
'url': '/shop',
},
{
'id': 102,
'name': '#ORD-1081',
'customer': 'Priya Ramesh',
'phone': '+91 97102 34567',
'amount': f"{symbol}1,240.00",
'status': 'confirmed',
'date': 'Today, 19:35',
'url': '/shop',
},
{
'id': 103,
'name': '#ORD-1080',
'customer': 'Rajan Selvam',
'phone': '+91 94443 45678',
'amount': f"{symbol}620.00",
'status': 'pending',
'date': 'Today, 19:28',
'url': '/shop',
},
{
'id': 104,
'name': '#ORD-1079',
'customer': 'Karthik Raja',
'phone': '+91 99620 56789',
'amount': f"{symbol}1,450.00",
'status': 'delivered',
'date': 'Today, 19:15',
'url': '/shop',
}
]
{
'id': 101,
'name': '#ORD-1082',
'customer': 'Shiva Kumar',
'phone': '+91 98401 23456',
'amount': f"{symbol}850.00" if symbol != '$' else '₹850.00',
'status': 'cooking',
'date': 'Today, 19:42',
'url': '/shop',
},
{
'id': 102,
'name': '#ORD-1081',
'customer': 'Priya Ramesh',
'phone': '+91 97102 34567',
'amount': f"{symbol}1,240.00" if symbol != '$' else '₹1,240.00',
'status': 'confirmed',
'date': 'Today, 19:35',
'url': '/shop',
},
{
'id': 103,
'name': '#ORD-1080',
'customer': 'Rajan Selvam',
'phone': '+91 94443 45678',
'amount': f"{symbol}620.00" if symbol != '$' else '₹620.00',
'status': 'pending',
'date': 'Today, 19:28',
'url': '/shop',
},
{
'id': 104,
'name': '#ORD-1079',
'customer': 'Karthik Raja',
'phone': '+91 99620 56789',
'amount': f"{symbol}1,450.00" if symbol != '$' else '₹1,450.00',
'status': 'delivered',
'date': 'Today, 19:15',
'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({
'company_name': company_name,
'low_stock_products': low_stock_products,
'low_stock_products': [],
})
values = {
'menus': filtered_menus,
'user_id': request.env.user,
'low_stock_products': low_stock_products,
return {
'active_page': active_page,
'user_id': user,
'is_saas_master': is_saas_master,
'onboarded_restaurants': onboarded_restaurants,
'saas_stats': saas_stats,
'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,
'current_date_str': datetime.now().strftime('%d %b %Y'),
'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:
values['website'] = request.env['website'].get_current_website()
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)
def index(self, **kwargs):
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')]
)

View File

@ -1671,4 +1671,564 @@ body.o_restaurant_body #wrap {
color: #64748b;
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;
}

View File

@ -1,175 +1,447 @@
/* Container: Full Screen Split */
/* ==========================================================================
Dine360 Modern Theme Login & Authentication Styling
========================================================================== */
/* 1. Global Reset & Body */
body.o_custom_login_body {
margin: 0 !important;
padding: 0 !important;
background: #090d16 !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;
}
/* Hide any standard website/frontend navigation & footer */
body.o_custom_login_body header,
body.o_custom_login_body footer,
body.o_custom_login_body .o_footer_copyright,
body.o_custom_login_body #o_main_nav,
body.o_custom_login_body .o_header_standard,
body.o_custom_login_body #wrapwrap > header,
body.o_custom_login_body #wrapwrap > footer,
body.o_custom_login_body .o_frontend_to_backend_nav {
display: none !important;
height: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
}
body.o_custom_login_body #wrapwrap {
padding-top: 0 !important;
margin-top: 0 !important;
background: transparent !important;
}
/* 2. Main Wrapper: Immersive Ambient Split */
.o_login_main_wrapper {
display: flex !important;
height: 100vh !important;
min-height: 100vh !important;
width: 100vw !important;
overflow: hidden;
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;
background-repeat: no-repeat !important;
background-position: center center !important;
background-size: cover !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;
}
/* Left Side: Background Image and Branding */
/* 3. Left Side: Brand Hero Showcase */
.o_login_left_side {
flex: 1.5 !important;
flex: 1.25 !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;
padding: 60px 80px !important;
position: relative !important;
z-index: 1 !important;
}
.o_login_content {
text-align: center;
max-width: 80%;
.o_login_hero_content {
max-width: 540px !important;
}
.o_login_content h1 {
font-size: 3.8rem !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;
margin-bottom: 20px;
text-shadow: 2px 4px 15px rgba(0, 0, 0, 0.6) !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_login_content p {
font-size: 1.4rem !important;
opacity: 0.9;
.o_hero_subtitle {
font-size: 1.1rem !important;
color: #94a3b8 !important;
line-height: 1.6 !important;
margin-bottom: 36px !important;
}
/* Right Side: Form Section */
.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;
/* background: #ffffff !important; */
display: flex !important;
justify-content: center !important;
align-items: center !important;
padding: 40px !important;
position: relative;
justify-content: center !important;
padding: 40px 24px !important;
position: relative !important;
z-index: 2 !important;
}
.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;
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;
}
/* Header Text Gradient */
.o_login_header_text h3 {
font-size: 2rem !important;
font-weight: 800 !important;
margin-bottom: 10px;
color: #e50000 !important;
}
/* Logo */
/* Brand Header */
.o_login_logo_container {
text-align: center;
margin-bottom: 30px;
text-align: center !important;
margin-bottom: 20px !important;
}
.o_login_logo_container img {
max-height: 120px;
/* Larger logo */
width: auto;
filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.2));
.o_login_brand_header {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
gap: 10px !important;
}
/* 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;
.o_brand_icon {
width: 44px !important;
height: 44px !important;
background: linear-gradient(135deg, #e50000 0%, #b91c1c 100%) !important;
border-radius: 12px !important;
padding: 12px 15px !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;
color: white !important;
transition: all 0.2s ease !important;
box-shadow: none !important;
}
.form-control::placeholder {
color: rgba(255, 255, 255, 0.6) !important;
.o_login_form_container .form-control::placeholder,
.oe_login_form .form-control::placeholder {
color: rgba(255, 255, 255, 0.4) !important;
}
/* Accessibility contrast for error messages */
.alert-danger {
background-color: rgba(220, 53, 69, 0.9) !important;
color: white !important;
border: none !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;
}
/* Login Button with Gradient */
/* 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: #e50000 !important;
background: linear-gradient(135deg, #e50000 0%, #b91c1c 100%) !important;
border: none !important;
border-radius: 12px !important;
padding: 14px !important;
padding: 13px !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;
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: #111111 !important;
color: #ffffff !important;
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(229, 0, 0, 0.4) !important;
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;
}
/* Responsive Handling */
/* 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;
flex-direction: column !important;
justify-content: center !important;
align-items: center !important;
}
.o_login_left_side {
@ -177,162 +449,11 @@ select.form-control option,
}
.o_login_right_side {
background: #0f172a !important;
width: 100% !important;
padding: 24px 16px !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;
.o_login_card {
padding: 28px 24px !important;
}
}
/* Hide Website Header/Footer ONLY on Login, Signup, Reset Password */
body.o_custom_login_body header,
body.o_custom_login_body footer,
body.o_custom_login_body .o_footer_copyright,
body.o_custom_login_body #o_main_nav,
body.o_custom_login_body .o_header_standard,
body.o_custom_login_body #wrapwrap>header,
body.o_custom_login_body #wrapwrap>footer {
display: none !important;
height: 0 !important;
width: 0 !important;
visibility: hidden !important;
opacity: 0 !important;
position: absolute !important;
pointer-events: none !important;
}
/* Ensure wrapwrap doesn't have padding/margin from header on these pages */
body.o_custom_login_body #wrapwrap {
padding-top: 0 !important;
margin-top: 0 !important;
}
/* Ensure the wrapper covers the entire screen, ignoring website container constraints */
body.o_custom_login_body .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;
justify-content: 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,
.oe_website_login_container .oe_signup_form,
.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 */
header .btn-primary,
header .btn-warning,
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,
header .btn-outline-warning,
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;
}

View File

@ -160,70 +160,130 @@ body,
}
/* ---------------------------------------------------------
TABLES (LIST VIEW)
WORKSPACE & VIEWS CONTAINER (CUSTOM DINE360 ERP UI)
--------------------------------------------------------- */
.o_list_view .o_list_table {
background: #ffffff !important;
border: 1px solid #e2e8f0 !important;
border-radius: 12px !important;
overflow: hidden;
box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.04) !important;
.o_action_manager .o_content {
background-color: #f8fafc !important;
padding: 18px 24px !important;
}
/* Table Header */
.o_list_view .o_list_table thead {
/* Elevated Card Wrapper for List View */
.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;
color: #64748b !important;
border-bottom: 1px solid #e2e8f0 !important;
}
.o_list_view .o_list_table thead th {
border: none !important;
text-transform: uppercase;
font-size: 11px !important;
letter-spacing: 0.05em;
.o_list_table thead th {
background: #f8fafc !important;
color: #475569 !important;
font-size: 11.5px !important;
font-weight: 700 !important;
padding: 12px 14px !important;
color: #64748b !important;
text-transform: uppercase !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 */
.o_list_view .o_list_table tbody tr.o_data_row {
transition: all 0.15s ease !important;
/* Rows with smooth hover */
.o_list_table tbody tr.o_data_row {
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;
}
.o_list_view .o_list_table tbody tr.o_data_row td {
padding: 12px 14px !important;
color: #334155 !important;
.o_list_table tbody tr.o_data_row td {
padding: 14px 16px !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-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 {
background: #ffffff !important;
border: 1px solid #e2e8f0 !important;
border-radius: 14px !important;
box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.05) !important;
border-radius: 16px !important;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.05) !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;
overflow: hidden;
}
/* Hover Effect for Cards */
.o_kanban_renderer .o_kanban_record:hover,
.o_kanban_view .o_kanban_record:hover {
transform: translateY(-3px) !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 */
@ -231,14 +291,16 @@ body,
.o_kanban_record strong {
color: #0f172a !important;
font-weight: 700 !important;
font-size: 14px !important;
font-size: 15px !important;
}
/* Kanban Group Headers */
.o_kanban_view .o_kanban_group_header {
background: transparent !important;
font-weight: 700 !important;
color: #475569 !important;
color: #334155 !important;
font-size: 14px !important;
padding: 8px 12px !important;
}

View 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;
}
});
}
});

View 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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>

View 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 &amp; 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>

View 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 &amp; 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&amp;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>

View File

@ -8,97 +8,23 @@
<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&amp;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 -->
<!-- ============================================================== -->
<t t-if="is_saas_master">
<div class="o_saas_layout">
<!-- Left Sidebar -->
<aside class="o_saas_sidebar">
<div class="o_sidebar_top">
<!-- Logo Brand -->
<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&amp;view_type=form" class="o_promo_btn">
Learn More <i class="fa fa-arrow-right ms-1"/>
</a>
</div>
</aside>
<!-- Left Sidebar (Shared Component) -->
<t t-call="dine360_dashboard.dashboard_sidebar">
<t t-set="active_page" t-value="'dashboard'"/>
</t>
<!-- Main Dashboard Content -->
<main class="o_saas_main">
<!-- Top Header Bar -->
<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 &amp; 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>
<!-- Top Header Bar (Shared Component) -->
<t t-call="dine360_dashboard.dashboard_topbar"/>
<!-- Page Title & Status Header -->
<section class="o_page_header">
@ -116,9 +42,9 @@
<span>Sep 26, 2026 - Oct 26, 2026</span>
<i class="fa fa-angle-down ms-2 text-muted"/>
</div>
<a href="/web#action=dine360_saas_master.action_saas_restaurant&amp;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
</a>
</button>
</div>
</section>
@ -414,10 +340,10 @@
<span class="o_count_pill"><t t-esc="saas_stats['total']"/> Registered</span>
</div>
<div class="d-flex align-items-center gap-2">
<a href="/web#action=dine360_saas_master.action_saas_restaurant&amp;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
</a>
<a href="/web#action=dine360_saas_master.action_saas_restaurant" class="o_btn_white_small">
</button>
<a href="/dashboard/restaurants" class="o_btn_white_small">
View All
</a>
</div>
@ -532,7 +458,7 @@
<div class="o_info_label">Plan Details</div>
<div class="o_info_val fw-bold">Starter • Monthly</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>
<!-- Next Billing Date -->
@ -554,94 +480,15 @@
<!-- ============================================================== -->
<t t-else="">
<div class="o_saas_layout">
<!-- Left Sidebar -->
<aside class="o_saas_sidebar">
<div class="o_sidebar_top">
<!-- Logo Brand -->
<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>
<!-- Left Sidebar (Shared Component) -->
<t t-call="dine360_dashboard.dashboard_sidebar">
<t t-set="active_page" t-value="'dashboard'"/>
</t>
<!-- Main Operations Content -->
<main class="o_saas_main">
<!-- Top Header Bar -->
<header class="o_saas_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 &amp; 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>
<!-- Top Header Bar (Shared Component) -->
<t t-call="dine360_dashboard.dashboard_topbar"/>
<!-- Page Title & Status Header -->
<section class="o_page_header">
@ -877,7 +724,7 @@
</div>
</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">
<i class="fa fa-th"/>
</div>
@ -907,7 +754,7 @@
</div>
</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">
<i class="fa fa-cubes"/>
</div>
@ -1108,7 +955,7 @@
<div class="o_info_label">Ingredient Reorder</div>
<div class="o_info_val fw-bold">Stock Management</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 class="o_next_billing_box">
@ -1123,6 +970,82 @@
</main>
</div>
</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">&amp;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 &amp; Launch Database
</button>
</div>
</form>
</div>
</div>
</t>
</template>
</odoo>

View File

@ -1,45 +1,92 @@
<odoo>
<template id="custom_login_layout" inherit_id="web.login_layout" name="Custom Login Layout" priority="10">
<xpath expr="//t[@t-call='web.frontend_layout']" position="inside">
<t t-set="body_classname" t-value="'o_custom_login_body'"/>
<t t-set="no_header" t-value="True"/>
<t t-set="no_footer" t-value="True"/>
<t t-set="head">
<link rel="stylesheet" type="text/css" href="/dine360_dashboard/static/src/css/login_style.css"/>
</t>
</xpath>
<template id="custom_login_layout" inherit_id="web.login_layout" name="Custom Dine360 Login Layout" priority="99">
<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="no_header" t-value="True"/>
<t t-set="no_footer" t-value="True"/>
<t t-set="title">Dine360 Restaurant - Login</t>
<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 href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&amp;display=swap" rel="stylesheet"/>
</t>
<xpath expr="//div[contains(@class,'container')]" position="replace">
<div class="o_login_main_wrapper">
<div class="o_login_left_side">
<!-- Space for right alignment -->
</div>
<div class="o_login_main_wrapper">
<!-- Left Side: Culinary Branding Hero Showcase -->
<div class="o_login_left_side">
<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 &amp; 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 &amp; Online Orders</h6>
<p>Zero-friction customer self-ordering and delivery dispatch</p>
</div>
</div>
</div>
</div>
</div>
<div class="o_login_right_side">
<div class="o_login_card">
<div class="o_login_logo_container">
<img t-attf-src="/web/binary/company_logo{{ '?db=' + request.db if request.db else '' }}" alt="Logo"/>
</div>
<div class="o_login_header_text text-center">
<h3>Login</h3>
<p style="color: rgba(255, 255, 255, 0.6) !important;">Welcome back! Please enter your details.</p>
</div>
<div class="o_login_form_container">
<t t-out="0"/>
</div>
<div class="o_login_footer text-center mt-4">
<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>
<!-- Right Side: Glassmorphic Login Card -->
<div class="o_login_right_side">
<div class="o_login_card">
<div class="o_login_logo_container">
<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 class="o_login_header_text text-center">
<h3>Welcome Back</h3>
<p>Sign in to access your restaurant operations dashboard</p>
</div>
<div class="o_login_form_container">
<t t-out="0"/>
</div>
<div class="o_login_footer text-center mt-3">
<a href="/web/database/selector" t-if="not disable_database_manager" class="o_login_link small text-decoration-none">
<i class="fa fa-database me-1"/>Switch Database
</a>
</div>
<div class="o_login_brand_footer text-center mt-3">
<p>Powered by <a href="https://dine360.com/" target="_blank">Dine360</a> Cloud ERP</p>
</div>
</div>
</div>
</div>
<div class="o_login_footer_custom">
<p>Powered by <a href="https://dine360.com/" target="_blank">Dine360 Inc</a></p>
</div>
</div>
</t>
</xpath>
</template>
</odoo>

View 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 &amp; 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 &amp; 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>

View 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 &amp; 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 &amp; 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>

View 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 &amp; Time Slot</th>
<th>Party &amp; 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 &amp; 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&amp;model=restaurant.reservation&amp;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">&amp;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 &amp; 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 &amp; Notify Guest
</button>
</div>
</form>
</div>
</div>
</t>
</template>
</odoo>

View 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">&amp;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 &amp; Launch Database
</button>
</div>
</form>
</div>
</div>
</t>
</template>
</odoo>

View 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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>

View 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 &amp; 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">&amp;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 &amp; Add to Floor
</button>
</div>
</form>
</div>
</div>
</t>
</template>
</odoo>

View File

@ -1,25 +1,10 @@
<odoo>
<template id="shivasakthi_web_layout" inherit_id="web.layout" name="Shivasakthi Title">
<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 expr="//link[@rel='shortcut icon']" position="replace">
<link rel="shortcut icon" href="/dine360_theme_shivasakthi/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>
<link rel="shortcut icon" href="/dine360_dashboard/static/description/icon.png" type="image/x-icon"/>
</xpath>
</template>
</odoo>

View File

@ -79,13 +79,17 @@ class PosOrderLine(models.Model):
if line.product_id.is_kitchen_item and line.product_id.name != 'Water':
# Send to global 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 = {
'line_id': line.id,
'order_id': line.order_id.id,
'product_name': line.product_id.name,
'qty': line.qty,
'table_name': line.table_id.name if line.table_id else '',
'floor_name': line.floor_id.name if line.floor_id else '',
'table_name': table.name if table else '',
'floor_name': floor.name if floor else '',
'order_source': source,
'customer_note': line.customer_note or '',
'preparation_status': line.preparation_status,
'create_date': line.create_date.isoformat() if line.create_date else '',

View File

@ -8,3 +8,13 @@ class ProductTemplate(models.Model):
default=False,
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,
)

View File

@ -13,8 +13,8 @@ class SaleOrder(models.Model):
res = super(SaleOrder, self).action_confirm()
for order in self:
# Check if it's a website order (usually has website_id)
if order.website_id:
# Check if it's a website order (usually has website_id or online source)
if order.website_id or getattr(order, 'order_source', False) == 'online':
try:
self._create_pos_order_for_kds(order)
except Exception as e:
@ -30,23 +30,29 @@ class SaleOrder(models.Model):
PosSession = self.env['pos.session']
PosConfig = self.env['pos.config']
# 1. Find a suitable POS Config (e.g., 'Website' or first available restaurant)
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
# 1. Look for currently open POS session first
session = PosSession.search([
('config_id', '=', config.id),
('state', '=', 'opened')
('state', '=', 'opened'),
('config_id.module_pos_restaurant', '=', True)
], limit=1)
if not session:
_logger.warning(f"No open POS session found for config {config.name}. Cannot send to KDS.")
return
# 3. Create POS Order Lines
if not session:
session = PosSession.search([('state', '=', 'opened')], limit=1)
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 = []
for line in sale_order.order_line:
if not line.product_id:
@ -56,11 +62,6 @@ class SaleOrder(models.Model):
if qty <= 0:
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, {
'product_id': line.product_id.id,
'qty': qty,
@ -71,11 +72,11 @@ class SaleOrder(models.Model):
'tax_ids': [(6, 0, line.tax_id.ids)],
# Online orders: hold for cashier confirmation before sending to KDS
'preparation_status': False,
'customer_note': 'Web Order',
'customer_note': getattr(line, 'customer_note', False) or 'Web Order',
}))
if not lines_data:
return
return False
# Generate proper POS reference matching Odoo's regex pattern '([0-9-]){14,}'
# 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}"
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
# Online orders will be sent to KDS only after cashier confirmation
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
_logger.info(f"Created POS Order {pos_order.name} from Website Order {sale_order.name} (pending cashier confirmation).")
return pos_order

View File

@ -25,6 +25,12 @@
'dine360_online_orders/static/src/css/service_mode.css',
'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,
'application': False,

View File

@ -171,15 +171,14 @@ class SaleOrderOnline(models.Model):
website sale order is confirmed.
"""
# 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
# We look for the most recent POS order linked to this sale order's partner
# with the note containing the sale order name
PosOrder = self.env['pos.order']
pos_order = PosOrder.search([
('note', 'like', sale_order.name),
], order='id desc', limit=1)
# Fallback search if super didn't return record directly
if not pos_order:
PosOrder = self.env['pos.order']
pos_order = PosOrder.search([
('note', 'like', sale_order.name),
], order='id desc', limit=1)
if pos_order:
pos_order.write({
@ -189,8 +188,6 @@ class SaleOrderOnline(models.Model):
'online_order_date': fields.Datetime.now(),
'order_source': sale_order.order_source or 'online',
'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
@ -225,10 +222,9 @@ class SaleOrderOnline(models.Model):
except Exception as 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:
if line.product_id.is_kitchen_item:
line.write({'preparation_status': 'waiting'})
line.write({'preparation_status': False})
# Send bus notification to POS
if pos_order.config_id:

View File

@ -17,7 +17,7 @@ export class OnlineOrdersScreen extends Component {
// Direct access to services to avoid 'methods is not iterable' error
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.state = useState({
@ -155,7 +155,13 @@ export class OnlineOrdersScreen extends Component {
}
formatCurrency(amount) {
return this.env.utils.formatCurrency(amount);
if (this.env?.utils?.formatCurrency) {
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() {

View File

@ -224,10 +224,11 @@ class SaasRestaurant(models.Model):
modules_to_install = [
'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:
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
self.status = 'provisioning'

View File

@ -11,7 +11,7 @@
- Integration with POS and KDS
""",
'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': [
'views/restaurant_table_views.xml',
'views/self_order_templates.xml',

View File

@ -62,11 +62,9 @@ class Dine360SelfOrderController(http.Controller):
try:
# 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([
('state', '=', 'opened'),
('config_id.module_pos_self_order', '=', True) # Or a custom flag
('config_id.module_pos_restaurant', '=', True)
], limit=1)
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
# Prepare order values
table_id = data.get('table_id')
lines = data.get('lines', [])
order_vals = {
'session_id': session.id,
'partner_id': partner.id if partner else False,
'table_id': int(table_id) if table_id else False,
'order_source': 'qr' if table_id else 'kiosk',
'fulfilment_type': data.get('fulfilment_type', 'dine_in'),
'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
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', ''),
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',
}))
# Use sudo to create the order since it's from public flow
if not order_lines:
return {'error': 'No valid products in order'}
amount_total = total_untaxed + total_tax
order_vals = {
'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,
'table_id': pos_table.id if pos_table else False,
'order_source': 'qr' if pos_table else 'kiosk',
'fulfilment_type': data.get('fulfilment_type', 'dine_in'),
'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,
}
order = request.env['pos.order'].sudo().create([order_vals])
# To trigger KDS, we might need to call specific methods or the confirmation flow
# For self-order, we often mark as "Pending Payment" or "Paid" depending on flow
# If kiosk, it might be unpaid until cashier handles it.
request.env.flush_all()
# Ensure related fields are populated and notify KDS
kitchen_lines = order.lines.filtered(
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 {
'success': True,
'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!")
}

View File

@ -1,14 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<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 -->
<record id="view_restaurant_floor_tree" model="ir.ui.view">
<field name="name">restaurant.floor.tree.dine360</field>
<field name="model">restaurant.floor</field>
<field name="arch" type="xml">
<tree string="Restaurant Floors">
<tree string="Restaurant Floors" sample="1">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="pos_config_ids" widget="many2many_tags"/>
<field name="name" string="Floor Name" class="fw-bold text-dark"/>
<field name="table_ids" widget="many2many_tags" string="Tables Placed"/>
<field name="pos_config_ids" widget="many2many_tags" string="Point of Sale Registers"/>
</tree>
</field>
</record>
@ -21,8 +83,8 @@
<form string="Restaurant Floor">
<sheet>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<label for="name" class="oe_edit_only text-muted small fw-bold"/>
<h1 class="text-danger fw-bold">
<field name="name" placeholder="e.g. Ground Floor, Garden, VIP..."/>
</h1>
</div>
@ -36,9 +98,9 @@
<page string="Tables on Floor" name="tables">
<field name="table_ids" context="{'default_floor_id': active_id}">
<tree string="Tables" editable="bottom">
<field name="name"/>
<field name="seats"/>
<field name="shape"/>
<field name="name" string="Table Label"/>
<field name="seats" string="Seats"/>
<field name="shape" string="Shape"/>
</tree>
</field>
</page>
@ -52,8 +114,8 @@
<record id="action_restaurant_floor_form" model="ir.actions.act_window">
<field name="name">Floors</field>
<field name="res_model">restaurant.floor</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_restaurant_floor_tree"/>
<field name="view_mode">kanban,tree,form</field>
<field name="view_id" ref="view_restaurant_floor_kanban"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new floor plan!

View File

@ -1,19 +1,119 @@
<?xml version="1.0" encoding="utf-8"?>
<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">
<field name="name">restaurant.table.tree.dine360</field>
<field name="model">restaurant.table</field>
<field name="arch" type="xml">
<tree string="Restaurant Tables">
<field name="floor_id"/>
<field name="name" string="Table Name"/>
<field name="seats" string="Seat Count"/>
<field name="shape"/>
<tree string="Restaurant Tables" sample="1">
<field name="name" string="Table Label / Number" class="fw-bold text-dark"/>
<field name="floor_id" string="Dining Floor" widget="badge" decoration-info="floor_id"/>
<field name="seats" string="Seating Capacity" widget="badge" decoration-danger="seats &gt;= 6" decoration-success="seats &lt; 6 and seats &gt;= 4" decoration-info="seats &lt; 4"/>
<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>
</field>
</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', '&gt;=', 4)]"/>
<filter string="Couples / Small (1-2 Seats)" name="filter_small" domain="[('seats', '&lt;=', 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 -->
<record id="view_restaurant_table_form" model="ir.ui.view">
<field name="name">restaurant.table.form.dine360</field>
@ -22,18 +122,18 @@
<form string="Restaurant Table">
<sheet>
<div class="oe_title">
<label for="name" string="Table Name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="e.g. Table 1, T2..."/>
<label for="name" string="Table Name / Number" class="oe_edit_only text-muted small fw-bold"/>
<h1 class="text-danger fw-bold">
<field name="name" placeholder="e.g. Table 1, VIP-1..."/>
</h1>
</div>
<group>
<group string="Basic Configuration" name="basic_config">
<field name="floor_id" options="{'no_create': True}"/>
<field name="seats" string="Seat Count"/>
<group string="Dining Setup" name="basic_config">
<field name="floor_id" options="{'no_create': True}" required="1"/>
<field name="seats" string="Seating Capacity (Seats)"/>
<field name="shape"/>
</group>
<group string="POS Position &amp; Size" name="pos_layout">
<group string="Point of Sale Visual Coordinates" name="pos_layout">
<field name="position_h"/>
<field name="position_v"/>
<field name="width"/>
@ -49,14 +149,32 @@
<record id="action_restaurant_table_form" model="ir.actions.act_window">
<field name="name">Tables</field>
<field name="res_model">restaurant.table</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_restaurant_table_tree"/>
<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 a new table!
Create your first dining table!
</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>
</field>
</record>