From 62ed045bc724b82aa06bb0f6c8e65ca786888a7d Mon Sep 17 00:00:00 2001 From: amansinghbais Date: Fri, 28 Jun 2024 20:09:27 +0530 Subject: [PATCH 01/18] Implemented: logic to fetch shopify shop jobs for publish section (#356) --- src/api/index.ts | 21 +++++- src/services/ChannelService.ts | 36 ++++++++++ src/store/modules/channel/ChannelState.ts | 1 + src/store/modules/channel/actions.ts | 78 +++++++++++++++++++++ src/store/modules/channel/getters.ts | 3 + src/store/modules/channel/index.ts | 3 +- src/store/modules/channel/mutation-types.ts | 3 +- src/store/modules/channel/mutations.ts | 3 + src/views/InventoryChannels.vue | 26 +++++-- 9 files changed, 163 insertions(+), 11 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index 84939cfe..7312a1ff 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -5,14 +5,22 @@ import emitter from "@/event-bus" import store from "@/store"; import { StatusCodes } from "http-status-codes"; +let apiConfig = {} as any + axios.interceptors.request.use((config: any) => { // TODO: pass csrf token const token = store.getters["user/getUserToken"]; - if (token) { + if (token && !apiConfig.useOmsRedirection) { config.headers["api_key"] = token; config.headers["Content-Type"] = "application/json"; } + const omsRedirectionInfo = store.getters["user/getOmsRedirectionInfo"] + if (apiConfig.useOmsRedirection && omsRedirectionInfo.token) { + config.headers["Authorization"] = `Bearer ${omsRedirectionInfo.token}`; + config.headers["Content-Type"] = "application/json"; + } + return config; }); @@ -77,6 +85,8 @@ const axiosCache = setupCache({ * @return {Promise} Response from API as returned by Axios */ const api = async (customConfig: any) => { + apiConfig = customConfig + // Prepare configuration const config: any = { url: customConfig.url, @@ -87,7 +97,14 @@ const api = async (customConfig: any) => { } const baseURL = store.getters["user/getInstanceUrl"]; - if (baseURL) config.baseURL = baseURL.startsWith('http') ? baseURL.includes('/rest/s1/available-to-promise') ? baseURL : `${baseURL}/rest/s1/available-to-promise/` : `https://${baseURL}.hotwax.io/rest/s1/available-to-promise/`; + const omsRedirectionInfo = store.getters["user/getOmsRedirectionInfo"] + + if(customConfig.useOmsRedirection) { + config.baseURL = omsRedirectionInfo.url.startsWith('http') ? omsRedirectionInfo.url.includes('/api') ? omsRedirectionInfo.url : `${omsRedirectionInfo.url}/api/` : `https://${omsRedirectionInfo.url}.hotwax.io/api/`; + } else if (baseURL) { + config.baseURL = baseURL.startsWith('http') ? baseURL.includes('/rest/s1/available-to-promise') ? baseURL : `${baseURL}/rest/s1/available-to-promise/` : `https://${baseURL}.hotwax.io/rest/s1/available-to-promise/`; + } + if(customConfig.cache) config.adapter = axiosCache.adapter; const networkStatus = await OfflineHelper.getNetworkStatus(); if (customConfig.queue && !networkStatus.connected) { diff --git a/src/services/ChannelService.ts b/src/services/ChannelService.ts index da4b9500..2bc95da8 100644 --- a/src/services/ChannelService.ts +++ b/src/services/ChannelService.ts @@ -1,4 +1,6 @@ import api from '@/api'; +import logger from '@/logger'; +import { hasError } from '@/utils'; const fetchInventoryChannels = async (payload: any): Promise => { return api({ @@ -64,11 +66,45 @@ const createFacility = async (payload: any): Promise => { }); } +const fetchShopifyConfigs = async (payload: any): Promise => { + return api({ + url: "performFind", + method: "post", + data: payload, + useOmsRedirection: true + }); +} + +const fetchJobInformation = async (payload: any): Promise => { + let jobs = []; + + try { + const resp = await api({ + url: "findJobs", + method: "post", + data: payload, + useOmsRedirection: true + }) as any; + + if(!hasError(resp)) { + jobs = resp.data.docs + } else { + throw resp.data; + } + } catch(error: any) { + logger.error(error); + } + + return jobs; +} + export const ChannelService = { createFacility, createFacilityGroup, fetchGroupFacilities, fetchInventoryChannels, + fetchJobInformation, + fetchShopifyConfigs, updateFacilityAssociationWithGroup, updateFacilityAssociationWithProductStore, updateGroupAssociationWithProductStore, diff --git a/src/store/modules/channel/ChannelState.ts b/src/store/modules/channel/ChannelState.ts index ad076b61..a98af2ac 100644 --- a/src/store/modules/channel/ChannelState.ts +++ b/src/store/modules/channel/ChannelState.ts @@ -1,3 +1,4 @@ export default interface ChannelState { inventoryChannels: any; + jobs: any; } \ No newline at end of file diff --git a/src/store/modules/channel/actions.ts b/src/store/modules/channel/actions.ts index e6843965..65cef72f 100644 --- a/src/store/modules/channel/actions.ts +++ b/src/store/modules/channel/actions.ts @@ -70,6 +70,84 @@ const actions: ActionTree = { commit(types.CHANNEL_INVENTORY_CHANNELS_UPDATED, groups); }, + async fetchShopifyConfigs ({ commit }) { + let shopifyConfigs = [] ; + try { + const payload = { + inputFields: { + productStoreId: store.state.user.currentEComStore.productStoreId, + }, + fieldList: ["shopifyConfigId", "name", "shopId"], + entityName: "ShopifyShopAndConfig", + noConditionFind: "Y", + } + + const resp = await ChannelService.fetchShopifyConfigs(payload); + if (!hasError(resp)) { + shopifyConfigs = resp.data.docs; + } else { + throw resp.data + } + } catch(error: any) { + logger.error(error) + } + return shopifyConfigs; + }, + + async fetchJobs ({ commit, dispatch }) { + const shopifyConfigs = await dispatch("fetchShopifyConfigs"); + + if(!shopifyConfigs.length) { + return; + } + + let params = {}, draftJob = {}; + + // Fetch draft job + params = { + inputFields: { + statusId: "SERVICE_DRAFT", + systemJobEnumId: "JOB_UL_INV" + } as any, + fieldList: ["systemJobEnumId", "runTime", "tempExprId", "parentJobId", "serviceName", "jobId", "jobName", "currentRetryCount", "statusId", "productStoreId", "runtimeDataId", "shopId", "description", "enumTypeId", "enumName"], + noConditionFind: "Y" + } + + const draftJobs = await ChannelService.fetchJobInformation(params); + if(draftJobs.length) draftJob = draftJobs[0]; + + // Fetching pending jobs + params = { + inputFields: { + statusId: "SERVICE_PENDING", + systemJobEnumId: "JOB_UL_INV", + "productStoreId": store.state.user.currentEComStore.productStoreId, + } as any, + fieldList: ["systemJobEnumId", "runTime", "tempExprId", "parentJobId", "serviceName", "jobId", "jobName", "currentRetryCount", "statusId", "productStoreId", "runtimeDataId", "shopId", "description", "enumTypeId", "enumName"], + noConditionFind: "Y" + } + + const pendingJobs = await ChannelService.fetchJobInformation(params); + + const jobs = shopifyConfigs.map((shop: any) => { + const pendingJob = pendingJobs.find((job: any) => job.shopId === shop.shopId) + + if(pendingJob.jobId) { + return { + ...shop, + ...pendingJob + } + } else { + return { + ...shop, + ...draftJob + } + } + }) + + commit(types.CHANNEL_JOBS_UPDATED, jobs) + }, + async clearChannelState({ commit }) { commit(types.CHANNEL_INVENTORY_CHANNELS_UPDATED, []) }, diff --git a/src/store/modules/channel/getters.ts b/src/store/modules/channel/getters.ts index ad2fb11b..c8ab722d 100644 --- a/src/store/modules/channel/getters.ts +++ b/src/store/modules/channel/getters.ts @@ -5,6 +5,9 @@ import ChannelState from './ChannelState'; const getters: GetterTree = { getInventoryChannels (state) { return state.inventoryChannels + }, + getJobs (state) { + return state.jobs } } export default getters; \ No newline at end of file diff --git a/src/store/modules/channel/index.ts b/src/store/modules/channel/index.ts index b1a9e8fe..193c2170 100644 --- a/src/store/modules/channel/index.ts +++ b/src/store/modules/channel/index.ts @@ -8,7 +8,8 @@ import ChannelState from './ChannelState' const channelModule: Module = { namespaced: true, state: { - inventoryChannels: [] + inventoryChannels: [], + jobs: [] }, getters, actions, diff --git a/src/store/modules/channel/mutation-types.ts b/src/store/modules/channel/mutation-types.ts index cfe05e1e..473b2321 100644 --- a/src/store/modules/channel/mutation-types.ts +++ b/src/store/modules/channel/mutation-types.ts @@ -1,2 +1,3 @@ export const SN_CHANNEL = 'channel' -export const CHANNEL_INVENTORY_CHANNELS_UPDATED = SN_CHANNEL + '/INVENTORY_CHANNELS_UPDATED' \ No newline at end of file +export const CHANNEL_INVENTORY_CHANNELS_UPDATED = SN_CHANNEL + '/INVENTORY_CHANNELS_UPDATED' +export const CHANNEL_JOBS_UPDATED = SN_CHANNEL + '/JOBS_UPDATED' \ No newline at end of file diff --git a/src/store/modules/channel/mutations.ts b/src/store/modules/channel/mutations.ts index 9a376d5e..9b641c45 100644 --- a/src/store/modules/channel/mutations.ts +++ b/src/store/modules/channel/mutations.ts @@ -6,5 +6,8 @@ const mutations: MutationTree = { [types.CHANNEL_INVENTORY_CHANNELS_UPDATED] (state, payload) { state.inventoryChannels = payload; }, + [types.CHANNEL_JOBS_UPDATED] (state, payload) { + state.jobs = payload; + }, } export default mutations; \ No newline at end of file diff --git a/src/views/InventoryChannels.vue b/src/views/InventoryChannels.vue index c24b812a..b9789f38 100644 --- a/src/views/InventoryChannels.vue +++ b/src/views/InventoryChannels.vue @@ -10,7 +10,7 @@ {{ translate("Channels") }} - + {{ translate("Publish") }} @@ -76,13 +76,13 @@
- +
- {{ "SHOP CONFIG ID" }} - {{ "Shop name" }} + {{ job.shopifyConfigId }} + {{ job.name ? job.name : job.shopifyConfigId }}
- {{ "in 2 minutes" }} + {{ translate("running") }} {{ timeFromNow(job.runTime) }}
@@ -139,11 +139,13 @@ import LinkThresholdFacilitiesToGroupModal from '@/components/LinkThresholdFacil import { useStore } from 'vuex'; import EditGroupModal from '@/components/EditGroupModal.vue'; import emitter from '@/event-bus'; +import { DateTime } from 'luxon'; const store = useStore(); const inventoryChannels = computed(() => store.getters["channel/getInventoryChannels"]) const selectedSegment = computed(() => store.getters["util/getSelectedSegment"]) +const jobs = computed(() => store.getters["channel/getJobs"]) onIonViewDidEnter(async() => { fetchInventoryChannels() @@ -158,6 +160,9 @@ async function fetchInventoryChannels() { emitter.emit("presentLoader"); if(!selectedSegment.value || (selectedSegment.value !== 'channels' && selectedSegment.value !== 'publish')) store.dispatch("util/updateSelectedSegment", "channels"); await Promise.allSettled([store.dispatch("channel/fetchInventoryChannels"), store.dispatch("util/fetchConfigFacilities")]); + if(selectedSegment.value === "publish") { + await store.dispatch("channel/fetchJobs"); + } emitter.emit("dismissLoader"); } @@ -216,10 +221,17 @@ function getFacilityCount(channel: any, facilityTypeId: string) { } } -function updateSegment(event: any) { - store.dispatch("util/updateSelectedSegment", event.detail.value); +async function updateSegment(event: any) { + await store.dispatch("util/updateSelectedSegment", event.detail.value); + if(selectedSegment.value === "publish") { + await store.dispatch("channel/fetchJobs"); + } } +function timeFromNow(time: any) { + const timeDiff = DateTime.fromMillis(time).diff(DateTime.local()); + return DateTime.local().plus(timeDiff).toRelative(); +} \ No newline at end of file From 5e6d7eddcdd21cf32bc586b1f24c2337ff3df49d Mon Sep 17 00:00:00 2001 From: amansinghbais Date: Wed, 3 Jul 2024 10:20:05 +0530 Subject: [PATCH 05/18] Improved: added logic to disable job (#356) --- src/components/ShopActionsPopover.vue | 48 +++++++++++++++++++++++- src/services/ChannelService.ts | 10 +++++ src/store/modules/channel/actions.ts | 8 ++-- src/views/InventoryChannels.vue | 53 ++++++++++++++++----------- 4 files changed, 93 insertions(+), 26 deletions(-) diff --git a/src/components/ShopActionsPopover.vue b/src/components/ShopActionsPopover.vue index 24a7882b..282e6c3a 100644 --- a/src/components/ShopActionsPopover.vue +++ b/src/components/ShopActionsPopover.vue @@ -14,7 +14,7 @@ {{ translate("Run now") }} - + {{ translate("Disable") }} @@ -30,7 +30,12 @@ import { defineProps } from "vue"; import JobHistoryModal from "@/components/JobHistoryModal.vue" import { Plugins } from '@capacitor/core'; import { hasError, showToast } from "@/utils"; +import logger from "@/logger"; +import { ChannelService } from "@/services/ChannelService"; import { DateTime } from 'luxon'; +import { useStore } from "vuex"; + +const store = useStore(); const props = defineProps(["job"]); @@ -63,4 +68,45 @@ async function copyJobInformation(job: any) { closePopover(); } + +async function disableJob() { + const alert = await alertController.create({ + header: translate('Cancel job'), + message: translate('Canceling this job will cancel this occurrence and all following occurrences. This job will have to be re-enabled manually to run it again.'), + buttons: [{ + text: translate("Don't cancel"), + role: 'cancel' + }, { + text: translate('Cancel'), + handler: async() => { + if(isRuntimePassed()) { + showToast(translate("Job runtime has passed. Please refresh to get the latest job data in order to perform any action.")) + return; + } + + try { + const resp = await ChannelService.disableJob({ + jobId: props.job.jobId + }) + + if(!hasError(resp)) { + showToast(translate("Successfully cancelled this job.")) + await store.dispatch("channel/fetchJobs"); + } else { + throw resp.data; + } + } catch(error: any) { + showToast(translate("Failed to cancel this job.")) + logger.error(error); + } + } + }], + }); + + return alert.present(); +} + +function isRuntimePassed() { + return props.job.runTime <= DateTime.now().toMillis() +} \ No newline at end of file diff --git a/src/services/ChannelService.ts b/src/services/ChannelService.ts index c10315bb..09428236 100644 --- a/src/services/ChannelService.ts +++ b/src/services/ChannelService.ts @@ -117,9 +117,19 @@ const fetchTemporalExpression = async (payload: any): Promise => { }); } +const disableJob = async (payload: any): Promise => { + return api({ + url: "service/cancelScheduledJob", + method: "post", + data: payload, + useOmsRedirection: true, + }); +} + export const ChannelService = { createFacility, createFacilityGroup, + disableJob, fetchGroupFacilities, fetchInventoryChannels, fetchJobInformation, diff --git a/src/store/modules/channel/actions.ts b/src/store/modules/channel/actions.ts index b0f7a8b2..54495a4c 100644 --- a/src/store/modules/channel/actions.ts +++ b/src/store/modules/channel/actions.ts @@ -101,7 +101,7 @@ const actions: ActionTree = { return; } - let params = {}, draftJob = {}; + let params = {}, draftJob = {} as any; // Fetch draft job params = { @@ -135,12 +135,14 @@ const actions: ActionTree = { if(pendingJob?.jobId) { return { ...shop, - ...pendingJob + ...pendingJob, + runTimeValue: pendingJob.runTime } } else { return { ...shop, - ...draftJob + ...draftJob, + runTimeValue: draftJob?.runTime } } }) diff --git a/src/views/InventoryChannels.vue b/src/views/InventoryChannels.vue index 223c1cb6..5e6b36ee 100644 --- a/src/views/InventoryChannels.vue +++ b/src/views/InventoryChannels.vue @@ -88,7 +88,7 @@ - + {{ runTime.label }} @@ -97,7 +97,7 @@ @@ -113,7 +113,7 @@ - + {{ channel.facilityGroupName ? channel.facilityGroupName : channel.facilityGroupId }} @@ -158,12 +158,25 @@ const store = useStore(); const inventoryChannels = computed(() => store.getters["channel/getInventoryChannels"]) const selectedSegment = computed(() => store.getters["util/getSelectedSegment"]) -const jobs = computed(() => store.getters["channel/getJobs"]) +const shopifyJobs = computed(() => store.getters["channel/getJobs"]) const getTemporalExpr = computed(() => store.getters["channel/getTemporalExpr"]) -const shopifyJobs = ref([]) as any; const isDateTimeModalOpen = ref(false); -const jobFrequencyOptions = ref([]); +const allowedFrequencies = ref([ + { + "id": "HOURLY", + "description": "Hourly" + }, { + "id": "EVERY_6_HOUR", + "description": "Every 6 hours" + }, { + "id": "EVERYDAY", + "description": "Every day" + }, { + "id": "CUSTOM", + "description": "Custom" + } +]) const allowedRunTimes = ref([ { "value": 0, @@ -185,7 +198,7 @@ const allowedRunTimes = ref([ "label": "Custom" } ]); - +const jobFrequencyOptions = ref([]) as any; const jobRuntimeOptions = ref([]) as any; onIonViewDidEnter(async() => { @@ -204,7 +217,6 @@ async function fetchInventoryChannels() { if(selectedSegment.value === "publish") { await store.dispatch("channel/fetchJobs"); await store.dispatch("channel/findTemporalExpression") - shopifyJobs.value = jobs.value.length ? JSON.parse(JSON.stringify(jobs.value)) : []; generateFrequencyOptions(); generateRuntimeOptions(); } @@ -271,7 +283,6 @@ async function updateSegment(event: any) { await store.dispatch("util/updateSelectedSegment", event.detail.value); if(selectedSegment.value === "publish") { await store.dispatch("channel/fetchJobs"); - shopifyJobs.value = jobs.value.length ? JSON.parse(JSON.stringify(jobs.value)) : []; await store.dispatch("channel/findTemporalExpression") generateFrequencyOptions() generateRuntimeOptions(); @@ -282,11 +293,6 @@ function isCustomRunTime(value: number) { return !allowedRunTimes.value.some((runTime: any) => runTime.value === value) } -function timeFromNow(time: any) { - const timeDiff = DateTime.fromMillis(time).diff(DateTime.local()); - return DateTime.local().plus(timeDiff).toRelative(); -} - function getJobStatus(job: any) { return job.statusId === "SERVICE_DRAFT" ? job.statusId : job.tempExprId; } @@ -314,14 +320,14 @@ async function setCustomFrequency(currentJob: any) { if(result.data?.frequencyId) { job.tempExprId = result.data.frequencyId } else { - job.tempExprId = jobs.value.find((job: any) => job.shopId === currentJob.shopId)?.tempExprId + job.tempExprId = shopifyJobs.value.find((job: any) => job.shopId === currentJob.shopId)?.tempExprId } generateFrequencyOptions() }); } function generateFrequencyOptions() { - const frequencyOptions = JSON.parse(process.env.VUE_APP_SHOPIFY_JOBS_FREQUENCY_OPTIONS); + const frequencyOptions = JSON.parse(JSON.stringify(allowedFrequencies.value)); shopifyJobs.value.map((job: any) => { const option = frequencyOptions.find((option: any) => option.id === job.tempExprId) @@ -338,9 +344,9 @@ function generateRuntimeOptions() { shopifyJobs.value.map((job: any) => { if(job.statusId === "SERVICE_PENDING") { - const selectedTime = runTimeOptions.find((option: any) => option.value === job.runTime); + const selectedTime = runTimeOptions.find((option: any) => option.value === job.runTimeValue); if(!selectedTime) { - runTimeOptions.push({ label: getTime(job.runTime), value: job.runTime }) + runTimeOptions.push({ label: getTime(job.runTimeValue), value: job.runTimeValue }) } } }) @@ -350,8 +356,7 @@ function generateRuntimeOptions() { function updateRunTime(event: CustomEvent, currentJob: any) { const value = event.detail.value if (value != 'CUSTOM') { - const job = shopifyJobs.value.find((job: any) => job.shopId === currentJob.shopId) - job.runTime = value + currentJob.runTimeValue = value generateRuntimeOptions() } else { isDateTimeModalOpen.value = true @@ -362,8 +367,7 @@ function updateCustomTime(event: CustomEvent, currentJob: any) { const currTime = DateTime.now().toMillis(); const setTime = handleDateTimeInput(event.detail.value); if(setTime > currTime) { - const job = shopifyJobs.value.find((job: any) => job.shopId === currentJob.shopId) - job.runTime = setTime + currentJob.runTimeValue = setTime generateRuntimeOptions() } else { showToast(translate("Provide a future date and time")) @@ -389,6 +393,11 @@ const getNowTimestamp = () => { function getTime (time: any) { return DateTime.fromMillis(time).toLocaleString(DateTime.DATETIME_MED); } + +function timeFromNow(time: any) { + const timeDiff = DateTime.fromMillis(time).diff(DateTime.local()); + return DateTime.local().plus(timeDiff).toRelative(); +}