diff --git a/src/components/BulkInventoryAdjustmentModal.vue b/src/components/BulkInventoryAdjustmentModal.vue new file mode 100644 index 00000000..4a727496 --- /dev/null +++ b/src/components/BulkInventoryAdjustmentModal.vue @@ -0,0 +1,118 @@ + + \ No newline at end of file diff --git a/src/components/Menu.vue b/src/components/Menu.vue index 7fd3b345..a5b6ddad 100644 --- a/src/components/Menu.vue +++ b/src/components/Menu.vue @@ -54,7 +54,8 @@ import { } from "@ionic/vue"; import { defineComponent, ref } from "vue"; import { mapGetters } from "vuex"; -import { bookmarkOutline, settings, calendar } from "ionicons/icons"; + +import { albumsOutline, bookmarkOutline, settings, calendar } from "ionicons/icons"; import { useStore } from "@/store"; export default defineComponent({ @@ -100,6 +101,12 @@ export default defineComponent({ const store = useStore(); const selectedIndex = ref(0); const appPages = [ + { + title: "Inventory", + url: "/inventory", + iosIcon: albumsOutline, + mdIcon: albumsOutline + }, { title: "Purchase order", url: "/purchase-order", @@ -122,6 +129,7 @@ export default defineComponent({ return { selectedIndex, appPages, + albumsOutline, calendar, settings, store diff --git a/src/components/MissingFacilitiesModal.vue b/src/components/MissingFacilitiesModal.vue index 2b48e246..1d4c812d 100644 --- a/src/components/MissingFacilitiesModal.vue +++ b/src/components/MissingFacilitiesModal.vue @@ -70,7 +70,7 @@ export default defineComponent({ IonTitle, IonToolbar }, - props: ["itemsWithMissingFacility", "facilities"], + props: ["itemsWithMissingFacility", "type"], data() { return { itemsByFacilityId: {}, @@ -79,23 +79,22 @@ export default defineComponent({ }, computed: { ...mapGetters({ - purchaseOrders: 'order/getPurchaseOrders' + purchaseOrders: 'order/getPurchaseOrders', + stockItems: 'stock/getStockItems', + facilities: 'util/getFacilities', }) }, mounted(){ this.groupItemsByFacilityId(); }, methods: { - save(){ - Object.keys(this.facilityMapping).map((facilityId: any) => { - Object.values(this.purchaseOrders.parsed).flat().map((item: any) => { - if(item.externalFacilityId === facilityId){ - item.externalFacilityId = ""; - item.facilityId = this.facilityMapping[facilityId]; - } - }) - }) - this.store.dispatch('order/updatePurchaseOrders', this.purchaseOrders); + async save(){ + if(this.type === 'order'){ + this.store.dispatch('order/updateMissingFacilities', this.facilityMapping) + } else { + this.store.dispatch('stock/updateMissingFacilities', this.facilityMapping) + } + this.closeModal(); showToast(translate("Changes have been successfully applied")); }, diff --git a/src/components/MissingSkuModal.vue b/src/components/MissingSkuModal.vue index e9f515b1..f5da4a13 100644 --- a/src/components/MissingSkuModal.vue +++ b/src/components/MissingSkuModal.vue @@ -14,7 +14,7 @@
- {{ $t("The SKU is successfully changed") }} + {{ $t("The SKU is successfully changed") }} {{ $t("This SKU is not available, please try again") }} {{ $t("Update") }} @@ -116,14 +116,20 @@ export default defineComponent({ updatedSku: '', unidentifiedProductSku: '', hasSkuUpdated: false, - isSkuInvalid: false + isSkuInvalid: false, + unidentifiedItems: [] as any } }, computed: { ...mapGetters({ purchaseOrders: 'order/getPurchaseOrders', + stockItems: 'stock/getStockItems', }) }, + mounted() { + this.unidentifiedItems = this.type ==='order' ? this.purchaseOrders.unidentifiedItems : this.stockItems.unidentifiedItems; + }, + props: ['type'], methods: { selectInputText(event: any) { event.target.getInputElement().then((element: any) => { @@ -134,13 +140,14 @@ export default defineComponent({ modalController.dismiss({ dismissed: true }); }, getPendingItems(){ - return this.purchaseOrders.unidentifiedItems.filter((item: any) => !item.updatedSku); + return this.unidentifiedItems.filter((item: any) => !item.updatedSku); }, getCompletedItems(){ - return this.purchaseOrders.unidentifiedItems.filter((item: any) => item.updatedSku); + return this.unidentifiedItems.filter((item: any) => item.updatedSku); }, - save(){ - this.store.dispatch('order/updateUnidentifiedItem', { unidentifiedItems: this.purchaseOrders.unidentifiedItems }); + save() { + if(this.type === 'order') this.store.dispatch('order/updateUnidentifiedItem', { unidentifiedItems: this.purchaseOrders.unidentifiedItems }); + else this.store.dispatch('stock/updateUnidentifiedItem', { unidentifiedItems: this.stockItems.unidentifiedItems }); this.closeModal(); }, async update() { @@ -152,22 +159,27 @@ export default defineComponent({ productIds: [this.updatedSku] } const products = await this.store.dispatch("product/fetchProducts", payload); - if (products.length) { - const item = products[0]; - const unidentifiedItem = this.purchaseOrders.unidentifiedItems.find((unidentifiedItem: any) => unidentifiedItem.shopifyProductSKU === this.unidentifiedProductSku); - - unidentifiedItem.updatedSku = this.updatedSku; - unidentifiedItem.parentProductId = item.parent.id; - unidentifiedItem.pseudoId = item.pseudoId; - unidentifiedItem.parentProductName = item.parent.productName; - unidentifiedItem.imageUrl = item.images.mainImageUrl; - unidentifiedItem.isNewProduct = "N"; - unidentifiedItem.isSelected = true; - - this.hasSkuUpdated = true; + const product = products[this.updatedSku]; + if (!product) { + this.isSkuInvalid = true; + return; + } + + const unidentifiedItem = this.unidentifiedItems.find((unidentifiedItem: any) => unidentifiedItem.shopifyProductSKU === this.unidentifiedProductSku); + + unidentifiedItem.updatedSku = this.updatedSku; + unidentifiedItem.parentProductId = product.parent.id; + unidentifiedItem.pseudoId = product.pseudoId; + unidentifiedItem.parentProductName = product.parent.productName; + unidentifiedItem.imageUrl = product.images.mainImageUrl; + unidentifiedItem.isSelected = true; + + this.hasSkuUpdated = true; + if (this.type === 'order'){ + unidentifiedItem.isNewProduct = 'N'; this.store.dispatch('order/updatePurchaseOrders', this.purchaseOrders); } else { - this.isSkuInvalid = true; + this.store.dispatch('stock/updateStockItems', this.stockItems); } }, }, diff --git a/src/components/ProductPopover.vue b/src/components/ProductPopover.vue index 66238fef..738f15da 100644 --- a/src/components/ProductPopover.vue +++ b/src/components/ProductPopover.vue @@ -23,11 +23,12 @@ import { arrowUndoOutline } from 'ionicons/icons'; export default defineComponent({ - props: ['id', 'isVirtual', 'item', 'poId'], + props: ['id', 'isVirtual', 'item', 'poId', 'type'], name: 'parentProductPopover', components: { IonContent, IonIcon, IonLabel, IonItem }, computed: { ...mapGetters({ + stockItems: 'stock/getStockItems', purchaseOrders: 'order/getPurchaseOrders', }), }, @@ -38,19 +39,39 @@ export default defineComponent({ onlySelect() { this.isVirtual ? this.onlySelectParentProduct() : this.onlySelectSingleProduct(); }, - onlySelectParentProduct() { + onlySelectParentProductForOrder() { Object.values(this.purchaseOrders.parsed).flat().map(item => { item.isSelected = item.parentProductId === this.id && item.orderId === this.poId; }); + this.store.dispatch('order/updatePurchaseOrders', this.purchaseOrders) + }, + onlySelectParentProductForStock() { + this.stockItems.parsed.map(item => { + item.isSelected = item.parentProductId === this.id; + }) + this.store.dispatch('stock/updateStockItems', this.stockItems) + }, + onlySelectParentProduct() { + this.type === 'order' ? this.onlySelectParentProductForOrder() : this.onlySelectParentProductForStock(); popoverController.dismiss({ dismissed: true }); }, - onlySelectSingleProduct() { + onlySelectSingleProductForOrder() { Object.values(this.purchaseOrders.parsed).flat().map(item => { item.isSelected = item.pseudoId === this.id && item.orderId === this.poId; }); + this.store.dispatch('order/updatePurchaseOrders', this.purchaseOrders) + }, + onlySelectSingleProductForStock() { + this.stockItems.parsed.map(item => { + item.isSelected = item.pseudoId === this.id; + }); + this.store.dispatch('stock/updateStockItems', this.stockItems) + }, + onlySelectSingleProduct() { + this.type === 'order' ? this.onlySelectSingleProductForOrder() : this.onlySelectSingleProductForStock(); popoverController.dismiss({ dismissed: true }); }, - revertProduct() { + revertProductForOrder() { const original = JSON.parse(JSON.stringify(this.purchaseOrders.original)); this.purchaseOrders.parsed[this.poId] = this.purchaseOrders.parsed[this.poId].map(element => { if(element.pseudoId === this.id) { @@ -62,13 +83,31 @@ export default defineComponent({ return element; }); this.store.dispatch('order/updatePurchaseOrders', this.purchaseOrders) + }, + revertProductForStock() { + const original = JSON.parse(JSON.stringify(this.stockItems.original)); + this.stockItems.parsed = this.stockItems.parsed.map(element => { + if(element.pseudoId === this.id) { + const item = original.find(item => { + return item.pseudoId === this.id; + }) + element = item; + } + return element; + }); + this.store.dispatch('stock/updateStockItems', this.stockItems) + }, + revertProduct() { + this.type === 'order' ? this.revertProductForOrder() : this.revertProductForStock(); popoverController.dismiss({ dismissed: true }); }, - revertParentProduct(){ + revertParentProductForOrder(){ const original = JSON.parse(JSON.stringify(this.purchaseOrders.original)); this.purchaseOrders.parsed[this.poId] = this.purchaseOrders.parsed[this.poId].map(element => { if(element.parentProductId === this.id) { const item = original[this.poId].find(item => { + // shopifyProductSKU check prevents reverting all the items of parent product to the first one as all the products have same parent product Id. + // shopifyProductSKU check prevents reverting all the items of parent product to the first one as all the products have same parent product Id. // shopifyProductSKU check prevents reverting all the items of parent product to the first one as all the products have same parent product Id. return item.parentProductId === this.id && item.shopifyProductSKU === element.shopifyProductSKU; }) @@ -77,6 +116,22 @@ export default defineComponent({ return element; }); this.store.dispatch('order/updatePurchaseOrders', this.purchaseOrders) + }, + revertParentProductForStock(){ + const original = JSON.parse(JSON.stringify(this.stockItems.original)); + this.stockItems.parsed = this.stockItems.parsed.map(element => { + if(element.parentProductId === this.id) { + const item = original.find(item => { + return item.parentProductId === this.id && item.shopifyProductSKU === element.shopifyProductSKU; + }) + element = item; + } + return element; + }); + this.store.dispatch('stock/updateStockItems', this.stockItems); + }, + revertParentProduct(){ + this.type === 'order' ? this.revertParentProductForOrder() : this.revertParentProductForStock(); popoverController.dismiss({ dismissed: true }); } }, @@ -88,5 +143,5 @@ export default defineComponent({ store } } -}); +}) \ No newline at end of file diff --git a/src/components/PurchaseOrderDetail.vue b/src/components/PurchaseOrderDetail.vue index f6089d81..f5a0babe 100644 --- a/src/components/PurchaseOrderDetail.vue +++ b/src/components/PurchaseOrderDetail.vue @@ -117,7 +117,7 @@ export default defineComponent({ event: ev, translucent: true, showBackdrop: true, - componentProps: { 'id': id, 'isVirtual': isVirtual, 'item': item, poId } + componentProps: { 'id': id, 'isVirtual': isVirtual, 'item': item, poId, 'type': 'order' } }); return popover.present(); }, diff --git a/src/locales/en.json b/src/locales/en.json index f18f655d..36ee66f3 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -2,7 +2,7 @@ "All": "All", "App": "App", "Apply": "Apply", - "Any edits made to this PO will be lost.": "Any edits made to this PO will be lost.", + "Any edits made on this page will be lost.": "Any edits made on this page made will be lost.", "Are you sure you want to change the time zone to?": "Are you sure you want to change the time zone to?", "Are you sure you want to change the date time format?": "Are you sure you want to change the date time format?", "Are you sure you want to delete this CSV mapping? This action cannot be undone.": "Are you sure you want to delete this CSV mapping? This action cannot be undone.", @@ -12,6 +12,7 @@ "Blank": "Blank", "Buffer days": "Buffer days", "Bulk adjustment": "Bulk adjustment", + "Buffer quantity": "Buffer quantity", "cancel": "cancel", "Cancel": "Cancel", "Catalog": "Catalog", @@ -41,6 +42,7 @@ "Enter product sku to search": "Enter product sku to search", "Facility": "Facility", "Facility ID": "Facility ID", + "Facility Location": "Facility Location", "Failed to save CSV mapping.": "Failed to save CSV mapping.", "Failed to delete CSV mapping.": "Failed to delete CSV mapping.", "Failed to update CSV mapping.": "Failed to update CSV mapping.", @@ -55,7 +57,9 @@ "items": "items", "items selected": "items selected", "Import": "Import", + "Inventory": "Inventory", "Instance Url": "Instance Url", + "Items": "Items", "Invalid input": "Invalid input", "Lead time": "Lead time", "LEAVE": "LEAVE", @@ -67,6 +71,7 @@ "Mapping": "Mapping", "Mapping details": "Mapping details", "Mapping name": "Mapping name", + "Make sure all the data you have entered is correct.": "Make sure all the data you have entered is correct.", "Luxon date time formats can be found": "Luxon date time formats can be found", "Make sure all the data you have entered is correct and only pre-order or backorder items are selected.": "Make sure all the data you have entered is correct and only pre-order or backorder items are selected.", "Map all fields": "Map all fields", @@ -91,16 +96,20 @@ "Password": "Password", "Pending": "Pending", "Please upload a valid purchase order csv to continue": "Please upload a valid purchase order csv to continue", + "Please upload a valid reset inventory csv to continue": "Please upload a valid reset inventory csv to continue", "PO External Order ID": "PO External Order ID", "Preorder": "Preorder", + "Product SKU": "Product SKU", "Product not found": "Product not found", "Purchase order": "Purchase order", "Purchase orders": "Purchase orders", + "Quantity": "Quantity", "Ready to create an app?": "Ready to create an app?", "Review": "Review", "Review PO details": "Review PO details", "Review purchase order": "Review purchase order", "Reset": "Reset", + "Reset inventory": "Reset inventory", "Reset password": "Reset password", "Results": "Results", "Safety stock": "Safety stock", @@ -130,6 +139,7 @@ "Start with Ionic": "Start with Ionic", "STAY": "STAY", "store name": "store name", + "The inventory has been updated successfully": "The inventory has been updated successfully", "The PO has been uploaded successfully": "The PO has been uploaded successfully", "The SKU is successfully changed": "The SKU is successfully changed", "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/mixins/parseFileMixin.ts b/src/mixins/parseFileMixin.ts new file mode 100644 index 00000000..5f73c3df --- /dev/null +++ b/src/mixins/parseFileMixin.ts @@ -0,0 +1,19 @@ +import { translate } from "@/i18n"; +import store from "@/store"; +import { showToast, parseCsv } from "@/utils"; + +export default { + methods: { + async parseCsv(file: any) { + if (file) { + store.dispatch('order/updateFileName', file.name); + const csvData = await parseCsv(file).then( res => { + return res; + }) + showToast(translate("File uploaded successfully")); + return csvData; + } + showToast(translate("Something went wrong, Please try again")); + } + } +} \ No newline at end of file diff --git a/src/router/index.ts b/src/router/index.ts index 5daae5dc..24378afa 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,6 +1,8 @@ import { createRouter, createWebHistory } from '@ionic/vue-router'; import { RouteRecordRaw } from 'vue-router'; import PurchaseOrder from '@/views/PurchaseOrder.vue' +import Inventory from '@/views/Inventory.vue' +import InventoryReview from '@/views/InventoryReview.vue' import PurchaseOrderReview from '@/views/PurchaseOrderReview.vue'; import Login from '@/views/Login.vue' import SavedMappings from '@/views/SavedMappings.vue' @@ -41,6 +43,18 @@ const routes: Array = [ component: PurchaseOrderReview, beforeEnter: authGuard }, + { + path: '/inventory', + name: 'Inventory', + component: Inventory, + beforeEnter: authGuard + }, + { + path: '/inventory-review', + name: 'InventoryDetail', + component: InventoryReview, + beforeEnter: authGuard + }, { path: '/login', name: 'Login', diff --git a/src/services/UploadService.ts b/src/services/UploadService.ts index d18c0681..79ed5f14 100644 --- a/src/services/UploadService.ts +++ b/src/services/UploadService.ts @@ -11,7 +11,7 @@ const uploadJsonFile = async (payload: any): Promise => { const prepareUploadJsonPayload = (request: UploadRequest) => { const blob = new Blob([JSON.stringify(request.uploadData)], { type: 'application/json'}); const formData = new FormData(); - const fileName = (request.fileName ? request.fileName : Date.now() ) +".json"; + const fileName = request.fileName ? request.fileName : Date.now() + ".json" ; formData.append("uploadedFile", blob, fileName); if (request.params) { for (const key in request.params) { diff --git a/src/services/UserService.ts b/src/services/UserService.ts index d81a141b..b5388834 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -43,7 +43,7 @@ const setUserTimeZone = async (payload: any): Promise => { data: payload }); } - + const createFieldMapping = async (payload: any): Promise => { return api({ url: "/service/createDataManagerMapping", diff --git a/src/services/UtilService.ts b/src/services/UtilService.ts index 42539512..138feda3 100644 --- a/src/services/UtilService.ts +++ b/src/services/UtilService.ts @@ -1,12 +1,23 @@ import { api } from '@/adapter' const getFacilities= async (payload: any): Promise => { - return api({ - url: "/performFind", - method: "POST", - data: payload - }); - } + return api({ + url: "/performFind", + method: "GET", + params: payload, + cache: true + }); +} + +const getFacilityLocations = async (payload: any): Promise => { + return api({ + url: "/performFind", + method: "POST", + data: payload + }) +} + export const UtilService = { - getFacilities - } \ No newline at end of file + getFacilities, + getFacilityLocations +} \ No newline at end of file diff --git a/src/store/index.ts b/src/store/index.ts index cdf3d4fa..0f60a0a0 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -7,6 +7,7 @@ import createPersistedState from "vuex-persistedstate"; import userModule from './modules/user'; import productModule from "./modules/product"; import orderModule from "./modules/order"; +import stockModule from "./modules/stock"; import utilModule from "./modules/util" @@ -35,6 +36,7 @@ const store = createStore({ 'user': userModule, 'product': productModule, 'order': orderModule, + 'stock': stockModule, 'util': utilModule }, }) diff --git a/src/store/modules/order/actions.ts b/src/store/modules/order/actions.ts index 81c0f4ac..58fc99b6 100644 --- a/src/store/modules/order/actions.ts +++ b/src/store/modules/order/actions.ts @@ -23,7 +23,7 @@ const actions: ActionTree = { items = items.filter((item: any) => item.shopifyProductSKU).map((item: any) => { const product = rootGetters['product/getProduct'](item.shopifyProductSKU) - if(Object.keys(product).length > 0){ + if (Object.keys(product).length > 0) { item.parentProductId = product?.parent?.id; item.pseudoId = product.pseudoId; item.parentProductName = product?.parent?.productName; @@ -35,7 +35,6 @@ const actions: ActionTree = { unidentifiedItems.push(item); return ; }).filter((item: any) => item); - const parsed = items.reduce((itemsByPoId: any, item: any) => { itemsByPoId[item.orderId] ? itemsByPoId[item.orderId].push(item) : itemsByPoId[item.orderId] = [item] @@ -65,6 +64,17 @@ const actions: ActionTree = { const original = JSON.parse(JSON.stringify(state.purchaseOrders.parsed)); commit(types.ORDER_PURCHASEORDERS_UPDATED, { parsed, original, unidentifiedItems}); + }, + updateMissingFacilities({state, dispatch}, facilityMapping){ + Object.keys(facilityMapping).map((facilityId: any) => { + Object.values(state.purchaseOrders.parsed).flat().map((item: any) => { + if(item.externalFacilityId === facilityId){ + item.externalFacilityId = ""; + item.facilityId = facilityMapping[facilityId]; + } + }) + }) + this.dispatch('order/updatePurchaseOrders', state.purchaseOrders); } } export default actions; diff --git a/src/store/modules/product/actions.ts b/src/store/modules/product/actions.ts index cbec473b..c9aafd09 100644 --- a/src/store/modules/product/actions.ts +++ b/src/store/modules/product/actions.ts @@ -9,6 +9,9 @@ import logger from "@/logger"; const actions: ActionTree = { async fetchProducts ( { commit, state }, { productIds }) { + + // TODO Add try-catch block + const cachedProductIds = Object.keys(state.cached); const productIdFilter= productIds.reduce((filter: Array, productId: any) => { // If product does not exist in cached products then add the id @@ -19,7 +22,7 @@ const actions: ActionTree = { }, []); // If there are no product ids to search skip the API call - if (productIdFilter.length == 0) return productIds.map((productId: any) => state.cached[productId]).filter((product: any) => product); + if (productIdFilter.length == 0) return state.cached; const resp = await fetchProducts({ filters: { 'internalName': { 'value': productIdFilter }}, @@ -31,12 +34,11 @@ const actions: ActionTree = { const products = resp.products; // Handled empty response in case of failed query if (resp.total) commit(types.PRODUCT_ADD_TO_CACHED_MULTIPLE, { products }); - return resp.products; } else { logger.error(resp.serverResponse) } // TODO Handle specific error - return []; + return state.cached; }, } diff --git a/src/store/modules/stock/StockState.ts b/src/store/modules/stock/StockState.ts new file mode 100644 index 00000000..c5924eef --- /dev/null +++ b/src/store/modules/stock/StockState.ts @@ -0,0 +1,8 @@ +export default interface StockState { + items: { + parsed: any, + original: any, + unidentifiedItems: any, + }, + fileName: string +} diff --git a/src/store/modules/stock/actions.ts b/src/store/modules/stock/actions.ts new file mode 100644 index 00000000..45151017 --- /dev/null +++ b/src/store/modules/stock/actions.ts @@ -0,0 +1,88 @@ +import { ActionTree } from 'vuex' +import store from '@/store' +import RootState from '@/store/RootState' +import StockState from './StockState' +import * as types from './mutation-types' + +const actions: ActionTree = { + async processUpdateStockItems ({ commit }, items) { + const productIds = items.map((item: any) => item.shopifyProductSKU) + + // We are getting external facilityId from CSV, extract facilityId and pass for getting locations + const externalFacilityIds = [...new Set(items.map((item: any) => item.externalFacilityId))] + const facilities = await store.dispatch('util/fetchFacilities'); + const facilityMapping = facilities.reduce((facilityMapping: any, facility: any) => { + if (facility.externalId) facilityMapping[facility.externalId] = facility.facilityId; + return facilityMapping; + }, {}) + const facilityIds = externalFacilityIds.map((externalFacilityId: any) => { + return facilityMapping[externalFacilityId]; + }).filter((facilityId: any) => facilityId) + store.dispatch('util/fetchFacilityLocations', facilityIds); + + const viewSize = productIds.length; + const viewIndex = 0; + const payload = { + viewSize, + viewIndex, + productIds + } + const cachedProducts = await store.dispatch("product/fetchProducts", payload); + const unidentifiedItems = [] as any; + const parsed = items.map((item: any) => { + const product = cachedProducts[item.shopifyProductSKU]; + + if(product){ + item.parentProductId = product?.parent?.id; + item.pseudoId = product.pseudoId; + item.parentProductName = product?.parent?.productName; + item.imageUrl = product.images?.mainImageUrl; + item.isSelected = true; + return item; + } + unidentifiedItems.push(item); + return; + }).filter((item: any) => item); + + const original = JSON.parse(JSON.stringify(parsed)); + + commit(types.STOCK_ITEMS_UPDATED, { parsed, original, unidentifiedItems }); + }, + updateStockItems({ commit }, stockItems){ + commit(types.STOCK_ITEMS_UPDATED, stockItems); + }, + clearStockItems({ commit }){ + commit(types.STOCK_ITEMS_UPDATED, { parsed: [], original: [], unidentifiedItems: []}); + }, + updateUnidentifiedItem({ commit, state }, payload: any) { + const parsed = state.items.parsed as any; + const unidentifiedItems = payload.unidentifiedItems.map((item: any) => { + if(item.updatedSku) { + item.shopifyProductSKU = item.updatedSku; + parsed.push(item); + state.items.original.push(item); + } else { + return item; + } + }).filter((item: any) => item); + + const original = JSON.parse(JSON.stringify(state.items.original)); + + commit(types.STOCK_ITEMS_UPDATED, { parsed, original, unidentifiedItems}); + }, + async updateMissingFacilities({ state }, facilityMapping){ + const facilityLocations = await this.dispatch('util/fetchFacilityLocations', Object.values(facilityMapping)); + Object.keys(facilityMapping).map((facilityId: any) => { + const locationSeqId = facilityLocations[facilityMapping[facilityId]].length ? facilityLocations[facilityMapping[facilityId]][0].locationSeqId : ''; + state.items.parsed.map((item: any) => { + if(item.externalFacilityId === facilityId){ + item.externalFacilityId = ""; + item.facilityId = facilityMapping[facilityId]; + item.locationSeqId = locationSeqId; + } + }) + }) + this.dispatch('stock/updateStockItems', state.items); + } +} +export default actions; diff --git a/src/store/modules/stock/getters.ts b/src/store/modules/stock/getters.ts new file mode 100644 index 00000000..81c0c5b7 --- /dev/null +++ b/src/store/modules/stock/getters.ts @@ -0,0 +1,10 @@ +import { GetterTree } from "vuex"; +import OrderState from "./StockState"; +import RootState from "../../RootState"; + +const getters: GetterTree = { + getStockItems(state) { + return JSON.parse(JSON.stringify(state.items)); + }, +}; +export default getters; \ No newline at end of file diff --git a/src/store/modules/stock/index.ts b/src/store/modules/stock/index.ts new file mode 100644 index 00000000..41751b03 --- /dev/null +++ b/src/store/modules/stock/index.ts @@ -0,0 +1,23 @@ +import getters from './getters' +import { Module } from 'vuex' +import actions from './actions' +import mutations from './mutations' +import StockState from './StockState' +import RootState from '../../RootState' + +const orderModule: Module = { + namespaced: true, + state: { + items: { + parsed: [], + original: [], + unidentifiedItems: [], + }, + fileName: "" + }, + actions, + getters, + mutations +} + +export default orderModule; \ No newline at end of file diff --git a/src/store/modules/stock/mutation-types.ts b/src/store/modules/stock/mutation-types.ts new file mode 100644 index 00000000..91e0c1a6 --- /dev/null +++ b/src/store/modules/stock/mutation-types.ts @@ -0,0 +1,2 @@ +export const SN_STOCK = 'stock' +export const STOCK_ITEMS_UPDATED = SN_STOCK + '/ITEMS_UPDATED' \ No newline at end of file diff --git a/src/store/modules/stock/mutations.ts b/src/store/modules/stock/mutations.ts new file mode 100644 index 00000000..637ed8f3 --- /dev/null +++ b/src/store/modules/stock/mutations.ts @@ -0,0 +1,12 @@ +import { MutationTree } from 'vuex' +import StockState from './StockState' +import * as types from './mutation-types' + +const mutations: MutationTree = { + [types.STOCK_ITEMS_UPDATED] (state, payload) { + state.items.parsed = payload.parsed; + state.items.original = payload.original; + state.items.unidentifiedItems = payload.unidentifiedItems; + } +} +export default mutations; \ No newline at end of file diff --git a/src/store/modules/user/actions.ts b/src/store/modules/user/actions.ts index 175c7fca..ae4dda37 100644 --- a/src/store/modules/user/actions.ts +++ b/src/store/modules/user/actions.ts @@ -126,8 +126,8 @@ const actions: ActionTree = { setUserInstanceUrl ({ commit }, payload){ commit(types.USER_INSTANCE_URL_UPDATED, payload) updateInstanceUrl(payload) - }, - + }, + updatePwaState({commit}, payload) { commit(types.USER_PWA_STATE_UPDATED, payload); }, diff --git a/src/store/modules/user/getters.ts b/src/store/modules/user/getters.ts index 5733eb8a..350ee617 100644 --- a/src/store/modules/user/getters.ts +++ b/src/store/modules/user/getters.ts @@ -30,7 +30,7 @@ const getters: GetterTree = { }, getPreferredDateTimeFormat (state) { return state.preferredDateTimeFormat; - }, + }, getCurrentMapping(state) { return JSON.parse(JSON.stringify(state.currentMapping)) } diff --git a/src/store/modules/user/index.ts b/src/store/modules/user/index.ts index 71531358..3ee72d04 100644 --- a/src/store/modules/user/index.ts +++ b/src/store/modules/user/index.ts @@ -12,8 +12,8 @@ const userModule: Module = { current: null, currentFacility: {}, instanceUrl: '', - fieldMappings: {}, preferredDateTimeFormat: '', + fieldMappings: {}, pwaState: { updateExists: false, registration: null, diff --git a/src/store/modules/user/mutations.ts b/src/store/modules/user/mutations.ts index 253d92a4..99ef46ac 100644 --- a/src/store/modules/user/mutations.ts +++ b/src/store/modules/user/mutations.ts @@ -29,7 +29,7 @@ const mutations: MutationTree = { }, [types.USER_DATETIME_FORMAT_UPDATED] (state, payload) { state.preferredDateTimeFormat = payload; - }, + }, [types.USER_CURRENT_FIELD_MAPPING_UPDATED] (state, payload) { state.currentMapping = payload }, diff --git a/src/store/modules/util/UtilState.ts b/src/store/modules/util/UtilState.ts index 08c46d25..ea7f9347 100644 --- a/src/store/modules/util/UtilState.ts +++ b/src/store/modules/util/UtilState.ts @@ -1,3 +1,4 @@ export default interface UserState { - facilities: [] + facilities: [], + facilityLocationsByFacilityId: any; } \ No newline at end of file diff --git a/src/store/modules/util/actions.ts b/src/store/modules/util/actions.ts index 639042d0..c0ef4f29 100644 --- a/src/store/modules/util/actions.ts +++ b/src/store/modules/util/actions.ts @@ -9,7 +9,7 @@ import logger from '@/logger' const actions: ActionTree = { async fetchFacilities({ state, commit}){ - if(state.facilities.length) return; + if(state.facilities.length) return state.facilities; const payload = { "inputFields": { "parentTypeId": "VIRTUAL_FACILITY", @@ -17,25 +17,70 @@ const actions: ActionTree = { "facilityTypeId": "VIRTUAL_FACILITY", "facilityTypeId_op": "notEqual", }, - "fieldList": ["facilityId", "facilityName", "parentTypeId"], + "fieldList": ["facilityId", "facilityName", "parentTypeId", "externalId"], "viewSize": 50, "entityName": "FacilityAndType", "noConditionFind": "Y" } try { const resp = await UtilService.getFacilities(payload); - if(resp.status === 200 && resp.data.docs && !hasError(resp)){ + if(resp.status === 200 && !hasError(resp)){ commit(types.UTIL_FACILITIES_UPDATED, resp.data.docs); } else { logger.error(resp) + commit(types.UTIL_FACILITIES_UPDATED, []); } } catch(err) { logger.error(err) + commit(types.UTIL_FACILITIES_UPDATED, []); } + return state.facilities; }, clearFacilities({commit}){ commit(types.UTIL_FACILITIES_UPDATED, []); - } + }, + async fetchFacilityLocations({ commit, state }, facilityIds){ + const unavailablefacilityIds = facilityIds.filter((facilityId: any) => !state.facilityLocationsByFacilityId[facilityId]) + + // We already have required facility locations in cache + if(!unavailablefacilityIds.length) return state.facilityLocationsByFacilityId; + + let resp; + const params = { + "inputFields": { + facilityId: unavailablefacilityIds, + "facilityId_op": 'in' + }, + // Assuming we will not have more than 15 facility locations. + "viewSize": unavailablefacilityIds.length * 15, + "fieldList": ["locationSeqId", "areaId", "aisleId", "sectionId", "levelId", "positionId", "facilityId"], + "entityName": "FacilityLocation", + "distinct": "Y", + "noConditionFind": "Y" + } + try { + resp = await UtilService.getFacilityLocations(params); + if(resp.status === 200 && !hasError(resp)) { + const facilityLocations = resp.data.docs + const facilityLocationsByFacilityId = facilityLocations.reduce((locations: any, location: any) => { + const locationPath = [location.areaId, location.aisleId, location.sectionId, location.levelId, location.positionId].filter((value: any) => value).join(""); + const facilityLocation = { + locationSeqId: location.locationSeqId, + locationPath: locationPath + } + locations[location.facilityId] ? locations[location.facilityId].push(facilityLocation) : locations[location.facilityId] = [facilityLocation]; + return locations; + }, {}); + commit(types.UTIL_FACILITY_LOCATIONS_BY_FACILITY_ID, facilityLocationsByFacilityId); + } else { + logger.error(resp.data) + } + } catch(err) { + logger.error(err); + } + return state.facilityLocationsByFacilityId; + }, } + export default actions; \ No newline at end of file diff --git a/src/store/modules/util/getters.ts b/src/store/modules/util/getters.ts index 3c0f4ba2..1e3d3afa 100644 --- a/src/store/modules/util/getters.ts +++ b/src/store/modules/util/getters.ts @@ -5,6 +5,9 @@ import RootState from '@/store/RootState' const getters: GetterTree = { getFacilities (state) { return state.facilities; - } + }, + getFacilityLocationsByFacilityId: (state) => (facilityId: string) => { + return state.facilityLocationsByFacilityId[facilityId] + }, } export default getters; \ No newline at end of file diff --git a/src/store/modules/util/index.ts b/src/store/modules/util/index.ts index c2c081a2..1e30065f 100644 --- a/src/store/modules/util/index.ts +++ b/src/store/modules/util/index.ts @@ -8,7 +8,8 @@ import RootState from '@/store/RootState' const userModule: Module = { namespaced: true, state: { - facilities: [] + facilities: [], + facilityLocationsByFacilityId: {}, }, getters, actions, diff --git a/src/store/modules/util/mutation-types.ts b/src/store/modules/util/mutation-types.ts index efe014b5..c828d11f 100644 --- a/src/store/modules/util/mutation-types.ts +++ b/src/store/modules/util/mutation-types.ts @@ -1,2 +1,3 @@ export const SN_UTIL = 'util' export const UTIL_FACILITIES_UPDATED = SN_UTIL + '/FACILITIES_UPDATED' +export const UTIL_FACILITY_LOCATIONS_BY_FACILITY_ID = SN_UTIL + '/FACILITY_LOCATIONS_BY_FACILITY_ID' diff --git a/src/store/modules/util/mutations.ts b/src/store/modules/util/mutations.ts index f3b3a02d..ffb69d95 100644 --- a/src/store/modules/util/mutations.ts +++ b/src/store/modules/util/mutations.ts @@ -6,5 +6,10 @@ const mutations: MutationTree = { [types.UTIL_FACILITIES_UPDATED] (state, payload) { state.facilities = payload }, + [types.UTIL_FACILITY_LOCATIONS_BY_FACILITY_ID] (state, facilityLocations) { + Object.keys(facilityLocations).map((facilityId: any) => { + state.facilityLocationsByFacilityId[facilityId] = facilityLocations[facilityId]; + }) + }, } export default mutations; \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts index 00dc135d..5a4296ea 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,7 +7,7 @@ import { DateTime } from "luxon"; // TODO Remove it when HC APIs are fully REST compliant const hasError = (response: any) => { - return !!response.data._ERROR_MESSAGE_ || !!response.data._ERROR_MESSAGE_LIST_; + return typeof response.data != "object" || !!response.data._ERROR_MESSAGE_ || !!response.data._ERROR_MESSAGE_LIST_ || !!response.data.error; } const showToast = async (message: string, configButtons?: any) => { diff --git a/src/views/Inventory.vue b/src/views/Inventory.vue new file mode 100644 index 00000000..ab222106 --- /dev/null +++ b/src/views/Inventory.vue @@ -0,0 +1,176 @@ + + + + \ No newline at end of file diff --git a/src/views/InventoryReview.vue b/src/views/InventoryReview.vue new file mode 100644 index 00000000..b3e94cbf --- /dev/null +++ b/src/views/InventoryReview.vue @@ -0,0 +1,477 @@ + + + + \ No newline at end of file diff --git a/src/views/PurchaseOrder.vue b/src/views/PurchaseOrder.vue index 67ee00af..8fdcd9cb 100644 --- a/src/views/PurchaseOrder.vue +++ b/src/views/PurchaseOrder.vue @@ -12,8 +12,8 @@ {{ $t("Purchase order") }} {{ file.name }} - - + + @@ -81,8 +81,9 @@ import { IonChip, IonPage, IonHeader, IonToolbar, IonTitle, IonContent, IonItem, IonLabel, IonList, IonListHeader, IonMenuButton, IonButton, IonSelect, IonSelectOption, IonIcon, modalController } from "@ionic/vue"; import { defineComponent } from "vue"; import { useRouter } from 'vue-router'; -import { showToast, parseCsv } from '@/utils'; +import { showToast } from '@/utils'; import { translate } from "@/i18n"; +import parseFileMixin from '@/mixins/parseFileMixin'; import { mapGetters, useStore } from "vuex"; import { addOutline, arrowForwardOutline } from 'ionicons/icons'; import CreateMappingModal from "@/components/CreateMappingModal.vue"; @@ -112,6 +113,7 @@ export default defineComponent({ fieldMappings: 'user/getFieldMappings' }) }, + mixins:[ parseFileMixin ], data() { return { file: {}, @@ -122,49 +124,62 @@ export default defineComponent({ orderDate: "", quantity: "", facility: "", - }, - PurchaseOrderItems: [], + } } }, + ionViewDidLeave() { + this.file = {} + this.content = [] + this.fieldMapping = { + orderId: "", + productSku: "", + orderDate: "", + quantity: "", + facility: "", + } + this.$refs.file.value = null; + }, methods: { - getFile(event) { + //Todo: Generating unique identifiers as we are currently storing in local storage. Need to remove it as we will be storing data on server. + generateUniqueMappingPrefId() { + const id = Math.floor(Math.random() * 1000); + return !this.fieldMappings[id] ? id : this.generateUniqueMappingPrefId(); + }, + async parse(event) { const file = event.target.files[0]; if(file){ this.file = file; - this.parseFile(); - this.store.dispatch('order/updateFileName', this.file.name); - Object.keys(this.fieldMapping).map(key => { this.fieldMapping[key] = '' }) + this.content = await this.parseCsv(this.file); showToast(translate("File uploaded successfully")); } else { showToast(translate("No new file upload. Please try again")); } }, - async parseFile(){ - await parseCsv(this.file).then(res => { - this.content = res; - }) - }, review() { if (this.content.length <= 0) { showToast(translate("Please upload a valid purchase order csv to continue")); - } else if (this.areAllFieldsSelected()) { - this.PurchaseOrderItems = this.content.map(item => { - return { - orderId: item[this.fieldMapping.orderId], - shopifyProductSKU: item[this.fieldMapping.productSku], - arrivalDate: item[this.fieldMapping.orderDate], - quantityOrdered: item[this.fieldMapping.quantity], - facilityId: '', - externalFacilityId: item[this.fieldMapping.facility] - } - }) - this.store.dispatch('order/fetchOrderDetails', this.PurchaseOrderItems); - this.router.push({ - name:'PurchaseOrderReview' - }) - } else { - showToast(translate("Select all the fields to continue")); + return; } + + if (!this.areAllFieldsSelected()) { + showToast(translate("Select all the fields to continue")); + return; + } + + const purchaseOrderItems = this.content.map(item => { + return { + orderId: item[this.fieldMapping.orderId], + shopifyProductSKU: item[this.fieldMapping.productSku], + arrivalDate: item[this.fieldMapping.orderDate], + quantityOrdered: item[this.fieldMapping.quantity], + facilityId: '', + externalFacilityId: item[this.fieldMapping.facility] + } + }) + this.store.dispatch('order/fetchOrderDetails', purchaseOrderItems); + this.router.push({ + name:'PurchaseOrderReview' + }) }, mapFields(mapping) { const fieldMapping = JSON.parse(JSON.stringify(mapping)); diff --git a/src/views/PurchaseOrderReview.vue b/src/views/PurchaseOrderReview.vue index 99fba765..b8a0ff6b 100644 --- a/src/views/PurchaseOrderReview.vue +++ b/src/views/PurchaseOrderReview.vue @@ -88,7 +88,7 @@ import { ellipsisVerticalOutline, businessOutline, shirtOutline, sendOutline, ch import PurchaseOrderDetail from '@/components/PurchaseOrderDetail.vue' import DateTimeParseErrorModal from '@/components/DateTimeParseErrorModal.vue'; import BulkAdjustmentModal from '@/components/BulkAdjustmentModal.vue'; -import MissingFacilityModal from '@/components/MissingFacilitiesModal.vue'; +import MissingFacilitiesModal from '@/components/MissingFacilitiesModal.vue'; import MissingSkuModal from "@/components/MissingSkuModal.vue" import { UploadService } from "@/services/UploadService"; import { showToast } from '@/utils'; @@ -145,7 +145,7 @@ export default defineComponent({ let canLeave = false; const alert = await alertController.create({ header: this.$t("Leave page"), - message: this.$t("Any edits made to this PO will be lost."), + message: this.$t("Any edits made on this page will be lost."), buttons: [ { text: this.$t("STAY"), @@ -177,8 +177,15 @@ export default defineComponent({ return Object.values(this.purchaseOrders.parsed).flat().filter((item: any) => !DateTime.fromFormat(item.arrivalDate, this.dateTimeFormat).isValid).length; }, getItemsWithMissingFacility() { - const facilityIds = this.facilities.map((facility: any) => facility.facilityId) - return Object.values(this.purchaseOrders.parsed).flat().filter((item: any) => !facilityIds.includes(item.externalFacilityId) && item.externalFacilityId !== ""); + const externalFacilityIds = this.facilities.reduce((externalFacilityIds: any, facility: any) => { + if (facility.externalId) externalFacilityIds.push(facility.externalId); + return externalFacilityIds; + }, []) + + // if facilityId is set, this is facility set from the facility list + // if externalFacilityId doesn't exist, case for missing facility + // if externalFacilityId exist and not found in facility list, case for missing facility + return Object.values(this.purchaseOrders.parsed).flat().filter((item: any) => !item.facilityId && (!item.externalFacilityId || (item.externalFacilityId && !externalFacilityIds.includes(item.externalFacilityId)))); }, isDateInvalid(){ // Checked if any of the date format is different than the selected format. @@ -187,7 +194,7 @@ export default defineComponent({ async openMissingSkuModal() { const missingSkuModal = await modalController.create({ component: MissingSkuModal, - componentProps: { 'unidentifiedItems': this.purchaseOrders.unidentifiedItems } + componentProps: { 'unidentifiedItems': this.purchaseOrders.unidentifiedItems, type: 'order' } }); return missingSkuModal.present(); }, @@ -269,8 +276,8 @@ export default defineComponent({ async openMissingFacilitiesModal() { const itemsWithMissingFacility = this.getItemsWithMissingFacility(); const missingFacilitiesModal = await modalController.create({ - component: MissingFacilityModal, - componentProps: { itemsWithMissingFacility, facilities: this.facilities } + component: MissingFacilitiesModal, + componentProps: { itemsWithMissingFacility, type: 'order' } }); return missingFacilitiesModal.present(); },