Skip to content

Commit ccd827d

Browse files
committed
Implemented: support for websocket for realtime counting (#598)
1 parent 7c809e3 commit ccd827d

File tree

5 files changed

+165
-2
lines changed

5 files changed

+165
-2
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// This module provides composable functions for managing NetSuite integrations, allowing for asynchronous operations such as
2+
//adding, editing, updating, and removing NetSuite IDs based on specified integration mapping data.
3+
declare module "@/composables/useWebSocketComposables" {
4+
export function useWebSocketComposables(webSocketUrl: string): {
5+
initWebSocket: () => Promise<void>;
6+
tryReopen: () => Promise<void>;
7+
registerListener: (topic: string, callback: any) => Promise<void>;
8+
};
9+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { onIonViewDidLeave } from '@ionic/vue';
2+
import { reactive } from 'vue';
3+
4+
export function useWebSocketComposables(webSocketUrl: string) {
5+
const state = reactive({
6+
webSocket: null,
7+
currentTopic: null,
8+
topicListener: null,
9+
tryReopenCount: 0,
10+
}) as any;
11+
12+
const initWebSocket = () => {
13+
state.webSocket = new WebSocket(webSocketUrl);
14+
15+
state.webSocket.onopen = () => {
16+
state.tryReopenCount = 0;
17+
18+
// Subscribe to all topics
19+
if(state.currentTopic) {
20+
state.webSocket.send(`subscribe:${state.currentTopic}`);
21+
}
22+
};
23+
24+
state.webSocket.onmessage = (event: any) => {
25+
const jsonObj = JSON.parse(event.data);
26+
if (jsonObj.topic === state.currentTopic && state.topicListener) {
27+
state.topicListener(jsonObj);
28+
}
29+
};
30+
31+
state.webSocket.onclose = () => {
32+
console.error('WebSocket closed');
33+
setTimeout(() => tryReopen(), 30 * 1000);
34+
};
35+
36+
state.webSocket.onerror = (event: any) => {
37+
console.error('WebSocket error', event);
38+
};
39+
};
40+
41+
const tryReopen = () => {
42+
if (
43+
(!state.webSocket ||
44+
state.webSocket.readyState === WebSocket.CLOSED ||
45+
state.webSocket.readyState === WebSocket.CLOSING) &&
46+
state.tryReopenCount < 6
47+
) {
48+
state.tryReopenCount += 1;
49+
initWebSocket();
50+
}
51+
};
52+
53+
const registerListener = (topic: string, callback: any) => {
54+
if (!state.webSocket) {
55+
initWebSocket();
56+
}
57+
58+
if (state.currentTopic !== topic) {
59+
// Unsubscribe from the previous topic
60+
if (state.currentTopic && state.webSocket.readyState === WebSocket.OPEN) {
61+
state.webSocket.send(`unsubscribe:${state.currentTopic}`);
62+
}
63+
64+
// Update the current topic and listener
65+
state.currentTopic = topic;
66+
state.topicListener = callback;
67+
68+
// Subscribe to the new topic
69+
if (state.webSocket.readyState === WebSocket.OPEN) {
70+
state.webSocket.send(`subscribe:${topic}`);
71+
}
72+
} else if (state.topicListener !== callback) {
73+
// Update the callback if it has changed
74+
state.topicListener = callback;
75+
}
76+
};
77+
78+
onIonViewDidLeave(() => {
79+
if (state.webSocket) {
80+
state.webSocket.onclose = null;
81+
state.webSocket.close();
82+
}
83+
});
84+
85+
return {
86+
registerListener
87+
};
88+
}

src/store/modules/user/getters.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ const getters: GetterTree <UserState, RootState> = {
5252
},
5353
getGoodIdentificationTypes(state) {
5454
return state.goodIdentificationTypes;
55+
},
56+
getWebSocketUrl(state) {
57+
let baseURL = state.instanceUrl
58+
if(baseURL.startsWith("http")) {
59+
baseURL = baseURL.replace(/https?:\/\/|\/api|\/+/g, "");
60+
}
61+
return `ws://${baseURL}/notws?api_key=${state.token}`;
5562
}
5663
}
5764
export default getters;

src/views/CountDetail.vue

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ import { paperPlaneOutline } from "ionicons/icons"
253253
import Image from "@/components/Image.vue";
254254
import router from "@/router"
255255
import { onBeforeRouteLeave } from 'vue-router';
256+
import { useWebSocketComposables } from '@/composables/useWebSocketComposables';
256257
257258
const store = useStore();
258259
@@ -263,6 +264,8 @@ const cycleCountItems = computed(() => store.getters["count/getCycleCountItems"]
263264
const userProfile = computed(() => store.getters["user/getUserProfile"])
264265
const productStoreSettings = computed(() => store.getters["user/getProductStoreSettings"])
265266
const currentItemIndex = computed(() => !product.value ? 0 : itemsList?.value.findIndex((item) => item.productId === product?.value.productId && item.importItemSeqId === product?.value.importItemSeqId));
267+
const currentFacility = computed(() => store.getters["user/getCurrentFacility"])
268+
const webSocketUrl = computed(() => store.getters["user/getWebSocketUrl"])
266269
267270
const itemsList = computed(() => {
268271
if (selectedSegment.value === 'all') {
@@ -283,6 +286,8 @@ const itemsList = computed(() => {
283286
}
284287
});
285288
289+
const { registerListener } = useWebSocketComposables(webSocketUrl.value);
290+
286291
const props = defineProps(["id"]);
287292
let selectedSegment = ref('all');
288293
let cycleCount = ref([]);
@@ -303,6 +308,7 @@ onIonViewDidEnter(async() => {
303308
previousItem = itemsList.value[0]
304309
await store.dispatch("product/currentProduct", itemsList.value[0])
305310
barcodeInput.value?.$el?.setFocus();
311+
registerListener(currentFacility.value.facilityId, handleNewMessage);
306312
emitter.emit("dismissLoader")
307313
})
308314
@@ -638,6 +644,27 @@ async function readyForReview() {
638644
});
639645
await alert.present();
640646
}
647+
648+
function handleNewMessage(jsonObj) {
649+
const updatedItem = jsonObj.message
650+
if(updatedItem.inventoryCountImportId !== cycleCount.value.inventoryCountImportId) return;
651+
652+
const items = JSON.parse(JSON.stringify(cycleCountItems.value.itemList))
653+
const currentItemIndex = items.findIndex((item) => item.productId === updatedItem.productId && item.importItemSeqId === updatedItem.importItemSeqId);
654+
655+
if(currentItemIndex !== -1) {
656+
items[currentItemIndex] = updatedItem
657+
} else {
658+
store.dispatch("product/fetchProducts", { productIds: [updatedItem.productId] })
659+
items.push(updatedItem)
660+
}
661+
662+
store.dispatch('count/updateCycleCountItems', items);
663+
if(product.value.productId === updatedItem.productId) {
664+
store.dispatch('product/currentProduct', updatedItem);
665+
}
666+
}
667+
641668
</script>
642669
643670
<style scoped>

src/views/HardCountDetail.vue

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ import { CountService } from "@/services/CountService";
218218
import Image from "@/components/Image.vue";
219219
import router from "@/router";
220220
import MatchProductModal from "@/components/MatchProductModal.vue";
221+
import { useWebSocketComposables } from '@/composables/useWebSocketComposables';
221222
222223
const store = useStore();
223224
@@ -228,6 +229,8 @@ const userProfile = computed(() => store.getters["user/getUserProfile"])
228229
const productStoreSettings = computed(() => store.getters["user/getProductStoreSettings"])
229230
const defaultRecountUpdateBehaviour = computed(() => store.getters["count/getDefaultRecountUpdateBehaviour"])
230231
const currentItemIndex = computed(() => !currentProduct.value ? 0 : currentProduct.value.scannedId ? itemsList.value?.findIndex((item: any) => item.scannedId === currentProduct.value.scannedId) : itemsList?.value.findIndex((item: any) => item.productId === currentProduct.value?.productId && item.importItemSeqId === currentProduct.value?.importItemSeqId));
232+
const currentFacility = computed(() => store.getters["user/getCurrentFacility"])
233+
const webSocketUrl = computed(() => store.getters["user/getWebSocketUrl"])
231234
232235
const itemsList = computed(() => {
233236
if(selectedSegment.value === "all") {
@@ -239,6 +242,8 @@ const itemsList = computed(() => {
239242
}
240243
});
241244
245+
const { registerListener } = useWebSocketComposables(webSocketUrl.value);
246+
242247
const props = defineProps(["id"]);
243248
const cycleCount = ref({}) as any;
244249
const queryString = ref("");
@@ -250,7 +255,7 @@ const inputCount = ref("") as any;
250255
const selectedCountUpdateType = ref("add");
251256
const isScrolling = ref(false);
252257
let isScanningInProgress = ref(false);
253-
258+
const productIdAdding = ref();
254259
255260
onIonViewDidEnter(async() => {
256261
emitter.emit("presentLoader");
@@ -260,6 +265,7 @@ onIonViewDidEnter(async() => {
260265
barcodeInputRef.value?.$el?.setFocus();
261266
selectedCountUpdateType.value = defaultRecountUpdateBehaviour.value
262267
window.addEventListener('beforeunload', handleBeforeUnload);
268+
registerListener(currentFacility.value.facilityId, handleNewMessage);
263269
emitter.emit("dismissLoader")
264270
})
265271
@@ -432,7 +438,10 @@ async function addProductToItemsList() {
432438
async function findProductFromIdentifier(scannedValue: string ) {
433439
const product = await store.dispatch("product/fetchProductByIdentification", { scannedValue })
434440
let newItem = {} as any;
435-
if(product?.productId) newItem = await addProductToCount(product.productId)
441+
if(product?.productId) {
442+
productIdAdding.value = product.productId
443+
newItem = await addProductToCount(product.productId)
444+
}
436445
437446
setTimeout(() => {
438447
updateCurrentItemInList(newItem, scannedValue);
@@ -512,6 +521,7 @@ async function updateCurrentItemInList(newItem: any, scannedValue: string) {
512521
items[currentItemIndex] = updatedItem
513522
514523
store.dispatch('count/updateCycleCountItems', items);
524+
productIdAdding.value = ""
515525
}
516526
517527
async function readyForReview() {
@@ -651,6 +661,7 @@ async function matchProduct(currentProduct: any) {
651661
addProductModal.onDidDismiss().then(async (result) => {
652662
if(result.data?.selectedProduct) {
653663
const product = result.data.selectedProduct
664+
productIdAdding.value = product.productId
654665
const newItem = await addProductToCount(product.productId)
655666
await updateCurrentItemInList(newItem, currentProduct.scannedId);
656667
}
@@ -659,6 +670,27 @@ async function matchProduct(currentProduct: any) {
659670
addProductModal.present();
660671
}
661672
673+
function handleNewMessage(jsonObj: any) {
674+
const updatedItem = jsonObj.message
675+
if(updatedItem.inventoryCountImportId !== cycleCount.value.inventoryCountImportId) return;
676+
if(productIdAdding.value === updatedItem.productId) return;
677+
678+
const items = JSON.parse(JSON.stringify(cycleCountItems.value.itemList))
679+
const currentItemIndex = items.findIndex((item: any) => item.productId === updatedItem.productId && item.importItemSeqId === updatedItem.importItemSeqId);
680+
681+
if(currentItemIndex !== -1) {
682+
items[currentItemIndex] = updatedItem
683+
} else {
684+
store.dispatch("product/fetchProducts", { productIds: [updatedItem.productId] })
685+
items.push(updatedItem)
686+
}
687+
688+
store.dispatch('count/updateCycleCountItems', items);
689+
if(currentProduct.value.productId === updatedItem.productId) {
690+
store.dispatch('product/currentProduct', updatedItem);
691+
}
692+
}
693+
662694
function getVariance(item: any , isRecounting: boolean) {
663695
const qty = item.quantity
664696
if(isRecounting && inputCount.value === "") return 0;

0 commit comments

Comments
 (0)