diff --git a/package-lock.json b/package-lock.json index e7c13ac8d..087f3bdee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,9 +18,8 @@ "@ionic/vue-router": "^7.6.0", "boon-js": "^2.0.3", "core-js": "^3.6.5", + "luxon": "^3.2.0", "mitt": "^2.1.0", - "moment": "^2.29.1", - "moment-timezone": "^0.5.33", "vue": "^3.2.26", "vue-i18n": "~9.1.6", "vue-router": "^4.0.12", @@ -30,6 +29,7 @@ "devDependencies": { "@capacitor/cli": "^2.4.7", "@intlify/vue-i18n-loader": "^2.1.0", + "@types/luxon": "^3.2.0", "@typescript-eslint/eslint-plugin": "~5.26.0", "@typescript-eslint/parser": "~5.26.0", "@vue/cli-plugin-babel": "~5.0.8", @@ -2811,6 +2811,12 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "dev": true + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -10248,6 +10254,14 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.10", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", @@ -10561,25 +10575,6 @@ "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==", "dev": true }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "engines": { - "node": "*" - } - }, - "node_modules/moment-timezone": { - "version": "0.5.45", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", - "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", - "dependencies": { - "moment": "^2.29.4" - }, - "engines": { - "node": "*" - } - }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", diff --git a/package.json b/package.json index 1af8fe5d0..2213beac2 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,8 @@ "@ionic/vue-router": "^7.6.0", "boon-js": "^2.0.3", "core-js": "^3.6.5", + "luxon": "^3.2.0", "mitt": "^2.1.0", - "moment": "^2.29.1", - "moment-timezone": "^0.5.33", "vue": "^3.2.26", "vue-i18n": "~9.1.6", "vue-router": "^4.0.12", @@ -33,6 +32,7 @@ }, "devDependencies": { "@capacitor/cli": "^2.4.7", + "@types/luxon": "^3.2.0", "@intlify/vue-i18n-loader": "^2.1.0", "@typescript-eslint/eslint-plugin": "~5.26.0", "@typescript-eslint/parser": "~5.26.0", diff --git a/src/App.vue b/src/App.vue index 00ef259ad..57d3356ff 100644 --- a/src/App.vue +++ b/src/App.vue @@ -7,6 +7,8 @@ \ No newline at end of file diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 5e2fc7e50..d2996cc04 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -19,11 +19,11 @@ const i18n = createI18n({ messages: loadLocaleMessages() }) -const translate = (key: string) => { +const translate = (key: string, named?: any) => { if (!key) { return ''; } - return i18n.global.t(key); + return i18n.global.t(key, named); }; export { i18n as default, translate } \ No newline at end of file diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 986b8fedc..894745af8 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -4,7 +4,9 @@ "Are you sure you want to save the changes?": "Are you sure you want to save the changes?", "Are you sure you want to cancel the order items?": "Are you sure you want to cancel the order items?", "Cancel": "Cancel", + "Cancel item": "Cancel item", "Cancel items": "Cancel items", + "Change pickup location": "Change pickup location", "Change Store": "Change Store", "Changes saved": "Changes saved", "City": "City", @@ -18,7 +20,7 @@ "Fetching order details.": "Fetching order details.", "Fetching stores.": "Fetching stores.", "First name": "First name", - "Failed to cancel the order": "Failed to cancel the order", + "Failed to cancel the order.": "Failed to cancel the order.", "Failed to update the pickup store": "Failed to update the pickup store", "Failed to update the shipping addess": "Failed to update the shipping addess", "Fetching address": "Fetching address", @@ -30,24 +32,32 @@ "Nearby stores": "Nearby stores", "OMS": "OMS", "Order ID": "Order ID", - "Order cancelled successfully": "Order cancelled successfully", + "Order cancelled successfully.": "Order cancelled successfully.", "Order item not eligible for reroute fulfilment": "Order item not eligible for reroute fulfilment", "Order not found": "Order not found", + "Out of stock": "Out of stock", "Shipment has been cancelled": "Shipment has been cancelled", "Password": "Password", "Please fill all the fields": "Please fill all the fields", + "Request cancel": "Request cancel", + "Request cancelation": "Request cancelation", "Save changes": "Save changes", "Save shipping address": "Save shipping address", "Select pickup location": "Select pickup location", + "Separate": "Separate", "Shipping address": "Shipping address", "Showing pickup locations near": "Showing pickup locations near", + "Showing pickup locations near your saved address": "Showing pickup locations near your saved address", "Something went wrong while fetching stores": "Something went wrong while fetching stores", "Something went wrong while fetching the order details": "Something went wrong while fetching the order details", "Settings": "Settings", "State": "State", "Street": "Street", + "These products are not available at a single store for pickup. Please select alternate options for items individually.": "These products are not available at a single store for pickup. Please select alternate options for items individually.", + "Together": "Together", "Tracking code": "Tracking code", "Username": "Username", + "was unable to prepare your order. Please select alternate options.": "{facilityName} was unable to prepare your order. Please select alternate options.", "Your Order": "Your Order", "Zipcode": "Zipcode" } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index f78422ecb..cf526acc2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,7 @@ import { createApp } from 'vue' import App from './App.vue' import router from './router'; -import moment from 'moment' -import "moment-timezone"; +import { DateTime } from 'luxon'; import { IonicVue } from '@ionic/vue'; @@ -47,17 +46,19 @@ const app = createApp(App) // Filters are removed in Vue 3 and global filter introduced https://v3.vuejs.org/guide/migration/filters.html#global-filters app.config.globalProperties.$filters = { - formatDate(value: any, inFormat?: string, outFormat?: string) { - // TODO Use Loxon instead + formatDate(value: any, inFormat?: string, outFormat = "MM-dd-yyyy") { // TODO Make default format configurable and from environment variables - return moment(value, inFormat).format(outFormat ? outFormat : 'MM-DD-YYYY'); + if(inFormat){ + return DateTime.fromFormat(value, inFormat).toFormat(outFormat); + } + return DateTime.fromISO(value).toFormat(outFormat); }, - formatUtcDate(value: any, inFormat?: string, outFormat?: string) { + formatUtcDate(value: any, inFormat?: any, outFormat = "MM-dd-yyyy") { // TODO Use Loxon instead // TODO Make default format configurable and from environment variables const userProfile = store.getters['user/getUserProfile']; // TODO Fix this setDefault should set the default timezone instead of getting it everytiem and setting the tz - return moment.utc(value, inFormat).tz(userProfile.userTimeZone).format(outFormat ? outFormat : 'MM-DD-YYYY'); + return DateTime.utc(value, inFormat).setZone(userProfile.userTimeZone).toFormat(outFormat) }, getFeature(featureHierarchy: any, featureKey: string) { let featureValue = '' diff --git a/src/services/OrderService.ts b/src/services/OrderService.ts index 5a34501d8..c389d1f0f 100644 --- a/src/services/OrderService.ts +++ b/src/services/OrderService.ts @@ -1,9 +1,6 @@ import { api } from '@/adapter'; -import store from '@/store'; const getOrder = async (payload: any): Promise => { - let baseURL = store.getters['user/getInstanceUrl']; - baseURL = baseURL && baseURL.startsWith('http') ? baseURL : `https://${baseURL}.hotwax.io/api/`; return api({ url: "getRerouteOrder", method: "post", @@ -43,10 +40,37 @@ const getProductStoreSetting = async (payload: any): Promise => { }); } +const getRerouteOrderFacilityChangeHistory = async (payload: any): Promise => { + return api({ + url: "getRerouteOrderFacilityChangeHistory", + method: "post", + data: payload + }); +} + +const releaseRerouteOrderItem = async (payload: any): Promise => { + return api({ + url: "releaseRerouteOrderItem", + method: "post", + data: payload + }); +} + +const requestCancelRerouteOrderItem = async (payload: any): Promise => { + return api({ + url: "requestCancelRerouteOrderItem", + method: "post", + data: payload + }); +} + export const OrderService = { getOrder, + getRerouteOrderFacilityChangeHistory, updateShippingAddress, updatePickupFacility, cancelOrderItem, - getProductStoreSetting + getProductStoreSetting, + releaseRerouteOrderItem, + requestCancelRerouteOrderItem } \ No newline at end of file diff --git a/src/store/modules/user/UserState.ts b/src/store/modules/user/UserState.ts index 12e8f49a4..a3938a933 100644 --- a/src/store/modules/user/UserState.ts +++ b/src/store/modules/user/UserState.ts @@ -4,4 +4,6 @@ export default interface UserState { instanceUrl: string; deliveryMethod: string; permissions: any; + isSplitEnabled: boolean; + isCancellationAllowed: boolean; } \ No newline at end of file diff --git a/src/store/modules/user/actions.ts b/src/store/modules/user/actions.ts index 3021eb862..720cef66e 100644 --- a/src/store/modules/user/actions.ts +++ b/src/store/modules/user/actions.ts @@ -5,9 +5,8 @@ import UserState from './UserState' import * as types from './mutation-types' import { hasError, showToast } from '@/utils' import { translate } from '@/i18n' -import moment from 'moment'; +import { DateTime } from 'luxon' import emitter from '@/event-bus' -import "moment-timezone"; import { updateInstanceUrl, updateToken, resetConfig } from '@/adapter' import { prepareAppPermissions, setPermissions } from '@/authorization' import { OrderService } from '@/services/OrderService' @@ -88,7 +87,7 @@ const actions: ActionTree = { async getProfile ( { commit }) { const resp = await UserService.getProfile() if (resp.status === 200) { - const localTimeZone = moment.tz.guess(); + const localTimeZone = DateTime.now().zoneName; if (resp.data.userTimeZone !== localTimeZone) { emitter.emit('timeZoneDifferent', { profileTimeZone: resp.data.userTimeZone, localTimeZone}); } @@ -120,7 +119,7 @@ const actions: ActionTree = { token, inputFields: { productStoreId, - "settingTypeEnumId": ["CUST_DLVRMTHD_UPDATE", "CUST_DLVRADR_UPDATE", "CUST_PCKUP_UPDATE", "CUST_ALLOW_CNCL", "RF_SHIPPING_METHOD"], + "settingTypeEnumId": ["CUST_DLVRMTHD_UPDATE", "CUST_DLVRADR_UPDATE", "CUST_PCKUP_UPDATE", "CUST_ALLOW_CNCL", "RF_SHIPPING_METHOD", "CUST_ORD_ITEM_SPLIT"], "settingTypeEnumId_op": "in" }, viewSize: 100 @@ -128,10 +127,14 @@ const actions: ActionTree = { if (!hasError(resp)) { const permissions = resp.data.docs.filter((permission: any) => permission.settingValue == 'true').map((permission: any) => permission.settingTypeEnumId) const deliveryMethod = resp.data.docs.find((permission: any) => permission.settingTypeEnumId === 'RF_SHIPPING_METHOD')?.settingValue + const isSplitEnabled = resp.data.docs.find((permission: any) => permission.settingTypeEnumId === 'CUST_ORD_ITEM_SPLIT')?.settingValue + const isCancellationAllowed = resp.data.docs.find((permission: any) => permission.settingTypeEnumId === 'CUST_ALLOW_CNCL')?.settingValue const appPermissions = prepareAppPermissions(permissions); setPermissions(appPermissions); commit(types.USER_DELIVERY_METHOD_UPDATED, deliveryMethod ? deliveryMethod : "STANDARD"); commit(types.USER_PERMISSIONS_UPDATED, appPermissions); + commit(types.USER_ORDER_SPLIT_CONFIG_UPDATED, isSplitEnabled === "true"); + commit(types.USER_ITEM_CANCELLATION_CONFIG_UPDATED, isCancellationAllowed === "true"); } } catch (error) { console.error(error) diff --git a/src/store/modules/user/getters.ts b/src/store/modules/user/getters.ts index 1a94d5e4f..9e4a4a4a2 100644 --- a/src/store/modules/user/getters.ts +++ b/src/store/modules/user/getters.ts @@ -24,6 +24,12 @@ const getters: GetterTree = { }, getUserPermissions (state) { return state.permissions; + }, + isSplitEnabled (state) { + return state.isSplitEnabled; + }, + isCancellationAllowed (state) { + return state.isCancellationAllowed } } export default getters; \ No newline at end of file diff --git a/src/store/modules/user/index.ts b/src/store/modules/user/index.ts index aabfd1c3a..57db4cfd1 100644 --- a/src/store/modules/user/index.ts +++ b/src/store/modules/user/index.ts @@ -13,6 +13,8 @@ const userModule: Module = { instanceUrl: '', deliveryMethod: '', permissions: [], + isSplitEnabled: false, + isCancellationAllowed: false }, getters, actions, diff --git a/src/store/modules/user/mutation-types.ts b/src/store/modules/user/mutation-types.ts index f530cd82b..76a47bae0 100644 --- a/src/store/modules/user/mutation-types.ts +++ b/src/store/modules/user/mutation-types.ts @@ -5,3 +5,5 @@ export const USER_INFO_UPDATED = SN_USER + '/INFO_UPDATED' export const USER_INSTANCE_URL_UPDATED = SN_USER + '/INSTANCE_URL_UPDATED' export const USER_PERMISSIONS_UPDATED = SN_USER + '/PERMISSIONS_UPDATED' export const USER_DELIVERY_METHOD_UPDATED = SN_USER + '/DELIVERY_METHOD_UPDATED' +export const USER_ORDER_SPLIT_CONFIG_UPDATED = SN_USER + '/ORDER_SPLIT_CONFIG_UPDATED' +export const USER_ITEM_CANCELLATION_CONFIG_UPDATED = SN_USER + '/ITEM_CANCELLATION_CONFIG_UPDATED' diff --git a/src/store/modules/user/mutations.ts b/src/store/modules/user/mutations.ts index 3b94fcd56..afb0c1e27 100644 --- a/src/store/modules/user/mutations.ts +++ b/src/store/modules/user/mutations.ts @@ -22,6 +22,12 @@ const mutations: MutationTree = { }, [types.USER_DELIVERY_METHOD_UPDATED] (state, payload) { state.deliveryMethod = payload + }, + [types.USER_ORDER_SPLIT_CONFIG_UPDATED] (state, payload) { + state.isSplitEnabled = payload + }, + [types.USER_ITEM_CANCELLATION_CONFIG_UPDATED] (state, payload) { + state.isCancellationAllowed = payload } } export default mutations; \ No newline at end of file diff --git a/src/theme/variables.css b/src/theme/variables.css index 1cbe9872e..664a45c16 100644 --- a/src/theme/variables.css +++ b/src/theme/variables.css @@ -233,4 +233,13 @@ http://ionicframework.com/docs/theming/ */ --ion-card-background: #1e1e1e; } +} + +.empty-state { + max-width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 10px; } \ No newline at end of file diff --git a/src/views/AddressModal.vue b/src/views/AddressModal.vue deleted file mode 100644 index 2135cfd05..000000000 --- a/src/views/AddressModal.vue +++ /dev/null @@ -1,175 +0,0 @@ - - - \ No newline at end of file diff --git a/src/views/Order.vue b/src/views/Order.vue index 498d80f09..9d8367bd6 100644 --- a/src/views/Order.vue +++ b/src/views/Order.vue @@ -4,7 +4,7 @@
-

{{ $t("Your Order") }}

+

{{ translate("Your Order") }}

@@ -12,80 +12,157 @@ {{ order.customerName }}

{{ order.id }}

- {{ $filters.formatDate(order.orderDate) }} + {{ $filters.formatDate(order.orderDate, "yyyy-MM-dd HH:mm:ss") }}
-
- - + + + + {{ translate("was unable to prepare your order. Please select alternate options.", { facilityName: originFacilityName }) }} + + + + + {{ method.name }} + + + + + + + + + + + + +
+ + {{ translate("Out of stock") }} + + +
+ + + + + +

{{ translate(isCancellationAllowed ? "Cancel" :"Request Cancellation") }}

+ {{ item.name }} +

+ {{ feature }}: {{ attribute }} +

+
+ + + + + + + + +
+
+
+ +
+ + {{ facilityId }} + + +
+ + + + + + {{ item.name }} +

+ {{ feature }}: {{ attribute }} +

+
+ + + + +
+
+
+ + + + {{ selectedFacility.facilityName }} +

{{ selectedFacility.address1 }}

+

{{ selectedFacility.city }} {{ selectedFacility.stateCode }} {{ order.shipGroup.shipTo.postalAddress.country }} {{ selectedFacility.postalCode }}

+
+
+ + + {{ customerAddress.toName }} +

{{ customerAddress.address1 }}

+

{{ customerAddress.city }} {{ customerAddress.stateCode }} {{ customerAddress.postalCode }}

+
+
+ + {{ selectedFacility.facilityId ? translate("Change pickup location") : translate("Select pickup location")}} + +
+ {{ translate("Save changes") }} + {{ translate(isCancellationAllowed ? "Cancel" : "Request cancel") }} +
+
- {{ $t("An email will be sent to you when your item(s) are ready to collect at the new requested location(s).") }} + {{ translate("An email will be sent to you when your item(s) are ready to collect at the new requested location(s).") }}
- {{ $t("Order item not eligible for reroute fulfilment") }} + {{ translate("Order item not eligible for reroute fulfilment") }}
- {{ $t("Order not found") }} + {{ translate("Order not found") }}
@@ -97,14 +174,18 @@ import { alertController, IonButton, IonCard, + IonChip, IonContent, + IonIcon, IonItem, + IonItemDivider, IonLabel, - IonList, IonNote, IonPage, IonSelect, IonSelectOption, + IonSegment, + IonSegmentButton, IonThumbnail, loadingController, modalController, @@ -116,11 +197,13 @@ import { OrderService } from "@/services/OrderService"; import { translate } from "@/i18n"; import { hasError, showToast } from "@/utils"; import Image from "@/components/Image.vue"; -import AddressModal from "@/views/AddressModal.vue"; import { ProductService } from "@/services/ProductService"; import PickupLocationModal from "@/views/PickupLocationModal.vue"; import { Actions, hasPermission } from '@/authorization' import { initialise } from '@/adapter' +import { addOutline, closeCircleOutline, colorWandOutline, medkitOutline, removeCircleOutline, storefrontOutline } from "ionicons/icons"; +import { FacilityService } from '@/services/FacilityService'; +import { StockService } from '@/services/StockService'; export default defineComponent({ name: "Order", @@ -128,13 +211,17 @@ export default defineComponent({ Image, IonButton, IonCard, + IonChip, IonContent, + IonIcon, IonItem, + IonItemDivider, IonLabel, - IonList, IonNote, IonSelect, IonSelectOption, + IonSegment, + IonSegmentButton, IonThumbnail, IonPage, }, @@ -154,17 +241,28 @@ export default defineComponent({ value: 'STANDARD' } ], - isOrderUpdated: false + originFacilityName: "", + selectedSegment: "together", + customerAddress: {} as any, + nearbyStores: [] as any, + availableStores: [] as any, + storesWithInventory: [] as any, + selectedFacility: {} as any, + selectedItemsByFacility: {} as any, + isOrderUpdated: false, + outOfStockItems: [] as any } }, computed: { ...mapGetters({ deliveryMethod: 'user/getDeliveryMethod', + isSplitEnabled: 'user/isSplitEnabled', + isCancellationAllowed: 'user/isCancellationAllowed' }) }, props: ["token"], async mounted() { - if (Object.keys(this.$route.query).length > 0) { + if(Object.keys(this.$route.query).length > 0) { if(!this.$route.query.oms || !this.token) { // invalid request return; @@ -181,19 +279,27 @@ export default defineComponent({ }) this.store.dispatch("user/setUserInstanceUrl", `${this.$route.query.oms}/api/`) await this.getOrder(); + if(this.order?.shipGroup && Object.keys(this.order.shipGroup).length){ + this.customerAddress = this.order.shipGroup.shipTo?.postalAddress ? this.order.shipGroup.shipTo.postalAddress : {} + await this.getPickupStores(); + this.fetchOrderFacilityChangeHistory() + if(!this.nearbyStores.length) { + this.selectedSegment = "separate"; + this.checkForOutOfStockItems(this.order.shipGroup) + } + } } }, methods: { async presentLoader() { - this.loader = await loadingController - .create({ - message: this.$t("Fetching order details."), - translucent: true, - }); + this.loader = await loadingController.create({ + message: translate("Fetching order details."), + translucent: true, + }); await this.loader.present(); }, dismissLoader() { - if (this.loader) { + if(this.loader) { this.loader.dismiss(); this.loader = null; } @@ -210,33 +316,98 @@ export default defineComponent({ resp = await OrderService.getOrder({ token: this.token }); - if (!hasError(resp) && resp.data.id) { + + if(!hasError(resp) && resp.data.id) { order = resp.data; const productIds: any = new Set(); - order.shipGroup = order.shipGroup.filter((group: any) => { - if(group.facilityId === 'PICKUP_REJECTED') { + const shipGroup = order.shipGroup.find((group: any) => { + if(group.facilityId === 'PICKUP_REJECTED' && group.shipmentMethodTypeId === "STOREPICKUP") { group.selectedShipmentMethodTypeId = group.shipmentMethodTypeId; group.items = group.items.filter((item: any) => { - if (item.status == 'ITEM_CANCELLED') return false; + if(item.status == "ITEM_CANCELLED" || item.status === "ITEM_REQ_CANCELATN") return false; productIds.add(item.productId); return true; }) return group.items.length > 0; } }) - if (productIds.length) await this.fetchProducts([...productIds]) + order.shipGroup = shipGroup ? shipGroup : {} + if(productIds.length) await this.fetchProducts([...productIds]) await this.store.dispatch("user/getConfiguration", { productStoreId: order.productStoreId, token: this.token}); this.order = order; - if (productIds.size) await this.fetchProducts([...productIds]) + if(productIds.size) await this.fetchProducts([...productIds]) } } catch (error) { console.error(error) } this.dismissLoader() }, + + async fetchOrderFacilityChangeHistory() { + let originFacilityName = "", resp; + + try { + resp = await OrderService.getRerouteOrderFacilityChangeHistory({ "token": this.token, facilityId: "PICKUP_REJECTED" }) + + if(!hasError(resp) && resp.data?.facilityChangeHistory?.length) { + const oldestBrokeringHistory = resp.data.facilityChangeHistory.reduce((oldest: any, current: any) => current.changeDatetime > oldest.changeDatetime ? current : oldest); + const fromFacilityId = oldestBrokeringHistory.fromFacilityId; + + originFacilityName = this.availableStores.find((store: any) => store.storeCode === fromFacilityId)?.storeName + + if(!originFacilityName) { + resp = await FacilityService.getStores({ + "viewSize": process.env.VUE_APP_VIEW_SIZE, + "filters": [`storeCode: ${fromFacilityId}`] + }) + + if(!hasError(resp) && resp.data.response.numFound) { + originFacilityName = resp.data?.response.docs[0].storeName + } else { + throw resp.data; + } + } + } else { + throw resp.data; + } + } catch(error: any) { + console.error(error); + } + + this.originFacilityName = originFacilityName + }, + + async getPickupStores() { + try { + let stores, point = ""; + + if(this.customerAddress?.latitude) { + point = `${this.customerAddress.latitude},${this.customerAddress.longitude}` + } + + stores = await this.getStores(point) + this.availableStores = stores; + + if(!stores?.length) return; + + const facilityIds = stores.map((store: any) => store.storeCode) + const productIds = [...new Set(this.order.shipGroup.items.map((item: any) => item.productId))] as any; + this.storesWithInventory = await this.checkInventory(facilityIds, productIds) + + if(!this.storesWithInventory?.length) return; + + stores.map((storeData: any) => { + const inventoryDetails = this.storesWithInventory.filter((store: any) => store.facilityId === storeData.storeCode); + if(inventoryDetails.length === productIds.length) this.nearbyStores.push({...storeData, ...inventoryDetails[0], distance: storeData.dist }); + }); + } catch (error) { + console.error(error) + } + }, + async fetchProducts(productIds: any) { const productIdFilter = productIds.reduce((filter: string, productId: any) => { - if (filter !== '') filter += ' OR ' + if(filter !== '') filter += ' OR ' return filter += productId; }, ''); @@ -246,7 +417,7 @@ export default defineComponent({ "viewSize": productIds.length }) - if (resp.status === 200 && !hasError(resp) && resp.data) { + if(resp.status === 200 && !hasError(resp) && resp.data) { resp.data.response.docs.forEach((product: any) => { this.products[product.productId] = product }); @@ -256,140 +427,181 @@ export default defineComponent({ } }, + segmentChanged(event: any, shipGroup: any) { + this.selectedSegment = event.detail.value + if(shipGroup.selectedShipmentMethodTypeId === "STOREPICKUP") { + this.selectedFacility = {} + this.selectedItemsByFacility = {} + this.order.shipGroup.items.map((item: any) => { + item.selectedFacilityId = "" + }) + } + }, + getProduct(productId: string) { return this.products[productId] ? this.products[productId] : {} }, - async updateShippingAddress(shipGroup: any) { - let resp - const payload = { - "orderId": this.order.id, - "shipGroupSeqId": shipGroup.shipGroupSeqId, - "contactMechId": shipGroup.shipmentMethodTypeId === 'STOREPICKUP' ? "" :shipGroup.shipTo.postalAddress.id, - "shipmentMethod": `${this.deliveryMethod}@_NA_`, - "contactMechPurposeTypeId": "SHIPPING_LOCATION", - "facilityId": shipGroup.facilityId, - "toName": `${shipGroup.updatedAddress.firstName} ${shipGroup.updatedAddress.lastName}`, - "address1": shipGroup.updatedAddress.address1, - "city": shipGroup.updatedAddress.city, - "stateProvinceGeoId": shipGroup.updatedAddress.stateProvinceGeoId, - "postalCode": shipGroup.updatedAddress.postalCode, - "countryGeoId": shipGroup.updatedAddress.countryGeoId, - "token": this.token - } as any - - if (shipGroup.selectedShipmentMethodTypeId === shipGroup.shipmentMethodTypeId) { - // In case of address edit, we honour the previously selected delivery method - payload.shipmentMethod = `${shipGroup.shipmentMethodTypeId}@_NA_` - payload.isEdited = true - } - - try { - resp = await OrderService.updateShippingAddress(payload); - if (resp.status === 200 && !hasError(resp) && resp.data) { - shipGroup.shipTo.postalAddress = shipGroup.updatedAddress - shipGroup.updatedAddress = null - showToast(translate("Changes saved")) - this.isOrderUpdated = true - } else { - showToast(translate("Failed to update the shipping addess")) - } - } catch (error) { - console.error(error) - showToast(translate("Failed to update the shipping addess")) + updateDeliveryMethod(event: any, shipGroup: any) { + shipGroup.selectedShipmentMethodTypeId = event.detail.value; + if(event.detail.value !== "STOREPICKUP") { + this.selectedFacility = {}; + this.selectedItemsByFacility = {}; + this.order.shipGroup.items.map((item: any) => { + item.selectedFacilityId = "" + item.isItemCancelled = false + }) } - this.getOrder(); }, - async updatePickupFacility(shipGroup: any) { - let resp - const payload = { - "orderId": this.order.id, - "shipGroupSeqId": shipGroup.shipGroupSeqId, - "contactMechId": shipGroup.shipTo.postalAddress.id, - "shipmentMethod": "STOREPICKUP@_NA_@CARRIER", // TODO Check why CARRIER is needed - "contactMechPurposeTypeId": "SHIPPING_LOCATION", - "facilityId": shipGroup.selectedFacility.facilityId, - "token": this.token - } + checkForOutOfStockItems(shipGroup: any) { + shipGroup.items.map((item: any) => { + const isInventoryAvailable = this.storesWithInventory.some((store: any) => store.productId === item.productId && Number(store.atp) > 0) - try { - resp = await OrderService.updatePickupFacility(payload); - if (resp.status === 200 && !hasError(resp)) { - shipGroup.facilityId = shipGroup.selectedFacility.facilityId - showToast(translate("Changes saved")) - this.isOrderUpdated = true - } else { - showToast(translate("Failed to update the pickup store")) + if(!isInventoryAvailable) { + item.isOutOfStock = true; + this.outOfStockItems.push(item); } - } catch (error) { - console.error(error) - showToast(translate("Failed to update the pickup store")) - } - this.getOrder(); + }) }, - updateDeliveryMethod(event: any, shipGroup: any) { - const group = this.order.shipGroup.find((group: any) => group.shipGroupSeqId === shipGroup.shipGroupSeqId); - group.selectedShipmentMethodTypeId = event.detail.value; - // Resetting the previous changes on method change - this.resetShipGroup(shipGroup) - }, + async updatePickupLocation(isPickupForAll: boolean, selectedFacilityId: any, item?: any) { + const modal = await modalController.create({ + component: PickupLocationModal, + componentProps: { + isPickupForAll, + storesWithInventory: this.storesWithInventory, + nearbyStores: this.nearbyStores, + availableStores: this.availableStores, + selectedFacilityId, + customerAddress: this.customerAddress, + currentProductId: item?.productId + } + }) - async updateDeliveryAddress(shipGroup: any) { - const modal = await modalController - .create({ - component: AddressModal, - // Adding backdropDismiss as false because on dismissing the modal through backdrop, - // backrop.role returns 'backdrop' giving unexpected result - backdropDismiss: false, - componentProps: { - shipGroup, - token: this.token - } - }) modal.onDidDismiss().then((result) => { - if (result.role) { - // role will have the passed data - shipGroup.updatedAddress = result.role + const selectedOptionId = result.data?.selectedOptionId; + + if(selectedOptionId) { + if(selectedOptionId === "cancel") { + item.isItemCancelled = true; + } else { + if(isPickupForAll) { + this.selectedFacility = this.nearbyStores.find((store: any) => store.facilityId === selectedOptionId); + } else { + item.selectedFacilityId = selectedOptionId + if(this.selectedItemsByFacility[selectedOptionId]?.length) this.selectedItemsByFacility[selectedOptionId].push(item); + else this.selectedItemsByFacility[selectedOptionId] = [item] + } + } } }); + return modal.present(); }, - async updatePickupLocation(shipGroup: any) { - const modal = await modalController - .create({ - component: PickupLocationModal, - // Adding backdropDismiss as false because on dismissing the modal through backdrop, - // backrop.role returns 'backdrop' giving unexpected result - backdropDismiss: false, - componentProps: { - shipGroup + async cancelShipGroup(shipGroup: any, cancelledItems: any) { + let resp; + const itemReasonMap = {} as any; + + try { + if(!this.isCancellationAllowed) { + const itemsToCancel = cancelledItems?.length ? cancelledItems : shipGroup.items + const itemIds = [] as any; + + itemsToCancel.map((item: any) => { + itemIds.push(item.itemSeqId) + itemReasonMap[`crm_cancellationReason:${item.itemSeqId}`] = "Customer cancellation from reroute app" + }) + + resp = await OrderService.requestCancelRerouteOrderItem({ + "orderId": this.order.id, + "token": this.token, + "orderItemSeqId": itemIds, + ...itemReasonMap + }) + + if(!hasError(resp)) { + return true; + } else { + throw resp.data; + } + } else { + let payload = { + "orderId": this.order.id, + "shipGroupSeqId": shipGroup.shipGroupSeqId, + "token": this.token + } as any + + if(!cancelledItems.length) { + shipGroup.items.map((item: any) => { + itemReasonMap[`irm_${item.itemSeqId}`] = "OICR_CHANGE_MIND" + itemReasonMap[`icm_${item.itemSeqId}`] = "Canceled by customer using Re-Route" + }) + + payload = { ...payload, ...itemReasonMap } + + resp = await OrderService.cancelOrderItem(payload); + if(!hasError(resp)) { + return true; + } else { + throw resp.data; + } + } else { + const responses = await Promise.allSettled(cancelledItems.map(async(item: any) => { + payload[`irm_${item.itemSeqId}`] = "OICR_CHANGE_MIND" + payload[`icm_${item.itemSeqId}`] = "Canceled by customer using Re-Route" + payload["orderItemSeqId"] = item.itemSeqId + + return await OrderService.cancelOrderItem(payload) + })) + + const hasFailedResponse = responses.some((response: any) => response.status === 'rejected') + return !hasFailedResponse } - }) - modal.onDidDismiss().then((result) => { - if (result.role) { - // role will have the passed data - shipGroup.selectedFacility = result.role } + } catch(error) { + console.error(error); + } + return false + }, + + async cancelOrder(shipGroup: any) { + const message = translate("Are you sure you want to cancel the order items?"); + const alert = await alertController.create({ + header: translate("Cancel items"), + message, + buttons: [ + { + text: translate("Don't Cancel"), + }, + { + text: translate("Cancel"), + handler: async () => { + // Todo: handle case for the request cancellation + const isCancelled = await this.cancelShipGroup(shipGroup, []); + showToast(translate(isCancelled ? "Order cancelled successfully." : "Failed to cancel the order.")) + this.getOrder() + } + } + ], }); - return modal.present(); + return alert.present(); }, - async save(shipGroup: any) { - const message = this.$t("Are you sure you want to save the changes?"); + async confirmSave(shipGroup: any) { + const message = translate("Are you sure you want to save the changes?"); const alert = await alertController.create({ - header: this.$t("Save changes"), + header: translate("Save changes"), message, buttons: [ { - text: this.$t("Cancel"), + text: translate("Cancel"), }, { - text: this.$t("Confirm"), + text: translate("Confirm"), handler: () => { - shipGroup.selectedShipmentMethodTypeId === 'STOREPICKUP' ? this.updatePickupFacility(shipGroup) : this.updateShippingAddress(shipGroup); + this.saveOrder(shipGroup) } } ], @@ -397,56 +609,186 @@ export default defineComponent({ return alert.present(); }, + async saveOrder(shipGroup: any) { + const isStorePickupSelected = (shipGroup.selectedShipmentMethodTypeId === "STOREPICKUP"); + let isUpdated = false, hasFailure = false; - async cancelShipGroup(shipGroup: any) { + if(this.selectedSegment === "together") { + if(isStorePickupSelected) { + isUpdated = await this.updatePickupFacility(shipGroup) + showToast(translate(isUpdated ? "Pickup facility updated successfully." : "Failed to update the pickup store.")) + } else { + isUpdated = await this.updateShippingMethod(shipGroup) + + if(isUpdated) { + const isUpdated = await this.brokerOrderItem(shipGroup.items, true); + if(!isUpdated) hasFailure = true; + } else { + hasFailure = true; + } + + showToast(translate(hasFailure ? "Failed to update the shipping addess." : "Shipping address updated successfully.")) + } + } else { + const itemsForCancellation = shipGroup.items.filter((item: any) => item.isItemCancelled); + const itemsWithFacility = shipGroup.items.filter((item: any) => item.selectedFacilityId) + const itemsForShipping = shipGroup.items.filter((item: any) => !(item.isItemCancelled || item.selectedFacilityId)) + + if(itemsForCancellation.length) { + isUpdated = await this.cancelShipGroup(shipGroup, itemsForCancellation) + if(!isUpdated) hasFailure = true; + } + + if(itemsWithFacility.length) { + isUpdated = await this.brokerOrderItem(itemsWithFacility, false) + if(!isUpdated) hasFailure = true; + } + + if(shipGroup.selectedShipmentMethodTypeId !== "STOREPICKUP" && this.customerAddress.address1 && itemsForShipping.length) { + if(await this.updateShippingMethod(shipGroup)) { + isUpdated = await this.brokerOrderItem(itemsForShipping, true); + if(!isUpdated) hasFailure = true; + } else { + hasFailure = true; + } + } + + showToast(translate(hasFailure ? "Failed to re-route some order items." : "Order items re-routed successfully.")) + } + this.getOrder(); + }, + + async brokerOrderItem(items: any, isShippingOrder: boolean) { + const responses = await Promise.allSettled(items.map(async(item: any) => await OrderService.releaseRerouteOrderItem({ + orderId: this.order.id, + orderItemSeqId: item.itemSeqId, + fromFacilityId: this.order.facilityId, + toFacilityId: isShippingOrder ? "_NA_" : item.selectedFacilityId, + token: this.token + }))) + const hasFailedResponse = responses.some((response: any) => response.status === 'rejected') + return !hasFailedResponse + }, + + isOrderItemsEligibleForUpdation(shipGroup: any) { + if(this.selectedSegment === "together") { + return shipGroup.selectedShipmentMethodTypeId === "STOREPICKUP" ? this.selectedFacility.facilityId : this.customerAddress.address1 + } else { + return shipGroup.selectedShipmentMethodTypeId === "STOREPICKUP" ? !shipGroup.items.some((item: any) => !(item.selectedFacilityId || item.isItemCancelled)) : this.customerAddress.address1 + } + }, + + async updatePickupFacility(shipGroup: any) { let resp - const itemReasonMap = {} as any - shipGroup.items.map((item: any) => itemReasonMap[item.itemSeqId] = 'OICR_CHANGE_MIND') const payload = { "orderId": this.order.id, "shipGroupSeqId": shipGroup.shipGroupSeqId, - "itemReasonMap": itemReasonMap, + "contactMechId": shipGroup.shipTo.postalAddress.id, + "shipmentMethod": "STOREPICKUP@_NA_@CARRIER", // TODO Check why CARRIER is needed + "contactMechPurposeTypeId": "SHIPPING_LOCATION", + "facilityId": this.selectedFacility.facilityId, + "token": this.token + } + + try { + resp = await OrderService.updatePickupFacility(payload); + if(resp.status === 200 && !hasError(resp)) { + shipGroup.facilityId = this.selectedFacility.facilityId + this.isOrderUpdated = true + return true; + } else { + throw resp.data; + } + } catch(error) { + console.error(error) + } + return false; + }, + + async updateShippingMethod(shipGroup: any) { + let resp + + const payload = { + "orderId": this.order.id, + "shipGroupSeqId": shipGroup.shipGroupSeqId, + "shipmentMethod": `${this.deliveryMethod}@_NA_`, + "contactMechPurposeTypeId": "SHIPPING_LOCATION", + "facilityId": "_NA_", + "contactMechId": this.customerAddress.id, "token": this.token } as any try { - resp = await OrderService.cancelOrderItem(payload); - if (resp.status === 200 && !hasError(resp) && resp.data.orderId == this.order.id) { - shipGroup.isCancelled = true; - showToast(translate("Order cancelled successfully")) + resp = await OrderService.updateShippingAddress(payload); + if(!hasError(resp)) { + this.isOrderUpdated = true + return true; } else { - showToast(translate("Failed to cancel the order")) + throw resp.data; } } catch (error) { console.error(error) - showToast(translate("Failed~ to cancel the order")) } - this.getOrder(); + return false; }, - async cancel(shipGroup: any) { - const message = this.$t("Are you sure you want to cancel the order items?"); - const alert = await alertController.create({ - header: this.$t("Cancel items"), - message, - buttons: [ - { - text: this.$t("Don't Cancel"), - }, - { - text: this.$t("Cancel"), - handler: () => { - this.cancelShipGroup(shipGroup); + async checkInventory(facilityIds: Array, productIds: Array) { + let isScrollable = true, viewSize = 250, viewIndex = 0, total = 0; + let productInventoryResp = [] as any; + + try { + while(isScrollable) { + const resp = await StockService.checkInventory({ + "filters": { + "productId": productIds, + "facilityId": facilityIds + }, + "fieldsToSelect": ["productId", "atp", "facilityName", "facilityId"], + viewSize, + viewIndex + }); + + if(!hasError(resp) && resp.data.count) { + if(!productInventoryResp.length) { + productInventoryResp = resp.data.docs + total = resp.data.count; + } else { + productInventoryResp = productInventoryResp.concat(resp.data.docs) } + if(productInventoryResp.length >= total) isScrollable = false; + viewIndex++; } - ], - }); - return alert.present(); + } + return productInventoryResp.filter((store: any) => store.atp > 0) + } catch (error) { + console.error(error) + } + }, + + async getStores(point?: string) { + let payload = { + "viewSize": process.env.VUE_APP_VIEW_SIZE, + "filters": ["storeType: RETAIL_STORE", "pickup_pref: true"] + } as any + + if(point) { + payload.point = point + } + + try { + const storeLookupResp = await FacilityService.getStores(payload) + if(storeLookupResp.status !== 200 || hasError(storeLookupResp) || !storeLookupResp.data.response.numFound) { + return []; + } + return storeLookupResp.data.response.docs + } catch (error) { + console.error(error) + } }, - resetShipGroup(shipGroup: any) { - shipGroup.updatedAddress = null - shipGroup.selectedFacility = null + removeItemFromFacility(item: any, facilityId: any) { + this.selectedItemsByFacility[facilityId] = this.selectedItemsByFacility[facilityId].filter((currentItem: any) => currentItem.itemSeqId !== item.itemSeqId); + item.selectedFacilityId = ""; } }, setup() { @@ -454,9 +796,16 @@ export default defineComponent({ const store = useStore(); return { Actions, + closeCircleOutline, + addOutline, hasPermission, + colorWandOutline, + medkitOutline, + removeCircleOutline, router, - store + store, + storefrontOutline, + translate }; } }); @@ -469,4 +818,14 @@ export default defineComponent({ margin: auto; } } + + .actions { + display: flex; + justify-content: space-between; + border-top: 1px solid var(--ion-color-light); + } + + .overline { + color: red; + } \ No newline at end of file diff --git a/src/views/PickupLocationModal.vue b/src/views/PickupLocationModal.vue index 54fced91f..d2eb803e2 100644 --- a/src/views/PickupLocationModal.vue +++ b/src/views/PickupLocationModal.vue @@ -1,60 +1,72 @@