@@ -121,8 +134,12 @@ import {
convertDateToTwoDigitDay,
convertDateToTwoDigitMonth,
getDisplayTextForDurationLength,
+ isAfternoon,
militaryToTwelveHourTime
} from '@/helpers/date-helper';
+import {
+ PREMIUM_TIME_SLOT_ID_FLAG
+} from '@/constants/schedule-constants';
import showIssLoadingModal from '@/helpers/loading-modal-helper.js';
import errorMessages from '@/constants/error-messages.js';
import { selectableDaysOptions } from './mixins/helpers';
@@ -207,6 +224,7 @@ export default {
selectableTimeSlotsData: [],
selectedTimeOfDayGrouping: null,
selectedDate: null,
+ selectedTime: null,
selectedTimeSlot: null,
activeDate: new Date(),
activePageIndex: 0,
@@ -262,7 +280,8 @@ export default {
watch: {
selectedTimeSlot(newValue, oldValue) {
if (newValue !== oldValue) {
- this.$emit('timeSlotSelected', newValue);
+ const testObj = this.getSelectedTimeSlotInfoObject(newValue);
+ this.$emit('timeSlotSelected', testObj);
}
},
selectedDate(newValue, oldValue) {
@@ -285,11 +304,54 @@ export default {
initializeComponent(initialData) {
this.setCalendarData(initialData);
},
+ addPremiumFlagToInput(routeCode) {
+ return (`${routeCode}${PREMIUM_TIME_SLOT_ID_FLAG}`);
+ },
+ removePremiumFlagFromInput(routeCode) {
+ return routeCode.replace(PREMIUM_TIME_SLOT_ID_FLAG, '');
+ },
+ getSelectedTimeSlotInfoObject(timeSlot) {
+ const routeCode = timeSlot?.id;
+ let routeCodeToUse = routeCode;
+ const routeCodeIncludesPremium = routeCode?.includes(PREMIUM_TIME_SLOT_ID_FLAG);
+ if (routeCodeIncludesPremium) {
+ routeCodeToUse = this.removePremiumFlagFromInput(routeCode);
+ }
+
+ const fullTimeSlot = this.selectableTimeSlotsData?.find((ts) => ts.id === routeCodeToUse);
+
+ if (fullTimeSlot) {
+ return {
+ timeSlot: {
+ date: this.selectedDate,
+ routeCode: fullTimeSlot.id,
+ startTime: fullTimeSlot.startTime,
+ endTime: fullTimeSlot.endTime,
+ jobMaxMinutes: this.appointmentDurationMinutesMaximum.toString(),
+ jobMinMinutes: this.appointmentDurationMinutesMinimum.toString()
+ },
+ isPremiumAppointment: !!routeCodeIncludesPremium
+ };
+ }
+
+ return {
+ timeSlot: {
+ date: null,
+ startTime: null,
+ endTime: null,
+ routeCode: null,
+ jobMaxMinutes: null,
+ jobMinMinutes: null
+ },
+ isPremiumAppointment: null
+ };
+ },
handleMqlChange(e) {
this.isMobileView = !e.matches;
this.activePageIndex = 0;
this.selectedDate = null;
this.selectedTimeOfDayGrouping = null;
+ this.selectedTime = null;
this.selectedTimeSlot = null;
this.findFirstAvailableDateInView();
},
@@ -311,6 +373,46 @@ export default {
}
}
},
+ findInitialPageIndex(daysFromStart) {
+ if (daysFromStart !== null && daysFromStart !== undefined) {
+ return {
+ initialPageIndex: Math.floor(daysFromStart / this.daysToAdd),
+ initialDayIndex: daysFromStart % this.daysToAdd
+ };
+ }
+ return {
+ initialPageIndex: 0,
+ initialDayIndex: null
+ };
+ },
+ findInitialDate(myDateString, initialDayIndex) {
+ const dateInView = this.getDateToShow(initialDayIndex);
+ const dateInViewString = convertDateToDateString(dateInView);
+ if (dateInViewString === myDateString) {
+ const isAfternoonSlot = isAfternoon(myDateString);
+ const selectableDateObj = this.selectableDatesData.find((dateObj) => dateObj.date === dateInViewString);
+ if (selectableDateObj) {
+ if (isAfternoonSlot) {
+ this.showAfternoonTimes(initialDayIndex);
+ this.findInitialTimeByStore(selectableDateObj.afternoonTimeSlots);
+ } else {
+ this.showMorningTimes(initialDayIndex);
+ this.findInitialTimeByStore(selectableDateObj.morningTimeSlots);
+ }
+ }
+ }
+ },
+ findInitialTimeByStore(timeSlots) {
+ const initialTimeSlot = this.mainStore.order.schedule;
+ if (!initialTimeSlot || !initialTimeSlot.startTime) {
+ return;
+ }
+ const initialStartTime = initialTimeSlot.startTime;
+ const foundTimeSlot = timeSlots.find((ts) => ts.startTime === initialStartTime);
+ if (foundTimeSlot) {
+ this.selectTimeSlotForDay(foundTimeSlot);
+ }
+ },
getDateToShow(index) {
const dayOffset = this.activePageIndex * this.daysToAdd;
const dateToShow = new Date(this.activeDate);
@@ -321,6 +423,38 @@ export default {
const dateToShow = this.getDateToShow(index);
return convertDateToDateString(dateToShow);
},
+ getEndDateForExistingDate(startDate, selectedDateString, daysInView) {
+ const endDate = new Date(startDate);
+ endDate.setDate(endDate.getDate() + (daysInView - 1));
+
+ if (selectedDateString) {
+ const selectedDate = new Date(`${selectedDateString}T00:00:00`);
+ const daysDifferenceComparedToStartDate = Math.ceil((selectedDate - startDate) / (1000 * 60 * 60 * 24));
+ if (selectedDate > endDate) {
+ const daysDifferenceComparedToStartDateAsPages = Math.ceil(daysDifferenceComparedToStartDate / daysInView);
+ const newEndDate = new Date(startDate);
+ newEndDate.setDate(newEndDate.getDate() + (daysDifferenceComparedToStartDateAsPages * daysInView) - 1);
+
+ return {
+ endDateString: convertDateToDateString(newEndDate),
+ newDaysLoaded: (daysDifferenceComparedToStartDateAsPages * daysInView),
+ daysFromStart: daysDifferenceComparedToStartDate
+ };
+ }
+
+ return {
+ endDateString: convertDateToDateString(endDate),
+ newDaysLoaded: null,
+ daysFromStart: daysDifferenceComparedToStartDate
+ };
+ }
+
+ return {
+ endDateString: convertDateToDateString(endDate),
+ newDaysLoaded: null,
+ daysFromStart: null
+ };
+ },
gotoNextPage() {
const maxPageIndex = Math.floor((this.daysLoaded - 1) / this.daysToAdd);
if (this.activePageIndex >= maxPageIndex) {
@@ -329,6 +463,7 @@ export default {
this.daysLoaded += this.initialDaysToLoad;
this.activePageIndex += 1;
this.selectedDate = null;
+ this.selectedTime = null;
this.selectedTimeSlot = null;
this.findFirstAvailableDateInView();
});
@@ -336,12 +471,14 @@ export default {
}
this.activePageIndex += 1;
this.selectedDate = null;
+ this.selectedTIme = null;
this.selectedTimeSlot = null;
this.findFirstAvailableDateInView();
},
gotoPreviousPage() {
this.activePageIndex -= 1;
this.selectedDate = null;
+ this.selectedTime = null;
this.selectedTimeSlot = null;
this.findFirstAvailableDateInView();
},
@@ -357,6 +494,7 @@ export default {
},
selectTimeSlotForDay(timeSlot) {
if (timeSlot) {
+ this.selectedTime = timeSlot.startTime;
this.selectedTimeSlot = timeSlot;
}
},
@@ -387,6 +525,7 @@ export default {
this.selectedDate = morningDateString;
this.selectedTimeOfDayGrouping = 'morning';
this.selectableTimeSlotsData = selectableDateObj.morningTimeSlots;
+ this.selectedTime = null;
this.selectedTimeSlot = null;
}
},
@@ -411,13 +550,18 @@ export default {
this.selectedTimeOfDayGrouping = 'afternoon';
this.selectableTimeSlotsData = selectableDateObj.afternoonTimeSlots;
this.selectedTimeSlot = null;
+ this.selectedTime = null;
}
},
+ timeSlotInputChanged(timeSlot) {
+ this.selectTimeSlotForDay(timeSlot);
+ },
async loadInitialData(config) {
/*
** NOTE: this _could_ be called by a parent before fully loaded, so data or computeds might not be available
*/
let todayDateString;
+ const todayDateObject = new Date();
const calendarViewDirection = 'future';
const initialDays = 15;
@@ -426,13 +570,23 @@ export default {
} else if (config.todayOverrideDateString) {
todayDateString = config.todayOverrideDateString;
} else {
- todayDateString = convertDateToDateString(new Date());
+ todayDateString = convertDateToDateString(todayDateObject);
}
const initialViewStartDate = todayDateString;
const initialEndDate = new Date();
initialEndDate.setDate(initialEndDate.getDate() + (initialDays - 1));
- const initialViewEndDate = convertDateToDateString(initialEndDate);
+ let initialViewEndDate = convertDateToDateString(initialEndDate);
+
+ const endDateObject = this.getEndDateForExistingDate(
+ todayDateObject,
+ config.preSelectedDate,
+ initialDays
+ );
+
+ if (endDateObject.endDateString !== initialViewEndDate) {
+ initialViewEndDate = endDateObject.endDateString;
+ }
const loadInitialDataPromise = new Promise((resolve) => {
const response = config.customSelectableDatesCallback(
@@ -452,7 +606,8 @@ export default {
calendarViewDirection,
initialShopTimeSlotsResponse: response,
preSelectedDate: config.preSelectedDate,
- initialDaysLoaded: initialDays
+ initialDaysLoaded: endDateObject.newDaysLoaded || initialDays,
+ daysFromStart: endDateObject.daysFromStart || null
};
return initialData;
@@ -480,7 +635,18 @@ export default {
this.appointmentDurationMinutesMaximum = config.initialShopTimeSlotsResponse.estimatedServiceMinutesMaximum;
this.appointmentDurationMinutesMinimum = config.initialShopTimeSlotsResponse.estimatedServiceMinutesMinimum;
- this.findFirstAvailableDateInView();
+ let pageIndexObj = { initialPageIndex: 0, initialDayIndex: null };
+ if (config.daysFromStart !== null && config.daysFromStart !== undefined) {
+ pageIndexObj = this.findInitialPageIndex(config.daysFromStart);
+ this.activePageIndex = pageIndexObj.initialPageIndex;
+ }
+
+ if (config.preSelectedDate) {
+ this.findInitialDate(config.preSelectedDate, pageIndexObj.initialDayIndex);
+ } else {
+ this.findFirstAvailableDateInView();
+ }
+
this.isLoading = false;
},
async loadMoreDays(maxPageIndex, daysInView, daysToFetch) {
@@ -590,6 +756,9 @@ export default {
.span-block-flex {
display: flex;
align-items: center;
+ border: none;
+ background-color: #fff;
+ color: #525656;
cursor: pointer;
&:hover {
@@ -601,6 +770,7 @@ export default {
text-align: end;
.span-block-flex {
+ justify-self: flex-end;
justify-content: flex-end;
img {
@@ -610,6 +780,7 @@ export default {
}
.header-middle {
font-weight: 600;
+ color: #000;
@include media-breakpoint-up(xs) {
grid-column: 2 / span 1;
}
@@ -623,6 +794,7 @@ export default {
text-align: start;
.span-block-flex {
+ justify-self: flex-start;
justify-content: flex-start;
img {
@@ -677,6 +849,10 @@ export default {
border: solid 1px #0070d1;
}
+ &:focus, &:focus-visible {
+ outline: none;
+ border: solid 2px #0070d1;
+ }
}
.span-block {
display: block;
@@ -697,7 +873,7 @@ export default {
.time-slot-button {
width: 100%;
border: 1px solid #b3b4b5;
- border-radius: 1.375rem;
+ border-radius: $border-radius-list-button;
box-shadow: 0 1px 5px 0 rgba(0, 0, 0, .2);
color: #525656;
padding: 0.5rem 0;
diff --git a/src/helpers/date-helper.js b/src/helpers/date-helper.js
index bedf9ca2..24a90760 100644
--- a/src/helpers/date-helper.js
+++ b/src/helpers/date-helper.js
@@ -66,6 +66,12 @@ export function getDisplayTextForDurationLength(durationMinimum, durationMaximum
return `${durationText} ${unitText}`;
}
+export function isAfternoon(timeString) {
+ if (typeof timeString !== 'string') return false;
+ const hours = parseInt(timeString.split(':')[0], 10);
+ return hours >= 12;
+}
+
export function militaryToTwelveHourTime(timeString) {
// Expected input: "HH:MM"
if (typeof timeString !== 'string') return null;
diff --git a/src/layouts/schedule-page/schedule-page.spec.js b/src/layouts/schedule-page/schedule-page.spec.js
index 49ef7264..133654da 100644
--- a/src/layouts/schedule-page/schedule-page.spec.js
+++ b/src/layouts/schedule-page/schedule-page.spec.js
@@ -6,7 +6,6 @@ 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';
-import { AppointmentTypeStrings } from '@/constants/schedule-constants';
// Mock fetchCmsContentForPage
jest.mock('@/helpers/cms-content-helper', () => ({
@@ -214,7 +213,7 @@ describe('schedule-page.vue', () => {
// Act
const newShopTimeSlots = await wrapper.vm.getAvailableDatesMethod(
'2023-01-01',
- '2023-01-31'
+ '2023-01-15'
);
// Assert
@@ -236,7 +235,7 @@ describe('schedule-page.vue', () => {
estimatedServiceMinutesMaximum: 120
});
});
- test('Should call API service in days of 34 or less when getAvailableDatesMethod is called with large date ranges', async () => {
+ test('Should call API service in days of 15 or less when getAvailableDatesMethod is called with large date ranges', async () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.selectableDatesData = {
@@ -264,7 +263,7 @@ describe('schedule-page.vue', () => {
// 2023-01-01 --> 2023-02-05
// 2023-02-06 --> 2023-03-12
// 2023-03-13 --> 2023-03-31
- expect(store.getShopTimeSlots).toHaveBeenCalledTimes(3);
+ expect(store.getShopTimeSlots).toHaveBeenCalledTimes(6);
});
});
describe('Rendering', () => {
@@ -296,7 +295,9 @@ describe('schedule-page.vue', () => {
const { wrapper } = getShallowMountedComponent();
wrapper.vm.$router.navigate = jest.fn(() => ({}));
wrapper.vm.selectedTimeSlotInfo = {
- id: 'test-id'
+ timeSlot: {
+ routeCode: 'test-id'
+ }
};
// Act
diff --git a/src/layouts/schedule-page/schedule-page.vue b/src/layouts/schedule-page/schedule-page.vue
index 8a5667b0..aa08e79a 100644
--- a/src/layouts/schedule-page/schedule-page.vue
+++ b/src/layouts/schedule-page/schedule-page.vue
@@ -73,7 +73,7 @@ import BaseFormMixin from '@/mixins/base-form-mixin.js';
import { useMainStore } from '@/store';
// Define constants
-const TIME_SLOTS_CALL_DAYS_LIMIT = 34; // needs to be 34 for API limits (35 does not consistently work)
+const TIME_SLOTS_CALL_DAYS_LIMIT = 15;
const getAvailableDates = async (
startDateString,
@@ -83,7 +83,7 @@ const getAvailableDates = async (
) => {
const apiEndDateLimit = sumDateString(
startDateString,
- TIME_SLOTS_CALL_DAYS_LIMIT
+ TIME_SLOTS_CALL_DAYS_LIMIT - 1
);
const difference = calcDaysBetweenDates(startDateString, endDateString);
const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT);
@@ -98,7 +98,7 @@ const getAvailableDates = async (
apiStartDate = sumDateString(apiEndDate, 1);
apiEndDate = sumDateString(
apiStartDate,
- TIME_SLOTS_CALL_DAYS_LIMIT
+ TIME_SLOTS_CALL_DAYS_LIMIT - 1
);
if (i === apiCallsCount) {
@@ -108,11 +108,7 @@ const getAvailableDates = async (
apiEndDate = apiEndDateLimit;
}
- if (
- appointmentType === AppointmentTypeStrings.MOBILE
- || appointmentType
- === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP
- ) {
+ if (appointmentType === AppointmentTypeStrings.MOBILE || appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
storeActionConfig = {
storeAction: GET_MOBILE_TIME_SLOTS,
payload: {
@@ -278,7 +274,7 @@ export default {
return useMainStore().order.serviceLocation.appointmentType;
},
isFormValid() {
- const hasTimeSlotSelected = this.selectedTimeSlotInfo?.id != null;
+ const hasTimeSlotSelected = this.selectedTimeSlotInfo?.timeSlot?.routeCode != null;
return hasTimeSlotSelected;
},
supportingItems() {
@@ -328,9 +324,6 @@ export default {
getServiceZipCtuCodeFromStore() {
return this.mainStore.order.serviceLocation.zipCodeCtu;
},
- dateTimeSelected() {
- console.log('dateTimeSelected called');
- },
getSelectedDate() {
return this.mainStore.order.schedule.date;
},
diff --git a/src/store/index.js b/src/store/index.js
index 1d6e44b7..5d2c3c15 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -2369,23 +2369,23 @@ export const useMainStore = defineStore({
const isMobileApt = appointmentType === AppointmentTypeStrings.MOBILE
|| appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
-
+
const retPricedLineItems = await globalMethods.callHttpClient({
method: endpoints.TaxOrderItems.method,
endpoint: endpoints.TaxOrderItems.url,
payload: {
- ParentAccountNumber: this.order.parentAccountNumber,
- BillToAccountNumber: this.billToAccountNumber,
- ProviderNumber: this.providerNumber,
- AppointmentType: appointmentType,
- PricedLineItems: getLineItemsFlattened(pricedLineItems),
- ServiceLocation: {
- City: isMobileApt ? serviceLocationCity : null,
- State: isMobileApt ? serviceLocationState : null,
- ZipCode: isMobileApt ? serviceLocationZipCode : null,
- },
- ServerData: lineItemServerData ? lineItemServerData : "",
- },
+ ParentAccountNumber: this.order.parentAccountNumber,
+ BillToAccountNumber: this.billToAccountNumber,
+ ProviderNumber: this.providerNumber,
+ AppointmentType: appointmentType,
+ PricedLineItems: getLineItemsFlattened(pricedLineItems),
+ ServiceLocation: {
+ City: isMobileApt ? serviceLocationCity : null,
+ State: isMobileApt ? serviceLocationState : null,
+ ZipCode: isMobileApt ? serviceLocationZipCode : null
+ },
+ ServerData: lineItemServerData || ''
+ }
}).then((response) => {
this.order.lineItems.serverData = response.data.serverData;
return addTaxesToPricedLineItems(pricedLineItems, response.data.taxedLineItems);
diff --git a/src/styles/ux-variables.scss b/src/styles/ux-variables.scss
index 41cd8269..280b9a24 100644
--- a/src/styles/ux-variables.scss
+++ b/src/styles/ux-variables.scss
@@ -154,6 +154,7 @@ $border-radius: 0.25rem;
$border-radius-sm: 0.2rem;
$border-radius-lg: 0.5rem; //Used for buttons. Can be used for other things, of course.
$border-radius-pill: 50rem;
+$border-radius-list-button: 1.375rem; // Used for list buttons
//Progress Bar Styling
$progress-bar-success-color: $green;