Skip to content

Commit

Permalink
Improved: inventory upload to support product identifiers. Also adjus…
Browse files Browse the repository at this point in the history
…ted UI to support large inventory import csv files.(#253)
  • Loading branch information
ravilodhi committed Jan 5, 2024
1 parent d5d0dfb commit 4824f0a
Show file tree
Hide file tree
Showing 23 changed files with 210 additions and 206 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ VUE_APP_PERMISSION_ID=
VUE_APP_ALIAS={}
VUE_APP_MAPPING_TYPES={"PO": "PO_MAPPING_PREF","RSTINV": "INV_MAPPING_PREF"}
VUE_APP_MAPPING_PO={"orderId": { "label": "Order ID", "required": true }, "productSku": { "label": "Shopify product SKU", "required": true },"orderDate": { "label": "Arrival date", "required": true }, "quantity": { "label": "Ordered quantity", "required": true }, "facility": { "label": "Facility ID", "required": true }}
VUE_APP_MAPPING_RSTINV={"productSku": { "label": "Product SKU", "required": true }, "quantity": { "label": "Quantity", "required": true }, "facility": { "label": "Facility ID", "required": true }, "locationSeqId": { "label": "Facility Location", "required": true }}
VUE_APP_MAPPING_RSTINV={"productIdentification": { "label": "Product Identification", "required": true }, "quantity": { "label": "Quantity", "required": true }, "facility": { "label": "Facility ID", "required": true }}
VUE_APP_DEFAULT_LOG_LEVEL="error"
VUE_APP_LOGIN_URL="http://launchpad.hotwax.io/login"
32 changes: 11 additions & 21 deletions src/components/BulkInventoryAdjustmentModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,6 @@
<ion-label>{{ $t("Buffer quantity") }}</ion-label>
<ion-input v-model="quantityBuffer" type="number" min="0" :placeholder = "$t('Quantity')" />
</ion-item>

<ion-item>
<ion-label>{{ $t("Facility") }}</ion-label>
<ion-select interface="popover" v-model="facilityId">
<ion-select-option v-for="facility in facilities" :key="facility.facilityId" :value="facility.facilityId">{{ facility.facilityName }}</ion-select-option>
</ion-select>
</ion-item>
</ion-list>

<ion-fab vertical="bottom" horizontal="end" slot="fixed">
Expand All @@ -45,8 +38,6 @@ import {
IonInput,
IonLabel,
IonList,
IonSelect,
IonSelectOption,
IonTitle,
IonToolbar,
modalController,
Expand All @@ -71,8 +62,6 @@ export default defineComponent({
IonInput,
IonLabel,
IonList,
IonSelect,
IonSelectOption,
IonTitle,
IonToolbar
},
Expand All @@ -85,24 +74,25 @@ export default defineComponent({
computed: {
...mapGetters({
stockItems: 'stock/getStockItems',
facilities: 'util/getFacilities',
})
},
methods: {
closeModal() {
modalController.dismiss({ dismissed: true });
},
async save() {
const facilityLocations = await this.store.dispatch('util/fetchFacilityLocations', [this.facilityId]);
const locationSeqId = facilityLocations[this.facilityId] && facilityLocations[this.facilityId][0] ? facilityLocations[this.facilityId][0].locationSeqId : '';
this.stockItems.parsed.map((item: any) => {
if (item.isSelected) {
item.quantity -= this.quantityBuffer;
if(this.facilityId) {
item.facilityId = this.facilityId;
item.externalFacilityId = "";
item.locationSeqId = locationSeqId;
}
item.quantity -= this.quantityBuffer;
if(this.facilityId) {
item.facilityId = this.facilityId;
item.externalFacilityId = "";
}
})
this.stockItems.initial.map((item: any) => {
item.quantity -= this.quantityBuffer;
if (this.facilityId) {
item.facilityId = this.facilityId;
item.externalFacilityId = "";
}
})
await this.store.dispatch('stock/updateStockItems', this.stockItems)
Expand Down
25 changes: 19 additions & 6 deletions src/components/CreateMappingModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,20 @@
<div>
<ion-list>
<ion-item :key="field" v-for="(fieldValues, field) in getFields()">
<ion-label>{{ $t(fieldValues.label) }}</ion-label>
<ion-select interface="popover" :placeholder = "$t('Select')" v-model="fieldMapping[field]">
<ion-select-option :key="index" v-for="(prop, index) in fileColumns">{{ prop }}</ion-select-option>
</ion-select>
<template v-if="field === 'productIdentification'">
<ion-select interface="popover" :placeholder = "$t('Select')" slot="start" v-model="identificationTypeId">
<ion-select-option :key="goodIdentificationType.goodIdentificationTypeId" v-for="goodIdentificationType in goodIdentificationTypes">{{ goodIdentificationType.description }}</ion-select-option>
</ion-select>
<ion-select interface="popover" v-if="content.length" :placeholder = "$t('Select')" slot="end" v-model="fieldMapping['productIdentification']">
<ion-select-option :key="index" v-for="(prop, index) in fileColumns">{{ prop }}</ion-select-option>
</ion-select>
</template>
<template v-else>
<ion-label>{{ $t(fieldValues.label) }}</ion-label>
<ion-select interface="popover" :placeholder = "$t('Select')" v-model="fieldMapping[field]">
<ion-select-option :key="index" v-for="(prop, index) in fileColumns">{{ prop }}</ion-select-option>
</ion-select>
</template>
</ion-item>
</ion-list>
</div>
Expand Down Expand Up @@ -82,17 +92,20 @@ export default defineComponent({
return {
mappingName: "",
fieldMapping: {} as any,
fileColumns: [] as any
fileColumns: [] as any,
identificationTypeId: 'SHOPIFY_PROD_SKU'
}
},
props: ["content", "seletedFieldMapping", "mappingType"],
mounted() {
this.fieldMapping = { ...this.seletedFieldMapping }
this.fileColumns = Object.keys(this.content[0]);
this.store.dispatch('util/fetchGoodIdentificationTypes');
},
computed: {
...mapGetters({
fieldMappings: 'user/getFieldMappings'
fieldMappings: 'user/getFieldMappings',
goodIdentificationTypes: 'util/getGoodIdentificationTypes'
})
},
methods: {
Expand Down
6 changes: 5 additions & 1 deletion src/components/MissingFacilitiesModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import { useStore } from "@/store";
import { mapGetters } from "vuex";
import { showToast } from "@/utils";
import { translate } from "@/i18n";
import emitter from "@/event-bus";
export default defineComponent({
name: "MissingFacilitiesModal",
components: {
Expand Down Expand Up @@ -95,7 +97,9 @@ export default defineComponent({
if(this.type === 'order'){
this.store.dispatch('order/updateMissingFacilities', this.facilityMapping)
} else {
this.store.dispatch('stock/updateMissingFacilities', this.facilityMapping)
emitter.emit('presentLoader');
await this.store.dispatch('stock/updateMissingFacilities', this.facilityMapping)
emitter.emit('dismissLoader');
}
this.closeModal();
showToast(translate("Changes have been successfully applied"));
Expand Down
3 changes: 3 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"Missing facilities": "Missing facilities",
"Missing products": "Missing products",
"Missing SKUs": "Missing SKUs",
"more items": "{remainingItemCount} more items",
"New mapping": "New mapping",
"New version available, please update the app.": "New version available, please update the app.",
"There are no saved CSV mappings to show. Create a new mapping from a file upload screen": "There are no saved CSV mappings to show. Create a new mapping from a file upload screen",
Expand All @@ -100,6 +101,7 @@
"Parsed output": "Parsed output",
"Password": "Password",
"Pending": "Pending",
"Please ensure that the uploaded file contains accurate product information. If a product does not exist, the corresponding records will not be processed.": "Please ensure that the uploaded file contains accurate product information. If a product does not exist, the corresponding records will not be processed.",
"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",
Expand All @@ -126,6 +128,7 @@
"Search": "Search",
"Search products": "Search products",
"Search time zones": "Search time zones",
"Seems like uploaded file has missing products, checked with initial records.": "Seems like uploaded file has missing products, checked with initial {initialCount} records.",
"Select": "Select",
"Select mapping": "Select mapping",
"Select SKU": "Select SKU",
Expand Down
9 changes: 9 additions & 0 deletions src/services/UtilService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,16 @@ const getFacilityLocations = async (payload: any): Promise<any> => {
})
}

const fetchGoodIdentificationTypes = async (payload: any): Promise<any> => {
return api({
url: "/performFind",
method: "POST",
data: payload
})
}

export const UtilService = {
fetchGoodIdentificationTypes,
getFacilities,
getFacilityLocations
}
11 changes: 6 additions & 5 deletions src/store/modules/order/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,22 @@ import * as types from './mutation-types'

const actions: ActionTree<OrderState, RootState> = {
async fetchOrderDetails ({commit, rootGetters}, items) {
const productIds = items.filter((item: any) => item.shopifyProductSKU).map((item: any) => {
return item.shopifyProductSKU
const productIds = items.filter((item: any) => item.identification).map((item: any) => {
return item.identification
})
const viewSize = productIds.length;
const viewIndex = 0;
const payload = {
viewSize,
viewIndex,
productIds
productIds,
identificationTypeId: items[0]?.identificationTypeId
}
await store.dispatch("product/fetchProducts", payload);
const unidentifiedItems = [] as any;

items = items.filter((item: any) => item.shopifyProductSKU).map((item: any) => {
const product = rootGetters['product/getProduct'](item.shopifyProductSKU)
items = items.filter((item: any) => item.identification).map((item: any) => {
const product = rootGetters['product/getProduct'](item.identification)

if (Object.keys(product).length > 0) {
item.parentProductId = product?.parent?.id;
Expand Down
7 changes: 4 additions & 3 deletions src/store/modules/product/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import logger from "@/logger";

const actions: ActionTree<ProductState, RootState> = {

async fetchProducts ( { commit, state }, { productIds }) {
async fetchProducts ( { commit, state }, { productIds, identificationTypeId }) {

// TODO Add try-catch block

Expand All @@ -23,9 +23,10 @@ const actions: ActionTree<ProductState, RootState> = {

// If there are no product ids to search skip the API call
if (productIdFilter.length == 0) return state.cached;


const modifiedProductIdFilters = productIdFilter.map((productId: string) => identificationTypeId + '/' + productId);
const resp = await fetchProducts({
filters: { 'internalName': { 'value': productIdFilter }},
filters: { 'goodIdentifications': { 'value': modifiedProductIdFilters }},
viewSize: productIdFilter.length,
viewIndex: 0
})
Expand Down
1 change: 1 addition & 0 deletions src/store/modules/stock/StockState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export default interface StockState {
parsed: any,
original: any,
unidentifiedItems: any,
initial: any,
},
fileName: string
}
48 changes: 20 additions & 28 deletions src/store/modules/stock/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import store from '@/store'
import RootState from '@/store/RootState'
import StockState from './StockState'
import * as types from './mutation-types'
import emitter from "@/event-bus";

const actions: ActionTree<StockState, RootState> = {
async processUpdateStockItems ({ commit }, items) {
const productIds = items.map((item: any) => item.shopifyProductSKU)
async processUpdateStockItems ({ commit, rootGetters }, items) {
emitter.emit('fileProcessing');
//Fetching only top
const productIds = items.slice(0, process.env['VUE_APP_VIEW_SIZE']).map((item: any) => item.identification);


// We are getting external facilityId from CSV, extract facilityId and pass for getting locations
const externalFacilityIds = [...new Set(items.map((item: any) => item.externalFacilityId))]
Expand All @@ -19,57 +23,45 @@ const actions: ActionTree<StockState, RootState> = {
return facilityMapping[externalFacilityId];
}).filter((facilityId: any) => facilityId)
store.dispatch('util/fetchFacilityLocations', facilityIds);


const viewSize = productIds.length;
const viewIndex = 0;
const payload = {
viewSize,
viewIndex,
productIds
productIds,
identificationTypeId: items[0]?.identificationTypeId //fetching identificationTypeId from first item, as all the items will have one identification type
}
const cachedProducts = await store.dispatch("product/fetchProducts", payload);
const unidentifiedItems = [] as any;
const parsed = items.map((item: any) => {
const product = cachedProducts[item.shopifyProductSKU];
const parsed = [] as any;
const initial = items.map((item: any) => {
const product = cachedProducts[item.identification];
const facilityLocation = rootGetters['util/getFacilityLocationsByFacilityId'](item.externalFacilityId)?.[0];
item.locationSeqId = facilityLocation?.locationSeqId;
parsed.push(item);

if(product){
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));
const original = JSON.parse(JSON.stringify(items));

commit(types.STOCK_ITEMS_UPDATED, { parsed, original, unidentifiedItems });
commit(types.STOCK_ITEMS_UPDATED, { parsed, original, initial });
emitter.emit('fileProcessed');
},
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.initialSKU = item.shopifyProductSKU;
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});
commit(types.STOCK_ITEMS_UPDATED, { parsed: [], original: []});
},
async updateMissingFacilities({ state }, facilityMapping){
const facilityLocations = await this.dispatch('util/fetchFacilityLocations', Object.values(facilityMapping));
Expand Down
1 change: 1 addition & 0 deletions src/store/modules/stock/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const orderModule: Module<StockState, RootState> = {
parsed: [],
original: [],
unidentifiedItems: [],
initial: []
},
fileName: ""
},
Expand Down
1 change: 1 addition & 0 deletions src/store/modules/stock/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const mutations: MutationTree <StockState> = {
state.items.parsed = payload.parsed;
state.items.original = payload.original;
state.items.unidentifiedItems = payload.unidentifiedItems;
state.items.initial = payload.initial;
}
}
export default mutations;
1 change: 1 addition & 0 deletions src/store/modules/util/UtilState.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export default interface UserState {
facilities: [],
facilityLocationsByFacilityId: any;
goodIdentificationTypes: []
}
Loading

0 comments on commit 4824f0a

Please sign in to comment.