'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), }))