Skip to content

Commit

Permalink
Merge pull request #38 from R-Sourabh/#37-NetSuite-Integration-UI
Browse files Browse the repository at this point in the history
Implemented: NetSuite Integration Management UI(#37)
  • Loading branch information
ymaheshwari1 authored Mar 5, 2025
2 parents 667376f + 95fdee3 commit e1afd7e
Show file tree
Hide file tree
Showing 50 changed files with 2,652 additions and 119 deletions.
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ VUE_APP_LOGIN_URL="https://launchpad.hotwax.io/login"
VUE_APP_FACILITIES_LOGIN_URL="https://facilities.hotwax.io/login"
VUE_APP_GITBOOK_API_KEY=""
VUE_APP_SPACE_ID=""
VUE_APP_GITBOOK_BASE_URL=""
VUE_APP_GITBOOK_BASE_URL=""
VUE_APP_NETSUITE_INTEGRATION_TYPE_MAPPING={"INVENTORY_VARIANCE_TYPE_ID":"NETSUITE_VAR_TRAN","SHIPPING_METHOD_TYPE_ID":"NETSUITE_SHP_MTHD","PAYMENT_METHOD_TYPE_ID":"NETSUITE_PMT_MTHD","PRICE_LEVEL_TYPE_ID":"NETSUITE_PRICE_LEVEL","DISCOUNT_TYPE_ID":"NETSUITE_DISC_MTHD"}
16 changes: 13 additions & 3 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
<template>
<ion-app>
<ion-router-outlet />
<IonSplitPane content-id="main-content" when="lg">
<Menu />
<ion-router-outlet id="main-content"></ion-router-outlet>
</IonSplitPane>
</ion-app>
</template>

<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from "vue";
import { IonApp, IonRouterOutlet, loadingController } from "@ionic/vue";
import { IonApp, IonRouterOutlet, IonSplitPane, loadingController } from "@ionic/vue";
import Menu from '@/components/Menu.vue';
import emitter from "@/event-bus"
import { Settings } from 'luxon'
import store from "./store";
Expand Down Expand Up @@ -78,4 +82,10 @@ onUnmounted(() => {
resetConfig()
})
</script>
</script>

<style scoped>
ion-split-pane {
--side-width: 304px;
}
</style>
2 changes: 1 addition & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const api = async (customConfig: any) => {
const baseURL = store.getters["user/getInstanceUrl"];

if (baseURL) {
config.baseURL = baseURL.startsWith('http') ? baseURL.includes('/rest/s1/admin') ? baseURL : `${baseURL}/rest/s1/admin/` : `https://${baseURL}.hotwax.io/rest/s1/admin/`;
config.baseURL = baseURL.startsWith('http') ? baseURL.includes('/rest/s1/') ? baseURL : `${baseURL}/rest/s1/` : `https://${baseURL}.hotwax.io/rest/s1/`;
}

if(customConfig.cache) config.adapter = axiosCache.adapter;
Expand Down
106 changes: 106 additions & 0 deletions src/components/DiscountsModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<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("Discounts") }}</ion-title>
</ion-toolbar>
</ion-header>

<ion-content>
<ion-item class="ion-margin-top">
<ion-icon slot="start" :icon="informationCircleOutline" />
<ion-label>
{{ translate("Learn more about discounts in NetSuite") }}
</ion-label>
<ion-button fill="clear" size="small" color="medium">
<ion-icon :icon="openOutline" slot="icon-only" />
</ion-button>
</ion-item>

<ion-item lines="full" class="ion-margin-top">
<ion-input v-model="orderLevelDiscount" :label="translate('Order level discount')" :placeholder="translate('NetSuite discount item ID')" />
</ion-item>

<ion-item lines="full">
<ion-input v-model="itemLevelDiscount" :label="translate('Item level discounts')" :placeholder="translate('NetSuite discount item ID')" />
</ion-item>

<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button @click="editNetSuiteDiscountItemIds" :disabled="isDiscountValueChanged()">
<ion-icon :icon="saveOutline" />
</ion-fab-button>
</ion-fab>
</ion-content>
</template>

<script setup lang="ts">
import { IonButton, IonButtons, IonContent, IonFab, IonFabButton, IonHeader, IonIcon, IonInput, IonItem, IonLabel, IonTitle, IonToolbar, modalController } from "@ionic/vue";
import { closeOutline, informationCircleOutline, openOutline, saveOutline } from 'ionicons/icons';
import { translate } from "@/i18n"
import { useStore } from "vuex";
import { computed, onMounted, ref } from "vue";
import { useNetSuiteComposables } from "@/composables/useNetSuiteComposables";
const store = useStore();
const discountTypeId = JSON.parse(process.env.VUE_APP_NETSUITE_INTEGRATION_TYPE_MAPPING)?.DISCOUNT_TYPE_ID
const { addNetSuiteId, updateNetSuiteId } = useNetSuiteComposables(discountTypeId)
const integrationTypeMappings = computed(() => store.getters["netSuite/getIntegrationTypeMappings"](discountTypeId))
const orderLevelDiscount = ref("");
const itemLevelDiscount = ref("");
const integrationMappingByKey = ref({}) as any
const mappingKeys = {
order: "SHOPIFY_DISC",
item: "SHOPIFY_ITEM_DISC"
}
onMounted(async () => {
// Set orderLevelDiscount and itemLevelDiscount based on their corresponding mapping keys in integration type mappings.
integrationTypeMappings.value.map((mapping: any) => {
integrationMappingByKey[mapping.mappingKey] = mapping
if(mapping.mappingKey === mappingKeys.order) {
orderLevelDiscount.value = mapping.mappingValue
} else {
itemLevelDiscount.value = mapping.mappingValue
}
});
})
function closeModal() {
modalController.dismiss({ dismissed: true });
}
function isDiscountValueChanged() {
return !(orderLevelDiscount.value?.trim() && itemLevelDiscount.value?.trim() && (orderLevelDiscount.value !== integrationMappingByKey[mappingKeys.order]?.mappingValue || itemLevelDiscount.value !== integrationMappingByKey[mappingKeys.item]?.mappingValue));
}
async function editNetSuiteDiscountItemIds() {
if(orderLevelDiscount.value !== integrationMappingByKey[mappingKeys.order].mappingValue) {
await updateMapping(mappingKeys.order, orderLevelDiscount.value)
}
if(!itemLevelDiscount.value !== integrationMappingByKey[mappingKeys.item].mappingValue) {
await updateMapping(mappingKeys.item, itemLevelDiscount.value)
}
closeModal();
}
async function updateMapping(mappingKey: any, mappingValue: any) {
const payload = {
integrationTypeId: discountTypeId,
mappingKey,
mappingValue
}
if(integrationMappingByKey[mappingKey]?.integrationMappingId) {
await updateNetSuiteId(payload, integrationMappingByKey[mappingKey].integrationMappingId);
} else {
await addNetSuiteId(payload);
}
}
</script>
87 changes: 87 additions & 0 deletions src/components/Menu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<template>
<ion-menu content-id="main-content" type="overlay" :disabled="!isUserAuthenticated">
<ion-header>
<ion-toolbar>
<ion-title>{{ translate("Company") }}</ion-title>
</ion-toolbar>
</ion-header>

<ion-content>
<ion-list id="company-list">
<ion-menu-toggle auto-hide="false" v-for="(p, i) in appPages" :key="i">
<ion-item button router-direction="root" :router-link="p.url" class="hydrated" :class="{ selected: selectedIndex === i }">
<ion-icon slot="start" :ios="p.iosIcon" :md="p.mdIcon" />
<ion-label>{{ p.title }}</ion-label>
</ion-item>
</ion-menu-toggle>
</ion-list>
</ion-content>
</ion-menu>
</template>

<script setup lang="ts">
import {
IonContent,
IonIcon,
IonHeader,
IonItem,
IonLabel,
IonList,
IonTitle,
IonToolbar,
IonMenu,
IonMenuToggle,
} from "@ionic/vue";
import { computed } from "vue";
import { businessOutline, settingsOutline, walletOutline } from "ionicons/icons";
import { useStore } from "@/store";
import { useRouter } from "vue-router";
import { translate } from "@/i18n";
const store = useStore();
const router = useRouter();
const appPages = [
{
title: "Product Store",
url: "/product-store",
childRoutes: ["/product-store/"],
iosIcon: businessOutline,
mdIcon: businessOutline,
},
// {
// title: "Shopify",
// url: "/shopify",
// childRoutes: ["/shopify/"],
// iosIcon: cartOutline,
// mdIcon: cartOutline,
// },
{
title: "NetSuite",
url: "/netsuite",
childRoutes: ["/netsuite/"],
iosIcon: walletOutline,
mdIcon: walletOutline
},
{
title: "Settings",
url: "/settings",
iosIcon: settingsOutline,
mdIcon: settingsOutline,
}
];
const isUserAuthenticated = computed(() => store.getters["user/isUserAuthenticated"])
const selectedIndex = computed(() => {
const path = router.currentRoute.value.path
return appPages.findIndex((screen) => screen.url === path || screen.childRoutes?.includes(path) || screen.childRoutes?.some((route) => path.includes(route)))
})
</script>

<style scoped>
ion-item.selected ion-icon {
color: var(--ion-color-secondary);
}
ion-item.selected {
--color: var(--ion-color-secondary);
}
</style>
104 changes: 104 additions & 0 deletions src/components/PriceLevelModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<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("Price level") }}</ion-title>
</ion-toolbar>
</ion-header>

<ion-content>
<ion-item class="ion-margin-top">
<ion-icon slot="start" :icon="informationCircleOutline" />
<ion-label>
{{ translate("Learn more about price levels in NetSuite") }}
</ion-label>
<ion-button fill="clear" size="small" color="medium" @click="openPriceLevelDoc">
<ion-icon :icon="openOutline" slot="icon-only" />
</ion-button>
</ion-item>

<ion-item lines="full" class="ion-margin-top">
<ion-input v-model="selectedPriceLevel" :label="translate('Price level')" :placeholder="translate('Base Price')"/>
</ion-item>

<ion-list>
<ion-list-header>{{ translate("Frequently used") }}</ion-list-header>
<ion-radio-group v-model="selectedPriceLevel">
<ion-item>
<ion-radio value="Base" label-placement="end" justify="start">
<ion-label>
{{ translate("Base Price") }}
<p>{{ translate("Defaults to product price set in NetSuite") }}</p>
</ion-label>
</ion-radio>
</ion-item>
<ion-item>
<ion-radio value="Custom" label-placement="end" justify="start">
<ion-label>
{{ translate("Custom") }}
<p>{{ translate("Use the price a product was sold at in the order.") }}</p>
</ion-label>
</ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>

<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button :disabled="isPriceLevelChanged()" @click="savePrice">
<ion-icon :icon="saveOutline" />
</ion-fab-button>
</ion-fab>
</ion-content>
</template>

<script setup lang="ts">
import { IonButton, IonButtons, IonContent, IonFab, IonFabButton, IonHeader, IonIcon, IonInput, IonItem, IonLabel, IonList, IonListHeader, IonRadio, IonRadioGroup, IonTitle, IonToolbar, modalController } from "@ionic/vue";
import { closeOutline, informationCircleOutline, openOutline, saveOutline } from 'ionicons/icons';
import { translate } from "@/i18n"
import { useStore } from "vuex"
import { ref, onMounted } from "vue"
import { useNetSuiteComposables } from "@/composables/useNetSuiteComposables";
const store = useStore();
const priceLevelTypeId = JSON.parse(process.env.VUE_APP_NETSUITE_INTEGRATION_TYPE_MAPPING)?.PRICE_LEVEL_TYPE_ID
const { updateNetSuiteId } = useNetSuiteComposables(priceLevelTypeId);
const integrationMapping = ref("") as any;
const selectedPriceLevel = ref("")
onMounted(async () => {
await store.dispatch("netSuite/fetchIntegrationTypeMappings", {
integrationTypeId: priceLevelTypeId,
mappingKey: "PRICE_LEVEL"
})
const integrationMappings = store.getters["netSuite/getIntegrationTypeMappings"](priceLevelTypeId);
selectedPriceLevel.value = (integrationMapping.value = integrationMappings[0]).mappingValue || "";
})
function isPriceLevelChanged() {
return (!selectedPriceLevel.value.trim() || selectedPriceLevel.value.trim() === integrationMapping.value.mappingValue)
}
// saves the selectedPriceLevel price level to Netsuite for integration type id: 'NETSUITE_PRICE_LEVEL' & mappingKey: 'PRICE_LEVEL'.
async function savePrice() {
const payload = {
integrationTypeId: priceLevelTypeId,
mappingKey: "PRICE_LEVEL",
mappingValue: selectedPriceLevel.value
};
await updateNetSuiteId(payload, integrationMapping.value.integrationMappingId);
closeModal();
}
function closeModal() {
modalController.dismiss({ dismissed: true });
}
function openPriceLevelDoc() {
window.open('https://docs.hotwax.co/documents/v/learn-netsuite/synchronization-flows/integration-mappings/price-levels', '_blank', 'noopener, noreferrer');
}
</script>
Loading

0 comments on commit e1afd7e

Please sign in to comment.