- Add SafeStats fallback wrapper to prevent QWeb KeyError exceptions - Safely resolve dictionary keys with .get() across all dashboard templates - Add support for production SaaS master database 'dine360db' - Add route mapping for '/dashboard' alongside '/' - Remove invalid tracking attribute from restaurant.table is_occupied field
322 lines
20 KiB
XML
322 lines
20 KiB
XML
<?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.get('total', 0)"/></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.get('active', 0)"/></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.get('provisioning', 0)"/></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.get('expired', 0)"/></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.get('total', 0)"/>)</div>
|
|
<div class="o_filter_pill" data-filter="active">Active (<t t-esc="saas_stats.get('active', 0)"/>)</div>
|
|
<div class="o_filter_pill" data-filter="provisioning">Provisioning (<t t-esc="saas_stats.get('provisioning', 0)"/>)</div>
|
|
<div class="o_filter_pill" data-filter="expired">Expired / Inactive (<t t-esc="saas_stats.get('expired', 0)"/>)</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.get('total', 0)"/> 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.get('name') else 'R'"/></span>
|
|
</div>
|
|
<div>
|
|
<div class="o_res_title"><t t-esc="restaurant['name']"/></div>
|
|
<div class="o_res_plan_sub"><t t-esc="restaurant['plan_name']"/> • <t t-esc="restaurant['billing_cycle']"/></div>
|
|
<t t-if="restaurant['status'] == 'active'">
|
|
<span class="o_res_pill_active">
|
|
<span class="o_pill_dot"/> Active
|
|
</span>
|
|
</t>
|
|
<t t-elif="restaurant['status'] == 'provisioning'">
|
|
<span class="o_res_pill_active" style="background:#fffbeb; color:#b45309; border-color:#fde68a;">
|
|
<span class="o_pill_dot" style="background:#f59e0b;"/> Provisioning
|
|
</span>
|
|
</t>
|
|
<t t-else="">
|
|
<span class="o_res_pill_active" style="background:#fef2f2; color:#b91c1c; border-color:#fecaca;">
|
|
<span class="o_pill_dot" style="background:#ef4444;"/> <t t-esc="restaurant['status']"/>
|
|
</span>
|
|
</t>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Col 2: Owner & Phone & Subdomain -->
|
|
<div class="o_res_col_info">
|
|
<div class="o_info_line">
|
|
<i class="fa fa-user-o o_info_icon text-muted"/>
|
|
<div>
|
|
<div class="o_info_label">Owner</div>
|
|
<div class="o_info_val"><t t-esc="restaurant['owner_name']"/></div>
|
|
</div>
|
|
</div>
|
|
<div class="o_info_line">
|
|
<i class="fa fa-phone o_info_icon text-muted"/>
|
|
<div>
|
|
<div class="o_info_label">Phone</div>
|
|
<div class="o_info_val"><t t-esc="restaurant['phone']"/></div>
|
|
</div>
|
|
</div>
|
|
<div class="o_info_line">
|
|
<i class="fa fa-globe o_info_icon text-muted"/>
|
|
<div>
|
|
<div class="o_info_label">Subdomain</div>
|
|
<div class="o_info_val text-danger"><t t-esc="restaurant['subdomain']"/></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Col 3: Email & Database & Expires -->
|
|
<div class="o_res_col_info">
|
|
<div class="o_info_line">
|
|
<i class="fa fa-envelope-o o_info_icon text-muted"/>
|
|
<div>
|
|
<div class="o_info_label">Email</div>
|
|
<div class="o_info_val"><t t-esc="restaurant['email']"/></div>
|
|
</div>
|
|
</div>
|
|
<div class="o_info_line">
|
|
<i class="fa fa-database o_info_icon text-muted"/>
|
|
<div>
|
|
<div class="o_info_label">Database</div>
|
|
<div class="o_info_val font-monospace"><t t-esc="restaurant['database_name']"/></div>
|
|
</div>
|
|
</div>
|
|
<div class="o_info_line">
|
|
<i class="fa fa-calendar-o o_info_icon text-muted"/>
|
|
<div>
|
|
<div class="o_info_label">Expires</div>
|
|
<div class="o_info_val"><t t-esc="restaurant['expiry_date']"/></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Col 4: Action Buttons -->
|
|
<div class="o_res_col_actions">
|
|
<a t-att-href="restaurant['launch_url']" target="_blank" class="o_btn_launch_red">
|
|
<i class="fa fa-rocket me-1"/> Launch DB
|
|
</a>
|
|
<a t-att-href="restaurant['manage_url']" class="o_btn_manage_white">
|
|
<i class="fa fa-cog me-1"/> Manage
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</t>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MODAL: Onboard New Restaurant -->
|
|
<div id="modalOnboardRestaurant" class="o_dine_modal">
|
|
<div class="o_modal_backdrop"/>
|
|
<div class="o_modal_dialog">
|
|
<div class="o_modal_header">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div class="o_settings_icon" style="width:34px;height:34px;font-size:15px;">
|
|
<i class="fa fa-cutlery"/>
|
|
</div>
|
|
<h4 class="o_modal_title">Onboard New Restaurant</h4>
|
|
</div>
|
|
<button type="button" class="o_modal_close_btn o_modal_close">&times;</button>
|
|
</div>
|
|
|
|
<form id="formOnboardRestaurant">
|
|
<div class="o_modal_body">
|
|
<div class="row">
|
|
<div class="col-md-6 o_form_group">
|
|
<label class="o_form_label">Restaurant Name *</label>
|
|
<input type="text" name="name" required="required" placeholder="e.g. Royal Bistro" class="o_form_control"/>
|
|
</div>
|
|
<div class="col-md-6 o_form_group">
|
|
<label class="o_form_label">Owner Full Name *</label>
|
|
<input type="text" name="owner_name" required="required" placeholder="e.g. John Doe" class="o_form_control"/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 o_form_group">
|
|
<label class="o_form_label">Owner Email *</label>
|
|
<input type="email" name="email" required="required" placeholder="owner@restaurant.com" class="o_form_control"/>
|
|
</div>
|
|
<div class="col-md-6 o_form_group">
|
|
<label class="o_form_label">Contact Phone *</label>
|
|
<input type="tel" name="phone" required="required" placeholder="+91 98765 43210" class="o_form_control"/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 o_form_group">
|
|
<label class="o_form_label">Subscription Plan *</label>
|
|
<select name="plan_id" class="o_form_control" required="required">
|
|
<t t-foreach="plans" t-as="plan">
|
|
<option t-att-value="plan['id']"><t t-esc="plan['name']"/> ($<t t-esc="plan['price_monthly']"/>/mo)</option>
|
|
</t>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6 o_form_group">
|
|
<label class="o_form_label">Billing Cycle *</label>
|
|
<select name="billing_cycle" class="o_form_control">
|
|
<option value="monthly">Monthly Billing</option>
|
|
<option value="yearly">Annual Billing (Save 20%)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="o_form_group">
|
|
<label class="o_form_label">Tenant Subdomain *</label>
|
|
<div class="d-flex align-items-center gap-1">
|
|
<input type="text" name="subdomain" required="required" placeholder="royalbistro" class="o_form_control font-monospace"/>
|
|
<span class="text-muted small fw-bold">.dine360.com</span>
|
|
</div>
|
|
<small class="text-muted">A dedicated PostgreSQL database and workspace will be provisioned automatically.</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="o_modal_footer">
|
|
<button type="button" class="btn btn-light o_modal_close">Cancel</button>
|
|
<button type="submit" class="o_btn_red_action">
|
|
<i class="fa fa-rocket me-1"/> Provision & Launch Database
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</t>
|
|
</template>
|
|
</odoo>
|