630 lines
21 KiB
JavaScript
630 lines
21 KiB
JavaScript
'use strict';
|
|
|
|
import {createWithEqualityFn} from "zustand/traditional";
|
|
import {DATA as shared_data, itemsUnfoldedList, API_RFQ} from "./utils";
|
|
import {FillExtCrateData, FillExtOrderData, true_type_of} 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";
|
|
|
|
|
|
const cards_to_pn_map = (cards) => {
|
|
let result = {};
|
|
Object.entries(cards).forEach(([key, card], _i) => { result[card.name_number] = key})
|
|
return result;
|
|
};
|
|
|
|
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,
|
|
notificationTimer: null,
|
|
_showNotification: (crate_id, card_index) => set(state => ({
|
|
notificationCrateId: crate_id,
|
|
notificationCardIndex: card_index,
|
|
notificationTimer: setTimeout(() => {
|
|
state.hideNotification()
|
|
}, 5000)
|
|
})),
|
|
showNotification: (crate_id, card_index) => {
|
|
get().hideNotification()
|
|
setTimeout(() => get()._showNotification(crate_id, card_index), 100);
|
|
},
|
|
hideNotification: () => set(state => ({
|
|
notificationCrateId: null,
|
|
notificationCardIndex: null,
|
|
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);
|
|
},
|
|
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
|
|
},
|
|
highlightedTimer: null,
|
|
|
|
// #!if disable_card_highlight === false
|
|
highlightCard: (crate_id, index) => set(state => ({
|
|
highlighted: {
|
|
crate: crate_id,
|
|
card: index
|
|
},
|
|
highlightedTimer: (!!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null) || (state.isTouch && setTimeout(() => {
|
|
get().highlightReset()
|
|
}, 2000))
|
|
})),
|
|
highlightReset: () => set(state => ({
|
|
highlighted: {
|
|
crate: "",
|
|
card: 0
|
|
},
|
|
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) => set(state => {
|
|
const take_from = (true_type_of(index_from) === "array" ? index_from : [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.items.length;
|
|
return {
|
|
...crate,
|
|
items: crate.items.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) => set(state => {
|
|
const the_card = state.crates.find((crate, _) => crate_from === crate.id ).items[index_from];
|
|
return {
|
|
crates: state.crates.map((crate, _i) => {
|
|
if (crate_to === crate_from && crate_to === crate.id) {
|
|
let items_copy = Array.from(crate.items);
|
|
let item = items_copy.splice(index_from, 1)[0]
|
|
items_copy.splice(index_to, 0, item).filter((item, _) => !!item)
|
|
return {
|
|
...crate,
|
|
items: items_copy
|
|
}
|
|
} else if (crate_to === crate.id) {
|
|
return {
|
|
...crate,
|
|
items: crate.items.toSpliced(index_to, 0, the_card)
|
|
}
|
|
} else if (crate_from === crate.id) {
|
|
return {
|
|
...crate,
|
|
items: crate.items.toSpliced(index_to, 1)
|
|
}
|
|
}
|
|
else return crate;
|
|
})
|
|
}
|
|
}),
|
|
_deleteCard: (crate_id, index) => set(state => ({
|
|
crates: state.crates.map((crate, _i) => {
|
|
if (crate_id === crate.id) {
|
|
return {
|
|
...crate,
|
|
items: crate.items.toSpliced(index, 1)
|
|
}
|
|
}
|
|
else return crate;
|
|
})
|
|
})),
|
|
_clearCrate: (id) => set(state => ({
|
|
crates: state.crates.map((crate, _i) => {
|
|
if (id === crate.id) {
|
|
return {
|
|
...crate,
|
|
items: []
|
|
}
|
|
}
|
|
else return crate;
|
|
})
|
|
})),
|
|
clearAll: () => set(state => ({
|
|
crates: state._defaultCrates
|
|
})),
|
|
_updateOptions: (crate_id, index, new_options) => set(state => ({
|
|
crates: state.crates.map((crate, _i) => {
|
|
if (crate_id === crate.id) {
|
|
let itemsCopy = Array.from(crate.items);
|
|
itemsCopy[index] = {
|
|
...itemsCopy[index],
|
|
options_data: {
|
|
...itemsCopy[index].options_data,
|
|
...new_options
|
|
}};
|
|
return {
|
|
...crate,
|
|
items: itemsCopy
|
|
}
|
|
}
|
|
else return crate;
|
|
})
|
|
})),
|
|
|
|
fillWarnings: (crate_id) => set(state => ({
|
|
crates: state.crates.map((crate, _i) => {
|
|
if (crate_id === crate.id) {
|
|
//console.log("--- CHECK ALERTS ---")
|
|
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) => set(state => ({
|
|
crates: state.crates.map((crate, _i) => {
|
|
if (crate_id === crate.id) {
|
|
let itemsCopy = Array.from(crate.items);
|
|
|
|
itemsCopy = itemsCopy.map((item, index) => {
|
|
if (!item.options) return item;
|
|
if (!item.options_data) item.options_data = {};
|
|
item.options_data.ext_data = FillExtCardData(itemsCopy, index);
|
|
return item;
|
|
});
|
|
return {
|
|
...crate,
|
|
items: 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.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().fillExtCrateData(crate_id);
|
|
get().fillOrderExtData();
|
|
get().fillWarnings(crate_id);
|
|
get()._updateTotalOrderPrice();
|
|
},
|
|
|
|
setCrateMode: (id, mode) => {
|
|
get()._setCrateMode(id, mode)
|
|
get().fillExtData(id);
|
|
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 dest = crate_to || get().active_crate;
|
|
if (!dest) {
|
|
console.warn("No destination");
|
|
get().noDestinationWarning();
|
|
return {};
|
|
}
|
|
get().showNotification(dest, index_to);
|
|
get()._addCardFromCatalog(dest, index_from, index_to)
|
|
get().fillExtData(dest);
|
|
get().fillWarnings(dest);
|
|
get().setActiveCrate(dest);
|
|
get()._updateTotalOrderPrice();
|
|
if (!just_mounted) {
|
|
get().cardAdded()
|
|
}
|
|
},
|
|
|
|
moveCard: (crate_from, index_from, crate_to, index_to) => {
|
|
get()._moveCard(crate_from, index_from, crate_to, index_to);
|
|
get().fillExtData(crate_to);
|
|
get().fillWarnings(crate_to);
|
|
get().setActiveCrate(crate_to);
|
|
get()._updateTotalOrderPrice();
|
|
if (crate_from !== crate_to) {
|
|
get().fillExtData(crate_from);
|
|
get().fillWarnings(crate_from);
|
|
}
|
|
},
|
|
deleteCard: (crate_id, index) => {
|
|
get()._deleteCard(crate_id, index);
|
|
get().fillExtData(crate_id);
|
|
get().fillWarnings(crate_id);
|
|
get()._updateTotalOrderPrice();
|
|
if (crate_id === 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) => {
|
|
get()._updateOptions(crate_id, index, new_options);
|
|
get().fillExtData(crate_id);
|
|
get().fillWarnings(crate_id);
|
|
},
|
|
|
|
initExtData: () => {
|
|
get().fillOrderExtData();
|
|
get().crates.forEach((crate, _i) => {
|
|
get().fillExtData(crate.id);
|
|
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),
|
|
})) |