diff --git a/src/locales/en.json b/src/locales/en.json index 5e22253f..08f810ec 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -128,6 +128,7 @@ "Pick up location": "Pick up location", "Pickers successfully replaced in the picklist with the new selections.": "Pickers successfully replaced in the picklist with the new selections.", "pieces in stock": "pieces in stock", + "Print picklists": "Print picklists", "Product details": "Product details", "Product not found": "Product not found", "Products not found": "Products not found", diff --git a/src/locales/es.json b/src/locales/es.json index 36b61da7..f2a9378f 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -125,6 +125,7 @@ "Pick up location": "Ubicación de recogida", "Pickers successfully replaced in the picklist with the new selections.": "Los recolectores fueron reemplazados exitosamente en la lista de selección con las nuevas selecciones.", "pieces in stock": "piezas en inventario", + "Print picklists": "Print picklists", "Product details": "Detalles del producto", "Product not found": "Producto no encontrado", "Products not found": "Productos no encontrados", diff --git a/src/locales/ja.json b/src/locales/ja.json index 7a82e6ca..00518d45 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -124,6 +124,7 @@ "Pick up location": "受取場所", "Pickers successfully replaced in the picklist with the new selections.": "ピッカーは新しい選択によりピックリストで正常に置き換えられました。", "pieces in stock": "在庫あり", + "Print picklists": "Print picklists", "Product details": "商品詳細", "Product not found": "商品が見つかりません", "Products not found": "商品が見つかりません", diff --git a/src/services/OrderService.ts b/src/services/OrderService.ts index 99de1971..5c0d5ce1 100644 --- a/src/services/OrderService.ts +++ b/src/services/OrderService.ts @@ -4,6 +4,7 @@ import { translate } from '@hotwax/dxp-components'; import store from '@/store'; import { formatPhoneNumber, showToast } from '@/utils'; import logger from '@/logger'; +import { cogOutline } from 'ionicons/icons'; const getOpenOrders = async (payload: any): Promise => { return api({ @@ -120,6 +121,37 @@ const createPicklist = async (query: any): Promise => { }) } +const printPicklist = async (picklistId: string): Promise => { + try { + // Get picklist from the server + const resp: any = await api({ + method: 'get', + url: 'PrintPicklist.pdf', + params: { + picklistId + }, + responseType: "blob" + }) + + if (!resp || resp.status !== 200 || hasError(resp)) { + throw resp.data; + } + + // Generate local file URL for the blob received + const pdfUrl = window.URL.createObjectURL(resp.data); + // Open the file in new tab + try { + (window as any).open(pdfUrl, "_blank").focus(); + } + catch { + showToast(translate('Unable to open as browser is blocking pop-ups.', {documentName: 'picklist'}), { icon: cogOutline }); + } + } catch (err) { + showToast(translate('Failed to print picklist')) + logger.error("Failed to print picklist", err) + } +} + const sendPickupScheduledNotification = async (payload: any): Promise => { return api({ url: "service/sendPickupScheduledNotification", @@ -307,6 +339,14 @@ const fetchTrackingCodes = async (shipmentIds: Array): Promise => { return shipmentTrackingCodes; } +const packOrder = async (payload: any): Promise => { + return api({ + url: "/service/packStoreFulfillmentOrder", + method: "post", + data: payload + }) +} + export const OrderService = { fetchOrderItems, fetchOrderPaymentPreferences, @@ -327,5 +367,7 @@ export const OrderService = { getShipmentItems, getCustomerContactDetails, getShippingPhoneNumber, + packOrder, + printPicklist, printShippingLabelAndPackingSlip } \ No newline at end of file diff --git a/src/store/modules/order/actions.ts b/src/store/modules/order/actions.ts index 26766c13..1f708f29 100644 --- a/src/store/modules/order/actions.ts +++ b/src/store/modules/order/actions.ts @@ -115,7 +115,7 @@ const actions: ActionTree ={ const orderQueryPayload = prepareOrderQuery({ ...payload, shipmentMethodTypeId: !store.state.user.preference.showShippingOrders ? 'STOREPICKUP' : '', - '-shipmentStatusId': '*', + '-shipmentStatusId': '(SHIPMENT_PACKED OR SHIPMENT_SHIPPED)', '-fulfillmentStatus': '(Cancelled OR Rejected)', orderStatusId: 'ORDER_APPROVED', orderTypeId: 'SALES_ORDER' @@ -170,7 +170,10 @@ const actions: ActionTree ={ }, []), placedDate: orderItem.orderDate, shippingInstructions: orderItem.shippingInstructions, - shipGroupSeqId: orderItem.shipGroupSeqId + shipGroupSeqId: orderItem.shipGroupSeqId, + isPicked: orderItem.isPicked, + picklistId: orderItem.picklistId, + picklistBinId: orderItem.picklistBinId } }) @@ -538,7 +541,7 @@ const actions: ActionTree ={ async packShipGroupItems ({ state, dispatch, commit }, payload) { emitter.emit("presentLoader") - if (store.state.user.preference.configurePicker) { + if (store.state.user.preference.configurePicker && payload.order.isPicked !== 'Y') { let resp; const items = payload.order.parts[0].items; @@ -547,6 +550,7 @@ const actions: ActionTree ={ items.map((item: any, index: number) => { formData.append("itemStatusId_o_"+index, "PICKITEM_PENDING") formData.append("pickerIds_o_"+index, payload.selectedPicker) + formData.append("picked_o_"+index, item.quantity) Object.keys(item).map((property) => { if(property !== "facilityId") formData.append(property+'_o_'+index, item[property]) }) @@ -589,7 +593,7 @@ const actions: ActionTree ={ const shipmentMethodTypeId = payload.part?.shipmentMethodEnum?.shipmentMethodEnumId if (shipmentMethodTypeId !== 'STOREPICKUP') { // TODO: find a better way to get the shipmentId - const shipmentId = resp.data._EVENT_MESSAGE_.match(/\d+/g)[0] + const shipmentId = resp.data.shipmentId ? resp.data.shipmentId : resp.data._EVENT_MESSAGE_.match(/\d+/g)[0] await dispatch('packDeliveryItems', shipmentId).then((data) => { if (!hasError(data) && !data.data._EVENT_MESSAGE_) { showToast(translate("Something went wrong")) @@ -606,7 +610,10 @@ const actions: ActionTree ={ } } }) + } else { + dispatch("removeOpenOrder", payload) } + // Adding readyToHandover or readyToShip because we need to show the user that the order has moved to the packed tab if(payload.order.part.shipmentMethodEnum.shipmentMethodEnumId === 'STOREPICKUP'){ payload.order = { ...payload.order, readyToHandover: true } @@ -628,6 +635,21 @@ const actions: ActionTree ={ return resp; }, + removeOpenOrder({ commit, state }, payload) { + const orders = JSON.parse(JSON.stringify(state.open.list)); + + const orderIndex = orders.findIndex((order: any) => { + return order.orderId === payload.order.orderId && order.parts.some((part: any) => { + return part.orderPartSeqId === payload.part.orderPartSeqId; + }); + }); + + if (orderIndex > -1) { + orders.splice(orderIndex, 1); + commit(types.ORDER_OPEN_UPDATED, { orders, total: state.open.total -1 }) + } + }, + // TODO: handle the unfillable items count async setUnfillableOrderOrItem ({ dispatch }, payload) { emitter.emit("presentLoader"); @@ -963,6 +985,10 @@ const actions: ActionTree ={ emitter.emit("dismissLoader"); }, + updateOpenOrder ({ commit }, payload) { + commit(types.ORDER_OPEN_UPDATED, {orders: payload.orders , total: payload.total}) + }, + // clearning the orders state when logout, or user store is changed clearOrders ({ commit }) { commit(types.ORDER_OPEN_UPDATED, {orders: {} , total: 0}) diff --git a/src/store/modules/user/getters.ts b/src/store/modules/user/getters.ts index 0fdab88f..e29565e3 100644 --- a/src/store/modules/user/getters.ts +++ b/src/store/modules/user/getters.ts @@ -30,6 +30,9 @@ const getters: GetterTree = { showShippingOrders (state) { return state.preference.showShippingOrders; }, + printPicklistPref (state) { + return state.preference.printPicklistPref; + }, configurePicker (state) { return state.preference.configurePicker; }, diff --git a/src/store/modules/user/index.ts b/src/store/modules/user/index.ts index ce8d6501..aa957af3 100644 --- a/src/store/modules/user/index.ts +++ b/src/store/modules/user/index.ts @@ -15,7 +15,8 @@ const userModule: Module = { preference: { showShippingOrders: false, showPackingSlip: false, - configurePicker: false + configurePicker: false, + printPicklistPref: false }, currentEComStore: {}, partialOrderRejectionConfig: {}, diff --git a/src/store/modules/user/mutations.ts b/src/store/modules/user/mutations.ts index c4aa1767..0b51de6e 100644 --- a/src/store/modules/user/mutations.ts +++ b/src/store/modules/user/mutations.ts @@ -14,7 +14,8 @@ const mutations: MutationTree = { state.preference= { showShippingOrders: false, showPackingSlip: false, - configurePicker: false + configurePicker: false, + printPicklistPref: false }, state.allNotificationPrefs = [] }, diff --git a/src/utils/index.ts b/src/utils/index.ts index 412ec862..3dfec677 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,14 +5,30 @@ import { DateTime } from "luxon"; // TODO Use separate files for specific utilities -const showToast = async (message: string) => { - const toast = await toastController - .create({ - message, - duration: 3000, - position: 'top', - }) - return toast.present(); +const showToast = async (message: string, options?: any) => { + const config = { + message, + ...options + } as any; + + if (!options?.position) { + config.position = 'top'; + } + if (options?.canDismiss) { + config.buttons = [ + { + text: translate('Dismiss'), + role: 'cancel', + }, + ] + } + if (!options?.manualDismiss) { + config.duration = 3000; + } + + const toast = await toastController.create(config) + // present toast if manual dismiss is not needed + return !options?.manualDismiss ? toast.present() : toast } const copyToClipboard = async (text: any, showCopiedValue = true) => { diff --git a/src/views/AssignPickerModal.vue b/src/views/AssignPickerModal.vue index 017ec96c..d009dc0b 100644 --- a/src/views/AssignPickerModal.vue +++ b/src/views/AssignPickerModal.vue @@ -124,8 +124,7 @@ export default defineComponent({ }, readyForPickup () { if (this.selectedPicker) { - this.store.dispatch('order/packShipGroupItems', { order: this.order, part: this.part, facilityId: this.facilityId, selectedPicker: this.selectedPicker }) - modalController.dismiss({ dismissed: true }); + modalController.dismiss({ dismissed: true, selectedPicker: this.selectedPicker }); } else { showToast(translate('Select a picker')) } diff --git a/src/views/OrderDetail.vue b/src/views/OrderDetail.vue index 320220e6..022b43e4 100644 --- a/src/views/OrderDetail.vue +++ b/src/views/OrderDetail.vue @@ -336,6 +336,13 @@ export default defineComponent({ component: AssignPickerModal, componentProps: { order, part, facilityId } }); + + assignPickerModal.onDidDismiss().then(async(result: any) => { + if(result.data.dismissed) { + await this.store.dispatch('order/packShipGroupItems', { order, part, facilityId, selectedPicker: result.data.selectedPicker }) + } + }) + return assignPickerModal.present(); }, async editPicker(order: any) { diff --git a/src/views/Orders.vue b/src/views/Orders.vue index c421dce4..e5ebc426 100644 --- a/src/views/Orders.vue +++ b/src/views/Orders.vue @@ -71,6 +71,10 @@ {{ order.part.shipmentMethodEnum?.shipmentMethodEnumId === 'STOREPICKUP' ? translate("Ready for pickup") : translate("Ready to ship") }} +
+ + + @@ -254,7 +258,8 @@ export default defineComponent({ isCompletedOrdersScrollable: 'order/isCompletedOrdersScrollable', showPackingSlip: 'user/showPackingSlip', notifications: 'user/getNotifications', - unreadNotificationsStatus: 'user/getUnreadNotificationsStatus' + unreadNotificationsStatus: 'user/getUnreadNotificationsStatus', + printPicklistPref: 'user/printPicklistPref' }) }, data() { @@ -269,6 +274,13 @@ export default defineComponent({ component: AssignPickerModal, componentProps: { order, part, facilityId } }); + + assignPickerModal.onDidDismiss().then(async(result: any) => { + if(result.data?.dismissed) { + await this.store.dispatch('order/packShipGroupItems', { order, part, facilityId, selectedPicker: result.data.selectedPicker }) + } + }) + return assignPickerModal.present(); }, timeFromNow (time: any) { @@ -380,7 +392,7 @@ export default defineComponent({ } }, async readyForPickup (order: any, part: any) { - if(this.configurePicker) return this.assignPicker(order, part, this.currentFacility.facilityId); + if(this.configurePicker && order.isPicked !== 'Y') return this.assignPicker(order, part, this.currentFacility.facilityId); const pickup = part.shipmentMethodEnum?.shipmentMethodEnumId === 'STOREPICKUP'; const header = pickup ? translate('Ready for pickup') : translate('Ready to ship'); const message = pickup ? translate('An email notification will be sent to that their order is ready for pickup. This order will also be moved to the packed orders tab.', { customerName: order.customer.name, space: '

'}) : ''; @@ -395,12 +407,34 @@ export default defineComponent({ },{ text: header, handler: () => { - this.store.dispatch('order/packShipGroupItems', {order, part, facilityId: this.currentFacility.facilityId}) + if(!pickup) { + this.packShippingOrders(order, part); + } else { + this.store.dispatch('order/packShipGroupItems', {order, part, facilityId: this.currentFacility.facilityId}) + } } }] }); return alert.present(); }, + async packShippingOrders(currentOrder: any, part: any) { + try { + const resp = await OrderService.packOrder({ + 'picklistBinId': currentOrder.picklistBinId, + 'orderId': currentOrder.orderId + }) + + if(!hasError(resp)) { + showToast(translate("Order packed and ready for delivery")); + this.store.dispatch("order/removeOpenOrder", { order: currentOrder, part }) + } else { + throw resp.data; + } + } catch(error: any) { + logger.error(error); + showToast(translate("Something went wrong")) + } + }, async deliverShipment (order: any) { await this.store.dispatch('order/deliverShipment', order) .then((resp) => { @@ -474,6 +508,65 @@ export default defineComponent({ viewNotifications() { this.store.dispatch('user/setUnreadNotificationsStatus', false) this.$router.push({ path: '/notifications' }) + }, + async printPicklist(order: any, part: any) { + if(order.isPicked === 'Y') { + await OrderService.printPicklist(order.picklistId) + return; + } + + if(!this.configurePicker) { + await this.createPicklist(order, "_NA_"); + return; + } + + const assignPickerModal = await modalController.create({ + component: AssignPickerModal, + componentProps: { order, part, facilityId: this.currentFacility.facilityId } + }); + + assignPickerModal.onDidDismiss().then(async(result: any) => { + if(result.data?.dismissed) { + this.createPicklist(order, result.data.selectedPicker) + } + }) + + return assignPickerModal.present(); + }, + async createPicklist(order: any, selectedPicker: any) { + let resp; + + const items = order.parts[0].items; + const formData = new FormData(); + formData.append("facilityId", items[0].facilityId); + items.map((item: any, index: number) => { + formData.append("itemStatusId_o_"+index, "PICKITEM_PENDING") + formData.append("pickerIds_o_"+index, selectedPicker) + formData.append("picked_o_"+index, item.quantity) + Object.keys(item).map((property) => { + if(property !== "facilityId") formData.append(property+'_o_'+index, item[property]) + }) + }); + + try { + resp = await OrderService.createPicklist(formData); + if(!hasError(resp)) { + // generating picklist after creating a new picklist + await OrderService.printPicklist(resp.data.picklistId) + const orders = JSON.parse(JSON.stringify(this.orders)) + const updatedOrder = orders.find((currentOrder: any) => currentOrder.orderId === order.orderId); + updatedOrder["isPicked"] = "Y" + updatedOrder["picklistId"] = resp.data.picklistId + updatedOrder["picklistBinId"] = resp.data.picklistBinId + this.store.dispatch("order/updateOpenOrder", { orders, total: orders.length }) + } else { + throw resp.data; + } + } catch (err) { + showToast(translate('Something went wrong. Picklist can not be created.')); + emitter.emit("dismissLoader"); + return; + } } }, ionViewWillEnter () { diff --git a/src/views/Settings.vue b/src/views/Settings.vue index ec86ffe6..5f64e1a3 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -151,9 +151,12 @@ {{ translate('Track who picked orders by entering picker IDs when packing an order.') }} - + {{ translate("Enable tracking") }} + + {{ translate("Print picklists") }} + @@ -271,7 +274,8 @@ export default defineComponent({ partialOrderRejectionConfig: 'user/getPartialOrderRejectionConfig', firebaseDeviceId: 'user/getFirebaseDeviceId', notificationPrefs: 'user/getNotificationPrefs', - allNotificationPrefs: 'user/getAllNotificationPrefs' + allNotificationPrefs: 'user/getAllNotificationPrefs', + printPicklistPref: "user/printPicklistPref", }) }, mounted() { @@ -333,6 +337,9 @@ export default defineComponent({ setConfigurePickerPreference (ev: any){ this.store.dispatch('user/setUserPreference', { configurePicker: ev.detail.checked }) }, + setPrintPicklistPreference (ev: any){ + this.store.dispatch('user/setUserPreference', { printPicklistPref: ev.detail.checked }) + }, getDateTime(time: any) { return DateTime.fromMillis(time).toLocaleString(DateTime.DATETIME_MED); },