web2019/static/js/shop/shop_store.js
Egor Savkin 84562f276a Add horizontal items into the cart
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-11-06 17:18:23 +08:00

667 lines
23 KiB
JavaScript

'use strict';
import {createWithEqualityFn} from "zustand/traditional";
import {DATA as shared_data, itemsUnfoldedList, API_RFQ} from "./utils";
import {FillExtCrateData, FillExtOrderData} from "./options/utils";
import {v4 as uuidv4} from "uuid";
import {FillResources} from "./count_resources";
import {FillExtCardData} from "./options/utils";
import {TriggerCrateWarnings, TriggerWarnings} from "./warnings";
import {Validation, validateEmail, validateNote, validateJSONInput} from "./validate";
import {CratesToJSON, JSONToCrates} from "./json_porter";
import {ProcessOptionsToData} from "./options/Options";
import {DomainedRFQMessages} from "./Domained";
export const HORIZONTAL_CART_MARKER = "_h";
const cards_to_pn_map = (cards) => {
let result = {};
Object.entries(cards).forEach(([key, card], _i) => { result[card.name_number] = key})
return result;
};
const toArray = (arg) => {
return Array.isArray(arg) ? arg : [arg];
};
const unwrapCrateId = (crate_id= "") => {
return crate_id.endsWith(HORIZONTAL_CART_MARKER) ? [true, crate_id.substring(0, crate_id.length - HORIZONTAL_CART_MARKER.length)] : [false, crate_id]
}
export const whichItems = (horizontal = false) => horizontal ? "h_items" : "items"
const useCatalog = ((set, get) => ({
cards: shared_data.items,
groups: shared_data.columns.catalog,
cards_list: itemsUnfoldedList,
currency: shared_data.currency,
pn_to_cards: cards_to_pn_map(shared_data.items),
getCardDescription: index => get().cards[get().cards_list[index]],
getCardDescriptionByPn: pn => get().cards[get().pn_to_cards[pn]],
cardIndexById: card_id => get().cards_list.findIndex((element) => (card_id === element))
}));
const useOptionsNotification = ((set, get) => ({
notificationCrateId: null,
notificationCardIndex: null,
notificationHorizontal: false,
notificationTimer: null,
_showNotification: (crate_id, card_index, horizontal) => set(state => ({
notificationCrateId: crate_id,
notificationCardIndex: card_index,
notificationHorizontal: horizontal,
notificationTimer: setTimeout(() => {
state.hideNotification()
}, 5000)
})),
showNotification: (crate_id, card_index, horizontal) => {
get().hideNotification()
setTimeout(() => get()._showNotification(crate_id, card_index, horizontal), 100);
},
hideNotification: () => set(state => ({
notificationCrateId: null,
notificationCardIndex: null,
notificationHorizontal: false,
notificationTimer: (state.notificationTimer && clearTimeout(state.notificationTimer)) || null,
}))
}));
const useSearch = ((set, get) => ({
search_index: Array.from(Object.values(shared_data.items)
.map((card, _) => (
[(card.name + " " + card.name_number + " " + card.name_codename).toLowerCase(), card.id]
))),
search_bar_value: "",
listed_cards: [],
updateSearchBar: text => set(state => ({
search_bar_value: text,
listed_cards: text.length > 0 ? Array.from(get().search_index
.filter((card, _) => card[0].includes(text.toLowerCase()))
.map(([index, card_id], _) => get().cards_list.findIndex(elem => elem === card_id))) : []
}))
}));
const useCrateModes = ((set, get) => ({
crate_modes: shared_data.crateModes,
modes_order: shared_data.crateModeOrder,
crateParams: mode => get().crate_modes[mode],
}));
const useCrateOptions = ((set, get) => ({
crate_options: shared_data.crateOptions.options,
crate_prices: shared_data.crateOptions.prices,
fillExtCrateData: (crate_id) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) {
const previous_options = crate.options_data || {};
return {
...crate,
options_data: {
...previous_options,
ext_data: FillExtCrateData(crate)
}
}
}
else return crate;
})
})),
_updateCrateOption: (crate_id, new_options) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) {
const previous_options = crate.options_data || {};
return {
...crate,
options_data: {
...previous_options,
...new_options
}
}
}
else return crate;
})
})),
updateCrateOptions: (crate_id, new_options) => {
get().fillExtCrateData(crate_id);
get().fillOrderExtData();
get()._updateCrateOption(crate_id, new_options);
get()._updateTotalOrderPrice();
}
}));
const useOrderOptions = ((set, get) => ({
order_options: shared_data.orderOptions.options,
order_prices: shared_data.orderOptions.prices,
shipping_summary: shared_data.orderOptions.shippingSummary,
order_options_data: {},
fillOrderExtData: () => set(state => ({
order_options_data: {
...state.order_options_data,
ext_data: FillExtOrderData(state.crates, state.modes_order)
}
})),
_updateOrderOptions: (new_options) => set(state => ({
order_options_data: {
...state.order_options_data,
...new_options
}
})),
updateOrderOptions: (new_options) => {
get()._updateOrderOptions(new_options);
get()._updateTotalOrderPrice();
get().fillOrderExtData();
}
}));
const useLayout = ((set, get) => ({
isTouch: window.isTouchEnabled(),
isMobile: window.deviceIsMobile(),
sideMenuIsOpen: false,
showCardAddedFeedback: false,
showNoDestination: false,
timerAdded: null,
_switchSideMenu: () => set(state => ({
sideMenuIsOpen: !state.sideMenuIsOpen
})),
switchSideMenu: () => {
if (!get().sideMenuIsOpen) {
get().hideNotification()
}
get()._switchSideMenu();
},
cardAdded: () => set(state => ({
showCardAddedFeedback: true,
showNoDestination: false,
timerAdded: (!!state.timerAdded ? clearTimeout(state.timerAdded) : null) || (state.isMobile && setTimeout(() => {
get()._endCardAdded()
}, 2000))
})),
noDestinationWarning: () => set(state => ({
showCardAddedFeedback: true,
showNoDestination: true,
timerAdded: (!!state.timerAdded ? clearTimeout(state.timerAdded) : null) || (setTimeout(() => {
get()._endCardAdded()
}, 2000))
})),
_endCardAdded: () => set(state => ({
showCardAddedFeedback: false,
timerAdded: !!state.timerAdded ? clearTimeout(state.timerAdded) : null
}))
}));
const useImportJSON = ((set, get) => ({
importShouldOpen: false,
importValue: {
value: "",
error: Validation.OK
},
openImport: () => set(state => ({
importShouldOpen: true
})),
closeImport: () => set(state => ({
importShouldOpen: false
})),
_loadDescription: () => set(state => {
const parsed = JSONToCrates(state.importValue.value);
// if (parsed.crates[-1].crate_mode !== "")
return {
importShouldOpen: false,
// additional fields go here
crates: parsed.crates,
order_options_data: parsed.order_options_data
}}),
loadDescription: () => {
get()._loadDescription()
get().fillOrderExtData();
get().crates.forEach((crate, _i) => {
get().fillExtData(crate.id);
get().fillWarnings(crate.id);
get().fillExtCrateData(crate.id);
});
get()._updateTotalOrderPrice();
get().showNotification(get().active_crate, null, false);
},
updateImportDescription: (new_description) => set(state => ({
importValue: {
value: new_description,
error: validateJSONInput(new_description)
}
}))
}));
const useSubmitForm = ((set, get) => ({
isProcessing: false,
shouldShowRFQFeedback: false,
processingResult: {
status: Validation.OK,
message: ""
},
API_RFQ: API_RFQ,
email: {
value: "",
error: null
},
note: {
value: "",
error: Validation.OK
},
description: "",
shouldShowDescription: false,
updateEmail: (new_email) => set(state => ({
email: {
value: new_email,
error: validateEmail(new_email)
}
})),
updateNote: (new_notes) => set(state => ({
note: {
value: new_notes,
error: validateNote(new_notes)
}
})),
resetEmailValidation: () => set(state => ({
email: {
value: state.email.value,
error: Validation.OK
}
})),
_revalidateForm: () => set(state => ({
email: {
value: state.email.value,
error: validateEmail(state.email.value)
},
note: {
value: state.note.value,
error: validateEmail(state.note.value)
},
})),
updateDescription: () => set(state => ({
description: CratesToJSON(state.crates)
})),
showDescription: () => set(state => ({
description: CratesToJSON(state.crates),
shouldShowDescription: true
})),
closeDescription: () => set(state => ({
shouldShowDescription: false
})),
_submitForm: () => set(state => ({isProcessing: true})),
finishSubmitForm: (result) => set(state => ({
isProcessing: false,
shouldShowRFQFeedback: true,
processingResult: result})),
submitDisabled: () => (get().email.error !== Validation.OK),
submitForm: () => {
get().updateDescription();
get()._revalidateForm();
get()._submitForm();
if (get().submitDisabled()) return;
fetch(get().API_RFQ, {
method: "POST",
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
email: get().email.value,
note: get().note.value,
configuration: get().description
})
}).then(response => {
if (response.status !== 200) {
throw Error("Response status is not OK: " + response.status + ".\n" + response);
}
get().finishSubmitForm({status: Validation.OK, message: DomainedRFQMessages.OK})
}, reason => {
console.error("Request rejected, reason:", reason)
get().finishSubmitForm({
status: Validation.Invalid,
message: DomainedRFQMessages.ERROR
})
}).catch(err => {
console.error("Request failed, reason:", err)
get().finishSubmitForm({
status: Validation.Invalid,
message: DomainedRFQMessages.ERROR
})
})
},
closeRFQFeedback: () => set(state => ({shouldShowRFQFeedback: false}))
}));
const useHighlighted = ((set, get) => ({
highlighted: {
crate: "",
card: 0,
horizontal: false
},
highlightedTimer: null,
// #!if disable_card_highlight === false
highlightCard: (crate_id, index, horizontal) => set(state => ({
highlighted: {
crate: crate_id,
card: index,
horizontal: horizontal
},
highlightedTimer: (!!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null) || (state.isTouch && setTimeout(() => {
get().highlightReset()
}, 2000))
})),
highlightReset: () => set(state => ({
highlighted: {
crate: "",
card: 0,
horizontal: false
},
highlightedTimer: !!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null
})),
// #!else
highlightCard: () => {return null;},
highlightReset: () => {return null;},
// #!endif
}));
const useCart = ((set, get) => ({
crates: shared_data.columns.crates,
active_crate: "crate0",
_defaultCrates: Array.from(shared_data.columns.crates),
total_order_price: 0,
_newCrate: (crate_id) => set((state) => ({
crates: state.crates.toSpliced(-1, 0, {
...state._defaultCrates[0],
id: crate_id || "crate" + state.crates.length,
}),
active_crate: crate_id || "crate" + state.crates.length
})),
_delCrate: (id) => set(state => ({
crates: state.crates.filter((crate => crate.id !== id || !state.modes_order.includes(crate.crate_mode))),
active_crate: state.active_crate === id ? null : state.active_crate,
})),
_setCrateMode: (id, mode) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate.id === id) {
return {
...crate,
crate_mode: mode
}
} else return crate;
})
})),
setActiveCrate: (id) => set(state => ({active_crate: id})),
_addCardFromCatalog: (crate_to, index_from, index_to, horizontal) => set(state => {
const whichH = whichItems(horizontal)
const take_from = toArray(index_from).map((item, _i) => (state.cards_list[item]));
const dest = crate_to || state.active_crate;
if (!dest) return {};
return {
crates: state.crates.map((crate, _i) => {
if (dest === crate.id) {
index_to = index_to != null ? index_to : crate[whichH].length;
return {
...crate,
[whichH]: crate[whichH].toSpliced(index_to, 0, ...take_from.map((card_name, _) => {
return {...state.cards[card_name], id: uuidv4()}
}))
}
} else return crate;
})
}
}),
_moveCard: (crate_from, index_from, crate_to, index_to, horizontal) => set(state => {
const whichH = whichItems(horizontal)
const the_card = state.crates.find((crate, _) => crate_from === crate.id )[whichH][index_from];
return {
crates: state.crates.map((crate, _i) => {
if (crate_to === crate_from && crate_to === crate.id) {
let items_copy = Array.from(crate[whichH]);
let item = items_copy.splice(index_from, 1)[0]
items_copy.splice(index_to, 0, item).filter((item, _) => !!item)
return {
...crate,
[whichH]: items_copy
}
} else if (crate_to === crate.id) {
return {
...crate,
[whichH]: crate[whichH].toSpliced(index_to, 0, the_card)
}
} else if (crate_from === crate.id) {
return {
...crate,
[whichH]: crate[whichH].toSpliced(index_to, 1)
}
}
else return crate;
})
}
}),
_deleteCard: (crate_id, index, horizontal) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) {
const whichH = whichItems(horizontal)
return {
...crate,
[whichH]: crate[whichH].toSpliced(index, 1)
}
}
else return crate;
})
})),
_clearCrate: (id) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (id === crate.id) {
return {
...crate,
items: [],
h_items: []
}
}
else return crate;
})
})),
clearAll: () => set(state => ({
crates: state._defaultCrates
})),
_updateOptions: (crate_id, index, new_options, horizontal) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) {
const whichH = whichItems(horizontal)
let itemsCopy = Array.from(crate[whichH]);
itemsCopy[index] = {
...itemsCopy[index],
options_data: {
...itemsCopy[index].options_data,
...new_options
}};
return {
...crate,
[whichH]: itemsCopy
}
}
else return crate;
})
})),
fillWarnings: (crate_id) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) {
// Warnings for horizontal items are not available
let itemsCopy = Array.from(crate.items);
const disabled = !!get().crateParams(crate.crate_mode).warnings_disabled;
itemsCopy = FillResources(itemsCopy, disabled);
itemsCopy = TriggerWarnings(itemsCopy, disabled);
const [crate_warnings, occupied] = TriggerCrateWarnings(crate);
return {
...crate,
items: itemsCopy,
warnings: crate_warnings,
occupiedHP: occupied
}
}
else return crate;
})
})),
fillExtData: (crate_id, horizontal) => set(state => ({
crates: state.crates.map((crate, _i) => {
// horizontal items do not interact with each other for now
if (crate_id === crate.id) {
const whichH = whichItems(horizontal)
const options_name = state.crateParams(crate.crate_mode).options;
let itemsCopy = Array.from(crate[whichH]);
itemsCopy = itemsCopy.map((item, index) => {
if (!item[options_name]) return item;
if (!item.options_data) item.options_data = {};
item.options_data.ext_data = FillExtCardData(itemsCopy, index);
return item;
});
return {
...crate,
[whichH]: Array.from(itemsCopy)
}
}
else return crate;
})
})),
_updateTotalOrderPrice: () => set(state => {
let sum = 0;
get().crates.forEach( (crate, i) => {
sum += get().crate_modes[crate.crate_mode].price;
const crate_options = ProcessOptionsToData({options: get().crate_prices, data: crate.options_data || {}});
sum += crate_options ? crate_options.reduce((accumulator, currentValue) => accumulator+currentValue.price, 0) : 0;
crate.items.concat(crate.h_items).forEach((item, _) => {
sum += item.price;
});
});
const order_options = ProcessOptionsToData({options: get().order_prices, data: get().order_options_data || {}});
sum += order_options ? order_options.reduce((accumulator, currentValue) => accumulator+currentValue.price, 0) : 0;
return {total_order_price: sum};
}),
// Composite actions that require warnings recalculation:
newCrate: () => {
const crate_id = "crate" + get().crates.length;
get()._newCrate(crate_id)
get().fillExtData(crate_id);
get().fillExtData(crate_id, true);
get().fillExtCrateData(crate_id);
get().fillOrderExtData();
get().fillWarnings(crate_id);
get()._updateTotalOrderPrice();
},
setCrateMode: (id, mode) => {
get()._setCrateMode(id, mode)
get().fillExtData(id);
get().fillExtData(id, true);
get().fillExtCrateData(id);
get().fillOrderExtData();
get().fillWarnings(id);
get().setActiveCrate(id);
get()._updateTotalOrderPrice();
},
delCrate: (id) => {
get()._delCrate(id);
get().fillOrderExtData();
get()._updateTotalOrderPrice();
},
addCardFromCatalog: (crate_to, index_from, index_to, just_mounted) => {
const isCrateless = toArray(index_from).some(value => get().getCardDescription(value).crateless === true);
const [isHorizontal, crateTo] = crate_to ? unwrapCrateId(crate_to) : [false, crate_to];
const isHorizontalOnly = toArray(index_from).some(value => !!get().getCardDescription(value).horizontal);
const dest = isCrateless ? "spare" : crateTo || get().active_crate;
if (!dest) {
console.warn("No destination");
get().noDestinationWarning();
return {};
}
get().showNotification(dest, index_to, isHorizontalOnly);
get()._addCardFromCatalog(dest, index_from, index_to, isHorizontalOnly)
get().fillExtData(dest, isHorizontalOnly);
get().fillWarnings(dest);
get().setActiveCrate(dest);
get()._updateTotalOrderPrice();
if (!just_mounted) {
get().cardAdded()
}
},
moveCard: (crate_from, index_from, crate_to, index_to) => {
const [isHorizontal, crateFrom] = unwrapCrateId(crate_from)
const [_, crateTo] = unwrapCrateId(crate_to)
get()._moveCard(crateFrom, index_from, crateTo, index_to, isHorizontal);
get().fillExtData(crateTo, isHorizontal);
get().fillWarnings(crateTo);
get().setActiveCrate(crateTo);
get()._updateTotalOrderPrice();
if (crateFrom !== crate_to) {
get().fillExtData(crateFrom, isHorizontal);
get().fillWarnings(crateFrom);
}
},
deleteCard: (crate_id, index, horizontal) => {
const [isHorizontal, crateId] = horizontal ? [horizontal, crate_id] : unwrapCrateId(crate_id);
get()._deleteCard(crateId, index, isHorizontal);
get().fillExtData(crateId, isHorizontal);
get().fillWarnings(crateId);
get()._updateTotalOrderPrice();
if (crateId === get().highlighted.crate && index === get().highlighted.card) get().highlightReset()
},
clearCrate: (id) => {
get()._clearCrate(id);
get().fillWarnings(id);
get()._updateTotalOrderPrice();
},
updateOptions: (crate_id, index, new_options, horizontal) => {
get()._updateOptions(crate_id, index, new_options, horizontal);
get().fillExtData(crate_id, horizontal);
if (!horizontal) {
get().fillWarnings(crate_id);
}
},
initExtData: () => {
get().fillOrderExtData();
get().crates.forEach((crate, _i) => {
get().fillExtData(crate.id, true);
get().fillExtData(crate.id, false);
get().fillExtCrateData(crate.id);
})
get()._updateTotalOrderPrice();
}
}))
export const useShopStore = createWithEqualityFn((...params) => ({
...useOptionsNotification(...params),
...useCatalog(...params),
...useSearch(...params),
...useCrateModes(...params),
...useCart(...params),
...useSubmitForm(...params),
...useLayout(...params),
...useHighlighted(...params),
...useImportJSON(...params),
...useCrateOptions(...params),
...useOrderOptions(...params),
}))