From 3fe4f1ff58cdb329e63f1692b162cc4de30421b8 Mon Sep 17 00:00:00 2001
From: Ravi Lodhi <ravi.lodhi@hotwaxsystems.com>
Date: Fri, 30 Aug 2024 16:18:26 +0530
Subject: [PATCH 1/4] Implemented: Added cycle count bulk import funtionality
 (#437).

---
 .env.example                              |   2 +
 src/components/CreateMappingModal.vue     | 106 ++++++++
 src/router/index.ts                       |  11 +
 src/services/CountService.ts              |  25 ++
 src/services/UserService.ts               |  45 ++++
 src/store/modules/count/CountState.ts     |   1 +
 src/store/modules/count/actions.ts        |  23 +-
 src/store/modules/count/getters.ts        |   3 +
 src/store/modules/count/index.ts          |   1 +
 src/store/modules/count/mutation-types.ts |   3 +-
 src/store/modules/count/mutations.ts      |   4 +
 src/store/modules/user/UserState.ts       |   7 +
 src/store/modules/user/actions.ts         | 164 ++++++++++++
 src/store/modules/user/getters.ts         |   7 +
 src/store/modules/user/index.ts           |   7 +
 src/store/modules/user/mutation-types.ts  |   5 +-
 src/store/modules/user/mutations.ts       |  12 +
 src/views/Draft.vue                       |  14 +-
 src/views/InventoryCountBulkImport.vue    | 310 ++++++++++++++++++++++
 19 files changed, 745 insertions(+), 5 deletions(-)
 create mode 100644 src/components/CreateMappingModal.vue
 create mode 100644 src/views/InventoryCountBulkImport.vue

diff --git a/.env.example b/.env.example
index 2509e925..ae77e304 100644
--- a/.env.example
+++ b/.env.example
@@ -5,4 +5,6 @@ VUE_APP_VIEW_SIZE=10
 VUE_APP_PERMISSION_ID="INVCOUNT_APP_VIEW"
 VUE_APP_DEFAULT_LOG_LEVEL="error"
 VUE_APP_PRDT_IDENT=["productId", "groupId", "groupName", "internalName", "parentProductName", "sku", "title", "SHOPIFY_PROD_SKU", "ERP_ID", "UPCA"]
+VUE_APP_MAPPING_TYPES={"INVCOUNT": "INVCNT_MAPPING_PREF"}
+VUE_APP_MAPPING_INVCOUNT={"countImportName": { "label": "Count name", "required": true}, "productSku": { "label": "Product SKU", "required": true, "description": "Products will not be deduplicated. Make sure products are only added to a count once."}, "facility": { "label": "Facility", "required": false, "description": "If a file includes multiple facilities, a count is created for every facility. All items with no facility location will be added to the same count." }, "statusId": { "label": "Status", "required": false, "description": "Defaults to 'Draft'" }, "dueDate": { "label": "Due date", "required": false }}
 VUE_APP_LOGIN_URL="http://launchpad.hotwax.io/login"
\ No newline at end of file
diff --git a/src/components/CreateMappingModal.vue b/src/components/CreateMappingModal.vue
new file mode 100644
index 00000000..68b8a42d
--- /dev/null
+++ b/src/components/CreateMappingModal.vue
@@ -0,0 +1,106 @@
+<template>
+    <ion-header>
+      <ion-toolbar>
+        <ion-buttons slot="start">
+          <ion-button @click="closeModal"> 
+            <ion-icon :icon="close" />
+          </ion-button>
+        </ion-buttons>
+        <ion-title>{{ translate("CSV Mapping") }}</ion-title>
+      </ion-toolbar>
+    </ion-header>
+  
+    <ion-item>
+      <ion-input :label="translate('Mapping name')" :placeholder="translate('Field mapping name')" v-model="mappingName" />
+    </ion-item>
+  
+    <ion-content class="ion-padding">
+      <div>
+        <ion-list>
+          <ion-item :key="field" v-for="(fieldValues, field) in getFields()">
+            <ion-select :label="translate(fieldValues.label)" interface="popover" :placeholder = "translate('Select')" v-model="fieldMapping[field]">
+              <ion-select-option :key="index" v-for="(prop, index) in fileColumns">{{ prop }}</ion-select-option>
+            </ion-select>
+          </ion-item>
+        </ion-list>
+      </div>
+      <ion-fab vertical="bottom" horizontal="end" slot="fixed">
+        <ion-fab-button @click="saveMapping">
+          <ion-icon :icon="saveOutline" />
+        </ion-fab-button>
+      </ion-fab>
+    </ion-content>
+  </template>
+
+<script setup>
+import {
+  IonButtons,
+  IonButton,
+  IonContent,
+  IonFab,
+  IonFabButton,
+  IonHeader,
+  IonIcon,
+  IonInput,
+  IonSelect,
+  IonSelectOption,
+  IonTitle,
+  IonToolbar,
+  IonItem,
+  IonList,
+  onIonViewDidEnter,
+  modalController
+} from '@ionic/vue';
+
+import { close, save, saveOutline } from "ionicons/icons";
+import { computed, defineProps, onMounted, ref } from "vue";
+import { useStore } from "vuex";
+import { showToast } from '@/utils';
+import { translate } from "@hotwax/dxp-components";
+
+const store = useStore();
+
+const props = defineProps(["content", "seletedFieldMapping", "mappingType"])
+const fieldMappings = computed(() => store.getters["user/getFieldMappings"])
+
+let mappingName = ref(null)
+let fieldMapping = ref ({})
+let fileColumns = ref([])
+let identificationTypeId = ref('SKU')
+
+onMounted(() => {
+  fieldMapping.value = { ...props.seletedFieldMapping }
+  fileColumns.value = Object.keys(props.content[0]);
+})
+
+function getFields() {
+  const fields = process.env["VUE_APP_MAPPING_" + props.mappingType];
+  return fields ? JSON.parse(fields) : {};
+}
+function closeModal() {
+  modalController.dismiss({ dismissed: true });
+}
+async function saveMapping() {
+  if(!mappingName.value) {
+    showToast(translate("Enter mapping name"));
+    return
+  }
+  if (!areAllFieldsSelected()) {
+    showToast(translate("Map all fields"));
+    return
+  }
+  const id = generateUniqueMappingPrefId();
+  await store.dispatch("user/createFieldMapping", { id, name: mappingName.value, value: fieldMapping.value, mappingType: props.mappingType })
+  closeModal();
+}
+function areAllFieldsSelected() {
+  return Object.values(fieldMapping.value).every(field => field !== "");
+}
+
+//Todo: Generating unique identifiers as we are currently storing in local storage. Need to remove it as we will be storing data on server.
+function generateUniqueMappingPrefId() {
+  const id = Math.floor(Math.random() * 1000);
+  return !fieldMappings.value[id] ? id : this.generateUniqueMappingPrefId();
+}
+
+</script>
\ No newline at end of file
diff --git a/src/router/index.ts b/src/router/index.ts
index 321ea5fd..1f33e265 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -18,6 +18,7 @@ import PendingReviewDetail from "@/views/PendingReviewDetail.vue";
 import Closed from "@/views/Closed.vue";
 import StorePermissions from "@/views/StorePermissions.vue";
 import Settings from "@/views/Settings.vue";
+import InventoryCountBulkImport from "@/views/InventoryCountBulkImport.vue"
 
 // Defining types for the meta values
 declare module 'vue-router' {
@@ -87,6 +88,16 @@ const routes: Array<RouteRecordRaw> = [
     ],
     beforeEnter: authGuard,
   },
+  
+  {
+    path: '/inventoryCountBulkImport',
+    name: 'Draft bulk',
+    component: InventoryCountBulkImport,
+    beforeEnter: authGuard,
+    meta: {
+      permissionId: "APP_DRAFT_VIEW"
+    }
+  },
   {
     path: '/draft',
     name: 'Draft',
diff --git a/src/services/CountService.ts b/src/services/CountService.ts
index f174253a..14cd2f5b 100644
--- a/src/services/CountService.ts
+++ b/src/services/CountService.ts
@@ -115,12 +115,37 @@ const acceptItem = async (payload: any): Promise<any> => {
   })
 }
 
+const bulkUploadInventoryCounts = async (payload: any): Promise <any>  => {
+  return api({
+    url: `cycleCounts/bulkUpload`,
+    method: "post",
+    ...payload
+  });
+}
+const fetchCycleCountImportSystemMessages = async (payload: any): Promise <any>  => {
+  return api({
+    url: `cycleCounts/systemMessages`,
+    method: "get",
+    params: payload
+  });
+}
+const cancelCycleCountFileProcessing = async (payload: any): Promise <any>  => {
+  return api({
+    url: `cycleCounts/systemMessages/${payload.systemMessageId}`,
+    method: "post",
+    data: payload
+  });
+}
+
 export const CountService = {
   acceptItem,
   addProductToCount,
+  bulkUploadInventoryCounts,
+  cancelCycleCountFileProcessing,
   createCycleCount,
   deleteCycleCountItem,
   fetchBulkCycleCountItems,
+  fetchCycleCountImportSystemMessages,
   fetchCycleCount,
   fetchCycleCountStats,
   fetchCycleCounts,
diff --git a/src/services/UserService.ts b/src/services/UserService.ts
index 1fb95505..c70928d4 100644
--- a/src/services/UserService.ts
+++ b/src/services/UserService.ts
@@ -208,16 +208,61 @@ const getUserPermissions = async (payload: any, url: string, token: any): Promis
     }
 }
 
+const createFieldMapping = async (payload: any): Promise <any> => {
+  return api({
+    url: "dataManagerMappings",
+    method: "POST",
+    data: payload
+  });
+}
+
+const updateFieldMapping = async (payload: any): Promise <any> => {
+  return api({
+    url: "dataManagerMappings/${payload.mappingPrefId}",
+    method: "POST",
+    data: payload
+  });
+}
+
+const deleteFieldMapping = async (payload: any): Promise <any> => {
+  return api({
+    url: "dataManagerMappings/${payload.mappingPrefId}",
+    method: "DELETE",
+    data: payload
+  });
+}
+
+const getFieldMappings = async (payload: any): Promise <any> => {
+  let url = "dataManagerMappings?"
+
+  if (Array.isArray(payload.mappingPrefTypeEnumId)) {
+    url += `mappingPrefTypeEnumId=${payload.mappingPrefTypeEnumId.join('&')}`
+  } else {
+    url += `mappingPrefTypeEnumId=${payload.mappingPrefTypeEnumId}`
+  }
+  delete payload.mappingPrefTypeEnumId
+
+  return api({
+    url,
+    method: "GET",
+    param: payload
+  });
+}
+
 export const UserService = {
+  createFieldMapping,
   createProductStoreSetting,
+  deleteFieldMapping,
   fetchAssociatedFacilities,
   fetchFacilities,
   fetchProductStores,
   fetchProductStoreSettings,
   getAvailableTimeZones,
+  getFieldMappings,
   getUserPermissions,
   getUserProfile,
   login,
+  updateFieldMapping,
   updateProductStoreSetting,
   setUserTimeZone,
 }
\ No newline at end of file
diff --git a/src/store/modules/count/CountState.ts b/src/store/modules/count/CountState.ts
index d0a5e059..4b52658b 100644
--- a/src/store/modules/count/CountState.ts
+++ b/src/store/modules/count/CountState.ts
@@ -8,4 +8,5 @@ export default interface CountState {
   stats: any;
   cycleCounts: any;
   cycleCountItems: any;
+  cycleCountImportSystemMessages: Array<any>
 }
\ No newline at end of file
diff --git a/src/store/modules/count/actions.ts b/src/store/modules/count/actions.ts
index 80811308..1bfa4daa 100644
--- a/src/store/modules/count/actions.ts
+++ b/src/store/modules/count/actions.ts
@@ -8,6 +8,7 @@ import emitter from "@/event-bus"
 import { translate } from "@/i18n"
 import router from "@/router"
 import logger from "@/logger";
+import { DateTime } from "luxon"
 
 const actions: ActionTree<CountState, RootState> = {
   async fetchCycleCounts({ commit, dispatch, state }, payload) {
@@ -177,7 +178,27 @@ const actions: ActionTree<CountState, RootState> = {
 
   async clearCycleCountItems ({ commit }) {
     commit(types.COUNT_ITEMS_UPDATED, [])
-  }
+  },
+  async fetchCycleCountImportSystemMessages({commit} ,payload) {
+    let systemMessages;
+    try {
+      const fifteenMinutesEarlier = DateTime.now().minus({ minutes: 15 });
+      const resp = await CountService.fetchCycleCountImportSystemMessages({
+        systemMessageTypeId: "ImportInventoryCounts",
+        initDate_from: fifteenMinutesEarlier.toMillis(),
+        orderByField: 'processedDate desc,initDate desc',
+        pageSize: 10
+      })
+      if (!hasError(resp)) {
+        systemMessages = resp.data
+      } else {
+        throw resp.data;
+      }
+    } catch (err: any) {
+      logger.error(err)
+    }
+    commit(types.COUNT_IMPORT_SYSTEM_MESSAGES_UPDATED, systemMessages)
+  },
 }	
 
 export default actions;	
diff --git a/src/store/modules/count/getters.ts b/src/store/modules/count/getters.ts
index ee9d7a37..e0590556 100644
--- a/src/store/modules/count/getters.ts
+++ b/src/store/modules/count/getters.ts
@@ -20,6 +20,9 @@ const getters: GetterTree<CountState, RootState> = {
   },
   getCycleCountItems(state) {
     return state.cycleCountItems
+  },
+  getCycleCountImportSystemMessages(state) {
+    return state.cycleCountImportSystemMessages
   }
 };
 
diff --git a/src/store/modules/count/index.ts b/src/store/modules/count/index.ts
index cec20a56..65ab3272 100644
--- a/src/store/modules/count/index.ts
+++ b/src/store/modules/count/index.ts
@@ -15,6 +15,7 @@ const countModule: Module<CountState, RootState> = {
       noFacility: false
     },
     stats: {},
+    cycleCountImportSystemMessages:[],
     cycleCounts: {
       list: [],
       isScrollable: true
diff --git a/src/store/modules/count/mutation-types.ts b/src/store/modules/count/mutation-types.ts
index ccbc73bd..24c90187 100644
--- a/src/store/modules/count/mutation-types.ts
+++ b/src/store/modules/count/mutation-types.ts
@@ -4,4 +4,5 @@ export const COUNT_QUERY_UPDATED = SN_COUNT + "/QUERY_UPDATED"
 export const COUNT_QUERY_CLEARED = SN_COUNT + "/QUERY_CLEARED"
 export const COUNT_STATS_UPDATED = SN_COUNT + "/STATS_UPDATED"
 export const COUNT_UPDATED = SN_COUNT + '/UPDATED'
-export const COUNT_ITEMS_UPDATED = SN_COUNT + '/ITEMS_UPDATED'
\ No newline at end of file
+export const COUNT_ITEMS_UPDATED = SN_COUNT + '/ITEMS_UPDATED'
+export const COUNT_IMPORT_SYSTEM_MESSAGES_UPDATED = SN_COUNT + 'IMPORT_SYSTEM_MESSAGES_UPDATED'
\ No newline at end of file
diff --git a/src/store/modules/count/mutations.ts b/src/store/modules/count/mutations.ts
index 4941a11a..d1a128bc 100644
--- a/src/store/modules/count/mutations.ts
+++ b/src/store/modules/count/mutations.ts
@@ -28,6 +28,10 @@ const mutations: MutationTree <CountState> = {
   },
   [types.COUNT_ITEMS_UPDATED] (state, payload) {
     state.cycleCountItems = payload
+  },
+  [types.COUNT_IMPORT_SYSTEM_MESSAGES_UPDATED] (state, payload) {
+    state.cycleCountImportSystemMessages = payload
   }
+  
 }	
 export default mutations;	
diff --git a/src/store/modules/user/UserState.ts b/src/store/modules/user/UserState.ts
index 3ab2cd3d..1000ac45 100644
--- a/src/store/modules/user/UserState.ts
+++ b/src/store/modules/user/UserState.ts
@@ -11,6 +11,13 @@ export default interface UserState {
   permissions: any;
   productStores: Array<any>;
   currentProductStore: any;
+  fieldMappings: any;
+  currentMapping: {
+    id: string;
+    mappingType: string;
+    name: string;
+    value: object;
+  },
   settings: {
     forceScan: boolean,
     showQoh: boolean,
diff --git a/src/store/modules/user/actions.ts b/src/store/modules/user/actions.ts
index cfc9c4b5..bd7567ac 100644
--- a/src/store/modules/user/actions.ts
+++ b/src/store/modules/user/actions.ts
@@ -71,6 +71,7 @@ const actions: ActionTree<UserState, RootState> = {
       commit(types.USER_PERMISSIONS_UPDATED, appPermissions);
       await dispatch("fetchFacilities")
       await dispatch("fetchProductStores")
+      await dispatch('getFieldMappings')
       emitter.emit("dismissLoader")
     } catch (err: any) {
       emitter.emit("dismissLoader")
@@ -382,5 +383,168 @@ const actions: ActionTree<UserState, RootState> = {
     }
     commit(types.USER_PRODUCT_STORE_SETTING_UPDATED, { [payload.key]: prefValue })
   },
+
+  async getFieldMappings({ commit }) {
+    let fieldMappings = {} as any;
+    try {
+      const payload = {
+        "mappingPrefTypeEnumId": Object.values(JSON.parse(process.env.VUE_APP_MAPPING_TYPES as string)),
+        "mappingPrefTypeEnumId_op": "in",
+        "pageSize": 20 // considered a user won't have more than 20 saved mappings
+      }
+
+      const mappingTypes = JSON.parse(process.env.VUE_APP_MAPPING_TYPES as string)
+      
+      // This is needed as it would easy to get app name to categorize mappings
+      const mappingTypesFlip = Object.keys(mappingTypes).reduce((mappingTypesFlip: any, mappingType) => {
+        // Updating fieldMpaaings here in case the API fails 
+        fieldMappings[mappingType] = {};
+        mappingTypesFlip[mappingTypes[mappingType]] = mappingType;
+        return mappingTypesFlip;
+      }, {});
+
+      const resp = await UserService.getFieldMappings(payload);
+      if(!hasError(resp)) {
+        // updating the structure for mappings so as to directly store it in state
+        fieldMappings = resp.data.reduce((mappings: any, fieldMapping: any) => {
+          const mappingType = mappingTypesFlip[fieldMapping.mappingPrefTypeEnumId]
+          const mapping = mappings[mappingType];
+
+          mapping[fieldMapping.mappingPrefId] = {
+            name: fieldMapping.mappingPrefName,
+            value: JSON.parse(fieldMapping.mappingPrefValue)
+          }
+
+          fieldMappings[mappingType] = mapping;
+          return mappings;
+        }, fieldMappings)
+
+      } else {
+        logger.error('error', 'No field mapping preference found')
+      }
+    } catch(err) {
+      logger.error('error', err)
+    }
+    commit(types.USER_FIELD_MAPPINGS_UPDATED, fieldMappings)
+  },
+
+  async createFieldMapping({ commit }, payload) {
+    try {
+
+      const mappingTypes = JSON.parse(process.env.VUE_APP_MAPPING_TYPES as string)
+      const mappingPrefTypeEnumId = mappingTypes[payload.mappingType];
+
+      const params = {
+        mappingPrefId: payload.id,
+        mappingPrefName: payload.name,
+        mappingPrefValue: JSON.stringify(payload.value),
+        mappingPrefTypeEnumId
+      }
+
+      const resp = await UserService.createFieldMapping(params);
+
+      if (!hasError(resp)) {
+
+        // using id coming from server, as the random generated id sent in payload is not set as mapping id
+        // and an auto generated mapping from server is set as id
+        const fieldMapping = {
+          id: resp.data.mappingPrefId,
+          name: payload.name,
+          value: payload.value,
+          type: payload.mappingType
+        }
+
+        commit(types.USER_FIELD_MAPPING_CREATED, fieldMapping)
+        showToast(translate('This CSV mapping has been saved.'))
+      } else {
+        logger.error('error', 'Failed to save CSV mapping.')
+        showToast(translate('Failed to save CSV mapping.'))
+      }
+    } catch(err) {
+      logger.error('error', err)
+      showToast(translate('Failed to save CSV mapping.'))
+    }
+  },
+
+  async updateFieldMapping({ commit, state }, payload) {
+    try {
+
+      const mappingTypes = JSON.parse(process.env.VUE_APP_MAPPING_TYPES as string)
+      const mappingPrefTypeEnumId = mappingTypes[payload.mappingType];
+
+      const params = {
+        mappingPrefId: payload.id,
+        mappingPrefName: payload.name,
+        mappingPrefValue: JSON.stringify(payload.value),
+        mappingPrefTypeEnumId
+      }
+
+      const resp = await UserService.updateFieldMapping(params);
+
+      if(!hasError(resp)) {
+        const mappings = JSON.parse(JSON.stringify(state.fieldMappings))
+
+        mappings[payload.mappingType][payload.id] = {
+          name: payload.name,
+          value: payload.value
+        }
+
+        commit(types.USER_FIELD_MAPPINGS_UPDATED, mappings)
+        showToast(translate('Changes to the CSV mapping has been saved.'))
+      } else {
+        logger.error('error', 'Failed to update CSV mapping.')
+        showToast(translate('Failed to update CSV mapping.'))
+      }
+    } catch(err) {
+      logger.error('error', err)
+      showToast(translate('Failed to update CSV mapping.'))
+    }
+  },
+
+  async deleteFieldMapping({ commit, state }, payload) {
+    try {
+      const resp = await UserService.deleteFieldMapping({
+        'mappingPrefId': payload.id
+      });
+
+      if(!hasError(resp)) {
+
+        const mappings = JSON.parse(JSON.stringify(state.fieldMappings))
+        delete mappings[payload.mappingType][payload.id]
+
+        commit(types.USER_FIELD_MAPPINGS_UPDATED, mappings)
+        commit(types.USER_CURRENT_FIELD_MAPPING_UPDATED, {
+          id: '',
+          mappingType: '',
+          name: '',
+          value: {}
+        })
+        showToast(translate('This CSV mapping has been deleted.'))
+      } else {
+        logger.error('error', 'Failed to delete CSV mapping.')
+        showToast(translate('Failed to delete CSV mapping.'))
+      }
+    } catch(err) {
+      logger.error('error', err)
+      showToast(translate('Failed to delete CSV mapping.'))
+    }
+  },
+
+  async updateCurrentMapping({ commit, state }, payload) {
+    const currentMapping = {
+      id: payload.id,
+      mappingType: payload.mappingType,
+      ...(state.fieldMappings as any)[payload.mappingType][payload.id]
+    }
+    commit(types.USER_CURRENT_FIELD_MAPPING_UPDATED, currentMapping)
+  },
+  async clearCurrentMapping({ commit }) {
+    commit(types.USER_CURRENT_FIELD_MAPPING_UPDATED, {
+      id: '',
+      mappingType: '',
+      name: '',
+      value: {}
+    })
+  }
 }
 export default actions;
\ No newline at end of file
diff --git a/src/store/modules/user/getters.ts b/src/store/modules/user/getters.ts
index f364dac6..66a113c4 100644
--- a/src/store/modules/user/getters.ts
+++ b/src/store/modules/user/getters.ts
@@ -42,6 +42,13 @@ const getters: GetterTree <UserState, RootState> = {
   },
   getProductStoreSettings(state) {
     return state.settings
+  },
+  getFieldMappings: (state) => (type?: string) => {
+    if (type) {
+        const fieldMapping = (state.fieldMappings as any)[type];
+        return fieldMapping ? fieldMapping : {} 
+    }
+    return state.fieldMappings;
   }
 }
 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 62bc6ce0..cd39ea09 100644
--- a/src/store/modules/user/index.ts
+++ b/src/store/modules/user/index.ts
@@ -20,6 +20,13 @@ const userModule: Module<UserState, RootState> = {
       permissions: [],
       productStores: [],
       currentProductStore: {},
+      fieldMappings: {},
+      currentMapping: {
+        id: '',
+        mappingType: '',
+        name: '',
+        value: {}
+      },
       settings: {
         forceScan: false,
         showQoh: false,
diff --git a/src/store/modules/user/mutation-types.ts b/src/store/modules/user/mutation-types.ts
index ca113e94..d2401776 100644
--- a/src/store/modules/user/mutation-types.ts
+++ b/src/store/modules/user/mutation-types.ts
@@ -9,4 +9,7 @@ export const USER_CURRENT_FACILITY_UPDATED = SN_USER + "/CURRENT_FACILITY_UPDATE
 export const USER_PERMISSIONS_UPDATED = SN_USER + '/PERMISSIONS_UPDATED'
 export const USER_PRODUCT_STORES_UPDATED = SN_USER + '/PRODUCT_STORES_UPDATED'
 export const USER_CURRENT_PRODUCT_STORE_UPDATED = SN_USER + '/CURRENT_PRODUCT_STORE_UPDATED'
-export const USER_PRODUCT_STORE_SETTING_UPDATED = SN_USER + '/PRODUCT_STORE_SETTING_UPDATED'
\ No newline at end of file
+export const USER_PRODUCT_STORE_SETTING_UPDATED = SN_USER + '/PRODUCT_STORE_SETTING_UPDATED'
+export const USER_FIELD_MAPPINGS_UPDATED = SN_USER + '/FIELD_MAPPINGS_UPDATED'
+export const USER_FIELD_MAPPING_CREATED = SN_USER + '/FIELD_MAPPING_CREATED'
+export const USER_CURRENT_FIELD_MAPPING_UPDATED = SN_USER + '/_CURRENT_FIELD_MAPPING_UPDATED'
\ No newline at end of file
diff --git a/src/store/modules/user/mutations.ts b/src/store/modules/user/mutations.ts
index 2b6255e1..755028e1 100644
--- a/src/store/modules/user/mutations.ts
+++ b/src/store/modules/user/mutations.ts
@@ -40,5 +40,17 @@ const mutations: MutationTree <UserState> = {
       (state.settings as any)[setting] = payload[setting]
     })
   },
+  [types.USER_FIELD_MAPPINGS_UPDATED] (state, payload) {
+    state.fieldMappings = payload;
+  },    
+  [types.USER_CURRENT_FIELD_MAPPING_UPDATED] (state, payload) {
+      state.currentMapping = payload
+  },
+  [types.USER_FIELD_MAPPING_CREATED] (state, payload) {
+      (state.fieldMappings as any)[payload.type][payload.id] = {
+          name: payload.name,
+          value: payload.value
+      };
+  }
 }
 export default mutations;
\ No newline at end of file
diff --git a/src/views/Draft.vue b/src/views/Draft.vue
index 6f1da1c8..83ed7630 100644
--- a/src/views/Draft.vue
+++ b/src/views/Draft.vue
@@ -28,10 +28,19 @@
       </ion-list>
 
       <ion-fab vertical="bottom" horizontal="end" slot="fixed">
-        <ion-fab-button @click="createCycleCount">
+        <ion-fab-button>
           <ion-icon :icon="addOutline" />
         </ion-fab-button>
+        <ion-fab-list side="top">
+          <ion-fab-button @click="createCycleCount">
+            <ion-icon :icon="documentOutline" />
+          </ion-fab-button>
+          <ion-fab-button @click="router.push('/inventoryCountBulkImport')">
+            <ion-icon :icon="documentsOutline" />
+          </ion-fab-button>
+        </ion-fab-list>
       </ion-fab>
+
     </ion-content>
   </ion-page>
 </template>
@@ -42,6 +51,7 @@ import {
   IonContent,
   IonFab,
   IonFabButton,
+  IonFabList,
   IonHeader,
   IonIcon,
   IonItem,
@@ -56,7 +66,7 @@ import {
   onIonViewDidEnter,
   onIonViewWillLeave
 } from "@ionic/vue";
-import { addOutline, filterOutline } from "ionicons/icons";
+import { addOutline, documentOutline, documentsOutline, filterOutline } from "ionicons/icons";
 import { computed } from "vue"
 import { translate } from "@/i18n";
 import Filters from "@/components/Filters.vue"
diff --git a/src/views/InventoryCountBulkImport.vue b/src/views/InventoryCountBulkImport.vue
new file mode 100644
index 00000000..40841952
--- /dev/null
+++ b/src/views/InventoryCountBulkImport.vue
@@ -0,0 +1,310 @@
+<template>
+  <ion-page>
+    <ion-header :translucent="true">
+      <ion-toolbar>
+        <ion-menu-button slot="start" />
+        <ion-title>{{ translate("Draft bulk") }}</ion-title>
+      </ion-toolbar>
+    </ion-header>
+
+    <ion-content>
+      <div class="main">
+        <ion-item>
+          <ion-label>{{ translate("Cycle count") }}</ion-label>
+          <ion-label class="ion-text-right ion-padding-end">{{ uploadedFile.name }}</ion-label>
+          <input @change="parse" ref="file" class="ion-hide" type="file" id="inventoryCountInputFile"/>
+          <label for="inventoryCountInputFile">{{ translate("Upload") }}</label>
+        </ion-item>
+
+        <ion-list>
+          <ion-list-header>{{ translate("Saved mappings") }}</ion-list-header>
+          <div>
+            <ion-chip :disabled="!content.length" outline="true" @click="addFieldMapping()">
+              <ion-icon :icon="addOutline" />
+              <ion-label>{{ translate("New mapping") }}</ion-label>
+            </ion-chip>
+            <ion-chip :disabled="!content.length" v-for="(mapping, index) in fieldMappings('INVCOUNT') ?? []" :key="index" @click="mapFields(mapping)" outline="true">
+              {{ mapping.name }}
+            </ion-chip>
+          </div>
+        </ion-list>   
+
+        <ion-list>
+          <ion-list-header>{{ translate("Select the following columns from the uploaded CSV") }}</ion-list-header>
+
+          <ion-item-divider>
+            <ion-label>{{ translate("Required") }} </ion-label>
+          </ion-item-divider>
+          <ion-item :key="field" v-for="(fieldValues, field) in getFilteredFields(fields, true)">
+            <ion-select interface="popover" :disabled="!content.length" :placeholder = "translate('Select')" v-model="fieldMapping[field]">
+              <ion-label slot="label" class="ion-text-wrap">
+                {{translate(fieldValues.label)}}
+                <p>{{ fieldValues.description }}</p>
+              </ion-label>
+              <ion-select-option :key="index" v-for="(prop, index) in fileColumns">{{ prop }}</ion-select-option>
+            </ion-select>
+          </ion-item>
+          <ion-item-divider>
+            <ion-label>{{ translate("Optional") }} </ion-label>
+          </ion-item-divider>
+          <ion-item :key="field" v-for="(fieldValues, field) in getFilteredFields(fields, false)">
+            <ion-select interface="popover" :disabled="!content.length" :placeholder = "translate('Select')" v-model="fieldMapping[field]">
+              <ion-label slot="label" class="ion-text-wrap">
+                {{translate(fieldValues.label)}}
+                <p>{{ fieldValues.description }}</p>
+              </ion-label>
+              <ion-select-option :key="index" v-for="(prop, index) in fileColumns">{{ prop }}</ion-select-option>
+            </ion-select>
+          </ion-item>
+        </ion-list>
+
+        <ion-button :disabled="!content.length" color="medium" @click="save" expand="block">
+          {{ translate("Upload") }}
+          <ion-icon slot="end" :icon="cloudUploadOutline" />
+        </ion-button>
+
+        <ion-list v-if="systemMessages.length" class="system-message-section">
+          <ion-list-header>{{ translate("Recently uploaded counts") }}</ion-list-header>
+          <ion-item v-for="systemMessage in systemMessages" :key="systemMessage.systemMessageId">
+            <ion-label>
+              <p class="overline">{{ systemMessage.systemMessageId }}</p>
+              {{ extractFilename(systemMessage.messageText) }}
+            </ion-label>
+            <div class="system-message-action">
+              <ion-note slot="end">{{ getFileProcessingStatus(systemMessage) }}</ion-note>
+              <ion-button :disabled="systemMessage.statusId === 'SmsgCancelled'" slot="end" fill="clear" color="medium" @click="cancelUpload(systemMessage)">
+                <ion-icon slot="icon-only" :icon="trashOutline" />
+              </ion-button>
+            </div>
+          </ion-item>
+        </ion-list>
+
+      </div>
+    </ion-content>
+  </ion-page>
+</template>
+
+<script setup>
+import {
+  IonButton,
+  IonChip,
+  IonContent,
+  IonHeader,
+  IonIcon,
+  IonItem,
+  IonLabel,
+  IonList,
+  IonListHeader,
+  IonMenuButton,
+  IonNote,
+  IonPage,
+  IonSegment,
+  IonSegmentButton,
+  IonSelect,
+  IonSelectOption,
+  IonTitle,
+  IonToolbar,
+  onIonViewDidEnter,
+  alertController,
+  modalController
+} from '@ionic/vue';
+import { addOutline, cloudUploadOutline, trashOutline } from "ionicons/icons";
+import { translate } from '@/i18n';
+import { computed, ref } from "vue";
+import { useStore } from 'vuex';
+import { useRouter } from 'vue-router'
+import { hasError, jsonToCsv, parseCsv, showToast } from "@/utils";
+import CreateMappingModal from "@/components/CreateMappingModal.vue";
+import { CountService } from "@/services/CountService"
+import logger from "@/logger";
+
+const store = useStore();
+const router = useRouter()
+
+const fieldMappings = computed(() => store.getters["user/getFieldMappings"])
+const systemMessages = computed(() => store.getters["count/getCycleCountImportSystemMessages"])
+
+let file = ref(null)
+let uploadedFile = ref({})
+let fileName = ref(null)
+let content = ref([])
+let fieldMapping = ref({})
+let fileColumns = ref([])
+const fileUploaded = ref(false);
+const fields = process.env["VUE_APP_MAPPING_INVCOUNT"] ? JSON.parse(process.env["VUE_APP_MAPPING_INVCOUNT"]) : {}
+
+
+onIonViewDidEnter(async() => {
+  uploadedFile.value = {}
+  content.value = []
+  fileName.value = null
+  
+  fieldMapping.value = Object.keys(fields).reduce((fieldMapping, field) => {
+    fieldMapping[field] = ""
+    return fieldMapping;
+  }, {})
+  file.value.value = null;
+  await store.dispatch('user/getFieldMappings')
+  await store.dispatch('count/fetchCycleCountImportSystemMessages')
+})
+
+function extractFilename(filePath) {
+  // Get the part of the string after the last '/'
+  const filenameWithTimestamp = filePath.substring(filePath.lastIndexOf('/') + 1);
+  
+  // Use a regex to remove the timestamp and return the base filename
+  const baseFilename = filenameWithTimestamp.replace(/_\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-\d{3}\.csv$/, '.csv');
+  
+  return baseFilename;
+}
+
+function getFilteredFields(fields, required = true) {
+  return Object.keys(fields).reduce((result, key) => {
+    if (fields[key].required === required) {
+      result[key] = fields[key];
+    }
+    return result;
+  }, {});
+}
+function getFileProcessingStatus(systemMessage) {
+  let processingStatus = "Waiting"
+  if (systemMessage.statusId === 'SmsgConsumed') {
+    processingStatus = "Processed"
+  } else if (systemMessage.statusId === 'SmsgConsuming') {
+    processingStatus = "Processing"
+  } else if (systemMessage.statusId === 'SmsgCancelled') {
+    processingStatus = 'Cancelled'
+  }
+  return processingStatus;
+}
+async function cancelUpload (systemMessage) {
+  try {
+    const resp = await CountService.cancelCycleCountFileProcessing({
+      systemMessageId: systemMessage.systemMessageId,
+      statusId: 'SmsgCancelled'
+    });
+    if (!hasError(resp)) {
+      showToast(translate('Count cancelled successfully'))
+      await store.dispatch('count/fetchCycleCountImportSystemMessages')
+    } else {
+      throw resp.data;
+    }
+  } catch (err) {
+    showToast(translate('Failed to cancel uploaded count'))
+    logger.error(err);
+  }
+}
+async function parse(event) {
+  const file = event.target.files[0];
+  try {
+    if (file) {
+      uploadedFile.value = file;
+      fileName.value = file.name
+      content.value = await parseCsv(uploadedFile.value);
+      fileColumns.value = Object.keys(content.value[0]);
+      showToast(translate("File uploaded successfully"));
+      fileUploaded.value =!fileUploaded.value; 
+    } else {
+      showToast(translate("No new file upload. Please try again"));
+    }
+  } catch {
+    content.value = []
+    showToast(translate("Please upload a valid inventory count csv to continue"));
+  }
+}
+async function save(){
+  if (!areAllFieldsSelected()) {
+    showToast(translate("Select all the fields to continue"));
+    return;
+  }
+
+  const uploadedData = content.value.map(item => {
+    return {
+      countImportName: item[fieldMapping.value.countImportName],
+      statusId: item[fieldMapping.value.statusId],
+      idValue: item[fieldMapping.value.productSku],
+      idType: "SKU",
+      dueDate: item[fieldMapping.value.dueDate],
+      facilityId: '',
+      externalFacilityId: item[fieldMapping.value.facility]
+    }
+  })
+  const alert = await alertController.create({
+    header: translate("Upload Inventory Counts"),
+    message: translate("Make sure all the columns are mapped correctly."),
+    buttons: [
+        {
+          text: translate("Cancel"),
+          role: 'cancel',
+        },
+        {
+          text: translate("Upload"),
+          handler: () => {
+            const data = jsonToCsv(uploadedData)
+            const formData = new FormData();
+            formData.append("uploadedFile", data, fileName.value);
+            formData.append("fileName", fileName.value.replace(".csv", ""));
+            
+            CountService.bulkUploadInventoryCounts({
+              data: formData,
+              headers: {
+                'Content-Type': 'multipart/form-data;'
+              }
+            }).then(async (resp) => {
+              if (hasError(resp)) {
+                throw resp.data
+              }
+              await store.dispatch('count/fetchCycleCountImportSystemMessages')
+              showToast(translate("The inventory counts file uploaded successfully"))
+            }).catch(() => {
+              showToast(translate("Something went wrong, please try again"));
+            })
+          },
+        },
+      ],
+    });
+  return alert.present();  
+}
+function mapFields(mapping) {
+  const fieldMappingData = JSON.parse(JSON.stringify(mapping));
+
+  // TODO: Store an object in this.content variable, so everytime when accessing it, we don't need to use 0th index
+  const csvFields = Object.keys(content.value[0]);
+
+  const missingFields = Object.values(fieldMappingData.value).filter(field => {
+    if(!csvFields.includes(field)) return field;
+  });
+
+  if(missingFields.length) showToast(translate("Some of the mapping fields are missing in the CSV: ", { missingFields: missingFields.join(", ") }))
+
+  Object.keys(fieldMappingData).map((key) => {
+    if(!csvFields.includes(fieldMappingData.value[key])){
+      fieldMappingData.value[key] = "";
+    }
+  })
+  fieldMapping.value = fieldMappingData.value;
+}
+function areAllFieldsSelected() {
+  return Object.values(fieldMapping).every(field => field !== "");
+}
+async function addFieldMapping() {
+  const createMappingModal = await modalController.create({
+    component: CreateMappingModal,
+    componentProps: { content: content.value, seletedFieldMapping: fieldMapping.value, mappingType: 'INVCOUNT'}
+  });
+  return createMappingModal.present();
+}
+</script> 
+<style scoped>
+.main {
+  max-width: 732px;
+  margin: var(--spacer-sm) auto 0; 
+}
+.system-message-section {
+  margin-bottom: 16px;
+}
+.system-message-action {
+  display: flex;
+  align-items: center;
+}
+</style>

From e636be5cf5077694aa81490c52822a0eb5188040 Mon Sep 17 00:00:00 2001
From: Ravi Lodhi <ravi.lodhi@hotwaxsystems.com>
Date: Mon, 2 Sep 2024 15:41:59 +0530
Subject: [PATCH 2/4] Improved: Added uiLables and renamed the file (#437).

---
 src/locales/en.json                           | 28 +++++++++++++++++++
 src/router/index.ts                           |  6 ++--
 ...toryCountBulkImport.vue => BulkUpload.vue} | 26 +++++++++--------
 src/views/Draft.vue                           |  2 +-
 4 files changed, 46 insertions(+), 16 deletions(-)
 rename src/views/{InventoryCountBulkImport.vue => BulkUpload.vue} (93%)

diff --git a/src/locales/en.json b/src/locales/en.json
index 90265baf..0054c287 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -27,8 +27,10 @@
   "Browser TimeZone": "Browser TimeZone",
   "Browser time zone": "Browser time zone",
   "Built: ": "Built: {builtDateTime}",
+  "Bulk Upload Cycle Counts": "Bulk Upload Cycle Counts",
   "Camera permission denied.": "Camera permission denied.",
   "Cancel": "Cancel",
+  "cancelled": "cancelled",
   "Change": "Change",
   "Choosing a product identifier allows you to view products with your preferred identifiers.": "Choosing a product identifier allows you to view products with your preferred identifiers.",
   "Click the backdrop to dismiss.": "Click the backdrop to dismiss.",
@@ -55,15 +57,20 @@
   "Created date": "Created date",
   "Created": "Created",
   "CSV data is missing or incorrect. Please check your file.": "CSV data is missing or incorrect. Please check your file.",
+  "CSV Mapping": "CSV Mapping",
   "Cycle Count": "Cycle Count",
+  "Cycle count": "Cycle count",
+  "Cycle count cancelled successfully.": "Cycle count cancelled successfully.",
   "Cycle count not found": "Cycle count not found",
   "Current on hand": "Current on hand",
+  "Defaults to 'Draft'": "Defaults to 'Draft'",
   "Due date": "Due date",
   "Date": "Date",
   "Discard": "Discard",
   "Discard re-count": "Discard re-count",
   "Discarding the re-count will revert the count to the previous count value.": "Discarding the re-count will revert the count to the previous count value.",
   "Draft": "Draft",
+  "Draft bulk": "Draft bulk",
   "Drafts": "Drafts",
   "Draft count": "Draft count",
   "Closed": "Closed",
@@ -91,6 +98,7 @@
   "Facility needs to be associated with a product store to change this configuration.": "Facility needs to be associated with a product store to change this configuration.",
   "Facility": "Facility",
   "Failed to complete cycle count": "Failed to complete cycle count",
+  "Failed to cancel uploaded cycle count.": "Failed to cancel uploaded cycle count.",
   "Failed to create cycle count": "Failed to create cycle count",
   "Failed to add product to count": "Failed to add product to count",
   "Failed to add products to count, as some products are not found.": "Failed to add products to count, as some products are not found.",
@@ -103,11 +111,14 @@
   "Failed to update items": "Failed to update items",
   "Failed to update cycle count information": "Failed to update cycle count information",
   "Fetching cycle counts...": "Fetching cycle counts...",
+  "Field mapping name": "Field mapping name",
+  "File uploaded successfully.": "File uploaded successfully.",
   "Filters": "Filters",
   "File uploaded successfully": "File uploaded successfully",
   "Force scan": "Force scan",
   "Go to Launchpad": "Go to Launchpad",
   "Go to OMS": "Go to OMS",
+  "If a file includes multiple facilities, a count is created for every facility. All items with no facility location will be added to the same count.": "If a file includes multiple facilities, a count is created for every facility. All items with no facility location will be added to the same count.",
   "Import CSV": "Import CSV",
   "In stock": "In stock",
   "inventory variance": "inventory variance", 
@@ -132,9 +143,13 @@
   "Logging out": "Logging out",
   "Logout": "Logout",
   "Line status": "Line status",
+  "Make sure all the columns are mapped correctly.": "Make sure all the columns are mapped correctly.",
   "Make sure you've reviewed the products and their counts before uploading them for review": "Make sure you've reviewed the products and their counts before uploading them for review",
+  "Mapping name": "Mapping name",
+  "New mapping": "New mapping",
   "No cycle counts found": "No cycle counts found",
   "No items found": "No items found",
+  "No new file upload. Please try again.": "No new file upload. Please try again.",
   "No products found.": "No products found.",
   "No new file upload. Please try again": "No new file upload. Please try again",
   "No rejection history": "No rejection history",
@@ -147,6 +162,7 @@
   "Ok": "Ok",
   "OMS": "OMS",
   "OMS instance": "OMS instance",
+  "Optional": "Optional",
   "Password": "Password",
   "Pending": "Pending",
   "PENDING": "PENDING",
@@ -162,8 +178,12 @@
   "Primary identifier": "Primary identifier",
   "Primary product ID": "Primary product ID",
   "Primary Product Identifier": "Primary Product Identifier",
+  "processed": "processed",
+  "processing": "processing",
   "Product Identifier": "Product Identifier",
   "Product Store": "Product Store",
+  "Product SKU": "Product SKU",
+  "Products will not be deduplicated. Make sure products are only added to a count once.": "Products will not be deduplicated. Make sure products are only added to a count once.",
   "Progress": "Progress",
   "products counted": "products counted",
   "QoH": "QoH",
@@ -172,11 +192,13 @@
   "pending review": "pending review",
   "Quantity": "Quantity",
   "Quantity on hand": "Quantity on hand",
+  "Recently uploaded counts": "Recently uploaded counts",
   "Remove": "Remove",
   "Re-count": "Re-count",
   "REJECTED": "REJECTED",
   "Rejected": "Rejected",
   "rejected": "rejected",
+  "Required": "Required",
   "Recount requested": "Recount requested",
   "Rename": "Rename",
   "Re-assign": "Re-assign",
@@ -191,6 +213,7 @@
   "Save re-count": "Save re-count",
   "Save Re-count": "Save Re-count",
   "Save new count": "Save new count",
+  "Saved mappings": "Saved mappings",
   "Saving recount will replace the existing count for item.": "Saving recount will replace the existing count for item.",
   "Scan": "Scan",
   "Scan or search products": "Scan or search products",
@@ -199,12 +222,14 @@
   "Select": "Select",
   "Select fields": "Select fields",
   "Select store": "Select store",
+  "Select the following columns from the uploaded CSV": "Select the following columns from the uploaded CSV",
   "Search": "Search",
   "Search result": "Search result",
   "Search SKU or product name": "Search SKU or product name",
   "Search time zones": "Search time zones",
   "Searching on SKU": "Searching on SKU",
   "Secondary": "Secondary",
+  "Select all the fields to continue": "Select all the fields to continue",
   "Select date": "Select date",
   "Select the column containing products": "Select the column containing products",
   "Secondary product ID": "Secondary product ID",
@@ -218,10 +243,12 @@
   "Show systemic inventory": "Show systemic inventory",
   "Show the current physical quantity expected at locations while counting to help gauge inventory accuracy.": "Show the current physical quantity expected at locations while counting to help gauge inventory accuracy.",
   "SKU": "SKU",
+  "Some of the mapping fields are missing in the CSV:" : "Some of the mapping fields are missing in the CSV: {missingFields}",
   "Some of the item(s) are failed to accept": "Some of the item(s) are failed to accept",
   "Something went wrong": "Something went wrong",
   "Something went wrong while login. Please contact administrator": "Something went wrong while login. Please contact administrator.",
   "Specify which facility you want to operate from. Order, inventory and other configuration data will be specific to the facility you select.": "Specify which facility you want to operate from. Order, inventory and other configuration data will be specific to the facility you select.",
+  "Status": "Status",
   "Stock": "Stock",
   "Store": "Store",
   "Submission date": "Submission date",
@@ -234,6 +261,7 @@
   "store name":"store name",
   "systemic": "systemic",
   "Results": "Results",
+  "The cycle counts file uploaded successfully.": "The cycle counts file uploaded successfully.",
   "The inventory will be immediately updated and cannot be undone. Are you sure you want to update the inventory variance?": "The inventory will be immediately updated and cannot be undone. Are you sure you want to update the inventory variance?",
   "The products in the upload list will be removed.": "The products in the upload list will be removed.",
   "The timezone you select is used to ensure automations you schedule are always accurate to the time you select.": "The timezone you select is used to ensure automations you schedule are always accurate to the time you select.",
diff --git a/src/router/index.ts b/src/router/index.ts
index 1f33e265..7ca0a198 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -18,7 +18,7 @@ import PendingReviewDetail from "@/views/PendingReviewDetail.vue";
 import Closed from "@/views/Closed.vue";
 import StorePermissions from "@/views/StorePermissions.vue";
 import Settings from "@/views/Settings.vue";
-import InventoryCountBulkImport from "@/views/InventoryCountBulkImport.vue"
+import BulkUpload from "@/views/BulkUpload.vue"
 
 // Defining types for the meta values
 declare module 'vue-router' {
@@ -90,9 +90,9 @@ const routes: Array<RouteRecordRaw> = [
   },
   
   {
-    path: '/inventoryCountBulkImport',
+    path: '/bulkUpload',
     name: 'Draft bulk',
-    component: InventoryCountBulkImport,
+    component: BulkUpload,
     beforeEnter: authGuard,
     meta: {
       permissionId: "APP_DRAFT_VIEW"
diff --git a/src/views/InventoryCountBulkImport.vue b/src/views/BulkUpload.vue
similarity index 93%
rename from src/views/InventoryCountBulkImport.vue
rename to src/views/BulkUpload.vue
index 40841952..15e8a5cd 100644
--- a/src/views/InventoryCountBulkImport.vue
+++ b/src/views/BulkUpload.vue
@@ -2,7 +2,7 @@
   <ion-page>
     <ion-header :translucent="true">
       <ion-toolbar>
-        <ion-menu-button slot="start" />
+        <ion-back-button default-href="/tabs/count" slot="start"></ion-back-button>
         <ion-title>{{ translate("Draft bulk") }}</ion-title>
       </ion-toolbar>
     </ion-header>
@@ -86,12 +86,14 @@
 
 <script setup>
 import {
+  IonBackButton,
   IonButton,
   IonChip,
   IonContent,
   IonHeader,
   IonIcon,
   IonItem,
+  IonItemDivider,
   IonLabel,
   IonList,
   IonListHeader,
@@ -167,13 +169,13 @@ function getFilteredFields(fields, required = true) {
   }, {});
 }
 function getFileProcessingStatus(systemMessage) {
-  let processingStatus = "Waiting"
+  let processingStatus = "pending"
   if (systemMessage.statusId === 'SmsgConsumed') {
-    processingStatus = "Processed"
+    processingStatus = "processed"
   } else if (systemMessage.statusId === 'SmsgConsuming') {
-    processingStatus = "Processing"
+    processingStatus = "processing"
   } else if (systemMessage.statusId === 'SmsgCancelled') {
-    processingStatus = 'Cancelled'
+    processingStatus = 'cancelled'
   }
   return processingStatus;
 }
@@ -184,13 +186,13 @@ async function cancelUpload (systemMessage) {
       statusId: 'SmsgCancelled'
     });
     if (!hasError(resp)) {
-      showToast(translate('Count cancelled successfully'))
+      showToast(translate('Cycle count cancelled successfully.'))
       await store.dispatch('count/fetchCycleCountImportSystemMessages')
     } else {
       throw resp.data;
     }
   } catch (err) {
-    showToast(translate('Failed to cancel uploaded count'))
+    showToast(translate('Failed to cancel uploaded cycle count.'))
     logger.error(err);
   }
 }
@@ -202,14 +204,14 @@ async function parse(event) {
       fileName.value = file.name
       content.value = await parseCsv(uploadedFile.value);
       fileColumns.value = Object.keys(content.value[0]);
-      showToast(translate("File uploaded successfully"));
+      showToast(translate("File uploaded successfully."));
       fileUploaded.value =!fileUploaded.value; 
     } else {
-      showToast(translate("No new file upload. Please try again"));
+      showToast(translate("No new file upload. Please try again."));
     }
   } catch {
     content.value = []
-    showToast(translate("Please upload a valid inventory count csv to continue"));
+    showToast(translate("Please upload a valid csv to continue"));
   }
 }
 async function save(){
@@ -230,7 +232,7 @@ async function save(){
     }
   })
   const alert = await alertController.create({
-    header: translate("Upload Inventory Counts"),
+    header: translate("Bulk Upload Cycle Counts"),
     message: translate("Make sure all the columns are mapped correctly."),
     buttons: [
         {
@@ -255,7 +257,7 @@ async function save(){
                 throw resp.data
               }
               await store.dispatch('count/fetchCycleCountImportSystemMessages')
-              showToast(translate("The inventory counts file uploaded successfully"))
+              showToast(translate("The cycle counts file uploaded successfully."))
             }).catch(() => {
               showToast(translate("Something went wrong, please try again"));
             })
diff --git a/src/views/Draft.vue b/src/views/Draft.vue
index 83ed7630..b5a1906e 100644
--- a/src/views/Draft.vue
+++ b/src/views/Draft.vue
@@ -35,7 +35,7 @@
           <ion-fab-button @click="createCycleCount">
             <ion-icon :icon="documentOutline" />
           </ion-fab-button>
-          <ion-fab-button @click="router.push('/inventoryCountBulkImport')">
+          <ion-fab-button @click="router.push('/bulkUpload')">
             <ion-icon :icon="documentsOutline" />
           </ion-fab-button>
         </ion-fab-list>

From 5e799f0438d94f5b3b48daea351ad58b51af2f30 Mon Sep 17 00:00:00 2001
From: Ravi Lodhi <ravi.lodhi@hotwaxsystems.com>
Date: Wed, 4 Sep 2024 15:00:55 +0530
Subject: [PATCH 3/4] Improved: Corrected api endpoint as per the change in
 backend (#437).

---
 src/services/CountService.ts | 2 +-
 src/views/BulkUpload.vue     | 5 ++++-
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/services/CountService.ts b/src/services/CountService.ts
index 14cd2f5b..51704a6a 100644
--- a/src/services/CountService.ts
+++ b/src/services/CountService.ts
@@ -117,7 +117,7 @@ const acceptItem = async (payload: any): Promise<any> => {
 
 const bulkUploadInventoryCounts = async (payload: any): Promise <any>  => {
   return api({
-    url: `cycleCounts/bulkUpload`,
+    url: `cycleCounts/upload`,
     method: "post",
     ...payload
   });
diff --git a/src/views/BulkUpload.vue b/src/views/BulkUpload.vue
index 15e8a5cd..b84fdbdd 100644
--- a/src/views/BulkUpload.vue
+++ b/src/views/BulkUpload.vue
@@ -151,8 +151,11 @@ onIonViewDidEnter(async() => {
 })
 
 function extractFilename(filePath) {
+  if (!filePath) {
+    return;
+  }
   // Get the part of the string after the last '/'
-  const filenameWithTimestamp = filePath.substring(filePath.lastIndexOf('/') + 1);
+  const filenameWithTimestamp = filePath?.substring(filePath?.lastIndexOf('/') + 1);
   
   // Use a regex to remove the timestamp and return the base filename
   const baseFilename = filenameWithTimestamp.replace(/_\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-\d{3}\.csv$/, '.csv');

From e499b087ad37b35f3fe00554a54f67e972af629c Mon Sep 17 00:00:00 2001
From: Ravi Lodhi <ravi.lodhi@hotwaxsystems.com>
Date: Wed, 4 Sep 2024 15:12:39 +0530
Subject: [PATCH 4/4] Improved: Changed icon (#437).

---
 src/views/BulkUpload.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/views/BulkUpload.vue b/src/views/BulkUpload.vue
index b84fdbdd..ea482724 100644
--- a/src/views/BulkUpload.vue
+++ b/src/views/BulkUpload.vue
@@ -73,7 +73,7 @@
             <div class="system-message-action">
               <ion-note slot="end">{{ getFileProcessingStatus(systemMessage) }}</ion-note>
               <ion-button :disabled="systemMessage.statusId === 'SmsgCancelled'" slot="end" fill="clear" color="medium" @click="cancelUpload(systemMessage)">
-                <ion-icon slot="icon-only" :icon="trashOutline" />
+                <ion-icon slot="icon-only" :icon="trashBinOutline" />
               </ion-button>
             </div>
           </ion-item>
@@ -110,7 +110,7 @@ import {
   alertController,
   modalController
 } from '@ionic/vue';
-import { addOutline, cloudUploadOutline, trashOutline } from "ionicons/icons";
+import { addOutline, cloudUploadOutline, trashBinOutline } from "ionicons/icons";
 import { translate } from '@/i18n';
 import { computed, ref } from "vue";
 import { useStore } from 'vuex';