- 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
245 lines
13 KiB
XML
245 lines
13 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
|
<odoo>
|
|
<template id="tables_page_template" name="Dine360 Table Management Page">
|
|
<t t-call="dine360_dashboard.dashboard_layout">
|
|
<t t-set="active_page" t-value="'tables'"/>
|
|
|
|
<!-- Page Title & Header -->
|
|
<section class="o_page_header">
|
|
<div>
|
|
<h1 class="o_heading_title">Table & Floor Management</h1>
|
|
<p class="o_heading_subtitle">Live visual table occupancy, dining floors layout, and guest seating capacity</p>
|
|
</div>
|
|
<div class="o_header_controls">
|
|
<div class="o_status_tag">
|
|
<span class="o_green_dot"/>
|
|
<span>Floor Plan Sync: Active</span>
|
|
</div>
|
|
<button type="button" class="o_btn_red_action" data-open-modal="modalAddTable">
|
|
<i class="fa fa-plus me-1"/> Add Table
|
|
</button>
|
|
</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.get('total', 0)"/></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.get('available', 0)"/></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.get('occupied', 0)"/></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.get('total', 0)"/>)</div>
|
|
<t t-foreach="floors" t-as="fl">
|
|
<div class="o_filter_pill" t-att-data-filter="fl.get('name', '').lower()">
|
|
<t t-esc="fl.get('name', '')"/> (<t t-esc="fl.get('table_count', 0)"/>)
|
|
</div>
|
|
</t>
|
|
<div class="o_filter_pill" data-filter="available">Available Only (<t t-esc="table_stats.get('available', 0)"/>)</div>
|
|
<div class="o_filter_pill" data-filter="occupied">Occupied Only (<t t-esc="table_stats.get('occupied', 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 table name or floor..."/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Row 3: Interactive Floor Plan Grid -->
|
|
<div class="o_widget_card mb-4">
|
|
<div class="o_card_header">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<h3 class="o_card_title mb-0">Live Floor Seating Matrix</h3>
|
|
<span class="o_count_pill"><t t-esc="len(tables)"/> Tables Configured</span>
|
|
</div>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<button type="button" class="o_btn_red_small" data-open-modal="modalAddTable">
|
|
<i class="fa fa-plus me-1"/> Add Table
|
|
</button>
|
|
<a href="/web#action=dine360_table.action_restaurant_table_form" class="o_btn_white_small">
|
|
Manage in ERP
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<t t-if="not tables">
|
|
<div class="text-center py-5 text-muted">
|
|
<i class="fa fa-th-large fa-3x mb-3 text-secondary"/>
|
|
<h5>No tables configured</h5>
|
|
<p class="small">Click "Add Table" to set up your restaurant dining floors.</p>
|
|
</div>
|
|
</t>
|
|
|
|
<!-- Tables Visual Grid -->
|
|
<div class="o_tables_grid">
|
|
<t t-foreach="tables" t-as="table">
|
|
<div t-att-class="'o_table_tile ' + ('occupied' if table['is_occupied'] else 'available')"
|
|
data-search-item="true"
|
|
data-filter-item="true"
|
|
t-att-data-status="'occupied' if table['is_occupied'] else 'available'"
|
|
t-att-data-floor="table.get('floor_name', '').lower()">
|
|
|
|
<!-- Icon -->
|
|
<div class="o_table_icon_wrap">
|
|
<i class="fa fa-cutlery"/>
|
|
</div>
|
|
|
|
<!-- Table Title & Floor -->
|
|
<div class="o_table_num"><t t-esc="table['name']"/></div>
|
|
<div class="o_table_meta">
|
|
<t t-esc="table.get('floor_name') or 'Main Dining'"/>
|
|
</div>
|
|
|
|
<!-- Capacity Pill -->
|
|
<div class="mb-3">
|
|
<span class="o_table_capacity_pill">
|
|
<i class="fa fa-user me-1 text-danger"/> <t t-esc="table.get('seats', 4)"/> Seats
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Toggle Occupancy Action -->
|
|
<button type="button"
|
|
class="o_btn_toggle_table"
|
|
t-att-data-table-id="table['id']"
|
|
t-att-data-occupied="'true' if table['is_occupied'] else 'false'">
|
|
<t t-if="table['is_occupied']">
|
|
<i class="fa fa-sign-out me-1"/> Mark Available
|
|
</t>
|
|
<t t-else="">
|
|
<i class="fa fa-sign-in me-1"/> Mark Occupied
|
|
</t>
|
|
</button>
|
|
</div>
|
|
</t>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MODAL: Add Table -->
|
|
<div id="modalAddTable" class="o_dine_modal">
|
|
<div class="o_modal_backdrop"/>
|
|
<div class="o_modal_dialog">
|
|
<div class="o_modal_header">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div class="o_settings_icon" style="width:34px;height:34px;font-size:15px;">
|
|
<i class="fa fa-th"/>
|
|
</div>
|
|
<h4 class="o_modal_title">Add Restaurant Table</h4>
|
|
</div>
|
|
<button type="button" class="o_modal_close_btn o_modal_close">&times;</button>
|
|
</div>
|
|
|
|
<form id="formAddTable">
|
|
<div class="o_modal_body">
|
|
<div class="row">
|
|
<div class="col-md-6 o_form_group">
|
|
<label class="o_form_label">Table Number / Label *</label>
|
|
<input type="text" name="name" required="required" placeholder="e.g. Table 12, VIP-1" class="o_form_control"/>
|
|
</div>
|
|
<div class="col-md-6 o_form_group">
|
|
<label class="o_form_label">Dining Floor *</label>
|
|
<select name="floor_id" class="o_form_control">
|
|
<t t-foreach="floors" t-as="fl">
|
|
<option t-att-value="fl['id']"><t t-esc="fl['name']"/></option>
|
|
</t>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 o_form_group">
|
|
<label class="o_form_label">Seating Capacity (Seats) *</label>
|
|
<input type="number" name="seats" required="required" min="1" max="30" value="4" class="o_form_control"/>
|
|
</div>
|
|
<div class="col-md-6 o_form_group">
|
|
<label class="o_form_label">Shape</label>
|
|
<select name="shape" class="o_form_control">
|
|
<option value="square">Square / Rectangle</option>
|
|
<option value="round">Round Table</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="o_modal_footer">
|
|
<button type="button" class="btn btn-light o_modal_close">Cancel</button>
|
|
<button type="submit" class="o_btn_red_action">
|
|
<i class="fa fa-plus me-1"/> Save & Add to Floor
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</t>
|
|
</template>
|
|
</odoo>
|