/**
* Creates a new State of equipment
* @class
* @since 0.0.7
*/
class State {
/**
* @param {struct} equipment
* @param {map} equipment_map
*/
constructor(
equipment,
equipment_map,
) {
// this.eventEmitter = new EventEmitter( 'options' );
this.selected = [];
// all equipment has its relevant selected and disabled properties set depending its dependencies
// this holds equipment that is meaningfully changed i.e if the selection is changed from a previously
// selection
this.arr_actioned_equipment = [];
this.totals = {
costGross: 0,
costNet: 0,
costTax: 0,
};
this.equipment = equipment;
this.equipment_map = equipment_map;
this.clicked_equipment = {};
this.arr_selected_option_ids = this.getSelectedOptionIDs();
}
/**
* Handle interaction
* @param {number} interaction_idx equipment id
* @param {boolean} is_initializing whether we're currently setting up the configurator
*/
process(interaction_idx, is_initializing = false) {
// make deep copy otherwise undo state updates along with current state
// let clicked_equipment = <Equipment>$.extend(true, {}, this.equipment[ interaction_idx ]);
this.clicked_equipment = this.equipment[interaction_idx];
// toggle this item, we don't need to swap the selected state if we are
// initializing the tool and its dependencies
if (!is_initializing) {
this.clicked_equipment.is_selected = !this.clicked_equipment.is_selected;
}
// flag user interaction
this.clicked_equipment.user_selected = this.clicked_equipment.is_selected;
// so the user can't deselect
if (this.clicked_equipment.is_swatch) {
this.clicked_equipment.is_enabled = false;
}
// reset the equipment that is being actioned so we don't get multiples
this.arr_actioned_equipment = [];
this.handleDependencies(this.clicked_equipment, []);
// for returning just the selected options
this.arr_selected_option_ids = this.getSelectedOptionIDs();
}
/* eslint-disable no-param-reassign */
/* eslint-disable no-continue */
/**
* Handle dependencies
* @param equipment
* @param already_handled
*/
handleDependencies(equipment, already_handled) {
// loop over affects relationships (what does this affect)
this.processAffects(equipment, already_handled);
// loop over affecting relationships (what will affect this)
this.processAffectedBy(equipment, already_handled);
}
/**
* Loop over the equipment to handle what this affects (what does this affect)
*/
processAffects(equipment, already_handled) {
for (let i = 0, cnt = equipment.affects.length; i < cnt; i += 1) {
const dependency = equipment.affects[i];
const affected_id = dependency.id;
const affected_relationship = dependency.relationship;
// get the affected equipment, set a flag to say if the dependency was actioned
const affected = this.equipment[this.equipment_map[affected_id]];
let affect_actioned = false;
// skip equipment if it's already been handled
if (already_handled.indexOf(affected) !== -1) {
continue;
}
switch (affected_relationship) {
case 'require': {
if (equipment.is_selected) {
// if selecting, tick and disable required item
affected.is_selected = true;
affected.is_enabled = false;
affect_actioned = true;
} else if (!affected.user_selected) {
// if deselecting, renable and deselect
// required item ONLY IF user_interaction is false
affected.is_selected = false;
affected.is_enabled = true;
affect_actioned = true;
} else {
// renable but leave selection state as it was
affected.is_enabled = true;
}
break;
}
case 'require1ofA':
case 'require1ofB':
case 'require1ofC':
case 'require1ofD': {
// if the affected equipment is required and selected,
// disable it to stop it being deselected
if (equipment.is_selected && affected.is_selected) {
affected.is_enabled = false;
} else if (!equipment.is_selected) {
// if deselecting, renable and deselect
// required item ONLY IF user_interaction is false
if (!affected.user_selected) {
affected.is_selected = false;
}
affected.is_enabled = true;
affect_actioned = true;
}
break;
}
case 'require1ofAdef':
case 'require1ofBdef':
case 'require1ofCdef':
case 'require1ofDdef': {
const non_def_relationship = affected_relationship.replace(/def$/, '');
// this needs to ONLY take effect if there is no
// require1ofX set
if (equipment.is_selected) {
if (!equipment.isDependencyResolved(non_def_relationship)) {
// if selecting, tick and disable required item
affected.is_selected = true;
affect_actioned = true;
this.arr_actioned_equipment.push(affected);
equipment.resolveDependency(
non_def_relationship,
true,
affected.id,
);
}
// if the affected is selected it will need disabling
if (affected.is_selected) {
affected.is_enabled = false;
}
} else {
// if deselecting, renable and deselect
// required item ONLY IF user_interaction is false
if (!affected.user_selected) {
affected.is_selected = false;
equipment.resolveDependency(
non_def_relationship,
false,
affected.id,
);
}
affected.is_enabled = true;
affect_actioned = true;
}
break;
}
case 'excludeEnable': {
// deselect but leave it enabled
// (common 17inch/18inch wheels scenario)
if (equipment.is_selected) {
if (affected.is_selected) {
this.arr_actioned_equipment.push(affected);
}
affected.is_selected = false;
affected.is_enabled = true;
affect_actioned = true;
}
break;
}
case 'replace': {
// when replacing, it means it's a direct swap.
// currently used in packed paint, when replacing
// a TBC with a color.
// when the color is unticked we have to put the
// tbc back
if (equipment.is_selected) {
if (affected.is_selected) {
this.arr_actioned_equipment.push(affected);
}
// deselect the original but enable it to
// undo the replacement
affected.is_selected = false;
affected.is_enabled = true;
// disable replacement, cannot untick, must re-tick
// original or a different replacement
equipment.is_enabled = false;
affect_actioned = true;
affected.resolveDependency(
affected_relationship,
true,
equipment.id,
);
}
break;
}
default: {
// Unhandled relationship
// this is ok, we don't handle them all here
}
}
// keep handling dependencies
if (affect_actioned && affected.affects.length) {
already_handled.push(equipment);
this.handleDependencies(affected, already_handled);
}
}
}
/**
* Loop over the equipment to handle affecting relationships (what will affect this)
*/
processAffectedBy(equipment, already_handled) {
for (let i = 0, cnt = equipment.affected_by.length; i < cnt; i += 1) {
const dependency = equipment.affected_by[i];
const affecting_id = dependency.id;
const affecting_relationship = dependency.relationship;
// get the affecting equipment, set a flag to say if the dependency was actioned
const affecting = this.equipment[this.equipment_map[affecting_id]];
let affect_actioned = false;
// skip equipment if it's already been handled
if (already_handled.indexOf(affecting) !== -1) {
continue;
}
// Where a user chooses a CAP packed vehicle that has a special
// residual value, the packed element must not be deslectable.
// otherwise this will break the assumptions on how package options
// are priced
if (equipment.matchtype_name === 'pack') {
// if this is required in a group by another option
// we need to let that option know
if (equipment.is_selected) {
affect_actioned = true;
affecting.is_enabled = false;
affecting.show_cost = false;
}
} else {
switch (affecting_relationship) {
case 'require1ofA':
case 'require1ofB':
case 'require1ofC':
case 'require1ofD':
// if this is required in a group by another option
// we need to let that option know
if (equipment.is_selected) {
affect_actioned = true;
affecting.resolveDependency(
affecting_relationship,
true,
equipment.id,
);
// if this is required and the affecting equipment
// is selected, we need to disable it
if (affecting.is_selected) {
equipment.is_enabled = false;
} else {
equipment.is_enabled = true;
}
}
break;
case 'require1ofAdef':
case 'require1ofBdef':
case 'require1ofCdef':
case 'require1ofDdef':
// if this is required in a group by another option
// we need to let that option know
if (equipment.is_selected) {
affect_actioned = true;
affecting.resolveDependency(
affecting_relationship,
true,
equipment.id,
);
// if this is required and the affecting equipment
// is selected, we need to disable it
if (affecting.is_selected) {
equipment.is_enabled = false;
} else {
equipment.is_enabled = true;
}
}
break;
case 'replace':
if (equipment.is_selected) {
equipment.is_enabled = false;
}
break;
default:
// Unhandled relationship
// this is ok, we don't handle them all here
}
}
// keep handling dependencies
if (affect_actioned && equipment.affects.length) {
already_handled.push(affecting);
this.handleDependencies(equipment, already_handled);
}
}
}
/**
* Loop over the equipment to see which options are selected, run after
* dependencies have been handled
* @returns {array} Array of selected options
*/
getSelectedOptionIDs() {
// loop over affects relationships (what does this affect)
const cnt_equipment = this.equipment.length;
const arr_return = [];
for (let i = 0; i < cnt_equipment; i += 1) {
const skv_equipment = this.equipment[i];
if (
(
skv_equipment.is_selected
// && !skv_equipment.is_packed
&& skv_equipment.type === 'option'
) || (
skv_equipment.is_selected
&& skv_equipment.user_selected
&& skv_equipment.type === 'standard'
)
) {
arr_return.push(skv_equipment.id);
}
}
return arr_return;
}
/* eslint-enable no-continue */
/* eslint-enable no-param-reassign */
}
export {
State,
};