596 lines
21 KiB
JavaScript
596 lines
21 KiB
JavaScript
import {
|
|
LocationSelectorDialog
|
|
} from '@delivery/js/location_selector/location_selector_dialog/location_selector_dialog';
|
|
import { _t } from '@web/core/l10n/translation';
|
|
import { rpc } from '@web/core/network/rpc';
|
|
import publicWidget from '@web/legacy/js/public/public_widget';
|
|
|
|
publicWidget.registry.WebsiteSaleCheckout = publicWidget.Widget.extend({
|
|
selector: '#shop_checkout',
|
|
events: {
|
|
// Addresses
|
|
'click .card': '_changeAddress',
|
|
'click .js_edit_address': '_preventChangingAddress',
|
|
'change #use_delivery_as_billing': '_toggleBillingAddressRow',
|
|
// Delivery methods
|
|
'click [name="o_delivery_radio"]': '_selectDeliveryMethod',
|
|
'click [name="o_pickup_location_selector"]': '_selectPickupLocation',
|
|
},
|
|
|
|
// #=== WIDGET LIFECYCLE ===#
|
|
|
|
async start() {
|
|
this.mainButton = document.querySelector('a[name="website_sale_main_button"]');
|
|
this.use_delivery_as_billing_toggle = document.querySelector('#use_delivery_as_billing');
|
|
this.billingContainer = this.el.querySelector('#billing_container');
|
|
await this._prepareDeliveryMethods();
|
|
},
|
|
|
|
// #=== EVENT HANDLERS ===#
|
|
|
|
/**
|
|
* Set the billing or delivery address on the order and update the corresponding card.
|
|
*
|
|
* @private
|
|
* @param {Event} ev
|
|
* @return {void}
|
|
*/
|
|
async _changeAddress(ev) {
|
|
const newAddress = ev.currentTarget;
|
|
if (newAddress.classList.contains('bg-primary')) { // If the card is already selected.
|
|
return;
|
|
}
|
|
const addressType = newAddress.dataset.addressType;
|
|
|
|
// Remove the highlighting from the previously selected address card.
|
|
const previousAddress = this._getSelectedAddress(addressType);
|
|
this._tuneDownAddressCard(previousAddress);
|
|
|
|
// Highlight the newly selected address card.
|
|
this._highlightAddressCard(newAddress);
|
|
const selectedPartnerId = newAddress.dataset.partnerId;
|
|
await this.updateAddress(addressType, selectedPartnerId);
|
|
if (addressType === 'delivery') { // A delivery address is changed.
|
|
if (this.use_delivery_as_billing_toggle.checked) {
|
|
await this._selectMatchingBillingAddress(selectedPartnerId);
|
|
}
|
|
// Update the available delivery methods.
|
|
document.getElementById('o_delivery_form').innerHTML = await rpc(
|
|
'/shop/delivery_methods'
|
|
);
|
|
await this._prepareDeliveryMethods();
|
|
}
|
|
this._enableMainButton(); // Try to enable the main button.
|
|
},
|
|
|
|
/**
|
|
* Show/hide the billing address row when the user toggles the 'use delivery as billing' input.
|
|
*
|
|
* The URLs of the "create address" buttons are updated to propagate the value of the input.
|
|
*
|
|
* @private
|
|
* @param ev
|
|
* @return {void}
|
|
*/
|
|
async _toggleBillingAddressRow(ev) {
|
|
const useDeliveryAsBilling = ev.target.checked;
|
|
|
|
const addDeliveryAddressButton = this.el.querySelector(
|
|
'.o_wsale_add_address[data-address-type="delivery"]'
|
|
);
|
|
if (addDeliveryAddressButton) { // If `Add address` button for delivery.
|
|
// Update the `use_delivery_as_billing` query param for a new delivery address URL.
|
|
const addDeliveryUrl = new URL(addDeliveryAddressButton.href);
|
|
addDeliveryUrl.searchParams.set(
|
|
'use_delivery_as_billing', encodeURIComponent(useDeliveryAsBilling)
|
|
);
|
|
addDeliveryAddressButton.href = addDeliveryUrl.toString();
|
|
}
|
|
|
|
// Toggle the billing address row.
|
|
if (useDeliveryAsBilling) {
|
|
this.billingContainer.classList.add('d-none'); // Hide the billing address row.
|
|
const selectedDeliveryAddress = this._getSelectedAddress('delivery');
|
|
await this._selectMatchingBillingAddress(selectedDeliveryAddress.dataset.partnerId);
|
|
} else {
|
|
this._disableMainButton();
|
|
this.billingContainer.classList.remove('d-none'); // Show the billing address row.
|
|
}
|
|
|
|
this._enableMainButton(); // Try to enable the main button.
|
|
},
|
|
|
|
/**
|
|
* Cancel the address change to allow the redirect to the edit page to take place.
|
|
*
|
|
* @private
|
|
* @param {Event} ev
|
|
* @return {void}
|
|
*/
|
|
_preventChangingAddress(ev) {
|
|
ev.stopPropagation();
|
|
},
|
|
|
|
/**
|
|
* Fetch the delivery rate for the selected delivery method and update the displayed amounts.
|
|
*
|
|
* @private
|
|
* @param {Event} ev
|
|
* @return {void}
|
|
*/
|
|
async _selectDeliveryMethod(ev) {
|
|
const checkedRadio = ev.currentTarget;
|
|
if (checkedRadio.disabled) { // The delivery rate request failed.
|
|
return; // Failing delivery methods cannot be selected.
|
|
}
|
|
|
|
// Disable the main button while fetching delivery rates.
|
|
this._disableMainButton();
|
|
|
|
// Hide and reset the order location name and address if defined.
|
|
this._hidePickupLocation();
|
|
|
|
// Fetch delivery rates and update the cart summary and the price badge accordingly.
|
|
await this._updateDeliveryMethod(checkedRadio);
|
|
|
|
// Re-enable the main button after delivery rates have been fetched.
|
|
this._enableMainButton();
|
|
|
|
// Show a button to open the location selector if required for the selected delivery method.
|
|
await this._showPickupLocation(checkedRadio);
|
|
},
|
|
|
|
/**
|
|
* Fetch and display the closest pickup locations based on the zip code.
|
|
*
|
|
* @private
|
|
* @param {Event} ev
|
|
* @return {void}
|
|
*/
|
|
async _selectPickupLocation(ev) {
|
|
const { zipCode, locationId } = ev.currentTarget.dataset;
|
|
const deliveryMethodContainer = this._getDeliveryMethodContainer(ev.currentTarget);
|
|
this.call('dialog', 'add', LocationSelectorDialog, {
|
|
zipCode: zipCode,
|
|
selectedLocationId: locationId,
|
|
isFrontend: true,
|
|
save: async location => {
|
|
const jsonLocation = JSON.stringify(location);
|
|
// Assign the selected pickup location to the order.
|
|
await this._setPickupLocation(jsonLocation);
|
|
|
|
// Show and set the order location details.
|
|
this._updatePickupLocation(deliveryMethodContainer, location, jsonLocation);
|
|
|
|
this._enableMainButton();
|
|
},
|
|
});
|
|
},
|
|
|
|
// #=== DOM MANIPULATION ===#
|
|
|
|
/**
|
|
* Update the pickup location address elements and the 'edit' button's values.
|
|
*
|
|
* @private
|
|
* @param deliveryMethodContainer - The container element of the delivery method.
|
|
* @param location - The selected location as an object.
|
|
* @param jsonLocation - The selected location as an JSON string.
|
|
* @return {void}
|
|
*/
|
|
_updatePickupLocation(deliveryMethodContainer, location, jsonLocation) {
|
|
const pickupLocation = deliveryMethodContainer.querySelector('[name="o_pickup_location"]');
|
|
pickupLocation.querySelector('[name="o_pickup_location_name"]').innerText = location.name;
|
|
pickupLocation.querySelector(
|
|
'[name="o_pickup_location_address"]'
|
|
).innerText = `${location.street} ${location.zip_code} ${location.city}`;
|
|
const editPickupLocationButton = pickupLocation.querySelector(
|
|
'span[name="o_pickup_location_selector"]'
|
|
);
|
|
editPickupLocationButton.dataset.locationId = location.id;
|
|
editPickupLocationButton.dataset.zipCode = location.zip_code;
|
|
editPickupLocationButton.dataset.pickupLocationData = jsonLocation;
|
|
pickupLocation.querySelector(
|
|
'[name="o_pickup_location_details"]'
|
|
).classList.remove('d-none');
|
|
|
|
// Remove the button.
|
|
pickupLocation.querySelector('button[name="o_pickup_location_selector"]')?.remove();
|
|
},
|
|
|
|
/**
|
|
* Remove the highlighting from the address card.
|
|
*
|
|
* @private
|
|
* @param card - The card element of the selected address.
|
|
* @return {void}
|
|
*/
|
|
_tuneDownAddressCard(card) {
|
|
if (!card) return;
|
|
card.classList.remove('bg-primary', 'border', 'border-primary');
|
|
},
|
|
|
|
/**
|
|
* Highlight the address card.
|
|
*
|
|
* @private
|
|
* @param card - The card element of the selected address.
|
|
* @return {void}
|
|
*/
|
|
_highlightAddressCard(card) {
|
|
if (!card) return;
|
|
card.classList.add('bg-primary', 'border', 'border-primary');
|
|
},
|
|
|
|
/**
|
|
* Disable the main button.
|
|
*
|
|
* @private
|
|
* @return {void}
|
|
*/
|
|
_disableMainButton() {
|
|
this.mainButton?.classList.add('disabled');
|
|
},
|
|
|
|
/**
|
|
* Enable the main button if all conditions are satisfied.
|
|
*
|
|
* @private
|
|
* @return {void}
|
|
*/
|
|
_enableMainButton() {
|
|
if (this._canEnableMainButton()) {
|
|
this.mainButton?.classList.remove('disabled');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Return whether a delivery method and a billing address are selected.
|
|
*
|
|
* @private
|
|
* @return {boolean}
|
|
*/
|
|
_canEnableMainButton(){
|
|
return this._isDeliveryMethodReady() && this._isBillingAddressSelected();
|
|
},
|
|
|
|
/**
|
|
* Hide the pickup location.
|
|
*
|
|
* @private
|
|
* @return {void}
|
|
*/
|
|
_hidePickupLocation() {
|
|
const pickupLocations = document.querySelectorAll(
|
|
'[name="o_pickup_location"]:not(.d-none)'
|
|
);
|
|
pickupLocations.forEach(pickupLocation => {
|
|
pickupLocation.classList.add('d-none'); // Hide the whole div.
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Set the delivery method on the order and update the price badge and cart summary.
|
|
*
|
|
* @private
|
|
* @param {HTMLInputElement} radio - The radio button linked to the delivery method.
|
|
* @return {void}
|
|
*/
|
|
async _updateDeliveryMethod(radio) {
|
|
this._showLoadingBadge(radio);
|
|
const result = await this._setDeliveryMethod(radio.dataset.dmId);
|
|
this._updateAmountBadge(radio, result);
|
|
this._updateCartSummary(result);
|
|
},
|
|
|
|
/**
|
|
* Display a loading spinner on the delivery price badge.
|
|
*
|
|
* @private
|
|
* @param {HTMLInputElement} radio - The radio button linked to the delivery method.
|
|
* @return {void}
|
|
*/
|
|
_showLoadingBadge(radio) {
|
|
const deliveryPriceBadge = this._getDeliveryPriceBadge(radio);
|
|
this._clearElement(deliveryPriceBadge);
|
|
deliveryPriceBadge.appendChild(this._createLoadingElement());
|
|
},
|
|
|
|
/**
|
|
* Update the delivery price badge with the delivery rate.
|
|
*
|
|
* If the rate is zero, the price badge displays "Free" instead.
|
|
*
|
|
* @private
|
|
* @param {HTMLInputElement} radio - The radio button linked to the delivery method.
|
|
* @param {Object} rateData - The delivery rate data.
|
|
* @return {void}
|
|
*/
|
|
_updateAmountBadge(radio, rateData) {
|
|
const deliveryPriceBadge = this._getDeliveryPriceBadge(radio);
|
|
if (rateData.success) {
|
|
// If it's a free delivery (`free_over` field), show 'Free', not '$ 0'.
|
|
if (rateData.is_free_delivery) {
|
|
deliveryPriceBadge.textContent = _t("Free");
|
|
} else {
|
|
deliveryPriceBadge.innerHTML = rateData.amount_delivery;
|
|
}
|
|
this._toggleDeliveryMethodRadio(radio);
|
|
} else {
|
|
deliveryPriceBadge.textContent = rateData.error_message;
|
|
this._toggleDeliveryMethodRadio(radio, true);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update the order summary table with the delivery rate of the selected delivery method.
|
|
*
|
|
* @private
|
|
* @param {Object} result - The order summary values.
|
|
* @return {void}
|
|
*/
|
|
_updateCartSummary(result) {
|
|
const amountDelivery = document.querySelector('#order_delivery .monetary_field');
|
|
const amountUntaxed = document.querySelector('#order_total_untaxed .monetary_field');
|
|
const amountTax = document.querySelector('#order_total_taxes .monetary_field');
|
|
const amountTotal = document.querySelectorAll(
|
|
'#order_total .monetary_field, #amount_total_summary.monetary_field'
|
|
);
|
|
amountDelivery.innerHTML = result.amount_delivery;
|
|
amountUntaxed.innerHTML = result.amount_untaxed;
|
|
amountTax.innerHTML = result.amount_tax;
|
|
amountTotal.forEach(total => total.innerHTML = result.amount_total);
|
|
},
|
|
|
|
/**
|
|
* Enable or disable radio selection for a delivery method.
|
|
*
|
|
* @private
|
|
* @param {HTMLInputElement} radio - The radio button linked to the delivery method.
|
|
* @param {Boolean} disable - Whether the radio should be disabled.
|
|
*/
|
|
_toggleDeliveryMethodRadio(radio, disable=false) {
|
|
const deliveryMethodContainer = this._getDeliveryMethodContainer(radio);
|
|
radio.disabled = disable;
|
|
if (disable) {
|
|
deliveryMethodContainer.classList.add('text-muted');
|
|
}
|
|
else {
|
|
deliveryMethodContainer.classList.remove('text-muted');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Remove all children of the provided element from the DOM.
|
|
*
|
|
* @private
|
|
* @param {Element} el - The element to clear.
|
|
* @return {void}
|
|
*/
|
|
_clearElement(el) {
|
|
while (el.firstChild) {
|
|
el.removeChild(el.lastChild);
|
|
}
|
|
},
|
|
|
|
// #=== ADDRESS FLOW ===#
|
|
|
|
/**
|
|
* Select the billing address matching the currently selected delivery address.
|
|
*
|
|
* @private
|
|
* @param selectedPartnerId - The partner id of the selected delivery address.
|
|
* @return {void}
|
|
*/
|
|
async _selectMatchingBillingAddress(selectedPartnerId) {
|
|
const previousAddress = this._getSelectedAddress('billing');
|
|
this._tuneDownAddressCard(previousAddress);
|
|
await this.updateAddress('billing', selectedPartnerId);
|
|
const billingAddress = this.el.querySelector(
|
|
`.card[data-partner-id="${selectedPartnerId}"][data-address-type="billing"]`
|
|
);
|
|
this._highlightAddressCard(billingAddress);
|
|
},
|
|
|
|
/**
|
|
* Set the billing or delivery address on the order.
|
|
*
|
|
* @param addressType - The type of the address to set: 'delivery' or 'billing'.
|
|
* @param partnerId - The partner id of the address to set.
|
|
* @return {void}
|
|
*/
|
|
async updateAddress(addressType, partnerId) {
|
|
await rpc('/shop/update_address', {address_type: addressType, partner_id: partnerId})
|
|
},
|
|
|
|
// #=== DELIVERY FLOW ===#
|
|
|
|
/**
|
|
* Change the delivery method to the one whose radio is selected and fetch all delivery rates.
|
|
*
|
|
* @private
|
|
* @return {void}
|
|
*/
|
|
async _prepareDeliveryMethods() {
|
|
// Load the radios from the DOM here to update them if the template is re-rendered.
|
|
this.dmRadios = Array.from(document.querySelectorAll('input[name="o_delivery_radio"]'));
|
|
if (this.dmRadios.length > 0) {
|
|
const checkedRadio = document.querySelector('input[name="o_delivery_radio"]:checked');
|
|
this._disableMainButton();
|
|
if (checkedRadio) {
|
|
await this._updateDeliveryMethod(checkedRadio);
|
|
this._enableMainButton();
|
|
}
|
|
}
|
|
// Asynchronously fetch delivery rates to mitigate delays from third-party APIs
|
|
await Promise.all(this.dmRadios.filter(radio => !radio.checked).map(async radio => {
|
|
this._showLoadingBadge((radio));
|
|
const rateData = await this._getDeliveryRate(radio);
|
|
this._updateAmountBadge(radio, rateData);
|
|
}));
|
|
},
|
|
|
|
/**
|
|
* Check if the delivery method is selected and if the pickup point is selected if needed.
|
|
*
|
|
* @private
|
|
* @return {boolean} Whether the delivery method is ready.
|
|
*/
|
|
_isDeliveryMethodReady() {
|
|
if (this.dmRadios.length === 0) { // No delivery method is available.
|
|
return true; // Ignore the check.
|
|
}
|
|
const checkedRadio = document.querySelector('input[name="o_delivery_radio"]:checked');
|
|
return checkedRadio
|
|
&& !checkedRadio.disabled
|
|
&& !this._isPickupLocationMissing(checkedRadio);
|
|
},
|
|
|
|
/**
|
|
* Get the delivery rate of the delivery method linked to the provided radio.
|
|
*
|
|
* @private
|
|
* @param {HTMLInputElement} radio - The radio button linked to the delivery method.
|
|
* @return {Object} The delivery rate data.
|
|
*/
|
|
async _getDeliveryRate(radio) {
|
|
return await rpc('/shop/get_delivery_rate', {'dm_id': radio.dataset.dmId});
|
|
},
|
|
|
|
/**
|
|
* Set the delivery method on the order and return the result values.
|
|
*
|
|
* @private
|
|
* @param {Integer} dmId - The id of selected delivery method.
|
|
* @return {Object} The result values.
|
|
*/
|
|
async _setDeliveryMethod(dmId) {
|
|
return await rpc('/shop/set_delivery_method', {'dm_id': dmId});
|
|
},
|
|
|
|
/**
|
|
* Show the pickup location information or the button to open the location selector.
|
|
*
|
|
* @private
|
|
* @param {HTMLInputElement} radio - The radio button linked to the delivery method.
|
|
* @return {void}
|
|
*/
|
|
async _showPickupLocation(radio) {
|
|
if (!radio.dataset.isPickupLocationRequired || radio.disabled) {
|
|
return; // Fetching the delivery rate failed.
|
|
}
|
|
const deliveryMethodContainer = this._getDeliveryMethodContainer(radio);
|
|
const pickupLocation = deliveryMethodContainer.querySelector('[name="o_pickup_location"]');
|
|
|
|
const editPickupLocationButton = pickupLocation.querySelector(
|
|
'span[name="o_pickup_location_selector"]'
|
|
);
|
|
if (editPickupLocationButton.dataset.pickupLocationData) {
|
|
await this._setPickupLocation(editPickupLocationButton.dataset.pickupLocationData);
|
|
}
|
|
|
|
pickupLocation.classList.remove('d-none'); // Show the whole div.
|
|
},
|
|
|
|
/**
|
|
* Set the pickup location on the order.
|
|
*
|
|
* @private
|
|
* @param {String} pickupLocationData - The pickup location's data to set.
|
|
* @return {void}
|
|
*/
|
|
async _setPickupLocation(pickupLocationData) {
|
|
await rpc('/website_sale/set_pickup_location', {pickup_location_data: pickupLocationData});
|
|
},
|
|
|
|
// #=== GETTERS & SETTERS ===#
|
|
|
|
/** Determine and return the selected address who card has the class rowAddrClass.
|
|
*
|
|
* @private
|
|
* @param addressType - The type of the address: 'billing' or 'delivery'.
|
|
* @return {Element}
|
|
*/
|
|
_getSelectedAddress(addressType) {
|
|
return this.el.querySelector(`.card.bg-primary[data-address-type="${addressType}"]`);
|
|
},
|
|
|
|
/**
|
|
* Return whether the "use delivery as billing" toggle is checked or a billing address is
|
|
* selected.
|
|
*
|
|
* @private
|
|
* @return {boolean} - Whether a billing address is selected.
|
|
*/
|
|
_isBillingAddressSelected() {
|
|
const billingAddressSelected = Boolean(
|
|
this.el.querySelector('.card.bg-primary[data-address-type="billing"]')
|
|
);
|
|
return billingAddressSelected || this.use_delivery_as_billing_toggle.checked;
|
|
},
|
|
|
|
/**
|
|
* Create and return an element representing a loading spinner.
|
|
*
|
|
* @private
|
|
* @return {Element} The created element.
|
|
*/
|
|
_createLoadingElement() {
|
|
const loadingElement = document.createElement('i');
|
|
loadingElement.classList.add('fa', 'fa-circle-o-notch', 'fa-spin', 'center');
|
|
return loadingElement;
|
|
},
|
|
|
|
/**
|
|
* Return the delivery price badge element of the delivery method linked to the provided radio.
|
|
*
|
|
* @private
|
|
* @param {HTMLInputElement} radio - The radio button linked to the delivery method.
|
|
* @return {Element} The delivery price badge element of the linked delivery method.
|
|
*/
|
|
_getDeliveryPriceBadge(radio) {
|
|
const deliveryMethodContainer = this._getDeliveryMethodContainer(radio);
|
|
return deliveryMethodContainer.querySelector('.o_wsale_delivery_price_badge');
|
|
},
|
|
|
|
/**
|
|
* Return the container element of the delivery method linked to the provided element.
|
|
*
|
|
* @private
|
|
* @param {Element} el - The element linked to the delivery method.
|
|
* @return {Element} The container element of the linked delivery method.
|
|
*/
|
|
_getDeliveryMethodContainer(el) {
|
|
return el.closest('[name="o_delivery_method"]');
|
|
},
|
|
|
|
/**
|
|
* Return whether a pickup location is required but not selected.
|
|
*
|
|
* @private
|
|
* @param {HTMLInputElement} radio - The radio button linked to the delivery method.
|
|
* @return {boolean} Whether a required pickup location is missing.
|
|
*/
|
|
_isPickupLocationMissing(radio) {
|
|
const deliveryMethodContainer = this._getDeliveryMethodContainer(radio);
|
|
if (!this._isPickupLocationRequired(radio)) return false;
|
|
return !deliveryMethodContainer.querySelector(
|
|
'span[name="o_pickup_location_selector"]'
|
|
).dataset.locationId;
|
|
},
|
|
|
|
/**
|
|
* Return whether a pickup is required for the delivery method linked to the provided radio.
|
|
*
|
|
* @private
|
|
* @param {HTMLInputElement} radio - The radio button linked to the delivery method.
|
|
* @return {bool} Whether a pickup is needed.
|
|
*/
|
|
_isPickupLocationRequired(radio) {
|
|
return Boolean(radio.dataset.isPickupLocationRequired);
|
|
},
|
|
|
|
});
|
|
|
|
export default publicWidget.registry.WebsiteSaleCheckout;
|