/* eslint-disable no-unused-vars */
import {filter} from "lodash";
import _cloneDeep from "lodash/cloneDeep";
import _isEmpty from "lodash/isEmpty";
import {config} from "utils";
/**
* Remove a modifier recursively from the mods state
* Structure as followed in notion.so/Item-Details-9bfe384406a64e71a2f23ac59afc50a1
*
* @param {object} args - list of arguments passed as an object
* @param {object} args.modifications - the current mod state
* @param {string} args.optionId - the option we are looking for
* @param {string} args.modId - the modifier id to remove
* @returns {object} Object of all modifiers with the modifier removed
*/
const removeNestedModifier = ({
modifications = {},
targetItem,
nestedIndex,
targetOption,
}) => {
if (_isEmpty(modifications)) {
return {};
}
// Go through the items in the modifier object
return Object.entries(modifications).reduce((mods, [id, items]) => {
if (!items.length) {
return {
...mods,
[id]: [],
};
}
const filteredItems = items?.filter(
({item, modifiers, option}) =>
item === targetItem && modifiers?.[targetOption],
);
const nestedOption =
filteredItems?.[nestedIndex]?.modifiers?.[targetOption] || [];
const reductionItems =
filteredItems.length && nestedIndex !== undefined ? nestedOption : items;
const removeIndex = reductionItems?.findIndex((i) => i.item === targetItem);
const newItems = reductionItems?.filter(
(i, index) => index !== removeIndex,
);
// Check if this option is the one to remove
if (id === targetOption) {
// The option we're looking to remove has been found (possibly in a nested tier)
// Find the index and filter from the mods list
// Replace the old mods list for this option with the new one
return {
...mods,
[id]: newItems,
};
}
if (filteredItems?.length && nestedIndex !== undefined) {
filteredItems[nestedIndex].modifiers[targetOption] = newItems;
return {
...mods,
[id]: filteredItems,
};
}
// Check the items in this option for nesting
// Search for the option id we're looking to remove
// If it can't be found and there are more modifiers,
// then recurisvely call this function searching for it
const newNestedItems = items.reduce((accu, itemObj) => {
if (itemObj?.option === targetOption && _isEmpty(itemObj?.modifiers)) {
return accu;
}
// If this item is not the option and there are modifiers,
// recursively call this function with the modifiers of this item
const nestedModifiers = removeNestedModifier({
modifications: itemObj?.modifiers,
nestedIndex,
targetItem,
targetOption,
});
return [
...accu,
{
...itemObj,
modifiers: nestedModifiers,
},
];
}, []);
return {
...mods,
[id]: newNestedItems,
};
}, modifications);
};
const nestInsertItemToOption = ({
modifications = {},
targetOption = "",
nestedIndex = 0,
payload = {},
}) => {
// Empty modifications state, skip
if (_isEmpty(modifications)) {
return {};
}
// Add item to current modifications state
if (modifications?.[targetOption]) {
return {
...modifications,
[targetOption]: [...modifications?.[targetOption], payload],
};
}
// Loop current modifications state to find the option
return Object.entries(modifications)?.reduce((acc, [id, items]) => {
// Skip empty items
if (_isEmpty(items)) {
return acc;
}
// Move deeper into each item and it's modifiers
const updatedItems = items?.reduce(
(accItems, {item, modifiers, option}) => {
// Skip items without modifiers (not nested)
if (_isEmpty(modifiers)) {
return accItems;
}
// Add item to nested item modifiers
const updatedModifiers = nestInsertItemToOption({
modifications: modifiers,
nestedIndex,
payload,
targetOption,
});
return [...accItems, {item, modifiers: updatedModifiers, option}];
},
items,
);
return {...acc, [id]: updatedItems};
}, modifications);
};
const modsReducer = (state, {type, payload}) => {
switch (type) {
case "ADD_MOD": {
const {
isNested = false,
item = "",
nestedIndex = 0,
option = "",
quantity = 0,
} = payload;
const modifiers = isNested ? payload?.modifiers : {};
const updatedState = nestInsertItemToOption({
payload: {
item,
modifiers,
option,
...(isNested && {quantity}),
},
modifications: state,
nestedIndex,
targetOption: option,
});
return updatedState;
}
case "REMOVE_MOD": {
const {item = "", nestedIndex = 0, option = ""} = payload;
const updatedState = removeNestedModifier({
modifications: state,
nestedIndex,
targetItem: item,
targetOption: option,
});
return updatedState;
}
case "CLEAR_MODS": {
return {};
}
case "SET_MODS": {
return payload;
}
default: {
return state;
}
}
};
const mapModifications = (mods) => {
if (_isEmpty(mods) || !mods) {
return [];
}
return Object.entries(mods).reduce((accu, [parentOption, items]) => {
const newItems =
items?.length &&
items.reduce((accu, {item, quantity = 1, option, modifiers = {}}) => {
const itemObj = {
item,
modifiers: mapModifications(modifiers),
option: option || parentOption,
};
const newItems = new Array(quantity).fill(0).map(() => itemObj);
return [...accu, ...newItems];
}, []);
return [
...accu,
{
modifiers: newItems || [],
option: parentOption,
},
];
}, []);
};
// return all mods as an array
const mergeModifications = (modifiers = {}) => {
if (_isEmpty(modifiers)) {
return [];
}
return Object.entries(modifiers).reduce((acc, [option, items]) => {
if (items?.length) {
const nestItems = items.reduce((acc, curItem) => {
return [...acc, curItem.item, ...mergeModifications(curItem.modifiers)];
}, []);
return [...acc, ...nestItems];
}
return acc;
}, []);
};
const sortOptions = (optionA = {}, optionB = {}) => {
if (optionA === null) optionA = {};
if (optionB === null) optionB = {};
let weightA = 0;
let weightB = 0;
if (optionA.min) {
weightA -= 1;
} else {
weightA += 1;
}
if (optionB.min) {
weightB -= 1;
} else {
weightB += 1;
}
if (optionA.min && optionB.min) {
if (optionB.min < optionA.min) {
weightB += 1;
} else if (optionB.min > optionA.min) {
weightA += 1;
}
}
if (optionA.max) {
weightA -= 1;
} else {
weightA += 1;
}
if (optionB.max) {
weightB -= 1;
} else {
weightB += 1;
}
if (optionA.max && optionB.max) {
if (optionB.max < optionA.max) {
weightB += 1;
} else if (optionB.max > optionA.max) {
weightA += 1;
}
}
return {
weightA,
weightB,
};
};
const mapItemTabs = (options, optionsHash, HARDCODES) => {
let tabs = options.map((i) => ({entities: [i], name: i.name}));
if (HARDCODES.optionsTabs && HARDCODES.optionsTabs.length) {
tabs = HARDCODES.optionsTabs
.map((tab) => {
const entities = options
.reduce((accu, option) => {
const formatedName = option.name.toLowerCase().trim();
// Find all options that match the hardcodes defined by the client
const isAnOption = tab.optionNames.filter((optionName) =>
formatedName.includes(optionName.toLowerCase()),
).length;
if (tab.type === 0) {
!isAnOption && accu.push(option.id);
} else {
isAnOption && accu.push(option.id);
}
return accu;
}, [])
.reduce((accu, optionId) => {
// Filter out all optionItems that match diet if diet is selected
const newOptions = _cloneDeep(optionsHash[optionId]);
return [...accu, newOptions];
}, []);
return {
entities: entities.filter((i) => i.items.length),
optionIds: entities.map((i) => i?.id).filter(Boolean),
name: tab.tabName,
};
})
.filter((i) => i.entities.length);
}
tabs = tabs.map((i) => ({
...i,
entities: config.option_weighted_sort
? i.entities.sort((optionA, optionB) => {
const {weightA = 0, weightB = 0} = sortOptions(optionA, optionB);
return weightA - weightB;
})
: i.entities,
}));
return tabs;
};
/**
* nestedModifierState - recursively search modifiers state for the modifiers of an option. Read {@link https://www.notion.so/Item-Details-9bfe384406a64e71a2f23ac59afc50a1} first.
*
* @param {string} optionId - the option to search
* @param {string} modId - the item id to search
* @param {object} modsState - the modifier state
* @todo Test using multiple nested tiers...
* @todo Move this to the mods context
*/
const nestedModifierState = ({optionId, modId, modsState}) => {
if (optionId && modId && !_isEmpty(modsState)) {
// When we are validating a nested item...
return Object.entries(modsState).reduce((accu, [id, options]) => {
// No options were found
if (!options?.length) return accu;
const modifiers = options.reduce((accu, itemObj) => {
if (id === optionId && itemObj?.item === modId) {
// The nested item was found, now include all of it's modifiers
return {
...accu,
...(itemObj?.modifiers || {}),
};
}
// Continue recursion when there are modifiers in search of a option & item match
if (Object.entries(itemObj?.modifiers ?? {}).length) {
const nestedModifiers = nestedModifierState({
modId,
modsState: itemObj?.modifiers,
optionId,
});
// Include all of it's modifiers if the nested item was found
return {
...accu,
...(nestedModifiers || {}),
};
}
return accu;
}, {});
// Only include the modifiers of the nested items
return {
...accu,
...modifiers,
};
}, {});
}
// Validating ordinary modifiers with nothing nested
return modsState;
};
export {
mapItemTabs,
mapModifications,
mergeModifications,
modsReducer,
nestedModifierState,
sortOptions,
};