[IMP] point_of_sale: improve UX of product configurator popup

- Improved the UX of the product configurator.
- Display of attribute name in orderline name.

Previously, the user had to click on the checkbox or radio to select
the option, clicking on the label didn't work.
The attribute name was not displayed in the orderline name, only the
values.

Now the input has been replaced by a button, making it easier to use
on touch-screen devices.
The attribute name is displayed in the orderline name in the format
`Size: M, Fabric: Leather`.

closes odoo/odoo#147744

Taskids: 3638192 & 3633555
Signed-off-by: Joseph Caburnay (jcb) <jcb@odoo.com>
This commit is contained in:
David Monnom (moda)
2024-01-24 17:49:01 +00:00
committed by Loukas Wets (lowe)
parent 9beebd4141
commit a261bd5bcd
5 changed files with 89 additions and 32 deletions
@@ -38,6 +38,11 @@ export class BaseProductAttribute extends Component {
extra,
};
}
getFormatPriceExtra(val) {
const sign = val < 0 ? "- " : "+ ";
return sign + this.env.utils.formatCurrency(Math.abs(val));
}
}
export class RadioProductAttribute extends BaseProductAttribute {
@@ -57,6 +62,10 @@ export class RadioProductAttribute extends BaseProductAttribute {
}
}
export class PillsProductAttribute extends BaseProductAttribute {
static template = "point_of_sale.PillsProductAttribute";
}
export class SelectProductAttribute extends BaseProductAttribute {
static template = "point_of_sale.SelectProductAttribute";
}
@@ -71,7 +80,7 @@ export class MultiProductAttribute extends BaseProductAttribute {
setup() {
super.setup();
this.state = useState({
attribute_value_ids: [],
attribute_value_ids: {},
custom_value: "",
});
@@ -91,6 +100,7 @@ export class ProductConfiguratorPopup extends AbstractAwaitablePopup {
static template = "point_of_sale.ProductConfiguratorPopup";
static components = {
RadioProductAttribute,
PillsProductAttribute,
SelectProductAttribute,
ColorProductAttribute,
MultiProductAttribute,
@@ -3,21 +3,46 @@
<t t-name="point_of_sale.RadioProductAttribute">
<div class="configurator_radio" t-ref="root">
<div class="d-flex flex-column gap-3">
<div class="d-flex flex-wrap gap-3">
<t t-foreach="values" t-as="value" t-key="value.id">
<div class="attribute-name-cell form-check">
<input class="form-check-input" type="radio" t-model="state.attribute_value_ids" t-att-name="attribute.id"
<input class="form-check-input radio-check" type="radio" t-model="state.attribute_value_ids" t-att-name="attribute.id"
t-attf-id="{{ attribute.id }}_{{ value.id }}" t-att-value="value.id"/>
<label class="form-check-label" t-att-name="value.name" t-attf-for="{{ attribute.id }}_{{ value.id }}">
<span t-esc="value.name"/>
</label>
<div t-if="value.price_extra" class="price-extra-cell d-inline-block ms-3">
<span t-esc="value.name"/>
<div t-if="value.price_extra" class="price-extra-cell d-inline-block ms-2">
<span class="price_extra px-2 py-1 rounded-pill text-bg-info">
+ <t t-esc="env.utils.formatCurrency(value.price_extra)"/>
<t t-esc="getFormatPriceExtra(value.price_extra)"/>
</span>
</div>
</div>
<div t-if="value.id == state.attribute_value_ids &amp;&amp; value.is_custom" class="custom-value-cell">
<div t-if="value.id == state.attribute_value_ids and value.is_custom" class="custom-value-cell">
<input class="custom_value form-control form-control-lg mt-2" type="text" t-model="state.custom_value"/>
</div>
</t>
</div>
</div>
</t>
<t t-name="point_of_sale.PillsProductAttribute">
<div class="configurator_radio" t-ref="root">
<div class="d-flex flex-wrap gap-2">
<t t-foreach="values" t-as="value" t-key="value.id">
<div class="attribute-name-cell">
<input class="form-check-input d-none" type="radio" t-model="state.attribute_value_ids" t-att-name="attribute.id"
t-attf-id="{{ attribute.id }}_{{ value.id }}" t-att-value="value.id"/>
<label
t-attf-class="btn d-flex {{ value.id == state.attribute_value_ids ? 'btn-primary' : 'btn-secondary' }}"
t-att-name="value.name"
t-attf-for="{{ attribute.id }}_{{ value.id }}">
<span t-esc="value.name"/>
<div t-if="value.price_extra" class="price-extra-cell d-inline-block ms-2">
<span class="price_extra px-2 py-1 rounded-pill text-bg-info">
<t t-esc="getFormatPriceExtra(value.price_extra)"/>
</span>
</div>
</label>
</div>
<div t-if="value.id == state.attribute_value_ids and value.is_custom" class="custom-value-cell">
<input class="custom_value form-control form-control-lg mt-2" type="text" t-model="state.custom_value"/>
</div>
</t>
@@ -29,12 +54,12 @@
<div>
<t t-set="is_custom" t-value="false"/>
<select class="configurator_select form-select form-select-lg" t-model="state.attribute_value_ids">
<select class="configurator_select form-select form-select-md" t-model="state.attribute_value_ids">
<option t-foreach="values" t-as="value" t-key="value.id" t-att-value="value.id">
<t t-set="is_custom" t-value="is_custom || (value.is_custom &amp;&amp; value.id == state.attribute_value_ids)"/>
<t t-set="is_custom" t-value="is_custom || (value.is_custom and value.id == state.attribute_value_ids)"/>
<t t-esc="value.name"/>
<t t-if="value.price_extra">
+ <t t-esc="env.utils.formatCurrency(value.price_extra)"/>
<t t-esc="getFormatPriceExtra(value.price_extra)"/>
</t>
</option>
</select>
@@ -49,13 +74,20 @@
<ul class="color_attribute_list d-flex gap-3">
<li t-foreach="values" t-as="value" t-key="value.id" class="color_attribute_list_item">
<t t-set="is_custom" t-value="is_custom || (value.is_custom &amp;&amp; value.id == state.attribute_value_ids)"/>
<t t-set="is_custom" t-value="is_custom || (value.is_custom and value.id == state.attribute_value_ids)"/>
<t t-set="img_style" t-value="value.image ? 'background:url(/web/image/product.template.attribute.value/' + value.id + '/image); background-size:cover;' : ''"/>
<t t-set="color_style" t-value="value.is_custom ? '' : 'background-color: ' + value.html_color" />
<label t-attf-class="configurator_color rounded border {{ value.id == state.attribute_value_ids ? 'active border-3 border-primary' : 'border-3 border-secondary' }}"
t-attf-style="#{img_style or color_style}" t-att-data-color="value.name">
<input class="m-4 opacity-0" type="radio" t-model="state.attribute_value_ids" t-att-value="value.id" t-att-name="attribute.id"/>
</label>
<span class="d-flex flex-row justify-content-center align-items-center">
<label t-attf-class="configurator_color rounded-circle border {{ value.id == state.attribute_value_ids ? 'active border-3 border-primary' : 'border-3 border-secondary' }}"
t-attf-style="{{ img_style or color_style }}" t-att-data-color="value.name">
<input class="m-2 opacity-0" type="radio" t-model="state.attribute_value_ids" t-att-value="value.id" t-att-name="attribute.id"/>
</label>
<div t-if="value.price_extra" class="price-extra-cell d-inline-block ms-2">
<span class="price_extra px-2 py-1 rounded-pill text-bg-info">
<t t-esc="getFormatPriceExtra(value.price_extra)"/>
</span>
</div>
</span>
</li>
</ul>
@@ -64,15 +96,25 @@
</t>
<t t-name="point_of_sale.MultiProductAttribute">
<div class="d-flex flex-column gap-3">
<div t-foreach="values" t-as="value" t-key="value.id" class="form-check">
<label class="form-check-label" t-attf-for="multi-{{value.id}}" t-esc="value.name" />
<input class="form-check-input" type="checkbox" t-attf-name="multi-{{value.id}}" t-model="state.attribute_value_ids[value.id]" />
<div t-if="value.price_extra" class="price-extra-cell d-inline-block ms-3">
<span class="price_extra px-2 py-1 rounded-pill text-bg-info">
+ <t t-esc="env.utils.formatCurrency(value.price_extra)"/>
</span>
</div>
<div class="d-flex gap-2 flex-wrap">
<div t-foreach="values" t-as="value" t-key="value.id">
<input
class="form-check-input d-none"
type="checkbox"
t-attf-id="multi-{{value.id}}"
t-attf-name="multi-{{value.id}}"
t-model="state.attribute_value_ids[value.id]"/>
<label
t-attf-class="form-check-label btn d-flex {{ state.attribute_value_ids[value.id] === true ? 'btn-primary' : 'btn-secondary' }}"
t-attf-name="multi-{{value.id}}"
t-attf-for="multi-{{value.id}}">
<span t-esc="value.name"/>
<div t-if="value.price_extra" class="price-extra-cell d-inline-block ms-2">
<span class="price_extra px-2 py-1 rounded-pill text-bg-info">
<t t-esc="getFormatPriceExtra(value.price_extra)"/>
</span>
</div>
</label>
</div>
</div>
</t>
@@ -85,9 +127,13 @@
</div>
<main class="body modal-body product_configurator_attributes m-0 text-start">
<div t-foreach="props.attributes" t-as="attribute" t-key="attribute.id" class="attribute mb-3">
<div class="attribute_name mb-2 fw-bolder" t-esc="attribute.name"/>
<RadioProductAttribute t-if="attribute.display_type === 'radio' or attribute.display_type === 'pills'" attribute="attribute"/>
<div t-foreach="props.attributes" t-as="attribute" t-key="attribute.id" class="attribute mb-4">
<div class="attribute_name mb-2 fw-bolder d-flex w-100 align-items-center gap-2">
<span t-esc="attribute.name" />
<hr class="hr flex-grow-1" />
</div>
<RadioProductAttribute t-if="attribute.display_type === 'radio'" attribute="attribute"/>
<PillsProductAttribute t-elif="attribute.display_type === 'pills'" attribute="attribute"/>
<SelectProductAttribute t-elif="attribute.display_type === 'select'" attribute="attribute"/>
<ColorProductAttribute t-elif="attribute.display_type === 'color'" attribute="attribute"/>
<MultiProductAttribute t-elif="attribute.display_type === 'multi'" attribute="attribute"/>
@@ -4,7 +4,7 @@ export function pickRadio(name) {
return [
{
content: `picking radio attribute with name ${name}`,
trigger: `.product-configurator-popup .attribute-name-cell label[name='${name}']`,
trigger: `.product-configurator-popup div.attribute-name-cell:contains('${name}') input`,
},
];
}
@@ -147,6 +147,7 @@ export class AttributeSelection extends Component {
getfPriceExtra(value) {
const priceExtra = this._getPriceExtra(value);
return this.selfOrder.formatMonetary(priceExtra);
const sign = priceExtra < 0 ? "- " : "+ ";
return sign + this.selfOrder.formatMonetary(Math.abs(priceExtra));
}
}
@@ -17,7 +17,7 @@
<div class="name position-relative d-flex flex-column justify-content-center align-items-center flex-grow-1 w-100 p-4 text-center">
<span t-out="value.name"/>
<span t-if="shouldShowPriceExtra(value)">
+ <t t-out="getfPriceExtra(value)" />
<t t-esc="getfPriceExtra(value)"/>
</span>
</div>
</label>