Skip to content

Facilities refactored - #414

Closed
Banibrata-Manna wants to merge 7 commits into
hotwax:mainfrom
Banibrata-Manna:facilities-refactored
Closed

Banibrata-Manna wants to merge 7 commits into
hotwax:mainfrom
Banibrata-Manna:facilities-refactored

Conversation

@Banibrata-Manna

Copy link
Copy Markdown

Related Issues

Short Description and Why It's Useful

Screenshots of Visual Changes before/after (If There Are Any)

Contribution and Currently Important Rules Acceptance

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request migrates the application from Vue CLI and Vuex to Vite and Pinia, refactoring components to the <script setup> syntax and updating environment variable and API call structures. The code review identified several critical and high-severity issues across the changes. Notably, a runtime TypeError was found in Image.vue due to component name shadowing of the global Image constructor, and a missing baseURL in the createFacility store action. Other issues include insufficient error checking on Promise.allSettled results, potential input validation lag, missing error notifications for phone number updates, a potential runtime crash in Settings.vue due to missing optional chaining on userProfile, and a lack of validation for calendar selection before saving operating hours.

Comment thread src/components/Image.vue
Comment on lines +15 to +22
function checkIfImageExists(src: string) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(true);
img.onerror = () => reject(false);
img.src = src;
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

In Vue 3 <script setup>, the component name is inferred from the filename (Image.vue), which shadows the global Image constructor. Calling new Image() will attempt to instantiate the Vue component as a constructor, throwing a TypeError: Image is not a constructor at runtime. Use new window.Image() or document.createElement('img') to reference the global HTMLImageElement constructor.

function checkIfImageExists(src: string) {
  return new Promise((resolve, reject) => {
    const img = new window.Image();
    img.onload = () => resolve(true);
    img.onerror = () => reject(false);
    img.src = src;
  });
}

Comment thread src/store/facility.ts
Comment on lines +54 to +60
async createFacility(payload: any) {
return api({
url: "admin/facilities",
method: "post",
data: payload
})
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The api call is missing the baseURL parameter. Since other actions in this store explicitly define baseURL: commonUtil.getOmsURL(), omitting it here will cause these requests to target the wrong endpoint or fail entirely if the default baseURL is different. Please ensure all api calls in this store consistently specify the baseURL.

    async createFacility(payload: any) {
      return api({
        baseURL: commonUtil.getOmsURL(),
        url: "admin/facilities",
        method: "post",
        data: payload
      })
    },

Comment on lines +134 to +135
const responses = await Promise.allSettled([...removePromises, ...addPromises]);
const hasFailed = responses.some((response: any) => response.status === 'rejected');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Checking only response.status === 'rejected' is insufficient when using Promise.allSettled because API calls that return a 200 OK with an error payload (common in OFBiz/HotWax OMS) will resolve successfully and be marked as 'fulfilled'. To correctly identify failures, you must also check commonUtil.hasError(response.value).

  const responses = await Promise.allSettled([...removePromises, ...addPromises]);
  const hasFailed = responses.some((response: any) => 
    response.status === 'rejected' || 
    (response.status === 'fulfilled' && commonUtil.hasError(response.value))
  );

Comment on lines +147 to +160
function validateFacilityGroupId(event: any) {
const value = event.target.value;
if (!facilityGroupIdInput.value) return;

const el = facilityGroupIdInput.value.$el;
el.classList.remove('ion-valid');
el.classList.remove('ion-invalid');

if (value === '') return;

formData.value.facilityGroupId.length <= 20
? el.classList.add('ion-valid')
: el.classList.add('ion-invalid');
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Validating input length using the potentially stale formData property instead of the immediate value from the event can lead to validation lag. Since @ionInput fires before v-model might have fully propagated the update to the reactive state, using value.length is much safer and more immediate.

function validateFacilityGroupId(event: any) {
  const value = event.target.value;
  if (!facilityGroupIdInput.value) return;

  const el = facilityGroupIdInput.value.$el;
  el.classList.remove('ion-valid');
  el.classList.remove('ion-invalid');

  if (value === '') return;

  value.length <= 20
    ? el.classList.add('ion-valid')
    : el.classList.add('ion-invalid');
}

Comment on lines +160 to +187
async function saveTelecomNumber() {
let resp = {} as any;
const payload = {
facilityId: props.facilityId,
contactMechPurposeTypeId: 'PRIMARY_PHONE',
contactNumber: telecomNumberValue.value.contactNumber.trim(),
countryCode: telecomNumberValue.value.countryCode.replace('+', '')
};

modalController.dismiss({ postalAddress })
emitter.emit('dismissLoader')
},
async saveTelecomNumber() {
let resp = {} as any;
try {
if (contactDetails.value.telecomNumber?.contactMechId) {
resp = await facilityStore.updateFacilityTelecomNumber({
...payload,
contactMechId: contactDetails.value.telecomNumber.contactMechId,
});
} else {
resp = await useFacilityStore().createFacilityTelecomNumber(payload);
}

const payload = {
facilityId: this.facilityId,
contactMechPurposeTypeId: 'PRIMARY_PHONE',
contactNumber: this.telecomNumberValue.contactNumber.trim(),
countryCode: this.telecomNumberValue.countryCode.replace('+', '')
}
if (!commonUtil.hasError(resp)) {
await facilityStore.fetchFacilityContactDetailsAndTelecom({ facilityId: props.facilityId });
} else {
throw resp.data;
}
} catch (err) {
logger.error(err);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

If updating the phone number fails, the error is caught and logged, but no toast notification is shown to the user. The modal dismisses and shows a success message for the address update, leading to a misleading user experience where some updates silently failed. Please show a toast notification on failure.

async function saveTelecomNumber() {
  let resp = {} as any;
  const payload = { 
    facilityId: props.facilityId,
    contactMechPurposeTypeId: 'PRIMARY_PHONE',
    contactNumber: telecomNumberValue.value.contactNumber.trim(),
    countryCode: telecomNumberValue.value.countryCode.replace('+', '')
  };

  try {
    if (contactDetails.value.telecomNumber?.contactMechId) {
      resp = await facilityStore.updateFacilityTelecomNumber({
        ...payload,
        contactMechId: contactDetails.value.telecomNumber.contactMechId,
      });
    } else {
      resp = await useFacilityStore().createFacilityTelecomNumber(payload);
    }

    if (!commonUtil.hasError(resp)) {
      await facilityStore.fetchFacilityContactDetailsAndTelecom({ facilityId: props.facilityId });
    } else {
      throw resp.data;
    }
  } catch (err) {
    commonUtil.showToast(translate("Failed to update phone number."));
    logger.error(err);
  }
}

Comment thread src/views/Settings.vue
Comment on lines 19 to 22
<ion-card-header class="ion-no-padding ion-padding-vertical">
<ion-card-subtitle>{{ userProfile?.userLoginId }}</ion-card-subtitle>
<ion-card-title>{{ userProfile?.partyName }}</ion-card-title>
<ion-card-subtitle>{{ userProfile.username }}</ion-card-subtitle>
<ion-card-title>{{ userProfile?.userFullName }}</ion-card-title>
</ion-card-header>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

userProfile is initialized as {} and can be empty or null. Accessing userProfile.username directly without optional chaining can cause a runtime crash, whereas userProfile?.userFullName is correctly guarded. Please use optional chaining for username as well.

            <ion-card-header class="ion-no-padding ion-padding-vertical">
              <ion-card-subtitle>{{ userProfile?.username }}</ion-card-subtitle>
              <ion-card-title>{{ userProfile?.userFullName }}</ion-card-title>
            </ion-card-header>

Comment on lines +103 to +112
async function addOperatingHours() {
emitter.emit('presentLoader');

try {
const resp = await facilityStore.associateCalendarToFacility({
facilityId: props.facilityId,
calendarId: selectedCalendarId.value,
fromDate: DateTime.now().toMillis(),
facilityCalendarTypeId: 'OPERATING_HOURS'
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

If the user clicks "Save" without selecting any calendar, selectedCalendarId.value will be empty/undefined, leading to an invalid API call. Please add a validation check to ensure a calendar is selected before proceeding.

async function addOperatingHours() {
  if (!selectedCalendarId.value) {
    commonUtil.showToast(translate("Please select a calendar."));
    return;
  }
  emitter.emit('presentLoader');

  try {
    const resp = await facilityStore.associateCalendarToFacility({
      facilityId: props.facilityId,
      calendarId: selectedCalendarId.value,
      fromDate: DateTime.now().toMillis(),
      facilityCalendarTypeId: 'OPERATING_HOURS'
    });

@patelanil patelanil left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Log message format review

I reviewed the log/error messages added in this PR against our best practice: log-messages.md.

These added messages do not follow the [Entity] [Context] - [Action/Outcome/Issue] pattern:

  1. Failed to add group to facility
    Suggest: Facility - Failed to add group
  2. Failed to remove group from facility
    Suggest: Facility - Failed to remove group
  3. Failed to find facility groups
    Suggest: Facility - Failed to find facility groups
  4. Failed to create facility location
    Suggest: Facility - Failed to create location
  5. Failed to update facility location
    Suggest: Facility - Failed to update location
  6. Failed to create external mapping
    Suggest: Facility - Failed to create external mapping
  7. Unable to find the latitude and longitude for the entered zip code.
    Suggest: Facility - Unable to find latitude and longitude for the entered zip code
  8. Failed to remove party from facility
    Suggest: Facility - Failed to remove party

...and 36 more in the same pattern.

Dynamic IDs belong in the [Context] block and the action text should stay constant, so messages group and count cleanly in Grafana/Loki during log analysis. Please align these before merge.

@ymaheshwari1

Copy link
Copy Markdown
Contributor

Closing as this has been moved to company app and thus not required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants