add dashboard views and controller for dine360 restaurant management

This commit is contained in:
Alaguraj0361 2026-10-06 11:17:52 +05:30
parent d6b5fce53f
commit e4fd895c5f
4 changed files with 221 additions and 363 deletions

View File

@ -62,13 +62,6 @@ def get_dashboard_context(active_page='dashboard'):
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')
@ -81,18 +74,18 @@ def get_dashboard_context(active_page='dashboard'):
elif r.status in ('expired', 'suspended', 'failed'):
saas_stats['expired'] += 1
expiry_fmt = r.expiry_date.strftime('%d %b %Y') if r.expiry_date else '08 Oct 2026'
expiry_fmt = r.expiry_date.strftime('%d %b %Y') if r.expiry_date else 'N/A'
onboarded_restaurants.append({
'id': r.id,
'name': r.name,
'owner_name': r.owner_name or 'Admin',
'email': r.email or 'owner@dine360.com',
'email': r.email or '',
'phone': r.phone or '',
'city': r.city or '',
'database_name': r.database_name or '',
'subdomain': r.subdomain or '',
'status': r.status or 'active',
'plan_name': r.plan_id.name if r.plan_id else 'Starter',
'plan_name': r.plan_id.name if r.plan_id else 'Standard',
'billing_cycle': 'Monthly' if r.billing_cycle == 'monthly' else 'Annual',
'start_date': str(r.start_date) if r.start_date else '',
'expiry_date': expiry_fmt,
@ -102,71 +95,66 @@ def get_dashboard_context(active_page='dashboard'):
except Exception as 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 master real activities and counts
currency = request.env.company.currency_id if request.env.company else None
symbol = currency.symbol if currency and currency.symbol else '₹'
res_count = 0
if 'restaurant.reservation' in request.env:
try:
res_count = request.env['restaurant.reservation'].sudo().search_count([])
except Exception:
pass
orders_count = 0
total_sales = 0.0
if 'sale.order' in request.env:
try:
orders = request.env['sale.order'].sudo().search([])
orders_count += len(orders)
total_sales += sum(orders.mapped('amount_total'))
except Exception:
pass
if 'pos.order' in request.env:
try:
pos_orders = request.env['pos.order'].sudo().search([])
orders_count += len(pos_orders)
total_sales += sum(pos_orders.mapped('amount_total'))
except Exception:
pass
avg_val = (total_sales / orders_count) if orders_count else 0.0
recent_activities = []
if is_saas_master and 'saas.restaurant' in request.env:
try:
for r in request.env['saas.restaurant'].sudo().search([], order='create_date desc', limit=5):
recent_activities.append({
'title': f"Restaurant {r.status.capitalize() if r.status else 'Registered'}",
'subtitle': f"{r.name} ({r.database_name or 'N/A'})",
'time': r.create_date.strftime('%d %b, %H:%M') if getattr(r, 'create_date', False) else '',
'badge': (r.status or 'active').capitalize(),
'badge_type': 'completed' if r.status == 'active' else 'info',
'icon': 'fa fa-cutlery'
})
except Exception:
pass
tot_restaurants = saas_stats.get('total', 0)
act_restaurants = saas_stats.get('active', 0)
onboard_pct = int((act_restaurants / tot_restaurants) * 100) if tot_restaurants > 0 else 0
saas_dashboard_data = {
'first_restaurant': first_res,
'reservations_count': 245,
'orders_count': '1,248',
'avg_order_value': '₹642',
'onboarding_percent': 25,
'date_range': 'Sep 26, 2026 - Oct 26, 2026',
'reservations_count': res_count,
'orders_count': f"{orders_count:,}",
'avg_order_value': f"{symbol}{avg_val:,.2f}",
'onboarding_percent': onboard_pct,
'date_range': f"{(datetime.now() - timedelta(days=30)).strftime('%b %d, %Y')} - {datetime.now().strftime('%b %d, %Y')}",
'system_status': 'All systems operational',
'recent_activities': [
{
'title': 'Restaurant onboarded',
'subtitle': f"{first_res['name'] if first_res else 'Shivasakthi'} restaurant",
'time': '2 hours ago',
'badge': 'Completed',
'badge_type': 'completed',
'icon': 'fa fa-cutlery'
},
{
'title': 'Subscription renewed',
'subtitle': f"{first_res['name'] if first_res else 'Shivasakthi'} - {first_res['billing_cycle'] if first_res else 'Monthly'}",
'time': '5 hours ago',
'badge': 'Success',
'badge_type': 'success',
'icon': 'fa fa-credit-card'
},
{
'title': 'Database launched',
'subtitle': first_res['database_name'] if first_res else 'dine360_restaurant_shivasakthi',
'time': '8 hours ago',
'badge': '+ Info',
'badge_type': 'info',
'icon': 'fa fa-database'
},
{
'title': 'Admin login',
'subtitle': request.env.user.email or 'admin@dine360.com',
'time': '1 day ago',
'badge': '+ Login',
'badge_type': 'login',
'icon': 'fa fa-user'
}
]
'recent_activities': recent_activities,
}
# 2. Table & Floor Management Data
@ -195,7 +183,6 @@ def get_dashboard_context(active_page='dashboard'):
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
@ -214,24 +201,6 @@ def get_dashboard_context(active_page='dashboard'):
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}
@ -245,12 +214,13 @@ def get_dashboard_context(active_page='dashboard'):
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'
start_fmt = r.start_time.strftime('%I:%M %p') if r.start_time else ''
date_fmt = r.start_time.strftime('%d %b %Y') if r.start_time else ''
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!"
whatsapp_url = getattr(r, 'whatsapp_url', False)
if not whatsapp_url and getattr(r, 'phone', False):
cleaned_phone = r.phone.replace('+', '').replace(' ', '').replace('-', '')
whatsapp_url = f"https://wa.me/{cleaned_phone}?text=Hello%20{r.customer_name or 'Guest'},%20your%20table%20reservation%20is%20confirmed!"
reservations_list.append({
'id': r.id,
@ -261,8 +231,8 @@ def get_dashboard_context(active_page='dashboard'):
'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 ''),
'floor_name': r.floor_id.name if getattr(r, 'floor_id', False) else '',
'table_name': r.table_id.name if getattr(r, 'table_id', False) else (r.table_ids[0].name if getattr(r, 'table_ids', False) else ''),
'occasion': r.occasion or 'casual',
'special_requests': r.special_requests or '',
'state': st,
@ -272,88 +242,14 @@ def get_dashboard_context(active_page='dashboard'):
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': 3,
'confirmed_count': 18,
'today_count': 28,
'total_revenue_formatted': f"{symbol}48,250.00",
'has_online_orders': False,
'pending_count': 0,
'confirmed_count': 0,
'today_count': 0,
'total_revenue_formatted': f"{symbol}0.00",
'avg_order_value': f"{symbol}0.00",
'orders': [],
'shop_url': '/shop',
}
@ -362,7 +258,28 @@ def get_dashboard_context(active_page='dashboard'):
if 'sale.order' in request.env:
SaleOrder = request.env['sale.order'].sudo()
domain = [('order_source', '=', 'online')] if 'order_source' in SaleOrder._fields else [('website_id', '!=', False)]
recent = SaleOrder.search(domain, limit=10, order='id desc')
all_online = SaleOrder.search(domain)
online_orders_data['has_online_orders'] = bool(all_online)
pending_domain = domain + [('state', 'in', ['draft', 'sent'])]
confirmed_domain = domain + [('state', 'in', ['sale', 'done'])]
online_orders_data['pending_count'] = SaleOrder.search_count(pending_domain)
online_orders_data['confirmed_count'] = SaleOrder.search_count(confirmed_domain)
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
today_orders = SaleOrder.search(domain + [('date_order', '>=', today_start)])
online_orders_data['today_count'] = len(today_orders) if today_orders else len(all_online)
revenue_orders = today_orders if today_orders else all_online
tot_rev = sum(revenue_orders.mapped('amount_total')) if revenue_orders else 0.0
online_orders_data['total_revenue_formatted'] = f"{symbol}{tot_rev:,.2f}"
if revenue_orders:
avg_ticket = tot_rev / len(revenue_orders)
online_orders_data['avg_order_value'] = f"{symbol}{avg_ticket:,.2f}"
recent = SaleOrder.search(domain, limit=20, order='id desc')
if recent:
orders_list = []
for o in recent:
@ -373,61 +290,47 @@ def get_dashboard_context(active_page='dashboard'):
'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',
'date': o.date_order.strftime('%d %b, %H:%M') if o.date_order else '',
'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 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',
}
]
# 5. Backups Data
backups_list = []
if is_saas_master and 'saas.backup' in request.env:
try:
records = request.env['saas.backup'].sudo().search([], order='timestamp desc', limit=20)
for b in records:
db_name = b.database_name or (b.restaurant_id.database_name if getattr(b, 'restaurant_id', False) else '')
fname = b.backup_file or f"{db_name}_backup.zip"
backups_list.append({
'id': b.id,
'database_name': db_name,
'filename': fname,
'backup_type': b.storage_type.upper() if getattr(b, 'storage_type', False) else 'Local',
'size': 'Archive',
'date': b.timestamp.strftime('%d %b %Y, %I:%M %p') if getattr(b, 'timestamp', False) else '',
'download_url': b.s3_url or '#',
})
except Exception as e:
_logger.warning("Error fetching saas.backup: %s", e)
# 6. Low stock products
low_stock = []
if 'product.product' in request.env:
try:
low_stock = request.env['product.product'].sudo().search([
('type', '=', 'product'),
('qty_available', '<=', 5)
], limit=5)
except Exception:
pass
stats = SafeStats({
'company_name': company_name,
'low_stock_products': [],
'low_stock_products': low_stock,
})
return {
@ -440,14 +343,15 @@ def get_dashboard_context(active_page='dashboard'):
'plans': plans,
'table_stats': table_stats,
'tables': tables_list,
'available_tables': tables_list,
'available_tables': [t for t in tables_list if not t.get('is_occupied')],
'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': [],
'low_stock_products': low_stock,
'backups': backups_list,
}
def render_dashboard(request):

View File

@ -104,23 +104,10 @@
<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 colspan="6" class="text-center py-5 text-muted">
<i class="fa fa-database fa-3x mb-3 text-secondary"/>
<h5>No database backups found</h5>
<p class="small">Tenant backup archives will appear here once generated.</p>
</td>
</tr>
</t>

View File

@ -39,7 +39,7 @@
</div>
<div class="o_date_picker_btn">
<i class="fa fa-calendar me-2 text-muted"/>
<span>Sep 26, 2026 - Oct 26, 2026</span>
<span><t t-esc="saas_dashboard_data.get('date_range', current_date_str)"/></span>
<i class="fa fa-angle-down ms-2 text-muted"/>
</div>
<button type="button" class="o_btn_red_action" data-open-modal="modalOnboardRestaurant">
@ -238,7 +238,13 @@
<a href="/web#action=dine360_saas_master.action_saas_restaurant" class="o_link_red">View All</a>
</div>
<div class="o_activity_timeline">
<t t-foreach="saas_dashboard_data['recent_activities']" t-as="act">
<t t-if="not saas_dashboard_data.get('recent_activities')">
<div class="text-center py-4 text-muted small">
<i class="fa fa-history fa-2x mb-2 text-secondary d-block"/>
No recent activities recorded yet.
</div>
</t>
<t t-foreach="saas_dashboard_data.get('recent_activities', [])" t-as="act">
<div class="o_activity_row">
<div class="o_act_icon_box">
<i t-att-class="act['icon'] + ' text-danger'"/>
@ -350,6 +356,13 @@
</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 restaurants onboarded yet</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">
<!-- Col 1: Logo & Name -->
@ -359,7 +372,7 @@
<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">S</span>
<span class="o_res_avatar_letter"><t t-esc="restaurant['name'][0].upper() if restaurant.get('name') else 'R'"/></span>
</div>
<div>
<div class="o_res_title"><t t-esc="restaurant['name']"/></div>
@ -466,7 +479,7 @@
<i class="fa fa-calendar text-muted me-2"/>
<div>
<div class="o_info_label">Next Billing Date</div>
<div class="o_info_val fw-bold">08 Oct 2026</div>
<div class="o_info_val fw-bold"><t t-esc="current_date_str"/></div>
</div>
</div>
</div>
@ -503,7 +516,7 @@
</div>
<div class="o_date_picker_btn">
<i class="fa fa-calendar me-2 text-muted"/>
<span>Today, Sep 30, 2026</span>
<span>Today, <t t-esc="current_date_str"/></span>
</div>
<a href="/shop" target="_blank" class="o_btn_red_action">
<i class="fa fa-shopping-bag me-1"/> Storefront
@ -563,7 +576,7 @@
<span class="o_kpi_trend text-warning"><i class="fa fa-arrow-up"/> 67%</span>
</div>
<div class="o_kpi_label">Occupied Tables</div>
<div class="o_kpi_number"><t t-esc="online_orders_data.get('table_stats', {}).get('occupied', 8)"/> / <t t-esc="online_orders_data.get('table_stats', {}).get('total', 12)"/></div>
<div class="o_kpi_number"><t t-esc="table_stats['occupied']"/> / <t t-esc="table_stats['total']"/></div>
<div class="o_kpi_footer">
<span class="o_kpi_sub">Active floor guests</span>
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
@ -662,8 +675,8 @@
<div>
<div class="o_substat_title">Reservations</div>
<div class="d-flex align-items-center gap-2">
<span class="o_substat_val">185</span>
<span class="o_substat_pct text-success"><i class="fa fa-arrow-up"/> +12%</span>
<span class="o_substat_val"><t t-esc="reservation_stats['total']"/></span>
<span class="o_substat_pct text-success"><i class="fa fa-arrow-up"/> Live</span>
</div>
</div>
</div>
@ -675,8 +688,8 @@
<div>
<div class="o_substat_title">Online Orders</div>
<div class="d-flex align-items-center gap-2">
<span class="o_substat_val">1,248</span>
<span class="o_substat_pct text-success"><i class="fa fa-arrow-up"/> +8%</span>
<span class="o_substat_val"><t t-esc="online_orders_data['today_count']"/></span>
<span class="o_substat_pct text-success"><i class="fa fa-arrow-up"/> Live</span>
</div>
</div>
</div>
@ -688,8 +701,8 @@
<div>
<div class="o_substat_title">Avg. Ticket Value</div>
<div class="d-flex align-items-center gap-2">
<span class="o_substat_val">₹642</span>
<span class="o_substat_pct text-success"><i class="fa fa-arrow-up"/> +6%</span>
<span class="o_substat_val"><t t-esc="online_orders_data['avg_order_value']"/></span>
<span class="o_substat_pct text-success"><i class="fa fa-arrow-up"/> Live</span>
</div>
</div>
</div>
@ -870,7 +883,15 @@
</tr>
</thead>
<tbody>
<t t-foreach="online_orders_data['orders']" t-as="order_item">
<t t-if="not online_orders_data.get('orders')">
<tr>
<td colspan="6" class="text-center py-4 text-muted">
<i class="fa fa-shopping-bag fa-2x mb-2 text-secondary d-block"/>
No online orders recorded yet today.
</td>
</tr>
</t>
<t t-foreach="online_orders_data.get('orders', [])" t-as="order_item">
<tr>
<td class="font-monospace fw-bold text-dark">
<t t-esc="order_item['name']"/>

View File

@ -19,117 +19,63 @@
<!-- 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
<t t-if="not plans">
<div class="text-center py-5 text-muted w-100">
<i class="fa fa-credit-card fa-3x mb-3 text-secondary"/>
<h5>No subscription plans configured</h5>
<p class="small mb-3">Create SaaS subscription plans in the backend to display tiers here.</p>
<a href="/web#action=dine360_saas_master.action_saas_plan" class="o_btn_red_small">
<i class="fa fa-plus me-1"/> Create Plan in ERP
</a>
</div>
</div>
</t>
<!-- 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>
<t t-foreach="plans" t-as="plan">
<div t-att-class="'o_plan_card ' + ('popular' if 'pro' in plan.get('name', '').lower() else '')">
<t t-if="'pro' in plan.get('name', '').lower()">
<span class="o_plan_popular_badge">Most Popular</span>
</t>
<div t-att-class="'o_plan_name ' + ('text-danger' if 'pro' in plan.get('name', '').lower() else '')">
<t t-esc="plan.get('name')"/>
</div>
<div class="o_plan_desc">
Tier plan configured for Dine360 restaurant operations.
</div>
<div class="o_plan_price_box">
<span class="o_plan_price">$149</span>
<span class="o_plan_period">/ month</span>
<div class="o_plan_price_box">
<span class="o_plan_price">$<t t-esc="int(plan.get('price_monthly', 0))"/></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"/>
<t t-if="plan.get('max_pos') == 0">Unlimited POS Terminals</t>
<t t-else=""><t t-esc="plan.get('max_pos')"/> POS Terminal(s)</t>
</li>
<li class="o_plan_feature_item">
<i class="fa fa-check-circle o_plan_feature_icon"/>
<t t-if="plan.get('max_users') == 0">Unlimited Staff Users</t>
<t t-else="">Up to <t t-esc="plan.get('max_users')"/> Staff Users</t>
</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"/> Table Reservations &amp; KDS
</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_red_action w-100 text-center py-2 justify-content-center">
Configure Plan
</a>
</div>
</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>
</t>
</div>
</t>
</template>