From 16296c7423a5a33e96b47a8fb9be1ff39ed61525 Mon Sep 17 00:00:00 2001 From: Bill Richardson Date: Fri, 29 Sep 2023 09:43:35 -0400 Subject: [PATCH 01/34] Mobile Options based on iTAC flag flag should include !(no comp) as well based on coverage statement page. --- src/constants/schedule-constants.js | 1 + .../coverage-statement/coverage-statement.vue | 13 +---------- .../appointment-type-question.vue | 22 +++++++++++++++---- .../mobile-location-modal-questions.vue | 5 +++++ .../service-location/service-location.vue | 10 +++++++-- .../shop-question/shop-question.vue | 3 ++- src/store/index.js | 5 +++++ 7 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/constants/schedule-constants.js b/src/constants/schedule-constants.js index eaa0d5fa..a728f1d4 100644 --- a/src/constants/schedule-constants.js +++ b/src/constants/schedule-constants.js @@ -1,6 +1,7 @@ const AppointmentTypeStrings = { IN_SHOP: 'Inshop', MOBILE: 'Mobile', + MOBILE_INSURANCE: 'Mobile-Insurance', DROP_OFF: 'Dropoff' }; const PREMIUM_FEE_PART_TYPE = 'EARLY BIRD'; diff --git a/src/layouts/coverage-statement/coverage-statement.vue b/src/layouts/coverage-statement/coverage-statement.vue index 9166c994..f1bc7677 100644 --- a/src/layouts/coverage-statement/coverage-statement.vue +++ b/src/layouts/coverage-statement/coverage-statement.vue @@ -140,8 +140,6 @@ export default { async beforeRouteEnter(to, from, next) { // Call APIs const cmsContentPromise = fetchCmsContentForPage(to?.query?.issPage); - const wipersPromise = await useMainStore().getWipers(); - const rainDefensePromise = await useMainStore().getRainDefense(); const supportingItemsPromise = await useMainStore().getSupportingItems(); // Settle promises and get results @@ -150,14 +148,6 @@ export default { resultKey: 'cmsContent', promise: cmsContentPromise }, - { - resultKey: 'wipers', - promise: wipersPromise - }, - { - resultKey: 'rainDefense', - promise: rainDefensePromise - }, { resultKey: 'supportingItems', promise: supportingItemsPromise @@ -169,9 +159,7 @@ export default { ? JSON.parse(JSON.stringify(useMainStore().order.lineItems.glassParts)) : []; const availableLineItems = [ - resultMap.rainDefense, ...(resultMap.supportingItems ?? []), - ...(resultMap.wipers ?? []), ...(clonedGlassParts ?? []) ]; @@ -357,6 +345,7 @@ export default { return this.navigateForward(); }, async navigateForward() { + useMainStore().updatePolicyITACFlag(this.verifiedITAC); if (this.unverified || this.verifiedDeductible) { this.mainStore.saveSupportingItems(this.supportingItems); this.$router.navigate( diff --git a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue index 25e7a529..c8eb4d56 100644 --- a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue +++ b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue @@ -51,13 +51,15 @@ export default { return this.getCmsContent(this.cmsWidgetName, 'Answers'); }, answersToDisplay() { - const shouldShowMobile = this.isServiceableMobile; + const shouldShowMobile = this.isServiceableMobile && this.isITAC; + const shouldShowMobileInsurance = this.isServiceableMobile && !this.isITAC; const shouldShowInshop = this.isServiceableInshop; const shouldShowDropoff = this.isServiceableInshop && !useMainStore().damage.isRepair; return this.answersFromCms ? this.answersFromCms.filter((answer) => ( (answer.Name === AppointmentTypeStrings.IN_SHOP && shouldShowInshop) || (answer.Name === AppointmentTypeStrings.MOBILE && shouldShowMobile) + || (answer.Name === AppointmentTypeStrings.MOBILE_INSURANCE && shouldShowMobileInsurance) || (answer.Name === AppointmentTypeStrings.DROP_OFF && shouldShowDropoff) )) : []; @@ -70,6 +72,9 @@ export default { this.$emit('update:modelValue', newValue); } }, + isITAC() { + return useMainStore().policy.isITAC; + }, isMobileOnly() { return this.isServiceableMobile && !this.isServiceableInshop; } @@ -80,9 +85,14 @@ export default { // If there is only one option to display and that option is 'Mobile' then select it if ( newValue.length === 1 - && newValue.findIndex((answer) => answer.Name === 'Mobile') !== -1 + && newValue.findIndex((answer) => (answer.Name === AppointmentTypeStrings.MOBILE + || answer.Name === AppointmentTypeStrings.MOBILE_INSURANCE)) !== -1 ) { - this.selectedValues = 'Mobile'; + if (this.isITAC) { + this.selectedValues = AppointmentTypeStrings.MOBILE; + } else { + this.selectedValues = AppointmentTypeStrings.MOBILE_INSURANCE; + } } }, immediate: true @@ -90,7 +100,11 @@ export default { isMobileOnly: { handler(newValue) { if (newValue) { - this.selectedValues = 'Mobile'; + if (this.isITAC) { + this.selectedValues = AppointmentTypeStrings.MOBILE; + } else { + this.selectedValues = AppointmentTypeStrings.MOBILE_INSURANCE; + } } } } diff --git a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue index d6d850b6..3829dd76 100644 --- a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue +++ b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue @@ -31,6 +31,7 @@ @@ -81,6 +82,7 @@ import addressQuestions from '@/iss-components/address-questions/address-questio import vehicleProtectedQuestion from '@/layouts/service-location/mobile-location-modal-questions/vehicle-protected-question/vehicle-protected-question.vue'; // Helpers +import { useMainStore } from '@/store'; import { getPricedMobileFeePart, getServiceabilityDetails, @@ -167,6 +169,9 @@ export default { }; }, computed: { + isITAC() { + return useMainStore().policy.isITAC; + }, mobileLocationLinkPromptText() { return this.getCmsContent(this.linkWidgetName, 'HeaderText'); }, diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue index 75b735f7..d0b8af66 100644 --- a/src/layouts/service-location/service-location.vue +++ b/src/layouts/service-location/service-location.vue @@ -59,7 +59,7 @@ cmsWidgetName="AppointmentTypeQuestionWidget" validationRules="option-required" /> + From 037ff1eb2a65f0df44aecd518bce0f8ba43e0944 Mon Sep 17 00:00:00 2001 From: Bill Richardson Date: Fri, 29 Sep 2023 20:46:25 -0400 Subject: [PATCH 08/34] preReq Tests --- .../schedule-page/schedule-page.spec.js | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 src/layouts/schedule-page/schedule-page.spec.js diff --git a/src/layouts/schedule-page/schedule-page.spec.js b/src/layouts/schedule-page/schedule-page.spec.js new file mode 100644 index 00000000..1e348080 --- /dev/null +++ b/src/layouts/schedule-page/schedule-page.spec.js @@ -0,0 +1,184 @@ +// Components +import schedule from '@/layouts/schedule-page/schedule-page.vue'; + +// Supporting Files +import { createTestingPinia } from '@pinia/testing'; +import { shallowMount } from '@vue/test-utils'; +import { getMountOptions } from '@/helpers/unit-test-helper.js'; +import { useMainStore } from '@/store/index.js'; + +const mockMixin = { + methods: { + getCmsContent: jest.fn().mockImplementation(() => ''), + setCmsContent: jest.fn() + } +}; + +const footerStub = { + render: () => {}, + methods: { + updateButtonText: jest.fn() + } +}; + +const loadingModalStub = { + render: () => {}, + methods: { + showModal: jest.fn(), + hideModal: jest.fn() + } +}; + +function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) { + const mountOptions = getMountOptions({ + router: { + navigate: jest.fn() + } + }); + + // mountOptions.global.mocks["$store"] = store; + + mountOptions.global.stubs = { + siteFooter: footerStub, + siteHeader: true, + recalModal: true, + contentGroupModal: true, + alert: true, + loadingModal: loadingModalStub + }; + + methodToRun(); + + mountOptions.mixins = [mockMixin]; + mountOptions.data = () => ( + initialData + ); + + const wrapper = shallowMount(schedule, mountOptions); + return { wrapper }; +} + +beforeEach(() => { + const testingPinia = createTestingPinia({ + initialState: { + main: { + order: { + schedule: { + date: '2019-01-01', + startTime: '09:00', + endTime: '10:00', + routeCode: '000' + }, + lineItems: { + glassParts: [ + { + partNumber: 'ABC123' + } + ], + supportingItems: [] + }, + serviceLocation: { + appointmentType: 'Inshop', + zipCode: '12345', + zipCodeCtu: '01234', + provider: { + providerNumber: '123' + } + }, + damage: { + isRepair: false + }, + referralNumber: '1234567' + }, + payment: { + isInsurance: true + }, + lineItems: { + glassParts: [], + supportingItems: [] + } + } + } + }); + useMainStore(testingPinia); +}); + +describe('schedule-page.vue', () => { + describe('Initial Load', () => { + test('Should pass arePagePrerequisitesValid with a mobile order and no providerNumber', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order.serviceLocation.appointmentType = 'Mobile'; + wrapper.vm.mainStore.order.serviceLocation.provider = null; + + // Act + const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(arePagePrerequisitesValid).toBe(true); + }); + test('Should pass arePagePrerequisitesValid with an inshop order and providerNumber', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + + // Act + const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(arePagePrerequisitesValid).toBe(true); + }); + test('Should fail arePagePrerequisitesValid with an inshop order and no providerNumber', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order.serviceLocation.provider.providerNumber = null; + + // Act + const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(arePagePrerequisitesValid).toBeFalsy(); + }); + test('Should fail arePagePrerequisitesValid without isInsurance', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.payment.isInsurance = null; + + // Act + const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(arePagePrerequisitesValid).toBe(false); + }); + test('Should fail arePagePrerequisitesValid if supportingItems is null', async () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order.lineItems.supportingItems = null; + + // Act + const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(arePagePrerequisitesValid).toBe(false); + }); + test('Should fail arePagePrerequisitesValid with an replace with no glass parts', async () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order.lineItems.glassParts = []; + + // Act + const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(arePagePrerequisitesValid).toBe(false); + }); + }); + describe('Rendering', () => { + test('Schedule page loads', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + + // Assert + expect(wrapper).toBeTruthy(); + }); + }); +}); From 3991bc52af2f87c1df651366c38be183d07c2ac0 Mon Sep 17 00:00:00 2001 From: Bill Richardson Date: Mon, 2 Oct 2023 15:40:47 -0400 Subject: [PATCH 09/34] Location Alerts on schedule page Mocked result Linting on shop question. --- src/constants/endpoints.js | 4 ++ .../schedule-page/helpers/schedule-helper.js | 30 ++++++++++ .../location-alerts/location-alerts.vue | 57 +++++++++++++++++++ src/layouts/schedule-page/schedule-page.vue | 18 +++++- .../shop-question/shop-question.vue | 6 +- src/store/index.js | 10 ++++ src/styles/ux-variables-svg-strings.scss | 1 + 7 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 src/layouts/schedule-page/helpers/schedule-helper.js create mode 100644 src/layouts/schedule-page/location-alerts/location-alerts.vue create mode 100644 src/styles/ux-variables-svg-strings.scss diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index a34d8990..162d90bf 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -11,6 +11,10 @@ const endpoints = Object.freeze({ url: (applicationAbbreviation, pageName) => `/content/api/v1/content/${applicationAbbreviation}/${pageName}`, method: 'GET' }, + GetAlertReasons: { + url: '/location/api/v1/location/alert-reasons', + method: 'GET' + }, GetVehicleYears: { url: '/vehicle/api/v1/vehicle/years', method: 'GET' diff --git a/src/layouts/schedule-page/helpers/schedule-helper.js b/src/layouts/schedule-page/helpers/schedule-helper.js new file mode 100644 index 00000000..7347bfca --- /dev/null +++ b/src/layouts/schedule-page/helpers/schedule-helper.js @@ -0,0 +1,30 @@ +import { useMainStore } from '@/store'; + +const getAlertReasons = async (ctu) => { + const store = useMainStore(); + const alertReasons = await store.getAlertReasonsByCtu(ctu); + + return Promise.resolve(alertReasons); +}; + +// This function will go away after testing. +// The Service already went through testing and had these removed from it. +export async function mockGetAlertReasons(ctu) { + let retList = []; + if (ctu === '01853') { // Ocala, FL 34470 + retList = [ + 'Hurricane', 'ExtremeTemperature' + ]; + } else if (ctu === '01814') { // Phoenix, AZ 85026 + retList = [ + 'ExtremeTemperature' + ]; + } else if (ctu === '01845') { // Raleigh, NC 27601 + retList = [ + 'Hurricane' + ]; + } + return Promise.resolve(retList); +} + +export default getAlertReasons; diff --git a/src/layouts/schedule-page/location-alerts/location-alerts.vue b/src/layouts/schedule-page/location-alerts/location-alerts.vue new file mode 100644 index 00000000..46a84e5c --- /dev/null +++ b/src/layouts/schedule-page/location-alerts/location-alerts.vue @@ -0,0 +1,57 @@ + + diff --git a/src/layouts/schedule-page/schedule-page.vue b/src/layouts/schedule-page/schedule-page.vue index 9d6116ec..8ba01a3e 100644 --- a/src/layouts/schedule-page/schedule-page.vue +++ b/src/layouts/schedule-page/schedule-page.vue @@ -18,6 +18,9 @@ marginTopSizeOverride="1" />
+ { vm.setCmsContent(resultMap.cmsContent); + vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons); }); }, setup() { diff --git a/src/layouts/service-location/shop-question/shop-question.vue b/src/layouts/service-location/shop-question/shop-question.vue index 53786682..220f4ad4 100644 --- a/src/layouts/service-location/shop-question/shop-question.vue +++ b/src/layouts/service-location/shop-question/shop-question.vue @@ -36,7 +36,7 @@ :text="showMoreShopsLinkText" href="#!" :aria-label="showMoreShopsLinkText" - @click-event="getNextShopsFromList" /> + @clickEvent="getNextShopsFromList" />
@@ -254,6 +254,8 @@ export default { diff --git a/src/digital-components/date-picker/mixins/constants.js b/src/digital-components/date-picker/mixins/constants.js new file mode 100644 index 00000000..70d4d383 --- /dev/null +++ b/src/digital-components/date-picker/mixins/constants.js @@ -0,0 +1,26 @@ +const TIMINGFUNC_MAP = { + linear: (t) => t, + 'ease-in': (t) => t * t, + 'ease-out': (t) => t * (2 - t), + 'ease-in-out': (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t) +}; +const BUFFER_OFFSET = 10; + +const MONTHS_OF_YEAR = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' +]; + +const DAYS_OF_WEEK = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + +export { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR, DAYS_OF_WEEK }; diff --git a/src/digital-components/date-picker/mixins/helpers.js b/src/digital-components/date-picker/mixins/helpers.js new file mode 100644 index 00000000..77c61fc9 --- /dev/null +++ b/src/digital-components/date-picker/mixins/helpers.js @@ -0,0 +1,10 @@ +const selectableDaysOptions = Object.freeze({ + CUSTOM: 'custom', + PAST: 'past' +}); + +const requiredParameter = () => { + throw new Error('parameter is required'); +}; + +export { selectableDaysOptions, requiredParameter }; diff --git a/src/helpers/date-helper.js b/src/helpers/date-helper.js new file mode 100644 index 00000000..ed943174 --- /dev/null +++ b/src/helpers/date-helper.js @@ -0,0 +1,12 @@ +const getDateDifferenceInDays = (startDate, endDate) => { + const date1 = new Date(endDate); + date1.setHours(0, 0, 0, 0); + const date2 = new Date(startDate); + date2.setHours(0, 0, 0, 0); + // To calculate the time difference of two dates + const DifferenceInTime = date1.getTime() - date2.getTime(); + // To calculate the no. of days between two dates + return DifferenceInTime / (1000 * 3600 * 24); +}; + +export default getDateDifferenceInDays; diff --git a/src/helpers/service-location-helper.js b/src/helpers/service-location-helper.js index a8ac5503..63f4c711 100644 --- a/src/helpers/service-location-helper.js +++ b/src/helpers/service-location-helper.js @@ -46,15 +46,7 @@ export async function getAvailabilityRating( providerNumber ) { // For a given shop provider number and date range, get the appointment time slots available - const shopTimeSlots = await useMainStore().getShopTimeSlots( - { - providerNumber, - startDate, - endDate, - shopAppointmentType - }, - false - ); + const shopTimeSlots = await useMainStore().getShopTimeSlots(startDate, endDate, shopAppointmentType, providerNumber); const numberOfDaysToEvaluate = 2; const isGoodAvailability = diff --git a/src/layouts/schedule-page/helpers/schedule-helper.js b/src/layouts/schedule-page/helpers/schedule-helper.js index 7347bfca..6f41d306 100644 --- a/src/layouts/schedule-page/helpers/schedule-helper.js +++ b/src/layouts/schedule-page/helpers/schedule-helper.js @@ -27,4 +27,68 @@ export async function mockGetAlertReasons(ctu) { return Promise.resolve(retList); } +export function calcDaysBetweenDates(dateString1, dateString2) { + const date1 = new Date(dateString1); + const date2 = new Date(dateString2); + const timeDifference = Math.abs(date2 - date1); // Calculate the time difference in milliseconds + return Math.ceil(timeDifference / (1000 * 60 * 60 * 24)); // Convert milliseconds to days +} + +export function convertDateToDateString(date) { + // returns YYYY-MM-DD format + if (date instanceof Date !== true) return null; + return ( + `${date.getFullYear() + }-${ + (`0${date.getMonth() + 1}`).slice(-2) + }-${ + (`0${date.getDate()}`).slice(-2)}` + ); +} + +export function convertDateStringToDate(dateString) { + // dateString must be YYYY-MM-DD format + if (typeof dateString !== 'string') return null; + const dateParts = dateString.split('-'); + return new Date(dateParts[0], parseInt(dateParts[1], 10) - 1, dateParts[2]); +} + +export function sumDateString(dateString, daysToAdd) { + // dateString must be YYYY-MM-DD format + if (typeof dateString !== 'string') return null; + const date = convertDateStringToDate(dateString); + date.setDate(date.getDate() + daysToAdd); + return convertDateToDateString(date); +} + +export function militaryToTwelveHourTime(timeString) { + // Expected input: "HH:MM" + if (typeof timeString !== 'string') return null; + let hours = parseInt(timeString.split(':')[0], 10); + const minutes = timeString.split(':')[1]; + const meridianNotation = hours > 11 ? 'PM' : 'AM'; + + if (hours > 12) { + hours -= 12; + } + + return `${hours}:${minutes} ${meridianNotation}`; +} + +export function getDisplayTextForDurationLength(durationMinimum, durationMaximum) { + const isLongAppointment = durationMaximum >= 120; + const isDurationRange = durationMinimum !== durationMaximum; + + const adjustedMinimum = isLongAppointment ? durationMinimum / 60 : durationMinimum; + const adjustedMaximum = isLongAppointment ? durationMaximum / 60 : durationMaximum; + + const durationText = isDurationRange + ? `${adjustedMinimum} - ${adjustedMaximum}` + : adjustedMinimum; + + const unitText = isLongAppointment ? 'hours' : 'minutes'; + + return `${durationText} ${unitText}`; +} + export default getAlertReasons; diff --git a/src/layouts/schedule-page/schedule-page.vue b/src/layouts/schedule-page/schedule-page.vue index 8ba01a3e..62f8f000 100644 --- a/src/layouts/schedule-page/schedule-page.vue +++ b/src/layouts/schedule-page/schedule-page.vue @@ -21,6 +21,15 @@ + { + if (value?.timeSlot?.routeCode == null) { + return errorMessages.DATE_REQUIRED; + } + return true; +}); + +// Define constants +const TIME_SLOTS_CALL_DAYS_LIMIT = 34; // needs to be 34 for API limits (35 does not consistently work) + +const getAvailableDates = async ( + startDateString, + endDateString, + appointmentType, + providerNumber +) => { + const apiEndDateLimit = sumDateString(startDateString, TIME_SLOTS_CALL_DAYS_LIMIT); + const difference = calcDaysBetweenDates(startDateString, endDateString); + const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT); + const storeActionConfigs = []; + const timeSlotsData = {}; + timeSlotsData.days = []; + let apiStartDate = startDateString; + let apiEndDate = endDateString; + + for (let i = 1; i <= apiCallsCount; i++) { + let storeActionConfig; + + if (i > 1) { + apiStartDate = sumDateString(apiEndDate, 1); + apiEndDate = sumDateString(apiStartDate, TIME_SLOTS_CALL_DAYS_LIMIT); + + if (i === apiCallsCount) { + apiEndDate = endDateString; + } + } else if (apiEndDate > apiEndDateLimit) { + apiEndDate = apiEndDateLimit; + } + + if (appointmentType === AppointmentTypeStrings.MOBILE) { + storeActionConfig = { + storeAction: GET_MOBILE_TIME_SLOTS, + payload: { + startDate: apiStartDate, + endDate: apiEndDate + } + }; + } else { + storeActionConfig = { + storeAction: GET_SHOP_TIME_SLOTS, + payload: { + startDate: apiStartDate, + endDate: apiEndDate, + shopAppointmentType: appointmentType, + providerNumber + } + }; + } + if (apiStartDate < apiEndDate) storeActionConfigs.push(storeActionConfig); + } + + const timeSlotsResponsesData = { + days: [] + }; + + function compareDayStrings(a, b) { + if (a.date < b.date) return -1; + if (a.date > b.date) return 1; + return 0; + } + + const makeParallelCalls = async () => { + await Promise.all(storeActionConfigs.map(async (storeAction) => { + let timeSlotsResponse = null; + if (storeAction.storeAction === GET_SHOP_TIME_SLOTS) { + timeSlotsResponse = await useMainStore().getShopTimeSlots( + storeAction.payload.startDate, + storeAction.payload.endDate, + storeAction.payload.appointmentType, + storeAction.payload.providerNumber + ); + } else { + timeSlotsResponse = await useMainStore().getMobileTimeSlots(storeAction.payload.startDate, storeAction.payload.endDate); + } + + timeSlotsResponsesData.estimatedServiceMinutesMinimum = timeSlotsResponse.data.estimatedServiceMinutesMinimum; + timeSlotsResponsesData.estimatedServiceMinutesMaximum = timeSlotsResponse.data.estimatedServiceMinutesMaximum; + timeSlotsResponsesData.days = [ + ...timeSlotsResponsesData.days, + ...timeSlotsResponse.data.days + ]; + })); + }; + + return makeParallelCalls().then(() => { + // sort days chronologically + timeSlotsResponsesData.days.sort(compareDayStrings); + return timeSlotsResponsesData; + }); +}; + export default { name: 'schedule-page', components: { siteHeader, siteSubHeader, locationAlerts, + datePicker, siteFooter, textBlock, // eslint-disable-next-line vue/no-reserved-component-names @@ -62,8 +189,20 @@ export default { mixins: [BaseFormMixin], async beforeRouteEnter(to, from, next) { // Call APIs + let preSelectedDate = await useMainStore().order.schedule.date; + if (!preSelectedDate || preSelectedDate.startTime === null) { + preSelectedDate = null; + } + const cmsContentPromise = fetchCmsContentForPage(to.query.issPage); + const datePickerInitialDataPromise = await datePicker.methods.loadInitialData({ + selectableDatesSetting: 'custom', + initialViewRowsToShow: 5, + customSelectableDatesCallback: getAvailableDates, + preSelectedDate + }); + const alertReasonsPromise = locationAlerts.methods.loadInitialData( useMainStore().order.serviceLocation.zipCodeCtu, useMainStore().order.serviceLocation.provider?.address?.zipCodeCtu @@ -78,6 +217,10 @@ export default { { resultKey: 'alertReasons', promise: alertReasonsPromise + }, + { + resultKey: 'datePickerInitialData', + promise: datePickerInitialDataPromise } ]; @@ -85,7 +228,10 @@ export default { const resultMap = await settleAllPromises(promiseResultMap); next((vm) => { vm.setCmsContent(resultMap.cmsContent); + vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData); vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons); + vm.setData(resultMap.datePickerInitialData.initialShopTimeSlotsResponse); + vm.updateFooterButtonText(vm.selectedTimeSlotInfo); }); }, setup() { @@ -100,8 +246,38 @@ export default { return this.getCmsContent('ChangeShopLink', 'Text'); }, ChangeShopLink() { - // Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed return this.splitCopyOnCMSPlaceHolder(this.ChangeShopLinkText); + }, + appointmentType() { + return this.mainStore.order.serviceLocation.appointmentType; + }, + timeSlotsForSelectedDate() { + if (!this.selectedDate) { + return null; + } + + return this.selectableDatesData.days?.find((selectableDate) => selectableDate.date === this.selectedDate); + } + }, + watch: { + selectedDate(newValue, oldValue) { + // Clear time slot selection if date selected changes + if (newValue !== oldValue) { + this.selectedTimeSlotInfo = { + timeSlot: { + date: null, + routeCode: null, + startTime: null, + endTime: null, + jobMaxMinutes: null, + jobMinMinutes: null + }, + isPremiumAppointment: null + }; + } + }, + selectedTimeSlotInfo(newValue) { + this.updateFooterButtonText(newValue); } }, methods: { @@ -121,20 +297,146 @@ export default { && useMainStore().order.lineItems.glassParts.length > 0); return serviceLocationPreReqs && paymentInfo && supportingItems && damageInfo; }, + setData(initialShopTimeSlotsResponse) { + this.selectableDatesData = initialShopTimeSlotsResponse; + }, + async getAvailableDatesMethod(startDate, endDate) { + const newShopTimeSlots = await getAvailableDates( + startDate, + endDate, + this.appointmentType, + this.mainStore.order.serviceLocation.provider.providerNumber + ); + // ADD API CALL RESULTS TO EXISTING DATE DATA + this.selectableDatesData.days = this.selectableDatesData.days.concat(newShopTimeSlots.days); + return newShopTimeSlots; + }, + getAvailableDates, + getServiceZipCtuCodeFromStore() { + return this.mainStore.order.serviceLocation.zipCodeCtu; + }, + openInshopTimeSlotsModal() { + this.$refs.timeSlotModalQuestion.openModal(); + }, + getSelectedDate() { + return this.mainStore.order.schedule.date; + }, + getSelectedTimeSlotInfo() { + const supportingItems = this.getSupportingItems(); + const isPremiumAppointment = + !!supportingItems.filter((lineItem) => lineItem.partType === PREMIUM_FEE_PART_TYPE) + .length > 0; + + const selectedTimeSlotInfo = { + timeSlot: this.mainStore.order.schedule, + isPremiumAppointment + }; + + return selectedTimeSlotInfo; + }, + getSupportingItems() { + return this.mainStore.lineItems.supportingItems; + }, + timeSlotModalClosed() { + // Clear the selectedDate if no timeSlot has been selected + if (this.selectedTimeSlotInfo.timeSlot.routeCode == null) { + this.selectedDate = null; + } + }, + updateFooterButtonText(timeSlotInfo) { + let navbarButtonText; + if (!timeSlotInfo || !timeSlotInfo.timeSlot.date) { + navbarButtonText = 'Continue'; + } else { + navbarButtonText = `Select ${this.convertSelectedDateToShortMonthAndDay(timeSlotInfo.timeSlot.date)}`; + if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) { + navbarButtonText += ` at ${this.getDisplayTextForMilitaryTime(timeSlotInfo.timeSlot.startTime)}`; + } else if ( + this.appointmentType === AppointmentTypeStrings.MOBILE + && !timeSlotInfo.isPremiumAppointment + ) { + navbarButtonText += ` at ${this.getDisplayTextForMilitaryTime( + timeSlotInfo.timeSlot.startTime, + true + )} - ${this.getDisplayTextForMilitaryTime( + timeSlotInfo.timeSlot.endTime, + true + )}`; + } + } + this.$refs.navbar.updateButtonText(navbarButtonText); + }, + convertSelectedDateToShortMonthAndDay(selectedDate) { + // This conversion ensures we don't get get GMT induced date changes + const dateObject = convertDateStringToDate(selectedDate); + return dateObject.toLocaleDateString('en-us', { month: 'short', day: 'numeric' }); + }, + getDisplayTextForMilitaryTime(militaryTimeInput, shouldTrimMinutesIfEmpty = false) { + // Expected input: "HH:MM" + let hours = parseInt(militaryTimeInput.split(':')[0], 10); + const minutes = militaryTimeInput.split(':')[1]; + const meridianNotation = hours > 11 ? 'PM' : 'AM'; + + if (hours > 12) { + hours -= 12; + } + + if (shouldTrimMinutesIfEmpty && minutes === '00') { + return `${hours} ${meridianNotation}`; + } + return `${hours}:${minutes} ${meridianNotation}`; + }, + updateSupportingItems() { + const supportingItems = this.getSupportingItems(); + + // if we have a premium fee(early bird), then save/update supporting items + if ( + this.appointmentType === AppointmentTypeStrings.MOBILE + && this.selectedTimeSlotInfo?.isPremiumAppointment + ) { + const earlyBirdIndex = supportingItems.findIndex((item) => item.partType === PREMIUM_FEE_PART_TYPE); + + if (earlyBirdIndex >= 0) { + supportingItems[earlyBirdIndex].laborAmount = + this.mobilePremiumAppointmentFee.laborAmount; + supportingItems[earlyBirdIndex].selingPrice = + this.mobilePremiumAppointmentFee.selingPrice; + supportingItems[earlyBirdIndex].kitPrice = + this.mobilePremiumAppointmentFee.kitPrice; + } else { + supportingItems.push(this.mobilePremiumAppointmentFee); + } + + this.dispatchStoreAction( + this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING, + supportingItems, + false + ); + } else { + // if it's not a mobile and/or premium early bird, then make sure we remove any that may have been added + const removeEarlyBirdIndex = supportingItems.findIndex((item) => item.partType === PREMIUM_FEE_PART_TYPE); + + if (removeEarlyBirdIndex >= 0) { + supportingItems.splice(removeEarlyBirdIndex, 1); + this.dispatchStoreAction( + this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING, + supportingItems, + false + ); + } + } + }, backButtonAction() { this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route); }, - forwardButtonAction() { // validate and save here this.$router.navigate(this.navigationScenarios.CLICKED_FORWARD, this.$route); - }, - - navigateForward() { } } }; + diff --git a/src/layouts/schedule-page/time-slot-modal-question/time-slot-modal-question.vue b/src/layouts/schedule-page/time-slot-modal-question/time-slot-modal-question.vue new file mode 100644 index 00000000..4fe1774a --- /dev/null +++ b/src/layouts/schedule-page/time-slot-modal-question/time-slot-modal-question.vue @@ -0,0 +1,520 @@ + + + + + diff --git a/src/store/index.js b/src/store/index.js index 20b48012..c9a98be6 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -12,9 +12,23 @@ import issPageValues from '@/router/router-constants/issPage-values'; import damageLocationsSelected from '@/constants/damage-locations-selected'; import coverageStatuses from '@/constants/coverage-statuses'; import { PREMIUM_FEE_PART_TYPE } from '@/constants/schedule-constants'; +import getDateDifferenceInDays from '@/helpers/date-helper'; const storeId = 'main'; +function getTimeSlotsAdditionalEventData( + provisionalTriggers, + zipCode, + firstAvailableAppointmentDateString, + shopAppointmentType +) { + let numberOfDays = null; + if (firstAvailableAppointmentDateString) numberOfDays = getDateDifferenceInDays(new Date(), firstAvailableAppointmentDateString); + + if (shopAppointmentType) return `FirstAvailableAppointment:${numberOfDays},Zip:${zipCode},ShopAppointmentType:${shopAppointmentType},ProvisionalTriggers:${provisionalTriggers.join(',')}`; + return `FirstAvailableAppointment:${numberOfDays},Zip:${zipCode},ProvisionalTriggers:${provisionalTriggers.join(',')}`; +} + const getDefaultState = () => ({ order: { vehicle: { @@ -600,6 +614,65 @@ export const useMainStore = defineStore({ }); }, + getMobileTimeSlots(startDate, endDate) { + const { order } = this; + const { vehicle } = this.order; + let lineItems = [ + ...(order.lineItems.supportingItems ?? []), + ...(order.lineItems.vaps ?? []), + ...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts) + ]; + lineItems = lineItems.map((lineItem) => ({ + partNumber: lineItem.partNumber, + partType: lineItem.partType + })); + const glassPieces = order.damage.glassToReplace + ? convertGlassPieceToBackEndCompatibleFormat(order.damage.glassToReplace) + : []; + const payload = { + startDate, + endDate, + applicationName: applicationConfig.APPLICATION_NAME, + parentAccountNumber: this.payment.parentAccountNumber, + carId: vehicle.carId, + lineItems, + glassPieces, + eon: order.eon, + coverage: { + status: '', + deductible: 0, + additionalAuthFlag: '' + }, + partSelection: { + hasAnsweredPartQuestions: !!order.damage.partQuestionAnswers?.length, + hasAnsweredMoldingQuestions: !!order.damage.moldingQuestionAnswers?.length, + hasAnsweredCapabilityQuestions: !!order.damage.capabilityQuestionAnswers?.length, + hasManuallySelectedParts: + !!this.applicationUser.pageData['vehicle-parts']?.partsOrQuestions + .length + }, + vehicle: { + year: vehicle.year, + make: vehicle.make, + model: vehicle.model, + style: vehicle.style, + vin: vehicle.vin ?? '' + }, + zipCode: order.serviceLocation.zipCode + }; + return globalMethods.callHttpClient({ + method: endpoints.GetMobileTimeSlots.method, + endpoint: endpoints.GetMobileTimeSlots.url, + payload, + logApiCall: true, + additionalSuccessEventDataHandler: (response) => + getTimeSlotsAdditionalEventData( + response.data.provisionalTriggers, + order.serviceLocation.zipCode, + response.data.days?.[0]?.date + ) + }); + }, getShopTimeSlots(startDate, endDate, shopAppointmentType, providerNumber) { const { order } = this; const { vehicle } = this.order; From 4b3a86638220d3a185b466182e565b4b6033042f Mon Sep 17 00:00:00 2001 From: Bill Richardson Date: Tue, 3 Oct 2023 17:02:27 -0400 Subject: [PATCH 12/34] schedule page updates --- src/layouts/schedule-page/schedule-page.vue | 9 ++++----- src/store/index.js | 6 ++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/layouts/schedule-page/schedule-page.vue b/src/layouts/schedule-page/schedule-page.vue index 62f8f000..624acf75 100644 --- a/src/layouts/schedule-page/schedule-page.vue +++ b/src/layouts/schedule-page/schedule-page.vue @@ -15,7 +15,7 @@ cmsWidgetName="ChangeShopLink" justifyText="center" class="mb-3 text-link-small" - marginTopSizeOverride="1" /> + :marginTopSizeOverride="1" />
Date: Wed, 4 Oct 2023 11:53:12 -0500 Subject: [PATCH 13/34] SSR-706 Fix Missing Fields in Save Session Adds Claim Number Adds Policy State --- src/store/index.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/store/index.js b/src/store/index.js index 20b48012..7f5ed853 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -107,7 +107,8 @@ const getDefaultState = () => ({ isInsurance: true, // TODO delete; irrelevant to ISS insuranceCoverage: { isVerified: false, - coverageStatus: coverageStatuses.PENDING + coverageStatus: coverageStatuses.PENDING, + claimNumber: null }, parentAccountNumber: 0 }, @@ -455,12 +456,14 @@ export const useMainStore = defineStore({ }).then((response) => { const registerClaimFailed = response.data.isError; order.payment.insuranceCoverage.isVerified = !registerClaimFailed; + order.payment.insuranceCoverage.claimNumber = null; if (registerClaimFailed) { this.order.payment.insuranceCoverage.coverageStatus = coverageStatuses.PENDING; } else if (this.policy.noCoverage) { this.order.payment.insuranceCoverage.coverageStatus = coverageStatuses.NO_COMP; } else { this.order.payment.insuranceCoverage.coverageStatus = coverageStatuses.VERIFIED; + this.order.payment.insuranceCoverage.claimNumber = response.data.claimNumber; } return resolve(response); }, (error) => { @@ -862,7 +865,8 @@ export const useMainStore = defineStore({ policyFirstName: customer.firstName, policyLastName: customer.lastName, policyPhoneNumber: customer.phoneNumber, - policyEmail: customer.emailAddress + policyEmail: customer.emailAddress, + policyState: customer.address.state }, policyNumber: policy.policyNumber, policyZipCode: policy.policyZipCode, @@ -893,7 +897,8 @@ export const useMainStore = defineStore({ payment: { InsuranceCoverage: { isVerified: payment.insuranceCoverage?.isVerified ?? false, - coverageStatus: payment.insuranceCoverage?.coverageStatus + coverageStatus: payment.insuranceCoverage?.coverageStatus, + claimNumber: payment.insuranceCoverage?.claimNumber }, isInsurance: payment.isInsurance ?? true, parentAccountNumber: this.issConfig.parentAccountNumber From 3b4f120943461b8a16f543f98f59edaeb6942d32 Mon Sep 17 00:00:00 2001 From: Josh Dassinger Date: Wed, 4 Oct 2023 12:30:21 -0500 Subject: [PATCH 14/34] SSR-706 Fix Store Unit Tests --- src/store/store.spec.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/store/store.spec.js b/src/store/store.spec.js index bd8a5f2a..9ada327b 100644 --- a/src/store/store.spec.js +++ b/src/store/store.spec.js @@ -615,7 +615,10 @@ describe('Store', () => { firstName: getRandomString(6, 6), lastName: getRandomString(6, 6), emailAddress: getRandomString(6, 6), - phoneNumber: getRandomString(6, 6) + phoneNumber: getRandomString(6, 6), + address: { + state: getRandomString(2, 2) + } }; const policy = { policyNumber: getRandomString(6, 6), From db55f66b748a550b781e42b499e3548b54a93cce Mon Sep 17 00:00:00 2001 From: Bill Richardson Date: Wed, 4 Oct 2023 14:35:04 -0400 Subject: [PATCH 15/34] linting update for svg strings --- src/digital-components/date-picker/date-picker.vue | 6 ++++-- src/styles/ux-variables-svg-strings.scss | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue index b994487c..398b92b3 100644 --- a/src/digital-components/date-picker/date-picker.vue +++ b/src/digital-components/date-picker/date-picker.vue @@ -717,6 +717,8 @@ export default { From 43466bb7a792c2678923d7b9a77543d5b031bd71 Mon Sep 17 00:00:00 2001 From: Katie Kroell Date: Fri, 6 Oct 2023 10:12:31 -0400 Subject: [PATCH 30/34] setup deductible modal link --- src/layouts/coverage-statement/coverage-statement.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/layouts/coverage-statement/coverage-statement.vue b/src/layouts/coverage-statement/coverage-statement.vue index 7a2f1c08..2c3166d0 100644 --- a/src/layouts/coverage-statement/coverage-statement.vue +++ b/src/layouts/coverage-statement/coverage-statement.vue @@ -329,6 +329,7 @@ export default { nextStepsBody(newValue, oldValue) { if (newValue !== oldValue) { setupModalLink(this, 'RecalModal'); + setupModalLink(this, 'DeductibleModal'); } } }, From ec9a8f0961e1963ac6e147a226467b918ff0280a Mon Sep 17 00:00:00 2001 From: Bill Richardson Date: Fri, 6 Oct 2023 12:54:58 -0400 Subject: [PATCH 31/34] Mobile Time Slot updates Fix for Date Picker error message --- src/constants/error-messages.js | 3 ++- .../date-picker/date-picker.vue | 4 ++-- src/layouts/schedule-page/schedule-page.vue | 20 +++++++++++++++++-- .../time-slot-modal-question.vue | 11 +++++++--- src/store/index.js | 9 +++++++++ 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/constants/error-messages.js b/src/constants/error-messages.js index 90f6f6f9..f91c7a2b 100644 --- a/src/constants/error-messages.js +++ b/src/constants/error-messages.js @@ -51,7 +51,8 @@ const errorMessages = Object.freeze({ MAKE_REQUIRED: 'Please select your vehicle make', MODEL_REQUIRED: 'Please select your vehicle model', STYLE_REQUIRED: 'Please select your vehicle style', - MOBILE_LOCATION_REQUIRED: 'Please enter your service address' + MOBILE_LOCATION_REQUIRED: 'Please enter your service address', + DATE_REQUIRED: 'Please select a date' }); export default errorMessages; diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue index 398b92b3..b57dcdc1 100644 --- a/src/digital-components/date-picker/date-picker.vue +++ b/src/digital-components/date-picker/date-picker.vue @@ -1,7 +1,7 @@