- 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
365 lines
22 KiB
XML
365 lines
22 KiB
XML
<?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.get('total', 0)"/></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.get('confirmed', 0)"/></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.get('draft', 0)"/></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.get('completed', 0)"/></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.get('total', 0)"/>)</div>
|
|
<div class="o_filter_pill" data-filter="confirmed">Confirmed (<t t-esc="reservation_stats.get('confirmed', 0)"/>)</div>
|
|
<div class="o_filter_pill" data-filter="draft">Draft / Pending (<t t-esc="reservation_stats.get('draft', 0)"/>)</div>
|
|
<div class="o_filter_pill" data-filter="completed">Completed (<t t-esc="reservation_stats.get('completed', 0)"/>)</div>
|
|
<div class="o_filter_pill" data-filter="cancelled">Cancelled (<t t-esc="reservation_stats.get('cancelled', 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 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.get('total', 0)"/> 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 & Time Slot</th>
|
|
<th>Party & 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 & 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&model=restaurant.reservation&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">&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 & 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 & Notify Guest
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</t>
|
|
</template>
|
|
</odoo>
|