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: except Exception as e:
_logger.warning("Error fetching saas.plan: %s", e) _logger.warning("Error fetching saas.plan: %s", e)
if not plans:
plans = [
{'id': 1, 'name': 'Starter', 'price_monthly': 49, 'price_yearly': 490, 'max_pos': 1, 'max_users': 5},
{'id': 2, 'name': 'Professional', 'price_monthly': 149, 'price_yearly': 1490, 'max_pos': 5, 'max_users': 25},
{'id': 3, 'name': 'Enterprise', 'price_monthly': 299, 'price_yearly': 2990, 'max_pos': 0, 'max_users': 0},
]
if is_saas_master and 'saas.restaurant' in request.env: if is_saas_master and 'saas.restaurant' in request.env:
try: try:
records = request.env['saas.restaurant'].sudo().search([], order='id desc') records = request.env['saas.restaurant'].sudo().search([], order='id desc')
@ -81,18 +74,18 @@ def get_dashboard_context(active_page='dashboard'):
elif r.status in ('expired', 'suspended', 'failed'): elif r.status in ('expired', 'suspended', 'failed'):
saas_stats['expired'] += 1 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({ onboarded_restaurants.append({
'id': r.id, 'id': r.id,
'name': r.name, 'name': r.name,
'owner_name': r.owner_name or 'Admin', 'owner_name': r.owner_name or 'Admin',
'email': r.email or 'owner@dine360.com', 'email': r.email or '',
'phone': r.phone or '', 'phone': r.phone or '',
'city': r.city or '', 'city': r.city or '',
'database_name': r.database_name or '', 'database_name': r.database_name or '',
'subdomain': r.subdomain or '', 'subdomain': r.subdomain or '',
'status': r.status or 'active', 'status': r.status or 'active',
'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', 'billing_cycle': 'Monthly' if r.billing_cycle == 'monthly' else 'Annual',
'start_date': str(r.start_date) if r.start_date else '', 'start_date': str(r.start_date) if r.start_date else '',
'expiry_date': expiry_fmt, 'expiry_date': expiry_fmt,
@ -102,71 +95,66 @@ def get_dashboard_context(active_page='dashboard'):
except Exception as e: except Exception as e:
_logger.warning("Error fetching SaaS restaurants: %s", e) _logger.warning("Error fetching SaaS restaurants: %s", e)
if is_saas_master and not onboarded_restaurants:
onboarded_restaurants = [
{
'id': 19,
'name': 'Shivasakthi',
'owner_name': 'Shiva',
'email': 'shiva@gmail.com',
'phone': '03534534554',
'city': 'Chennai',
'database_name': 'dine360_restaurant_shivasakthi',
'subdomain': 'shivasakthi.dine360.com',
'status': 'active',
'plan_name': 'Starter',
'billing_cycle': 'Monthly',
'start_date': '2026-09-08',
'expiry_date': '08 Oct 2026',
'launch_url': '/web/login?db=dine360_restaurant_shivasakthi',
'manage_url': '/web#id=19&model=saas.restaurant&view_type=form',
}
]
saas_stats = {'total': 1, 'active': 1, 'provisioning': 0, 'expired': 0}
first_res = onboarded_restaurants[0] if onboarded_restaurants else None first_res = onboarded_restaurants[0] if onboarded_restaurants else None
# SaaS 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 = { saas_dashboard_data = {
'first_restaurant': first_res, 'first_restaurant': first_res,
'reservations_count': 245, 'reservations_count': res_count,
'orders_count': '1,248', 'orders_count': f"{orders_count:,}",
'avg_order_value': '₹642', 'avg_order_value': f"{symbol}{avg_val:,.2f}",
'onboarding_percent': 25, 'onboarding_percent': onboard_pct,
'date_range': 'Sep 26, 2026 - Oct 26, 2026', 'date_range': f"{(datetime.now() - timedelta(days=30)).strftime('%b %d, %Y')} - {datetime.now().strftime('%b %d, %Y')}",
'system_status': 'All systems operational', 'system_status': 'All systems operational',
'recent_activities': [ 'recent_activities': 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'
}
]
} }
# 2. Table & Floor Management Data # 2. Table & Floor Management Data
@ -195,7 +183,6 @@ def get_dashboard_context(active_page='dashboard'):
for t in all_tables: for t in all_tables:
seats = getattr(t, 'seats', 4) or 4 seats = getattr(t, 'seats', 4) or 4
total_seats += seats total_seats += seats
# Check simulated/occupied status
is_occ = getattr(t, 'is_occupied', False) or getattr(t, 'state', '') in ('occupied', 'reserved') is_occ = getattr(t, 'is_occupied', False) or getattr(t, 'state', '') in ('occupied', 'reserved')
if is_occ: if is_occ:
occupied_count += 1 occupied_count += 1
@ -214,24 +201,6 @@ def get_dashboard_context(active_page='dashboard'):
except Exception as e: except Exception as e:
_logger.warning("Error fetching restaurant.table: %s", 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 # 3. Reservations Data
reservations_list = [] reservations_list = []
reservation_stats = {'total': 0, 'confirmed': 0, 'draft': 0, 'completed': 0, 'cancelled': 0} 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: if st in reservation_stats:
reservation_stats[st] += 1 reservation_stats[st] += 1
start_fmt = r.start_time.strftime('%I:%M %p') if r.start_time else '07:30 PM' 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 'Today' date_fmt = r.start_time.strftime('%d %b %Y') if r.start_time else ''
whatsapp_url = r.whatsapp_url whatsapp_url = getattr(r, 'whatsapp_url', False)
if not whatsapp_url and r.phone: if not whatsapp_url and getattr(r, 'phone', False):
whatsapp_url = f"https://wa.me/{r.phone.replace('+', '').replace(' ', '')}?text=Hello%20{r.customer_name},%20your%20table%20reservation%20is%20confirmed!" 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({ reservations_list.append({
'id': r.id, 'id': r.id,
@ -261,8 +231,8 @@ def get_dashboard_context(active_page='dashboard'):
'num_people': r.num_people or 2, 'num_people': r.num_people or 2,
'start_time_fmt': start_fmt, 'start_time_fmt': start_fmt,
'date_fmt': date_fmt, 'date_fmt': date_fmt,
'floor_name': r.floor_id.name if r.floor_id else '', 'floor_name': r.floor_id.name if getattr(r, 'floor_id', False) else '',
'table_name': r.table_id.name if r.table_id else (r.table_ids[0].name if r.table_ids 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', 'occasion': r.occasion or 'casual',
'special_requests': r.special_requests or '', 'special_requests': r.special_requests or '',
'state': st, 'state': st,
@ -272,88 +242,14 @@ def get_dashboard_context(active_page='dashboard'):
except Exception as e: except Exception as e:
_logger.warning("Error fetching reservations: %s", 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 # 4. Online Orders Data
currency = request.env.company.currency_id if request.env.company else None
symbol = currency.symbol if currency and currency.symbol else '₹'
online_orders_data = { online_orders_data = {
'has_online_orders': True, 'has_online_orders': False,
'pending_count': 3, 'pending_count': 0,
'confirmed_count': 18, 'confirmed_count': 0,
'today_count': 28, 'today_count': 0,
'total_revenue_formatted': f"{symbol}48,250.00", 'total_revenue_formatted': f"{symbol}0.00",
'avg_order_value': f"{symbol}0.00",
'orders': [], 'orders': [],
'shop_url': '/shop', 'shop_url': '/shop',
} }
@ -362,7 +258,28 @@ def get_dashboard_context(active_page='dashboard'):
if 'sale.order' in request.env: if 'sale.order' in request.env:
SaleOrder = request.env['sale.order'].sudo() SaleOrder = request.env['sale.order'].sudo()
domain = [('order_source', '=', 'online')] if 'order_source' in SaleOrder._fields else [('website_id', '!=', False)] 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: if recent:
orders_list = [] orders_list = []
for o in recent: 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 '', 'phone': o.partner_id.phone or o.partner_id.mobile or '',
'amount': f"{symbol}{o.amount_total:,.2f}", 'amount': f"{symbol}{o.amount_total:,.2f}",
'status': getattr(o, 'online_order_status', o.state) or 'confirmed', '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", 'url': f"/web#id={o.id}&model=sale.order&view_type=form",
}) })
online_orders_data['orders'] = orders_list online_orders_data['orders'] = orders_list
online_orders_data['today_count'] = len(recent)
except Exception as e: except Exception as e:
_logger.warning("Error fetching sale.order: %s", e) _logger.warning("Error fetching sale.order: %s", e)
if not online_orders_data['orders']: # 5. Backups Data
online_orders_data['orders'] = [ backups_list = []
{ if is_saas_master and 'saas.backup' in request.env:
'id': 101, try:
'name': '#ORD-1082', records = request.env['saas.backup'].sudo().search([], order='timestamp desc', limit=20)
'customer': 'Shiva Kumar', for b in records:
'phone': '+91 98401 23456', db_name = b.database_name or (b.restaurant_id.database_name if getattr(b, 'restaurant_id', False) else '')
'amount': f"{symbol}850.00", fname = b.backup_file or f"{db_name}_backup.zip"
'status': 'cooking', backups_list.append({
'date': 'Today, 19:42', 'id': b.id,
'url': '/shop', 'database_name': db_name,
}, 'filename': fname,
{ 'backup_type': b.storage_type.upper() if getattr(b, 'storage_type', False) else 'Local',
'id': 102, 'size': 'Archive',
'name': '#ORD-1081', 'date': b.timestamp.strftime('%d %b %Y, %I:%M %p') if getattr(b, 'timestamp', False) else '',
'customer': 'Priya Ramesh', 'download_url': b.s3_url or '#',
'phone': '+91 97102 34567', })
'amount': f"{symbol}1,240.00", except Exception as e:
'status': 'confirmed', _logger.warning("Error fetching saas.backup: %s", e)
'date': 'Today, 19:35',
'url': '/shop', # 6. Low stock products
}, low_stock = []
{ if 'product.product' in request.env:
'id': 103, try:
'name': '#ORD-1080', low_stock = request.env['product.product'].sudo().search([
'customer': 'Rajan Selvam', ('type', '=', 'product'),
'phone': '+91 94443 45678', ('qty_available', '<=', 5)
'amount': f"{symbol}620.00", ], limit=5)
'status': 'pending', except Exception:
'date': 'Today, 19:28', pass
'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',
}
]
stats = SafeStats({ stats = SafeStats({
'company_name': company_name, 'company_name': company_name,
'low_stock_products': [], 'low_stock_products': low_stock,
}) })
return { return {
@ -440,14 +343,15 @@ def get_dashboard_context(active_page='dashboard'):
'plans': plans, 'plans': plans,
'table_stats': table_stats, 'table_stats': table_stats,
'tables': tables_list, 'tables': tables_list,
'available_tables': tables_list, 'available_tables': [t for t in tables_list if not t.get('is_occupied')],
'floors': floors_list, 'floors': floors_list,
'reservation_stats': reservation_stats, 'reservation_stats': reservation_stats,
'reservations': reservations_list, 'reservations': reservations_list,
'online_orders_data': online_orders_data, 'online_orders_data': online_orders_data,
'current_date_str': datetime.now().strftime('%d %b %Y'), 'current_date_str': datetime.now().strftime('%d %b %Y'),
'stats': stats, 'stats': stats,
'backups': [], 'low_stock_products': low_stock,
'backups': backups_list,
} }
def render_dashboard(request): def render_dashboard(request):

View File

@ -104,23 +104,10 @@
<tbody> <tbody>
<t t-if="not backups"> <t t-if="not backups">
<tr> <tr>
<td><span class="font-monospace fw-bold text-dark">dine360_restaurant_shivasakthi</span></td> <td colspan="6" class="text-center py-5 text-muted">
<td><span class="text-muted small font-monospace">backup_shivasakthi_20261001.zip</span></td> <i class="fa fa-database fa-3x mb-3 text-secondary"/>
<td><span class="o_badge_status success"><span class="o_badge_dot"/> Automated</span></td> <h5>No database backups found</h5>
<td><span class="fw-bold">46.2 MB</span></td> <p class="small">Tenant backup archives will appear here once generated.</p>
<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> </td>
</tr> </tr>
</t> </t>

View File

@ -39,7 +39,7 @@
</div> </div>
<div class="o_date_picker_btn"> <div class="o_date_picker_btn">
<i class="fa fa-calendar me-2 text-muted"/> <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"/> <i class="fa fa-angle-down ms-2 text-muted"/>
</div> </div>
<button type="button" class="o_btn_red_action" data-open-modal="modalOnboardRestaurant"> <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> <a href="/web#action=dine360_saas_master.action_saas_restaurant" class="o_link_red">View All</a>
</div> </div>
<div class="o_activity_timeline"> <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_activity_row">
<div class="o_act_icon_box"> <div class="o_act_icon_box">
<i t-att-class="act['icon'] + ' text-danger'"/> <i t-att-class="act['icon'] + ' text-danger'"/>
@ -350,6 +356,13 @@
</div> </div>
<div class="o_restaurants_list"> <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"> <t t-foreach="onboarded_restaurants" t-as="restaurant">
<div class="o_res_row_card"> <div class="o_res_row_card">
<!-- Col 1: Logo & Name --> <!-- 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="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"/> <path d="M12 22V25C12 26 13 27 14 27H22C23 27 24 26 24 25V22" stroke="#e50000" stroke-width="2.5"/>
</svg> </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> <div>
<div class="o_res_title"><t t-esc="restaurant['name']"/></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"/> <i class="fa fa-calendar text-muted me-2"/>
<div> <div>
<div class="o_info_label">Next Billing Date</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> </div>
</div> </div>
@ -503,7 +516,7 @@
</div> </div>
<div class="o_date_picker_btn"> <div class="o_date_picker_btn">
<i class="fa fa-calendar me-2 text-muted"/> <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> </div>
<a href="/shop" target="_blank" class="o_btn_red_action"> <a href="/shop" target="_blank" class="o_btn_red_action">
<i class="fa fa-shopping-bag me-1"/> Storefront <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> <span class="o_kpi_trend text-warning"><i class="fa fa-arrow-up"/> 67%</span>
</div> </div>
<div class="o_kpi_label">Occupied Tables</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"> <div class="o_kpi_footer">
<span class="o_kpi_sub">Active floor guests</span> <span class="o_kpi_sub">Active floor guests</span>
<svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none"> <svg class="o_sparkline" viewBox="0 0 100 30" preserveAspectRatio="none">
@ -662,8 +675,8 @@
<div> <div>
<div class="o_substat_title">Reservations</div> <div class="o_substat_title">Reservations</div>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<span class="o_substat_val">185</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"/> +12%</span> <span class="o_substat_pct text-success"><i class="fa fa-arrow-up"/> Live</span>
</div> </div>
</div> </div>
</div> </div>
@ -675,8 +688,8 @@
<div> <div>
<div class="o_substat_title">Online Orders</div> <div class="o_substat_title">Online Orders</div>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<span class="o_substat_val">1,248</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"/> +8%</span> <span class="o_substat_pct text-success"><i class="fa fa-arrow-up"/> Live</span>
</div> </div>
</div> </div>
</div> </div>
@ -688,8 +701,8 @@
<div> <div>
<div class="o_substat_title">Avg. Ticket Value</div> <div class="o_substat_title">Avg. Ticket Value</div>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<span class="o_substat_val">₹642</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"/> +6%</span> <span class="o_substat_pct text-success"><i class="fa fa-arrow-up"/> Live</span>
</div> </div>
</div> </div>
</div> </div>
@ -870,7 +883,15 @@
</tr> </tr>
</thead> </thead>
<tbody> <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> <tr>
<td class="font-monospace fw-bold text-dark"> <td class="font-monospace fw-bold text-dark">
<t t-esc="order_item['name']"/> <t t-esc="order_item['name']"/>

View File

@ -19,117 +19,63 @@
<!-- Pricing Cards Grid --> <!-- Pricing Cards Grid -->
<div class="o_plans_grid"> <div class="o_plans_grid">
<!-- Plan 1: Starter --> <t t-if="not plans">
<div class="o_plan_card"> <div class="text-center py-5 text-muted w-100">
<div class="o_plan_name">Starter</div> <i class="fa fa-credit-card fa-3x mb-3 text-secondary"/>
<div class="o_plan_desc">Ideal for small cafes, takeaway counters, and food trucks.</div> <h5>No subscription plans configured</h5>
<p class="small mb-3">Create SaaS subscription plans in the backend to display tiers here.</p>
<div class="o_plan_price_box"> <a href="/web#action=dine360_saas_master.action_saas_plan" class="o_btn_red_small">
<span class="o_plan_price">$49</span> <i class="fa fa-plus me-1"/> Create Plan in ERP
<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> </a>
</div> </div>
</div> </t>
<!-- Plan 2: Professional (Popular) --> <t t-foreach="plans" t-as="plan">
<div class="o_plan_card popular"> <div t-att-class="'o_plan_card ' + ('popular' if 'pro' in plan.get('name', '').lower() else '')">
<span class="o_plan_popular_badge">Most Popular</span> <t t-if="'pro' in plan.get('name', '').lower()">
<div class="o_plan_name text-danger">Professional</div> <span class="o_plan_popular_badge">Most Popular</span>
<div class="o_plan_desc">For busy dine-in restaurants, bistros, and high-volume dining rooms.</div> </t>
<div t-att-class="'o_plan_name ' + ('text-danger' if 'pro' in plan.get('name', '').lower() else '')">
<div class="o_plan_price_box"> <t t-esc="plan.get('name')"/>
<span class="o_plan_price">$149</span> </div>
<span class="o_plan_period">/ month</span> <div class="o_plan_desc">
Tier plan configured for Dine360 restaurant operations.
</div>
<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> </div>
</t>
<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> </div>
</t> </t>
</template> </template>