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 @@
+
+
+
+
+
+
+
+
+ {{ $t("Bulk adjustment") }}
+
+
+
+
+
+ {{ $t("Buffer quantity") }}
+
+
+
+
+ {{ $t("Facility") }}
+
+ {{ facility.facilityName }}
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+ {{ $t("Inventory") }}
+
+
+
+
+
+
+ {{ $t("Inventory") }}
+ {{ file.name }}
+
+
+
+
+
+ {{ $t("Select the column index for the following information in the uploaded CSV.") }}
+
+
+ {{ $t("Product SKU") }}
+
+ {{ prop }}
+
+
+
+
+ {{ $t("Quantity") }}
+
+ {{ prop }}
+
+
+
+
+ {{ $t("Facility ID") }}
+
+ {{ prop }}
+
+
+
+
+ {{ $t("Facility Location") }}
+
+ {{ prop }}
+
+
+
+
+
+ {{ $t("Review") }}
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ searchedProduct.pseudoId }}
+
+
+
+
+ {{ searchedProduct.quantity }} {{ $t("Items") }}
+
+
+
+ {{ searchedProduct.externalFacilityId ? searchedProduct.externalFacilityId : searchedProduct.facilityId }}
+
+
+
+
+
+ {{ facilityLocation.locationSeqId }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.pseudoId }}
+
+
+
+
+ {{ item.quantity }} {{ $t("Items") }}
+
+
+
+ {{ getFacilityName(item.facilityId, item.externalFacilityId) }}
+
+
+
+
+ {{ facilityLocation.locationPath }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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();
},