Skip to content

Commit

Permalink
Merge pull request #791 from hotwax/#715_rejection_page
Browse files Browse the repository at this point in the history
Rejections monitoring page implementation (#715)
  • Loading branch information
ravilodhi authored Oct 15, 2024
2 parents c203e7a + fdcc87b commit e39e167
Show file tree
Hide file tree
Showing 23 changed files with 1,511 additions and 8 deletions.
1 change: 1 addition & 0 deletions src/authorization/Actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ export default {
"APP_COLLATERAL_REJECTION_CONFIG_UPDATE": "APP_COLLATERAL_REJECTION_CONFIG_UPDATE",
"APP_UPDT_FULFILL_FORCE_SCAN_CONFIG": "APP_UPDT_FULFILL_FORCE_SCAN_CONFIG",
"APP_ORGANIZATION_HEADER_VIEW": "APP_ORGANIZATION_HEADER_VIEW",
"APP_REJECTIONS_VIEW": "APP_REJECTIONS_VIEW",
"APP_INVOICING_STATUS_VIEW": "APP_INVOICING_STATUS_VIEW"
}
1 change: 1 addition & 0 deletions src/authorization/Rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ export default {
"APP_COLLATERAL_REJECTION_CONFIG_UPDATE": "COMMON_ADMIN",
"APP_UPDT_FULFILL_FORCE_SCAN_CONFIG": "COMMON_ADMIN",
"APP_ORGANIZATION_HEADER_VIEW": "SFA_ADMIN OR CARRIER_SETUP_VIEW OR FF_ORDER_LOOKUP_VIEW",
"APP_REJECTIONS_VIEW": "SFA_ADMIN",
"FULFILLMENT_APP_VIEW": "FULFILLMENT_APP_VIEW"
} as any
314 changes: 314 additions & 0 deletions src/components/DownloadRejectedOrdersModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
<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("Download results") }}</ion-title>
</ion-toolbar>
</ion-header>

<ion-content>
<ion-list>
<ion-list-header>
<ion-label>{{ translate("Select fields") }}</ion-label>
</ion-list-header>
<ion-item v-for="selectedField in selectedFields" :key="selectedField.name">
<template v-if="selectedField.name === 'primaryProductId'">
<ion-checkbox justify="start" label-placement="end" v-model="selectedField.value" :checked="selectedField.value" :disabled="selectedField.disabled">{{ translate(selectedField.description) }}</ion-checkbox>
<ion-select aria-label="primaryProduct" interface="popover" value="default" slot="end" v-model="selectedPrimaryProductId">
<ion-select-option v-for="(value, identificationsType) in productIdentifications" :key="identificationsType" :value="value">{{ identificationsType }}</ion-select-option>
</ion-select>
</template>
<template v-else-if="selectedField.name === 'secondaryProductId'">
<ion-checkbox justify="start" label-placement="end" v-model="selectedField.value" :checked="selectedField.value" :disabled="selectedField.disabled">{{ translate(selectedField.description) }}</ion-checkbox>
<ion-select aria-label="primaryProduct" interface="popover" value="default" slot="end" v-model="selectedSecondaryProductId">
<ion-select-option v-for="(value, identificationsType) in productIdentifications" :key="identificationsType" :value="value">{{ identificationsType }}</ion-select-option>
</ion-select>
</template>
<template v-else-if="selectedField.name === 'rejectedFrom'">
<ion-checkbox justify="start" label-placement="end" v-model="selectedField.value" :checked="selectedField.value" :disabled="selectedField.disabled">{{ translate("Facility") }}</ion-checkbox>
<ion-select aria-label="facilityField" interface="popover" v-model="selectedFacilityId" slot="end">
<ion-select-option value="facilityId">Internal ID</ion-select-option>
<ion-select-option value="externalId">External ID</ion-select-option>
</ion-select>
</template>
<template v-else>
<ion-checkbox justify="start" label-placement="end" @ionChange="selectField(selectedField.name)" :checked="selectedField.value" :disabled="selectedField.disabled">{{ translate(selectedField.description) }}</ion-checkbox>
</template>
</ion-item>
</ion-list>
</ion-content>

<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button @click="downloadCSV">
<ion-icon :icon="cloudDownloadOutline" />
</ion-fab-button>
</ion-fab>
</template>

<script lang="ts">
import {
IonButton,
IonButtons,
IonCheckbox,
IonContent,
IonFab,
IonFabButton,
IonHeader,
IonIcon,
IonItem,
IonLabel,
IonList,
IonListHeader,
IonSelect,
IonSelectOption,
IonTitle,
IonToolbar,
modalController,
alertController
} from '@ionic/vue';
import { computed, defineComponent } from 'vue';
import { closeOutline, cloudDownloadOutline} from 'ionicons/icons';
import { getProductIdentificationValue, translate, useProductIdentificationStore } from '@hotwax/dxp-components';
import { mapGetters, useStore } from 'vuex';
import { escapeSolrSpecialChars, prepareSolrQuery } from '@/utils/solrHelper'
import { RejectionService } from '@/services/RejectionService'
import { UtilService } from "@/services/UtilService";
import { hasError } from '@/adapter'
import logger from '@/logger';
import emitter from "@/event-bus";
import { getDateWithOrdinalSuffix, jsonToCsv } from "@/utils";
import { DateTime } from 'luxon';
export default defineComponent({
name: 'Rejections',
components: {
IonButton,
IonButtons,
IonCheckbox,
IonContent,
IonFab,
IonFabButton,
IonHeader,
IonIcon,
IonItem,
IonLabel,
IonList,
IonListHeader,
IonSelect,
IonSelectOption,
IonTitle,
IonToolbar
},
data() {
return {
selectedFacilityId: "facilityId",
selectedPrimaryProductId: "productId",
selectedSecondaryProductId: "productId",
selectedFields: [
{"name": "orderId", "value": true, "description": "Order ID", "disabled": true},
{"name": "orderItemSeqId", "value": true, "description": "Order item sequence ID", "disabled": true},
{"name": "itemDescription", "value": true, "description": "Item description", "disabled": false},
{"name": "rejectedFrom", "value": true, "description": "Rejected from", "disabled": true},
{"name": "primaryProductId", "value": true, "description": "Primary product ID", "disabled": true},
{"name": "secondaryProductId", "value": true, "description": "Secondary product ID", "disabled": false},
{"name": "availableToPromise", "value": true, "description": "Available to promise", "disabled": false},
{"name": "rejectedBy", "value": true, "description": "Rejected by", "disabled": false},
{"name": "rejectedAt", "value": true, "description": "Rejected at", "disabled": false},
{"name": "rejectionReasonId", "value": true, "description": "Rejection reason ID", "disabled": false},
{"name": "rejectionReasonDesc", "value": true, "description": "Rejection reason description", "disabled": false},
{"name": "brokeredAt", "value": true, "description": "Brokered at", "disabled": false},
{"name": "brokeredBy", "value": true, "description": "Brokered by", "disabled": false}
],
productIdentifications: {
"Internal ID": "productId",
"Internal Name": "internalName",
"SKU": "SKU",
"UPC": "UPCA"
}
}
},
computed: {
...mapGetters({
getProduct: 'product/getProduct',
rejectedOrders: 'rejection/getRejectedOrders',
currentFacility: 'user/getCurrentFacility',
})
},
methods: {
closeModal() {
modalController.dismiss({ dismissed: true});
},
selectField(fieldName: string) {
const selectedField = this.selectedFields.find(selectedField => selectedField.name === fieldName);
if (selectedField) {
selectedField.value = selectedField.value ? false : true
}
},
async downloadCSV() {
const alert = await alertController.create({
header: translate("Download rejected orders"),
message: translate("Are you sure you want to download the rejected orders?"),
buttons: [{
text: translate("Cancel"),
role: 'cancel',
}, {
text: translate("Download"),
handler: async () => {
await modalController.dismiss({ dismissed: true });
await alert.dismiss();
emitter.emit("presentLoader", { message: "Preparing file to downlaod...", backdropDismiss: true });
const selectedFields = this.selectedFields.filter((field) => field.value) as any;
const rejectedItems = await this.bulkFetchRejectedItems();
const facilityDetail = await this.fetchFacilityDetail();
const downloadData = await Promise.all(rejectedItems.map(async (item: any) => {
const product = this.getProduct(item.productId)
if (product) {
const rejectedItemDetails = selectedFields.reduce((details: any, field: any) => {
if (field.name === 'rejectedAt') {
details[field.name] = getDateWithOrdinalSuffix(DateTime.fromISO(item.rejectedAt).toMillis());
} else if (field.name === 'primaryProductId') {
details[field.name] = getProductIdentificationValue(this.selectedPrimaryProductId, product);
} else if (field.name === 'secondaryProductId') {
details[field.name] = getProductIdentificationValue(this.selectedSecondaryProductId, product);
} else if (field.name === 'rejectedFrom') {
details[field.name] = facilityDetail[this.selectedFacilityId];
} else {
details[field.name] = item[field.name];
}
return details;
}, {});
return rejectedItemDetails;
}
}));
const fileName = `RejectedOrders-${this.currentFacility.facilityId}-${DateTime.now().toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS)}.csv`
await jsonToCsv(downloadData, { download: true, name: fileName });
emitter.emit("dismissLoader")
}
}]
});
return alert.present();
},
async bulkFetchRejectedItems() {
const rejectedOrderQuery = this.rejectedOrders.query
const filters = {
rejectedFrom_txt_en: { value: escapeSolrSpecialChars(this.currentFacility.facilityId) },
} as any
//when user search the rejected results are not bound to time duration
if (!rejectedOrderQuery.queryString) {
let rejectionPeriodFilter = "[NOW-24HOURS TO NOW]"
if (rejectedOrderQuery.rejectionPeriodId === 'LAST_SEVEN_DAYS') {
rejectionPeriodFilter = "[NOW-7DAYS TO NOW]"
}
filters.rejectedAt_dt = {value: rejectionPeriodFilter}
}
if (rejectedOrderQuery.rejectionReasons.length) {
filters.rejectionReasonId_txt_en = {value: rejectedOrderQuery.rejectionReasons}
}
const query = prepareSolrQuery({
coreName: "logInsights",
docType: "FULFILLMENT_REJECTION",
queryString: rejectedOrderQuery.queryString,
queryFields: 'orderId_s itemDescription_txt_en productId_s rejectedFrom_txt_en rejectedBy_txt_en rejectionReasonId_txt_en rejectionReasonDesc_txt_en',
viewIndex: 0,
viewSize: 100,
sort: 'rejectedAt_dt desc',
isGroupingRequired: true,
groupBy: 'orderId_s',
filters
})
let allItems = [] as any;
let resp;
try {
do {
resp = await RejectionService.fetchRejctedOrders(query);
if (!hasError(resp)) {
let orders = resp.data.grouped.orderId_s.groups
orders = orders.map((order: any) => {
const orderItemDocs = order.doclist.docs.map((doc: any) => {
return {
orderId: doc.orderId_s,
orderItemSeqId: doc.orderItemSeqId_s,
itemDescription: doc.itemDescription_txt_en,
productId: doc.productId_s,
availableToPromise: doc.availableToPromise_d,
rejectedFrom: order.rejectedFrom_txt_en,
rejectedBy: doc.rejectedBy_txt_en,
rejectedAt: doc.rejectedAt_dt,
rejectionReasonId: doc.rejectionReasonId_txt_en,
rejectionReasonDesc: doc.rejectionReasonDesc_txt_en,
brokeredAt: doc.brokeredAt_dt,
brokeredBy: doc.brokeredBy_txt_en,
};
});
allItems = allItems.concat(orderItemDocs);
this.store.dispatch("product/fetchProducts", { productIds: [... new Set(orderItemDocs.map((item: any) => item.productId))] });
});
query.viewIndex++;
} else {
throw resp.data;
}
} while (resp.data.grouped.orderId_s.groups.length >= query.viewSize);
} catch (err) {
logger.error(err);
return [];
}
return allItems
},
async fetchFacilityDetail() {
let facilityDetail = {} as any;
try {
const payload = {
"inputFields": {
"facilityId": this.currentFacility.facilityId,
},
"entityName": "Facility",
"fieldList": ["facilityId", "facilityName", "externalId"],
"viewSize": 1
}
const resp = await UtilService.fetchFacilities(payload)
if (!hasError(resp) && resp.data.count > 0) {
facilityDetail = resp.data.docs[0]
} else {
throw resp.data
}
} catch (err) {
logger.error('Failed to fetch facilities', err)
}
return facilityDetail;
}
},
setup() {
const store = useStore()
const productIdentificationStore = useProductIdentificationStore();
let productIdentificationPref = computed(() => productIdentificationStore.getProductIdentificationPref)
return {
closeOutline,
cloudDownloadOutline,
getProductIdentificationValue,
productIdentificationPref,
store,
translate,
}
}
});
</script>
12 changes: 11 additions & 1 deletion src/components/Menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {
} from "@ionic/vue";
import { computed, defineComponent } from "vue";
import { mapGetters } from "vuex";
import { arrowBackOutline, mailUnreadOutline, mailOpenOutline, checkmarkDoneOutline, settingsOutline, swapVerticalOutline } from "ionicons/icons";
import { arrowBackOutline, backspaceOutline, mailUnreadOutline, mailOpenOutline, checkmarkDoneOutline, settingsOutline, swapVerticalOutline } from "ionicons/icons";
import { useStore } from "@/store";
import { useRouter } from "vue-router";
import { hasPermission } from "@/authorization";
Expand Down Expand Up @@ -113,6 +113,15 @@ export default defineComponent({
permissionId: "APP_COMPLETED_ORDERS_VIEW"
}
},
{
title: "Rejections",
url: "/rejections",
iosIcon: backspaceOutline,
mdIcon: backspaceOutline,
meta: {
permissionId: "APP_REJECTIONS_VIEW"
}
},
{
title: "Transfer Orders",
url: "/transfer-orders",
Expand Down Expand Up @@ -178,6 +187,7 @@ export default defineComponent({
return {
appPages,
backspaceOutline,
checkmarkDoneOutline,
hasPermission,
arrowBackOutline,
Expand Down
Loading

0 comments on commit e39e167

Please sign in to comment.