Skip to content

Commit

Permalink
Merge pull request #309 from R-Sourabh/#304-unified-inventory-upload
Browse files Browse the repository at this point in the history
Implemented: Unified inventory upload(#304)
  • Loading branch information
ymaheshwari1 authored Jan 13, 2025
2 parents f9da38b + 55f13ff commit 407f986
Show file tree
Hide file tree
Showing 43 changed files with 2,458 additions and 494 deletions.
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ VUE_APP_ALIAS={}
VUE_APP_MAPPING_TYPES={"PO": "PO_MAPPING_PREF","RSTINV": "INV_MAPPING_PREF","RSTSTK": "STK_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={"productIdentification": { "label": "Product Identification", "required": true }, "quantity": { "label": "Quantity", "required": true }, "facility": { "label": "Facility ID", "required": true }}
VUE_APP_MAPPING_RSTSTK={"productIdentification": { "label": "Product Identification", "required": true }, "restockQuantity": { "label": "Restock quantity", "required": true }}
VUE_APP_MAPPING_RSTSTK={"productIdentification": { "label": "Product Identification", "required": true }, "facility": { "label": "Facility", "required": true }, "quantity": { "label": "Quantity", "required": true }}
VUE_APP_MAPPING_ADJINV={"productIdentification": { "label": "Product Identification", "required": true }, "facility": { "label": "Facility", "required": true }, "quantity": { "label": "Quantity", "required": true }}
VUE_APP_DEFAULT_LOG_LEVEL="error"
VUE_APP_LOGIN_URL="http://launchpad.hotwax.io/login"
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
207 changes: 207 additions & 0 deletions src/components/AddProductModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<template>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="closeModal">
<ion-icon slot="icon-only" :icon="closeOutline" />
</ion-button>
</ion-buttons>
<ion-title>{{ translate("Add product") }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content ref="contentRef" :scroll-events="true" @ionScroll="enableScrolling()">
<ion-searchbar v-model="queryString" :placeholder="translate('Search SKU or product name')" @keyup.enter="handleSearch" @ionInput="handleInput"/>

<template v-if="products.length">
<ion-list v-for="product in products" :key="product.productId">
<ion-item lines="none">
<ion-thumbnail slot="start">
<Image :src="product.mainImageUrl" />
</ion-thumbnail>
<ion-label>
<h2>{{ getProductIdentificationValue(productIdentificationPref.primaryId, getProductById(product.productId)) ? getProductIdentificationValue(productIdentificationPref.primaryId, getProductById(product.productId)) : getProductById(product.productId).productName }}</h2>
<p>{{ getProductIdentificationValue(productIdentificationPref.secondaryId, getProductById(product.productId)) }}</p>
</ion-label>
<ion-icon v-if="isProductAvailableInShipment(product.productId)" color="success" :icon="checkmarkCircle" />
<ion-button v-else fill="outline" @click="addToShipment(product.productId)">{{ translate("Add to shipment") }}</ion-button>
</ion-item>
</ion-list>

<ion-infinite-scroll @ionInfinite="loadMoreProducts($event)" threshold="100px" v-show="isScrollable" ref="infiniteScrollRef">
<ion-infinite-scroll-content loading-spinner="crescent" :loading-text="translate('Loading')" />
</ion-infinite-scroll>
</template>

<div v-else-if="queryString && isSearching && !products.length" class="empty-state">
<p>{{ translate("No product found") }}</p>
</div>
<div v-else class="empty-state">
<img src="../assets/images/empty-state-add-product-modal.png" alt="empty-state" />
<p>{{ translate("Enter a SKU, or product name to search a product") }}</p>
</div>
</ion-content>
</template>

<script>
import {
IonButton,
IonButtons,
IonContent,
IonHeader,
IonIcon,
IonInfiniteScroll,
IonItem,
IonLabel,
IonList,
IonSearchbar,
IonThumbnail,
IonTitle,
IonToolbar,
modalController,
} from "@ionic/vue";
import { closeOutline, checkmarkCircle } from "ionicons/icons"
import { defineComponent, computed } from "vue"
import { mapGetters, useStore } from "vuex";
import { useRouter } from "vue-router"
import store from "@/store"
import { translate, getProductIdentificationValue, useProductIdentificationStore } from "@hotwax/dxp-components";
import Image from "@/components/Image.vue"
import { UtilService } from "@/services/UtilService";
import { hasError } from "@/adapter";
import { showToast } from '@/utils';
import logger from "@/logger";
export default defineComponent({
name: "AddProductModal",
components: {
IonButton,
IonButtons,
IonContent,
IonHeader,
IonIcon,
IonInfiniteScroll,
IonItem,
IonLabel,
IonList,
IonSearchbar,
IonThumbnail,
IonTitle,
IonToolbar,
Image,
},
data() {
return {
queryString: '',
isSearching: false,
isScrollingEnabled: false
}
},
async ionViewWillEnter() {
this.isScrollingEnabled = false;
},
unmounted() {
store.dispatch("product/clearProducts");
},
props: ["shipmentId"],
computed: {
...mapGetters({
products: 'product/getProducts',
isScrollable: 'product/isScrollable',
getProductById: 'product/getProductById',
isProductAvailableInShipment: 'product/isProductAvailableInShipment',
})
},
methods: {
enableScrolling() {
const parentElement = this.$refs.contentRef.$el
const scrollEl = parentElement.shadowRoot.querySelector("main[part='scroll']")
let scrollHeight = scrollEl.scrollHeight, infiniteHeight = this.$refs.infiniteScrollRef.$el.offsetHeight, scrollTop = scrollEl.scrollTop, threshold = 100, height = scrollEl.offsetHeight
const distanceFromInfinite = scrollHeight - infiniteHeight - scrollTop - threshold - height
if(distanceFromInfinite < 0) {
this.isScrollingEnabled = false;
} else {
this.isScrollingEnabled = true;
}
},
async handleSearch() {
if(!this.queryString) {
this.isSearching = false;
store.dispatch("product/clearProducts");
return;
}
await this.getProducts();
this.isSearching = true;
},
async getProducts( vSize, vIndex) {
const viewSize = vSize ? vSize : process.env.VUE_APP_VIEW_SIZE;
const viewIndex = vIndex ? vIndex : 0;
const payload = {
viewSize,
viewIndex,
queryString: this.queryString
}
if(this.queryString) {
await this.store.dispatch("product/findProduct", payload);
}
},
async loadMoreProducts(event) {
if(!(this.isScrollingEnabled && this.isScrollable)) {
await event.target.complete();
}
this.getProducts(
undefined,
Math.ceil(this.products.length / process.env.VUE_APP_VIEW_SIZE).toString()
).then(() => {
event.target.complete();
})
},
async addToShipment(productId) {
let resp;
const payload = {
productId: productId,
shipmentId: this.shipmentId,
quantity: 1
}
try {
resp = await UtilService.addProductToShipment(payload)
if(!hasError(resp)) {
showToast(translate("Product added successfully"));
await this.store.dispatch('util/fetchShipmentItems', { shipmentId: this.shipmentId });
} else {
throw resp.data;
}
} catch(err) {
showToast(translate("Failed to add product to shipment"))
logger.error(err)
}
},
closeModal() {
modalController.dismiss({ dismissed: true });
},
handleInput() {
if(!this.queryString) {
this.isSearching = false;
store.dispatch("product/clearProducts");
}
},
},
setup() {
const store = useStore();
const router = useRouter();
const productIdentificationStore = useProductIdentificationStore();
let productIdentificationPref = computed(() => productIdentificationStore.getProductIdentificationPref);
return {
closeOutline,
checkmarkCircle,
translate,
store,
getProductIdentificationValue,
productIdentificationPref,
router
}
}
})
</script>
78 changes: 78 additions & 0 deletions src/components/DownloadLogsFilePopover.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<template>
<ion-content>
<ion-list>
<ion-list-header>{{ dataManagerLog.logId }}</ion-list-header>
<ion-item button @click="downloadFile('logFile')">
<ion-label>{{ translate("Log file") }}</ion-label>
</ion-item>
<ion-item button @click="downloadFile('uploadedFile')">
<ion-label>{{ translate("Uploaded file") }}</ion-label>
</ion-item>
<ion-item button :disabled="!dataManagerLog?.errorRecordContentId" lines="none" @click="downloadFile('failedRecords')">
<ion-label>{{ translate("Failed records") }}</ion-label>
</ion-item>
</ion-list>
</ion-content>
</template>

<script lang="ts">
import {
IonContent,
IonItem,
IonLabel,
IonList,
IonListHeader,
popoverController
} from "@ionic/vue";
import { defineComponent } from "vue";
import { translate } from "@hotwax/dxp-components";
import { UtilService } from "@/services/UtilService";
import { saveDataFile, showToast } from '@/utils';
import logger from "@/logger";
export default defineComponent({
name: "DownloadLogsFilePopover",
components: {
IonContent,
IonItem,
IonLabel,
IonList,
IonListHeader
},
props: ["dataManagerLog"],
methods: {
async downloadFile(type: string) {
let dataResource = {} as any;
if(type === 'logFile') {
dataResource.dataResourceId = this.dataManagerLog.logFileDataResourceId
dataResource.name = this.dataManagerLog.logFileContentName
} else if(type === 'uploadedFile') {
dataResource.name = this.dataManagerLog.contentName
dataResource.dataResourceId = this.dataManagerLog.dataResourceId
} else if(type === 'failedRecords') {
dataResource.dataResourceId = this.dataManagerLog.errorRecordDataResourceId
dataResource.name = this.dataManagerLog.errorRecordContentName
}
if(dataResource.dataResourceId) {
try {
const response = await UtilService.fetchFileData({
dataResourceId: dataResource.dataResourceId
});
saveDataFile(response.data, dataResource.name);
} catch(error) {
showToast(translate("Error downloading file"))
logger.error(error)
}
}
popoverController.dismiss();
}
},
setup() {
return {
translate
}
}
});
</script>
95 changes: 95 additions & 0 deletions src/components/HelpModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<template>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="closeModal">
<ion-icon :icon="close" />
</ion-button>
</ion-buttons>
<ion-title>{{ translate("Help") }}</ion-title>
</ion-toolbar>
</ion-header>

<ion-content>
<ion-list>
<ion-item lines="full">
<ion-label>{{ translate("Sample CSV") }}</ion-label>
<ion-button fill="outline" @click="downloadSampleCsv">
<ion-icon slot="start" :icon="downloadOutline" />
{{ translate("Download") }}
</ion-button>
</ion-item>

<ion-item>
<ion-label>{{ translate("Product: Select the type of product identification used in the CSV. If the selected identification is not available on a row in the uploaded CSV, it will be skipped and presented in an error file.") }}</ion-label>
</ion-item>

<ion-item>
<ion-label>{{ translate("Facility: Use the internal ID of facilities in HotWax. Select external ID option if the uploaded CSV file is coming from a system like an ERP.") }}</ion-label>
</ion-item>

<ion-item>
<ion-label>{{ translate("Quantity: The amount to adjust the inventory by or the inventory level the inventory should be set to.") }}</ion-label>
</ion-item>
</ion-list>
</ion-content>
</template>

<script lang="ts">
import {
IonButtons,
IonButton,
IonContent,
IonHeader,
IonItem,
IonIcon,
IonLabel,
IonList,
IonTitle,
IonToolbar,
modalController
} from "@ionic/vue";
import { defineComponent } from "vue";
import { close, downloadOutline } from "ionicons/icons";
import { translate } from "@hotwax/dxp-components";
import { jsonToCsv } from "@/utils";
export default defineComponent({
name: "HelpModal",
components: {
IonButtons,
IonButton,
IonContent,
IonHeader,
IonIcon,
IonItem,
IonLabel,
IonList,
IonTitle,
IonToolbar
},
data() {
return {
sampleData: [{ identification: "", quantity: "", facilityId: "" }]
}
},
methods: {
closeModal() {
modalController.dismiss({ dismissed: true });
},
downloadSampleCsv() {
jsonToCsv(this.sampleData, {
download: true,
name: "Sample CSV.csv"
})
}
},
setup() {
return {
close,
downloadOutline,
translate
};
}
})
</script>
Loading

0 comments on commit 407f986

Please sign in to comment.