mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-06 05:39:12 +00:00
Website: Product Configurator and Bootstrap 4 (#15965)
- Refactored Homepage with customisable Hero Section - New Homepage Section to add content on Homepage as cards or using Custom HTML - Products page at "/all-products" with customisable filters - Item Configure dialog to find an Item Variant filtered by attribute values - Contact Us dialog on Item page - Customisable Item page content using the Website Content field
This commit is contained in:
318
erpnext/templates/generators/item/item_configure.js
Normal file
318
erpnext/templates/generators/item/item_configure.js
Normal file
@@ -0,0 +1,318 @@
|
||||
class ItemConfigure {
|
||||
constructor(item_code, item_name) {
|
||||
this.item_code = item_code;
|
||||
this.item_name = item_name;
|
||||
|
||||
this.get_attributes_and_values()
|
||||
.then(attribute_data => {
|
||||
this.attribute_data = attribute_data;
|
||||
this.show_configure_dialog();
|
||||
});
|
||||
}
|
||||
|
||||
show_configure_dialog() {
|
||||
const fields = this.attribute_data.map(a => {
|
||||
return {
|
||||
fieldtype: 'Select',
|
||||
label: a.attribute,
|
||||
fieldname: a.attribute,
|
||||
options: a.values.map(v => {
|
||||
return {
|
||||
label: v,
|
||||
value: v
|
||||
};
|
||||
}),
|
||||
change: (e) => {
|
||||
this.on_attribute_selection(e);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
this.dialog = new frappe.ui.Dialog({
|
||||
title: __('Configure {0}', [this.item_name]),
|
||||
fields,
|
||||
on_hide: () => {
|
||||
set_continue_configuration();
|
||||
}
|
||||
});
|
||||
|
||||
this.attribute_data.forEach(a => {
|
||||
const field = this.dialog.get_field(a.attribute);
|
||||
const $a = $(`<a href>${__("Clear")}</a>`);
|
||||
$a.on('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.dialog.set_value(a.attribute, '');
|
||||
});
|
||||
field.$wrapper.find('.help-box').append($a);
|
||||
});
|
||||
|
||||
this.append_status_area();
|
||||
this.dialog.show();
|
||||
|
||||
this.dialog.set_values(JSON.parse(localStorage.getItem(this.get_cache_key())));
|
||||
|
||||
$('.btn-configure').prop('disabled', false);
|
||||
}
|
||||
|
||||
on_attribute_selection(e) {
|
||||
if (e) {
|
||||
const changed_fieldname = $(e.target).data('fieldname');
|
||||
this.show_range_input_if_applicable(changed_fieldname);
|
||||
} else {
|
||||
this.show_range_input_for_all_fields();
|
||||
}
|
||||
|
||||
const values = this.dialog.get_values();
|
||||
if (Object.keys(values).length === 0) {
|
||||
this.clear_status();
|
||||
localStorage.removeItem(this.get_cache_key());
|
||||
return;
|
||||
}
|
||||
|
||||
// save state
|
||||
localStorage.setItem(this.get_cache_key(), JSON.stringify(values));
|
||||
|
||||
// show
|
||||
this.set_loading_status();
|
||||
|
||||
this.get_next_attribute_and_values(values)
|
||||
.then(data => {
|
||||
const {
|
||||
valid_options_for_attributes,
|
||||
} = data;
|
||||
|
||||
this.set_item_found_status(data);
|
||||
|
||||
for (let attribute in valid_options_for_attributes) {
|
||||
const valid_options = valid_options_for_attributes[attribute];
|
||||
const options = this.dialog.get_field(attribute).df.options;
|
||||
const new_options = options.map(o => {
|
||||
o.disabled = !valid_options.includes(o.value);
|
||||
return o;
|
||||
});
|
||||
|
||||
this.dialog.set_df_property(attribute, 'options', new_options);
|
||||
this.dialog.get_field(attribute).set_options();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
show_range_input_for_all_fields() {
|
||||
this.dialog.fields.forEach(f => {
|
||||
this.show_range_input_if_applicable(f.fieldname);
|
||||
});
|
||||
}
|
||||
|
||||
show_range_input_if_applicable(fieldname) {
|
||||
const changed_field = this.dialog.get_field(fieldname);
|
||||
const changed_value = changed_field.get_value();
|
||||
if (changed_value && changed_value.includes(' to ')) {
|
||||
// possible range input
|
||||
let numbers = changed_value.split(' to ');
|
||||
numbers = numbers.map(number => parseFloat(number));
|
||||
|
||||
if (!numbers.some(n => isNaN(n))) {
|
||||
numbers.sort((a, b) => a - b);
|
||||
if (changed_field.$input_wrapper.find('.range-selector').length) {
|
||||
return;
|
||||
}
|
||||
const parent = $('<div class="range-selector">')
|
||||
.insertBefore(changed_field.$input_wrapper.find('.help-box'));
|
||||
const control = frappe.ui.form.make_control({
|
||||
df: {
|
||||
fieldtype: 'Int',
|
||||
label: __('Enter value betweeen {0} and {1}', [numbers[0], numbers[1]]),
|
||||
change: () => {
|
||||
const value = control.get_value();
|
||||
if (value < numbers[0] || value > numbers[1]) {
|
||||
control.$wrapper.addClass('was-validated');
|
||||
control.set_description(
|
||||
__('Value must be between {0} and {1}', [numbers[0], numbers[1]]));
|
||||
control.$input[0].setCustomValidity('error');
|
||||
} else {
|
||||
control.$wrapper.removeClass('was-validated');
|
||||
control.set_description('');
|
||||
control.$input[0].setCustomValidity('');
|
||||
this.update_range_values(fieldname, value);
|
||||
}
|
||||
}
|
||||
},
|
||||
render_input: true,
|
||||
parent
|
||||
});
|
||||
control.$wrapper.addClass('mt-3');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_range_values(attribute, range_value) {
|
||||
this.range_values = this.range_values || {};
|
||||
this.range_values[attribute] = range_value;
|
||||
}
|
||||
|
||||
show_remaining_optional_attributes() {
|
||||
// show all attributes if remaining
|
||||
// unselected attributes are all optional
|
||||
const unselected_attributes = this.dialog.fields.filter(df => {
|
||||
const value_selected = this.dialog.get_value(df.fieldname);
|
||||
return !value_selected;
|
||||
});
|
||||
const is_optional_attribute = df => {
|
||||
const optional_attributes = this.attribute_data
|
||||
.filter(a => a.optional).map(a => a.attribute);
|
||||
return optional_attributes.includes(df.fieldname);
|
||||
};
|
||||
if (unselected_attributes.every(is_optional_attribute)) {
|
||||
unselected_attributes.forEach(df => {
|
||||
this.dialog.fields_dict[df.fieldname].$wrapper.show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
set_loading_status() {
|
||||
this.dialog.$status_area.html(`
|
||||
<div class="alert alert-warning d-flex justify-content-between align-items-center" role="alert">
|
||||
${__('Loading...')}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
set_item_found_status(data) {
|
||||
const html = this.get_html_for_item_found(data);
|
||||
this.dialog.$status_area.html(html);
|
||||
}
|
||||
|
||||
clear_status() {
|
||||
this.dialog.$status_area.empty();
|
||||
}
|
||||
|
||||
get_html_for_item_found({ filtered_items_count, filtered_items, exact_match, product_info }) {
|
||||
const exact_match_message = __('1 exact match.');
|
||||
const one_item = exact_match.length === 1 ?
|
||||
exact_match[0] :
|
||||
filtered_items_count === 1 ?
|
||||
filtered_items[0] : '';
|
||||
|
||||
const item_add_to_cart = one_item ? `
|
||||
<div class="alert alert-success d-flex justify-content-between align-items-center" role="alert">
|
||||
<div>
|
||||
<div>${one_item} ${product_info && product_info.price ? '(' + product_info.price.formatted_price_sales_uom + ')' : ''}</div>
|
||||
</div>
|
||||
<a href data-action="btn_add_to_cart" data-item-code="${one_item}">
|
||||
${__('Add to cart')}
|
||||
</a>
|
||||
</div>
|
||||
`: '';
|
||||
|
||||
const items_found = filtered_items_count === 1 ?
|
||||
__('{0} item found.', [filtered_items_count]) :
|
||||
__('{0} items found.', [filtered_items_count]);
|
||||
|
||||
const item_found_status = `
|
||||
<div class="alert alert-warning d-flex justify-content-between align-items-center" role="alert">
|
||||
<span>
|
||||
${exact_match.length === 1 ? '' : items_found}
|
||||
${exact_match.length === 1 ? `<span>${exact_match_message}</span>` : ''}
|
||||
</span>
|
||||
<a href data-action="btn_clear_values">
|
||||
${__('Clear values')}
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return `
|
||||
${item_add_to_cart}
|
||||
${item_found_status}
|
||||
`;
|
||||
}
|
||||
|
||||
btn_add_to_cart(e) {
|
||||
if (frappe.session.user !== 'Guest') {
|
||||
localStorage.removeItem(this.get_cache_key());
|
||||
}
|
||||
const item_code = $(e.currentTarget).data('item-code');
|
||||
const additional_notes = Object.keys(this.range_values || {}).map(attribute => {
|
||||
return `${attribute}: ${this.range_values[attribute]}`;
|
||||
}).join('\n');
|
||||
erpnext.shopping_cart.update_cart({
|
||||
item_code,
|
||||
additional_notes,
|
||||
qty: 1
|
||||
});
|
||||
this.dialog.hide();
|
||||
}
|
||||
|
||||
btn_clear_values() {
|
||||
this.dialog.fields_list.forEach(f => {
|
||||
f.df.options = f.df.options.map(option => {
|
||||
option.disabled = false;
|
||||
return option;
|
||||
});
|
||||
});
|
||||
this.dialog.clear();
|
||||
this.on_attribute_selection();
|
||||
}
|
||||
|
||||
append_status_area() {
|
||||
this.dialog.$status_area = $('<div class="status-area">');
|
||||
this.dialog.$wrapper.find('.modal-body').prepend(this.dialog.$status_area);
|
||||
this.dialog.$wrapper.on('click', '[data-action]', (e) => {
|
||||
e.preventDefault();
|
||||
const $target = $(e.currentTarget);
|
||||
const action = $target.data('action');
|
||||
const method = this[action];
|
||||
method.call(this, e);
|
||||
});
|
||||
this.dialog.$body.css({ maxHeight: '75vh', overflow: 'auto', overflowX: 'hidden' });
|
||||
}
|
||||
|
||||
get_next_attribute_and_values(selected_attributes) {
|
||||
return this.call('erpnext.portal.product_configurator.utils.get_next_attribute_and_values', {
|
||||
item_code: this.item_code,
|
||||
selected_attributes
|
||||
});
|
||||
}
|
||||
|
||||
get_attributes_and_values() {
|
||||
return this.call('erpnext.portal.product_configurator.utils.get_attributes_and_values', {
|
||||
item_code: this.item_code
|
||||
});
|
||||
}
|
||||
|
||||
get_cache_key() {
|
||||
return `configure:${this.item_code}`;
|
||||
}
|
||||
|
||||
call(method, args) {
|
||||
// promisified frappe.call
|
||||
return new Promise((resolve, reject) => {
|
||||
frappe.call(method, args)
|
||||
.then(r => resolve(r.message))
|
||||
.fail(reject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function set_continue_configuration() {
|
||||
const $btn_configure = $('.btn-configure');
|
||||
const { itemCode } = $btn_configure.data();
|
||||
|
||||
if (localStorage.getItem(`configure:${itemCode}`)) {
|
||||
$btn_configure.text(__('Continue Configuration'));
|
||||
} else {
|
||||
$btn_configure.text(__('Configure'));
|
||||
}
|
||||
}
|
||||
|
||||
frappe.ready(() => {
|
||||
const $btn_configure = $('.btn-configure');
|
||||
if (!$btn_configure.length) return;
|
||||
const { itemCode, itemName } = $btn_configure.data();
|
||||
|
||||
set_continue_configuration();
|
||||
|
||||
$btn_configure.on('click', () => {
|
||||
$btn_configure.prop('disabled', true);
|
||||
new ItemConfigure(itemCode, itemName);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user