Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented: Unified inventory upload(#304) #309

Merged
merged 14 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading