update
This commit is contained in:
585
fusion_schedule/views/public_booking.xml
Normal file
585
fusion_schedule/views/public_booking.xml
Normal file
@@ -0,0 +1,585 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ==================== PUBLIC BOOKING PAGE ==================== -->
|
||||
|
||||
<template id="public_booking_page" name="Public Booking Page">
|
||||
<t t-call="website.layout">
|
||||
<div class="container py-5" style="max-width: 700px;">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-4">
|
||||
<div class="d-inline-flex align-items-center justify-content-center rounded-circle mb-3"
|
||||
style="width: 64px; height: 64px; background: linear-gradient(135deg, #5ba848, #3a8fb7);">
|
||||
<i class="fa fa-calendar-check-o text-white" style="font-size: 28px;"/>
|
||||
</div>
|
||||
<h2 class="mb-1">Book a Time with <t t-out="staff_user.name"/></h2>
|
||||
<p class="text-muted">Select a date and time that works for you</p>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<t t-if="success">
|
||||
<div class="card border-0 shadow-sm text-center p-5" style="border-radius: 12px;">
|
||||
<div class="mb-3">
|
||||
<i class="fa fa-check-circle text-success" style="font-size: 48px;"/>
|
||||
</div>
|
||||
<h4 class="mb-2">Appointment Booked!</h4>
|
||||
<p class="text-muted mb-0"><t t-out="success"/></p>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Error Message -->
|
||||
<t t-if="error">
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="fa fa-exclamation-circle me-2"/><t t-out="error"/>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-if="not success">
|
||||
<form t-att-action="'/schedule/%s/book' % booking_slug" method="post" id="publicBookingForm">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
|
||||
<!-- Step 1: Date & Time -->
|
||||
<div class="card border-0 shadow-sm mb-4" style="border-radius: 12px;">
|
||||
<div class="card-header bg-white border-bottom pt-3 pb-2 px-4"
|
||||
style="border-radius: 12px 12px 0 0;">
|
||||
<h5 class="mb-0">
|
||||
<span class="badge rounded-pill me-2"
|
||||
style="background: linear-gradient(135deg, #5ba848, #3a8fb7);">1</span>
|
||||
Select Date & Time
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body px-4 pb-4">
|
||||
<!-- Appointment Type -->
|
||||
<t t-if="appointment_types and len(appointment_types) > 1">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Appointment Type</label>
|
||||
<select name="appointment_type_id" class="form-select"
|
||||
id="publicAppointmentType">
|
||||
<t t-foreach="appointment_types" t-as="atype">
|
||||
<option t-att-value="atype.id">
|
||||
<t t-out="atype.name"/>
|
||||
(<t t-out="'%.0f' % (atype.appointment_duration * 60)"/> min)
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</t>
|
||||
<t t-elif="appointment_types">
|
||||
<input type="hidden" name="appointment_type_id"
|
||||
t-att-value="appointment_types[0].id"/>
|
||||
</t>
|
||||
|
||||
<!-- Date -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Select Date</label>
|
||||
<input type="date" class="form-control" id="publicBookingDate"
|
||||
name="date" required="required"
|
||||
t-att-min="today"/>
|
||||
</div>
|
||||
|
||||
<!-- Available Slots -->
|
||||
<div id="publicSlotsContainer" style="display: none;">
|
||||
<label class="form-label fw-semibold">Available Time Slots</label>
|
||||
<div id="publicSlotsLoading" class="text-center py-3" style="display: none;">
|
||||
<div class="spinner-border spinner-border-sm text-primary me-2" role="status"/>
|
||||
Loading available slots...
|
||||
</div>
|
||||
<div id="publicSlotsGrid" class="d-flex flex-wrap gap-2 mb-2"></div>
|
||||
<div id="publicNoSlots" class="text-muted py-2" style="display: none;">
|
||||
<i class="fa fa-info-circle me-1"/> No available slots for this date.
|
||||
</div>
|
||||
<input type="hidden" name="slot_datetime" id="publicSlotDatetime"/>
|
||||
<input type="hidden" name="slot_duration" id="publicSlotDuration"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Your Details -->
|
||||
<div class="card border-0 shadow-sm mb-4" style="border-radius: 12px;">
|
||||
<div class="card-header bg-white border-bottom pt-3 pb-2 px-4"
|
||||
style="border-radius: 12px 12px 0 0;">
|
||||
<h5 class="mb-0">
|
||||
<span class="badge rounded-pill me-2"
|
||||
style="background: linear-gradient(135deg, #5ba848, #3a8fb7);">2</span>
|
||||
Your Details
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body px-4 pb-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Your Name <span class="text-danger">*</span></label>
|
||||
<input type="text" name="visitor_name" class="form-control"
|
||||
placeholder="Full name" required="required"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Email <span class="text-danger">*</span></label>
|
||||
<input type="email" name="visitor_email" class="form-control"
|
||||
placeholder="your@email.com" required="required"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Phone</label>
|
||||
<input type="tel" name="visitor_phone" class="form-control"
|
||||
placeholder="(optional)"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Address</label>
|
||||
<input type="text" name="client_street" class="form-control mb-2"
|
||||
id="publicClientStreet"
|
||||
placeholder="Start typing an address..."/>
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<input type="text" name="client_city" class="form-control"
|
||||
id="publicClientCity" placeholder="City"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="text" name="client_province" class="form-control"
|
||||
id="publicClientProvince" placeholder="Province"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="text" name="client_postal" class="form-control"
|
||||
id="publicClientPostal" placeholder="Postal Code"/>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="client_lat" id="publicClientLat" value="0"/>
|
||||
<input type="hidden" name="client_lng" id="publicClientLng" value="0"/>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label class="form-label fw-semibold">Notes</label>
|
||||
<textarea name="visitor_notes" class="form-control" rows="3"
|
||||
placeholder="Anything you'd like us to know..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<div class="text-end">
|
||||
<button type="submit" class="btn btn-primary btn-lg px-4" id="publicBtnSubmit"
|
||||
disabled="disabled">
|
||||
<i class="fa fa-calendar-check-o me-1"/> Confirm Booking
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</t>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="text-center mt-4">
|
||||
<small class="text-muted">Powered by Fusion Schedule</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Public Booking JS -->
|
||||
<script type="text/javascript">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var dateInput = document.getElementById('publicBookingDate');
|
||||
var slotsContainer = document.getElementById('publicSlotsContainer');
|
||||
var slotsGrid = document.getElementById('publicSlotsGrid');
|
||||
var slotsLoading = document.getElementById('publicSlotsLoading');
|
||||
var noSlots = document.getElementById('publicNoSlots');
|
||||
var slotDatetime = document.getElementById('publicSlotDatetime');
|
||||
var slotDuration = document.getElementById('publicSlotDuration');
|
||||
var submitBtn = document.getElementById('publicBtnSubmit');
|
||||
var typeSelect = document.getElementById('publicAppointmentType');
|
||||
var selectedSlotBtn = null;
|
||||
|
||||
var slug = '<t t-out="booking_slug"/>';
|
||||
|
||||
function getTypeId() {
|
||||
if (typeSelect) return typeSelect.value;
|
||||
var hidden = document.querySelector('input[name="appointment_type_id"]');
|
||||
return hidden ? hidden.value : null;
|
||||
}
|
||||
|
||||
function fetchSlots(date) {
|
||||
var typeId = getTypeId();
|
||||
if (!typeId || !date) return;
|
||||
|
||||
slotsContainer.style.display = 'block';
|
||||
slotsLoading.style.display = 'block';
|
||||
slotsGrid.innerHTML = '';
|
||||
noSlots.style.display = 'none';
|
||||
slotDatetime.value = '';
|
||||
if (submitBtn) submitBtn.disabled = true;
|
||||
selectedSlotBtn = null;
|
||||
|
||||
fetch('/schedule/' + slug + '/available-slots', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'call',
|
||||
params: {
|
||||
selected_date: date,
|
||||
appointment_type_id: parseInt(typeId),
|
||||
},
|
||||
}),
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
slotsLoading.style.display = 'none';
|
||||
var result = data.result || {};
|
||||
var slots = result.slots || [];
|
||||
|
||||
if (!slots.length) {
|
||||
noSlots.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
var morningSlots = [];
|
||||
var afternoonSlots = [];
|
||||
slots.forEach(function (s) {
|
||||
var hour = parseInt(s.start_hour);
|
||||
if (isNaN(hour)) {
|
||||
var match = s.start_hour.match(/(\d+)/);
|
||||
hour = match ? parseInt(match[1]) : 0;
|
||||
if (s.start_hour.toLowerCase().indexOf('pm') > -1 && hour !== 12) hour += 12;
|
||||
if (s.start_hour.toLowerCase().indexOf('am') > -1 && hour === 12) hour = 0;
|
||||
}
|
||||
(hour < 12 ? morningSlots : afternoonSlots).push(s);
|
||||
});
|
||||
|
||||
function renderGroup(label, icon, group) {
|
||||
if (!group.length) return;
|
||||
var h = document.createElement('div');
|
||||
h.className = 'w-100 mt-2 mb-1';
|
||||
h.innerHTML = '<small class="text-muted fw-semibold"><i class="fa ' + icon + ' me-1"></i>' + label + '</small>';
|
||||
slotsGrid.appendChild(h);
|
||||
group.forEach(function (s) {
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn btn-outline-primary btn-sm';
|
||||
btn.style.cssText = 'min-width: 100px; border-radius: 8px; padding: 8px 14px;';
|
||||
btn.textContent = s.start_hour;
|
||||
btn.addEventListener('click', function () {
|
||||
if (selectedSlotBtn) {
|
||||
selectedSlotBtn.classList.remove('btn-primary');
|
||||
selectedSlotBtn.classList.add('btn-outline-primary');
|
||||
}
|
||||
btn.classList.remove('btn-outline-primary');
|
||||
btn.classList.add('btn-primary');
|
||||
selectedSlotBtn = btn;
|
||||
slotDatetime.value = s.datetime;
|
||||
slotDuration.value = s.duration;
|
||||
if (submitBtn) submitBtn.disabled = false;
|
||||
});
|
||||
slotsGrid.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
renderGroup('Morning', 'fa-sun-o', morningSlots);
|
||||
renderGroup('Afternoon', 'fa-cloud', afternoonSlots);
|
||||
})
|
||||
.catch(function () {
|
||||
slotsLoading.style.display = 'none';
|
||||
noSlots.textContent = 'Failed to load slots. Please try again.';
|
||||
noSlots.style.display = 'block';
|
||||
});
|
||||
}
|
||||
|
||||
if (dateInput) {
|
||||
dateInput.addEventListener('change', function () { fetchSlots(this.value); });
|
||||
}
|
||||
if (typeSelect) {
|
||||
typeSelect.addEventListener('change', function () {
|
||||
if (dateInput && dateInput.value) fetchSlots(dateInput.value);
|
||||
});
|
||||
}
|
||||
|
||||
var form = document.getElementById('publicBookingForm');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function (e) {
|
||||
if (!slotDatetime.value) { e.preventDefault(); alert('Please select a time slot.'); return; }
|
||||
if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin me-1"></i> Booking...'; }
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<t t-if="google_maps_api_key">
|
||||
<script>
|
||||
function initPublicAddressAutocomplete() {
|
||||
var streetInput = document.getElementById('publicClientStreet');
|
||||
if (!streetInput || typeof google === 'undefined') return;
|
||||
var autocomplete = new google.maps.places.Autocomplete(streetInput, {
|
||||
types: ['address'],
|
||||
componentRestrictions: { country: 'ca' },
|
||||
fields: ['address_components', 'geometry'],
|
||||
});
|
||||
autocomplete.addListener('place_changed', function () {
|
||||
var place = autocomplete.getPlace();
|
||||
if (!place.address_components) return;
|
||||
var streetNumber = '', streetName = '', city = '', province = '', postalCode = '';
|
||||
place.address_components.forEach(function (c) {
|
||||
var t = c.types;
|
||||
if (t.indexOf('street_number') > -1) streetNumber = c.long_name;
|
||||
if (t.indexOf('route') > -1) streetName = c.long_name;
|
||||
if (t.indexOf('locality') > -1) city = c.long_name;
|
||||
if (t.indexOf('administrative_area_level_1') > -1) province = c.short_name;
|
||||
if (t.indexOf('postal_code') > -1) postalCode = c.long_name;
|
||||
});
|
||||
streetInput.value = (streetNumber + ' ' + streetName).trim();
|
||||
var ci = document.getElementById('publicClientCity');
|
||||
if (ci) ci.value = city;
|
||||
var pr = document.getElementById('publicClientProvince');
|
||||
if (pr) pr.value = province;
|
||||
var po = document.getElementById('publicClientPostal');
|
||||
if (po) po.value = postalCode;
|
||||
if (place.geometry && place.geometry.location) {
|
||||
var la = document.getElementById('publicClientLat');
|
||||
var ln = document.getElementById('publicClientLng');
|
||||
if (la) la.value = place.geometry.location.lat();
|
||||
if (ln) ln.value = place.geometry.location.lng();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script t-attf-src="https://maps.googleapis.com/maps/api/js?key=#{google_maps_api_key}&libraries=places&callback=initPublicAddressAutocomplete"
|
||||
async="async" defer="defer"></script>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- ==================== PUBLIC MANAGE PAGE ==================== -->
|
||||
|
||||
<template id="public_manage_page" name="Manage Your Appointment">
|
||||
<t t-call="website.layout">
|
||||
<div class="container py-5" style="max-width: 600px;">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-4">
|
||||
<div class="d-inline-flex align-items-center justify-content-center rounded-circle mb-3"
|
||||
style="width: 64px; height: 64px; background: linear-gradient(135deg, #5ba848, #3a8fb7);">
|
||||
<i class="fa fa-calendar-check-o text-white" style="font-size: 28px;"/>
|
||||
</div>
|
||||
<h2 class="mb-1">Your Appointment</h2>
|
||||
<p class="text-muted">Manage your booking below</p>
|
||||
</div>
|
||||
|
||||
<!-- Cancelled state -->
|
||||
<t t-if="cancelled">
|
||||
<div class="card border-0 shadow-sm text-center p-5" style="border-radius: 12px;">
|
||||
<div class="mb-3">
|
||||
<i class="fa fa-times-circle text-danger" style="font-size: 48px;"/>
|
||||
</div>
|
||||
<h4 class="mb-2">Appointment Cancelled</h4>
|
||||
<p class="text-muted mb-0">Your appointment has been cancelled.</p>
|
||||
<t t-if="booking_slug">
|
||||
<a t-attf-href="/schedule/#{booking_slug}" class="btn btn-primary mt-3">Book a New Appointment</a>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Rescheduled state -->
|
||||
<t t-elif="rescheduled">
|
||||
<div class="card border-0 shadow-sm text-center p-5" style="border-radius: 12px;">
|
||||
<div class="mb-3">
|
||||
<i class="fa fa-check-circle text-success" style="font-size: 48px;"/>
|
||||
</div>
|
||||
<h4 class="mb-2">Appointment Rescheduled</h4>
|
||||
<p class="text-muted mb-3">Your appointment has been moved to the new time.</p>
|
||||
<t t-if="event">
|
||||
<div class="bg-light rounded-3 p-3 d-inline-block mx-auto">
|
||||
<strong><t t-out="event.start.astimezone(user_tz).strftime('%A, %B %d, %Y')"/></strong>
|
||||
<br/>
|
||||
<t t-out="event.start.astimezone(user_tz).strftime('%I:%M %p')"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Active appointment -->
|
||||
<t t-elif="event">
|
||||
<t t-if="error">
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="fa fa-exclamation-circle me-2"/><t t-out="error"/>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Appointment details card -->
|
||||
<div class="card border-0 shadow-sm mb-4" style="border-radius: 12px;">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="mb-3"><t t-out="event.name"/></h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-6">
|
||||
<small class="text-muted d-block">Date</small>
|
||||
<strong><t t-out="event.start.astimezone(user_tz).strftime('%A, %b %d, %Y')"/></strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<small class="text-muted d-block">Time</small>
|
||||
<strong><t t-out="event.start.astimezone(user_tz).strftime('%I:%M %p')"/></strong>
|
||||
</div>
|
||||
<t t-if="event.location">
|
||||
<div class="col-12">
|
||||
<small class="text-muted d-block">Location</small>
|
||||
<span><t t-out="event.location"/></span>
|
||||
</div>
|
||||
</t>
|
||||
<div class="col-6">
|
||||
<small class="text-muted d-block">Duration</small>
|
||||
<span><t t-out="'%.0f' % (event.duration * 60)"/> minutes</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reschedule section -->
|
||||
<div class="card border-0 shadow-sm mb-4" style="border-radius: 12px;">
|
||||
<div class="card-header bg-white border-bottom pt-3 pb-2 px-4"
|
||||
style="border-radius: 12px 12px 0 0; cursor: pointer;"
|
||||
data-bs-toggle="collapse" data-bs-target="#rescheduleSection"
|
||||
aria-expanded="false">
|
||||
<h6 class="mb-0">
|
||||
<i class="fa fa-clock-o me-2 text-primary"/>Reschedule
|
||||
<i class="fa fa-chevron-down float-end text-muted" style="font-size: 12px;"/>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="collapse" id="rescheduleSection">
|
||||
<div class="card-body px-4 pb-4">
|
||||
<form t-attf-action="/schedule/manage/#{token}/reschedule" method="post"
|
||||
id="publicRescheduleForm">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">New Date</label>
|
||||
<input type="date" class="form-control" id="publicRescheduleDate"
|
||||
required="required"/>
|
||||
</div>
|
||||
<div id="publicRescheduleSlotsContainer" style="display: none;">
|
||||
<label class="form-label fw-semibold">Available Slots</label>
|
||||
<div id="publicRescheduleSlotsLoading" class="text-center py-2"
|
||||
style="display: none;">
|
||||
<div class="spinner-border spinner-border-sm text-primary me-2"
|
||||
role="status"/>
|
||||
Loading...
|
||||
</div>
|
||||
<div id="publicRescheduleSlotsGrid"
|
||||
class="d-flex flex-wrap gap-2 mb-2"></div>
|
||||
<div id="publicRescheduleNoSlots" class="text-muted py-2"
|
||||
style="display: none;">
|
||||
No slots available for this date.
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="slot_datetime"
|
||||
id="publicRescheduleSlotDatetime"/>
|
||||
<button type="submit" class="btn btn-primary mt-2"
|
||||
id="publicRescheduleSubmit" disabled="disabled">
|
||||
<i class="fa fa-check me-1"/> Confirm New Time
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cancel section -->
|
||||
<div class="card border-0 shadow-sm" style="border-radius: 12px;">
|
||||
<div class="card-body p-4">
|
||||
<form t-attf-action="/schedule/manage/#{token}/cancel" method="post"
|
||||
id="publicCancelForm"
|
||||
onsubmit="return confirm('Are you sure you want to cancel this appointment?');">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<button type="submit" class="btn btn-outline-danger w-100">
|
||||
<i class="fa fa-times me-1"/> Cancel Appointment
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Event not found (already cancelled) -->
|
||||
<t t-else="">
|
||||
<div class="card border-0 shadow-sm text-center p-5" style="border-radius: 12px;">
|
||||
<div class="mb-3">
|
||||
<i class="fa fa-calendar-times-o text-muted" style="font-size: 48px;"/>
|
||||
</div>
|
||||
<h4 class="mb-2">Appointment Not Found</h4>
|
||||
<p class="text-muted mb-0">This appointment may have been cancelled or the link is invalid.</p>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<small class="text-muted">Powered by Fusion Schedule</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Public Reschedule JS -->
|
||||
<t t-if="event and not cancelled and not rescheduled">
|
||||
<script type="text/javascript">
|
||||
(function () {
|
||||
'use strict';
|
||||
var token = '<t t-out="token"/>';
|
||||
var dateInput = document.getElementById('publicRescheduleDate');
|
||||
var container = document.getElementById('publicRescheduleSlotsContainer');
|
||||
var grid = document.getElementById('publicRescheduleSlotsGrid');
|
||||
var loading = document.getElementById('publicRescheduleSlotsLoading');
|
||||
var noSlots = document.getElementById('publicRescheduleNoSlots');
|
||||
var slotInput = document.getElementById('publicRescheduleSlotDatetime');
|
||||
var submitBtn = document.getElementById('publicRescheduleSubmit');
|
||||
var selectedBtn = null;
|
||||
|
||||
if (!dateInput) return;
|
||||
|
||||
dateInput.addEventListener('change', function () {
|
||||
var date = this.value;
|
||||
if (!date) return;
|
||||
container.style.display = 'block';
|
||||
loading.style.display = 'block';
|
||||
grid.innerHTML = '';
|
||||
noSlots.style.display = 'none';
|
||||
slotInput.value = '';
|
||||
submitBtn.disabled = true;
|
||||
selectedBtn = null;
|
||||
|
||||
fetch('/schedule/manage/' + token + '/available-slots', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0', method: 'call',
|
||||
params: { selected_date: date },
|
||||
}),
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
loading.style.display = 'none';
|
||||
var slots = (data.result || {}).slots || [];
|
||||
if (!slots.length) { noSlots.style.display = 'block'; return; }
|
||||
slots.forEach(function (s) {
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn btn-outline-primary btn-sm';
|
||||
btn.style.cssText = 'min-width: 90px; border-radius: 8px; padding: 8px 12px;';
|
||||
btn.textContent = s.start_hour;
|
||||
btn.addEventListener('click', function () {
|
||||
if (selectedBtn) {
|
||||
selectedBtn.classList.remove('btn-primary');
|
||||
selectedBtn.classList.add('btn-outline-primary');
|
||||
}
|
||||
btn.classList.remove('btn-outline-primary');
|
||||
btn.classList.add('btn-primary');
|
||||
selectedBtn = btn;
|
||||
slotInput.value = s.datetime;
|
||||
submitBtn.disabled = false;
|
||||
});
|
||||
grid.appendChild(btn);
|
||||
});
|
||||
})
|
||||
.catch(function () {
|
||||
loading.style.display = 'none';
|
||||
noSlots.textContent = 'Failed to load slots.';
|
||||
noSlots.style.display = 'block';
|
||||
});
|
||||
});
|
||||
|
||||
var form = document.getElementById('publicRescheduleForm');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function (e) {
|
||||
if (!slotInput.value) { e.preventDefault(); alert('Please select a time slot.'); }
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user