Skip to content
Open
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
132 changes: 132 additions & 0 deletions src/components/CreateMappingModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<template>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="closeModal">
<ion-icon :icon="close" />
</ion-button>
</ion-buttons>
<ion-title>{{ translate("CSV Mapping") }}</ion-title>
</ion-toolbar>
</ion-header>

<ion-item>
<ion-input :label="translate('Mapping name')" :placeholder="translate('Field mapping name')" v-model="mappingName" />
</ion-item>

<ion-content>
<div>
<ion-list>
<ion-item-divider>
<ion-label>{{ translate("Required") }} </ion-label>
</ion-item-divider>
<ion-item :key="field" v-for="(fieldValues, field) in getFields(fields, true)">
<ion-select :label="translate(fieldValues.label)" interface="popover" :placeholder = "translate('Select')" v-model="fieldMapping[field]">
<ion-select-option :key="index" v-for="(prop, index) in fileColumns">{{ prop }}</ion-select-option>
</ion-select>
</ion-item>
<ion-item-divider>
<ion-label>{{ translate("Optional") }} </ion-label>
</ion-item-divider>
<ion-item :key="field" v-for="(fieldValues, field) in getFields(fields, false)">
<ion-select :label="translate(fieldValues.label)" interface="popover" :placeholder = "translate('Select')" v-model="fieldMapping[field]">
<ion-select-option :key="index" v-for="(prop, index) in fileColumns">{{ prop }}</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
</div>
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button @click="saveMapping">
<ion-icon :icon="saveOutline" />
</ion-fab-button>
</ion-fab>
</ion-content>
</template>

<script setup>
import {
IonButtons,
IonButton,
IonContent,
IonFab,
IonFabButton,
IonHeader,
IonIcon,
IonInput,
IonItem,
IonItemDivider,
IonLabel,
IonSelect,
IonSelectOption,
IonTitle,
IonToolbar,
IonList,
modalController
} from '@ionic/vue';
import { close, save, saveOutline } from "ionicons/icons";
import { computed, defineProps, onMounted, ref } from "vue";
import { useStore } from "vuex";
import { showToast } from '@/utils';
import { translate } from "@hotwax/dxp-components";
const store = useStore();
const props = defineProps(["content", "seletedFieldMapping", "mappingType"])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is a typo in the prop name seletedFieldMapping. It should be selectedFieldMapping. This typo is also present where the prop is used (e.g., line 85).

const props = defineProps(["content", "selectedFieldMapping", "mappingType"])

const fieldMappings = computed(() => store.getters["user/getFieldMappings"])
let mappingName = ref(null)
let fieldMapping = ref ({})
let fileColumns = ref([])
let identificationTypeId = ref('SKU')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The variable identificationTypeId is defined but never used within the component. It should be removed to improve code clarity.

const fields = process.env["VUE_APP_MAPPING_INVCOUNT"] ? JSON.parse(process.env["VUE_APP_MAPPING_INVCOUNT"]) : {}
onMounted(() => {
fieldMapping.value = { ...props.seletedFieldMapping }
fileColumns.value = Object.keys(props.content[0]);
})
function getFields(fields, required = true) {
return Object.keys(fields).reduce((result, key) => {
if (fields[key].required === required) {
result[key] = fields[key];
}
return result;
}, {});
}
function closeModal() {
modalController.dismiss({ dismissed: true });
}
async function saveMapping() {
if(!mappingName.value || !mappingName.value.trim()) {
showToast(translate("Enter mapping name"));
return
}
if (!areAllFieldsSelected()) {
showToast(translate("Map all required fields"));
return
}
const id = generateUniqueMappingPrefId();
await store.dispatch("user/createFieldMapping", { id, name: mappingName.value, value: fieldMapping.value, mappingType: props.mappingType })
closeModal();
}
function areAllFieldsSelected() {
const requiredFields = Object.keys(getFields(fields, true));
const selectedFields = Object.keys(fieldMapping.value).filter(key => fieldMapping.value[key] !== '')
return requiredFields.every(field => selectedFields.includes(field));
}
//Todo: Generating unique identifiers as we are currently storing in local storage. Need to remove it as we will be storing data on server.
function generateUniqueMappingPrefId() {
const id = Math.floor(Math.random() * 1000);
return !fieldMappings.value[id] ? id : this.generateUniqueMappingPrefId();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The recursive call this.generateUniqueMappingPrefId() will cause a runtime error because this is undefined within the <script setup> context. You should make a direct recursive call to the function instead.

  return !fieldMappings.value[id] ? id : generateUniqueMappingPrefId();

}
</script>
<style scoped>
ion-content {
--padding-bottom: 80px;
}
</style>
132 changes: 132 additions & 0 deletions src/components/CycleCountUploadActionPopover.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<template>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="closeModal">
<ion-icon :icon="close" />
</ion-button>
</ion-buttons>
<ion-title>{{ translate("CSV Mapping") }}</ion-title>
</ion-toolbar>
</ion-header>

<ion-item>
<ion-input :label="translate('Mapping name')" :placeholder="translate('Field mapping name')" v-model="mappingName" />
</ion-item>

<ion-content>
<div>
<ion-list>
<ion-item-divider>
<ion-label>{{ translate("Required") }} </ion-label>
</ion-item-divider>
<ion-item :key="field" v-for="(fieldValues, field) in getFields(fields, true)">
<ion-select :label="translate(fieldValues.label)" interface="popover" :placeholder = "translate('Select')" v-model="fieldMapping[field]">
<ion-select-option :key="index" v-for="(prop, index) in fileColumns">{{ prop }}</ion-select-option>
</ion-select>
</ion-item>
<ion-item-divider>
<ion-label>{{ translate("Optional") }} </ion-label>
</ion-item-divider>
<ion-item :key="field" v-for="(fieldValues, field) in getFields(fields, false)">
<ion-select :label="translate(fieldValues.label)" interface="popover" :placeholder = "translate('Select')" v-model="fieldMapping[field]">
<ion-select-option :key="index" v-for="(prop, index) in fileColumns">{{ prop }}</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
</div>
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button @click="saveMapping">
<ion-icon :icon="saveOutline" />
</ion-fab-button>
</ion-fab>
</ion-content>
</template>

<script setup>
import {
IonButtons,
IonButton,
IonContent,
IonFab,
IonFabButton,
IonHeader,
IonIcon,
IonInput,
IonItem,
IonItemDivider,
IonLabel,
IonSelect,
IonSelectOption,
IonTitle,
IonToolbar,
IonList,
modalController
} from '@ionic/vue';
import { close, save, saveOutline } from "ionicons/icons";
import { computed, defineProps, onMounted, ref } from "vue";
import { useStore } from "vuex";
import { showToast } from '@/utils';
import { translate } from "@hotwax/dxp-components";
const store = useStore();
const props = defineProps(["content", "seletedFieldMapping", "mappingType"])
const fieldMappings = computed(() => store.getters["user/getFieldMappings"])
let mappingName = ref(null)
let fieldMapping = ref ({})
let fileColumns = ref([])
let identificationTypeId = ref('SKU')
const fields = process.env["VUE_APP_MAPPING_INVCOUNT"] ? JSON.parse(process.env["VUE_APP_MAPPING_INVCOUNT"]) : {}
onMounted(() => {
fieldMapping.value = { ...props.seletedFieldMapping }
fileColumns.value = Object.keys(props.content[0]);
})
function getFields(fields, required = true) {
return Object.keys(fields).reduce((result, key) => {
if (fields[key].required === required) {
result[key] = fields[key];
}
return result;
}, {});
}
function closeModal() {
modalController.dismiss({ dismissed: true });
}
async function saveMapping() {
if(!mappingName.value || !mappingName.value.trim()) {
showToast(translate("Enter mapping name"));
return
}
if (!areAllFieldsSelected()) {
showToast(translate("Map all required fields"));
return
}
const id = generateUniqueMappingPrefId();
await store.dispatch("user/createFieldMapping", { id, name: mappingName.value, value: fieldMapping.value, mappingType: props.mappingType })
closeModal();
}
function areAllFieldsSelected() {
const requiredFields = Object.keys(getFields(fields, true));
const selectedFields = Object.keys(fieldMapping.value).filter(key => fieldMapping.value[key] !== '')
return requiredFields.every(field => selectedFields.includes(field));
}
//Todo: Generating unique identifiers as we are currently storing in local storage. Need to remove it as we will be storing data on server.
function generateUniqueMappingPrefId() {
const id = Math.floor(Math.random() * 1000);
return !fieldMappings.value[id] ? id : this.generateUniqueMappingPrefId();
}
</script>
<style scoped>
ion-content {
--padding-bottom: 80px;
}
</style>
Comment on lines +1 to +132

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This component is an exact copy of CreateMappingModal.vue, which introduces significant code duplication. Furthermore, it's being used as a popover for actions on uploaded files in BulkUpload.vue, but its implementation is for creating a field mapping. The props it receives in BulkUpload.vue (systemMessage, fileName) do not match the props it declares (content, seletedFieldMapping, mappingType), which will lead to incorrect behavior. This component needs to be implemented correctly for its intended purpose, or removed if CreateMappingModal.vue can be reused in a more appropriate way.

6 changes: 6 additions & 0 deletions src/components/Menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ export default defineComponent({
const store = useStore();
const router = useRouter();
const appPages = [
{
title: "Bulk Upload",
url: "/bulkUpload",
iosIcon: createOutline,
mdIcon: createOutline
},
Comment on lines +65 to +70

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The new "Bulk Upload" menu item is missing the meta property with a permissionId. This will cause the menu item to be displayed to all users, even those without the necessary APP_DRAFT_VIEW permission. While the route guard will prevent access, this results in a poor user experience. Please add the permission metadata to ensure the menu item is only visible to authorized users.

      {
        title: "Bulk Upload",
        url: "/bulkUpload",
        iosIcon: createOutline,
        mdIcon: createOutline,
        meta: {
          permissionId: "APP_DRAFT_VIEW"
        }
      },

{
title: "Draft",
url: "/draft",
Expand Down
10 changes: 10 additions & 0 deletions src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import Settings from "@/views/Settings.vue";
import HardCount from "@/views/HardCount.vue"
import HardCountDetail from "@/views/HardCountDetail.vue"
import SessionCountDetail from "@/views/SessionCountDetail.vue"
import BulkUpload from "@/views/BulkUpload.vue";

// Defining types for the meta values
declare module 'vue-router' {
Expand Down Expand Up @@ -146,6 +147,15 @@ const routes: Array<RouteRecordRaw> = [
permissionId: "APP_PENDING_REVIEW_VIEW"
}
},
{
path: '/bulkUpload',
name: 'Draft bulk',
component: BulkUpload,
beforeEnter: authGuard,
meta: {
permissionId: "APP_DRAFT_VIEW"
}
},
{
path: '/settings',
name: 'Settings',
Expand Down
20 changes: 19 additions & 1 deletion src/services/CountService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,26 @@ const getInventoryCountImportSession = async (params: { workEffortId: string; in
});
}

const bulkUploadInventoryCounts = async (payload: any): Promise <any> => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The payload and return types for bulkUploadInventoryCounts (and fetchCycleCountImportSystemMessages on line 39) are typed as any. Using any undermines the benefits of TypeScript. Please define specific interfaces for payloads and response data to improve type safety and code maintainability.

return api({
url: `inventory-cycle-count/cycleCounts/upload`,
method: "post",
...payload
});
}

const fetchCycleCountImportSystemMessages = async (payload: any): Promise <any> => {
return api({
url: `inventory-cycle-count/cycleCounts/systemMessages`,
method: "get",
params: payload
});
}

export const CountService = {
getAssignedWorkEfforts,
getInventoryCountImportsByWorkEffort,
getInventoryCountImportSession
getInventoryCountImportSession,
bulkUploadInventoryCounts,
fetchCycleCountImportSystemMessages
}
20 changes: 20 additions & 0 deletions src/store/modules/count/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,26 @@ const actions: ActionTree<CountState, RootState> = {

commit(types.COUNT_ASSIGNED_WORK_EFFORTS_UPDATED, { assignedWorkEfforts, total, isScrollable })
},
async fetchCycleCountImportSystemMessages({commit} ,payload) {
let systemMessages;
try {
const twentyFourHoursEarlier = DateTime.now().minus({ hours: 24 });
const resp = await CountService.fetchCycleCountImportSystemMessages({
systemMessageTypeId: "ImportInventoryCounts",
initDate_from: twentyFourHoursEarlier.toMillis(),
orderByField: 'initDate desc, processedDate desc',
pageSize: 100
})
if (!hasError(resp)) {
systemMessages = resp.data
} else {
throw resp.data;
}
} catch (err: any) {
logger.error(err)
}
commit(types.COUNT_IMPORT_SYSTEM_MESSAGES_UPDATED, systemMessages)
},
Comment on lines +62 to +81

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

In the fetchCycleCountImportSystemMessages action, if an API error occurs, systemMessages will remain undefined. Committing undefined to the store will cause a runtime error in BulkUpload.vue when systemMessages.length is accessed. To prevent this, initialize systemMessages to an empty array. Also, the payload parameter is unused and can be removed.

Suggested change
async fetchCycleCountImportSystemMessages({commit} ,payload) {
let systemMessages;
try {
const twentyFourHoursEarlier = DateTime.now().minus({ hours: 24 });
const resp = await CountService.fetchCycleCountImportSystemMessages({
systemMessageTypeId: "ImportInventoryCounts",
initDate_from: twentyFourHoursEarlier.toMillis(),
orderByField: 'initDate desc, processedDate desc',
pageSize: 100
})
if (!hasError(resp)) {
systemMessages = resp.data
} else {
throw resp.data;
}
} catch (err: any) {
logger.error(err)
}
commit(types.COUNT_IMPORT_SYSTEM_MESSAGES_UPDATED, systemMessages)
},
async fetchCycleCountImportSystemMessages({commit}) {
let systemMessages = [];
try {
const twentyFourHoursEarlier = DateTime.now().minus({ hours: 24 });
const resp = await CountService.fetchCycleCountImportSystemMessages({
systemMessageTypeId: "ImportInventoryCounts",
initDate_from: twentyFourHoursEarlier.toMillis(),
orderByField: 'initDate desc, processedDate desc',
pageSize: 100
})
if (!hasError(resp)) {
systemMessages = resp.data
} else {
throw resp.data;
}
} catch (err: any) {
logger.error(err)
}
commit(types.COUNT_IMPORT_SYSTEM_MESSAGES_UPDATED, systemMessages)
},

setCountDetailPageActive({ commit }, isPageActive) {
commit(types.COUNT_DETAIL_PAGE_ACTIVE_UPDATED, isPageActive);
}
Expand Down
3 changes: 3 additions & 0 deletions src/store/modules/count/getters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const getters: GetterTree<CountState, RootState> = {
},
isCountDetailPageActive(state) {
return state.isCountDetailPageActive;
},
getCycleCountImportSystemMessages(state) {
return state.cycleCountImportSystemMessages
}
};

Expand Down
3 changes: 2 additions & 1 deletion src/store/modules/count/mutation-types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const SN_COUNT = "count"
export const COUNT_DETAIL_PAGE_ACTIVE_UPDATED = SN_COUNT + '/DETAIL_PAGE_ACTIVE_UPDATED'
export const COUNT_ASSIGNED_WORK_EFFORTS_UPDATED = SN_COUNT + "/ASSIGNED_WORK_EFFORTS_UPDATED"
export const COUNT_ASSIGNED_WORK_EFFORTS_UPDATED = SN_COUNT + "/ASSIGNED_WORK_EFFORTS_UPDATED"
export const COUNT_IMPORT_SYSTEM_MESSAGES_UPDATED = SN_COUNT + "/IMPORT_SYSTEM_MESSAGES_UPDATED"
3 changes: 3 additions & 0 deletions src/store/modules/count/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ const mutations: MutationTree <CountState> = {
state.assignedWorkEfforts = payload.assignedWorkEfforts
state.total = payload.total
state.isScrollable = payload.isScrollable;
},
[types.COUNT_IMPORT_SYSTEM_MESSAGES_UPDATED] (state, payload) {
state.cycleCountImportSystemMessages = payload
}
}
export default mutations;
Loading