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';