Merge branch 'develop' into feature/CSR-1144
This commit is contained in:
commit
c550c8bede
12 changed files with 530 additions and 35 deletions
|
|
@ -26,7 +26,7 @@ module.exports = {
|
||||||
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
|
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
statements: 82,
|
statements: 80,
|
||||||
// Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90
|
// Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,11 @@ const endpoints = {
|
||||||
mockUrl: "https://mockey.qa.sagaws.net/service/shop-time-slots", // TODO: REMOVE MOCKURL
|
mockUrl: "https://mockey.qa.sagaws.net/service/shop-time-slots", // TODO: REMOVE MOCKURL
|
||||||
method: "POST",
|
method: "POST",
|
||||||
},
|
},
|
||||||
|
GetMobileEarlyBirdFee: {
|
||||||
|
url: "/parts/api/v1/parts/mobile-early-bird-fee",
|
||||||
|
mockUrl: "https://mockey.qa.sagaws.net/service/mobile-early-bird-fee", // TODO: REMOVE MOCKURL
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
SaveSession: {
|
SaveSession: {
|
||||||
url: "/order/api/v1/order/save-session",
|
url: "/order/api/v1/order/save-session",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ const storeActions = {
|
||||||
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
|
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
|
||||||
GET_SHOP_TIME_SLOTS: "getShopTimeSlots",
|
GET_SHOP_TIME_SLOTS: "getShopTimeSlots",
|
||||||
GET_PROVIDERS: "getProviders",
|
GET_PROVIDERS: "getProviders",
|
||||||
|
GET_MOBILE_EARLY_BIRD_FEE: "getMobileEarlyBirdFee",
|
||||||
SAVE_SESSION: "saveSession",
|
SAVE_SESSION: "saveSession",
|
||||||
LOAD_SESSION: "loadSession",
|
LOAD_SESSION: "loadSession",
|
||||||
UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE: "updateStoreWithSaveSessionResponse",
|
UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE: "updateStoreWithSaveSessionResponse",
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,6 @@ export default {
|
||||||
isRequired: Boolean,
|
isRequired: Boolean,
|
||||||
isOverflowScrollable: Boolean,
|
isOverflowScrollable: Boolean,
|
||||||
isWide: Boolean,
|
isWide: Boolean,
|
||||||
isCashOrInsurance: Boolean,
|
|
||||||
modelValue: [Array, Number, String],
|
modelValue: [Array, Number, String],
|
||||||
value: [Number, String],
|
value: [Number, String],
|
||||||
validationRules: String,
|
validationRules: String,
|
||||||
|
|
@ -182,6 +181,7 @@ export default {
|
||||||
getComponentLoopWrapperClasses() {
|
getComponentLoopWrapperClasses() {
|
||||||
let classes;
|
let classes;
|
||||||
switch (this.buttonTypeString) {
|
switch (this.buttonTypeString) {
|
||||||
|
case "timeSlotModalListButton":
|
||||||
case "listButton":
|
case "listButton":
|
||||||
classes = "w-100";
|
classes = "w-100";
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@
|
||||||
type="radio"
|
type="radio"
|
||||||
name="day-of-month"
|
name="day-of-month"
|
||||||
v-model="selectedDate"
|
v-model="selectedDate"
|
||||||
|
@click="fireDateClickedEvent"
|
||||||
:value="date.inputValue"
|
:value="date.inputValue"
|
||||||
:id="`${month.monthLabel}-${date.dateNum.toString()}`" />
|
:id="`${month.monthLabel}-${date.dateNum.toString()}`" />
|
||||||
<label :for="`${month.monthLabel}-${date.dateNum.toString()}`">
|
<label :for="`${month.monthLabel}-${date.dateNum.toString()}`">
|
||||||
|
|
@ -145,6 +146,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
fireDateClickedEvent() {
|
||||||
|
this.$emit("date-clicked");
|
||||||
|
},
|
||||||
getWeekStartDate(date) {
|
getWeekStartDate(date) {
|
||||||
// Get the day of the week for date
|
// Get the day of the week for date
|
||||||
let dayOfWeek = date.getDay();
|
let dayOfWeek = date.getDay();
|
||||||
|
|
@ -306,8 +310,7 @@ export default {
|
||||||
|
|
||||||
const monthsAfterToLoadOffset = 12; // TO BE MADE "CONSTANTS"
|
const monthsAfterToLoadOffset = 12; // TO BE MADE "CONSTANTS"
|
||||||
const monthsBeforeToLoadOffset = 36; // TO BE MADE "CONSTANTS"
|
const monthsBeforeToLoadOffset = 36; // TO BE MADE "CONSTANTS"
|
||||||
|
config.initialShopTimeSlotsResponse.days.forEach((selectableDate) => {
|
||||||
config.initialShopTimeSlotsResponse.forEach((selectableDate) => {
|
|
||||||
this.selectableDatesData.push(selectableDate);
|
this.selectableDatesData.push(selectableDate);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -522,7 +525,7 @@ export default {
|
||||||
monthStart,
|
monthStart,
|
||||||
monthEnd
|
monthEnd
|
||||||
);
|
);
|
||||||
moreSelectableDates.forEach((selectableDate) => {
|
moreSelectableDates.days.forEach((selectableDate) => {
|
||||||
const index = this.selectableDatesData.findIndex(
|
const index = this.selectableDatesData.findIndex(
|
||||||
(obj) => obj.dateString === selectableDate.dateString
|
(obj) => obj.dateString === selectableDate.dateString
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -21,4 +21,6 @@ const MONTHS_OF_YEAR = [
|
||||||
"December",
|
"December",
|
||||||
];
|
];
|
||||||
|
|
||||||
export { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR };
|
const DAYS_OF_WEEK = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||||
|
|
||||||
|
export { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR, DAYS_OF_WEEK };
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ export default {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.text-block {
|
.text-block {
|
||||||
|
display: flex;
|
||||||
&.left {
|
&.left {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,23 @@
|
||||||
selectableDatesSetting="custom"
|
selectableDatesSetting="custom"
|
||||||
ref="datePicker"
|
ref="datePicker"
|
||||||
v-model="selectedDate"
|
v-model="selectedDate"
|
||||||
:customSelectableDatesCallback="getAvailableDatesMethod" />
|
:customSelectableDatesCallback="getAvailableDatesMethod"
|
||||||
|
@date-clicked="openInshopTimeSlotsModal" />
|
||||||
|
<!-- todayOverrideDateString="2023-08-06T03:00:00" -->
|
||||||
|
<time-slot-modal-question
|
||||||
|
ref="timeSlotModalQuestion"
|
||||||
|
cmsWidgetName="TimeSlotModalQuestion"
|
||||||
|
earlyBirdCmsWidgetName="EarlyBirdTimeSlotModal"
|
||||||
|
mobileCmsWidgetName="MobileTimeSlotModal"
|
||||||
|
dropoffCmsWidgetName="DropOffTimeSlotModal"
|
||||||
|
v-model="selectedTimeSlotId"
|
||||||
|
@time-slot-modal-closed="timeSlotModalClosed"
|
||||||
|
:appointmentType="appointmentType"
|
||||||
|
:mobileEarlyBirdFee="mobileEarlyBirdFee"
|
||||||
|
:dateAndTimeSlotData="timeSlotsForSelectedDate"
|
||||||
|
:estimatedServiceMinutesMinimum="selectableDatesData.estimatedServiceMinutesMinimum"
|
||||||
|
:estimatedServiceMinutesMaximum="selectableDatesData.estimatedServiceMinutesMaximum"
|
||||||
|
validationRules="time-slot-selection-required" />
|
||||||
<funnel-footer
|
<funnel-footer
|
||||||
cmsWidgetName="FunnelFooterWidget"
|
cmsWidgetName="FunnelFooterWidget"
|
||||||
ref="funnelFooter"
|
ref="funnelFooter"
|
||||||
|
|
@ -53,6 +69,7 @@ import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-heade
|
||||||
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
||||||
import { Form, defineRule } from "vee-validate";
|
import { Form, defineRule } from "vee-validate";
|
||||||
import datePicker from "@/digital-components/date-picker/date-picker";
|
import datePicker from "@/digital-components/date-picker/date-picker";
|
||||||
|
import timeSlotModalQuestion from "./time-slot-modal-question/time-slot-modal-question";
|
||||||
|
|
||||||
// Supporting files
|
// Supporting files
|
||||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||||
|
|
@ -72,6 +89,7 @@ import { required } from "@/helpers/validation-rules";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
|
|
||||||
defineRule("date-required", required(errorMessages.DATE_REQUIRED));
|
defineRule("date-required", required(errorMessages.DATE_REQUIRED));
|
||||||
|
defineRule("time-slot-selection-required", required(errorMessages.DATE_REQUIRED));
|
||||||
|
|
||||||
const getAvailableDates = async (startDate, endDate) => {
|
const getAvailableDates = async (startDate, endDate) => {
|
||||||
// USING DATES PASSED, MAKE AN API CALL
|
// USING DATES PASSED, MAKE AN API CALL
|
||||||
|
|
@ -85,11 +103,11 @@ const getAvailableDates = async (startDate, endDate) => {
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
const newShopTimeSlots = newShopTimeSlotsResponse.data;
|
const newShopTimeSlots = newShopTimeSlotsResponse.data;
|
||||||
return convertApiResponse(newShopTimeSlots.days);
|
return convertApiResponse(newShopTimeSlots);
|
||||||
};
|
};
|
||||||
const convertApiResponse = (responseData) => {
|
const convertApiResponse = (responseData) => {
|
||||||
// DATA CONVERSION
|
// DATA CONVERSION
|
||||||
responseData?.forEach((date) => {
|
responseData?.days.forEach((date) => {
|
||||||
const dateString = date.date;
|
const dateString = date.date;
|
||||||
date.dateString = dateString;
|
date.dateString = dateString;
|
||||||
const dateStringPieces = dateString.split("-");
|
const dateStringPieces = dateString.split("-");
|
||||||
|
|
@ -105,8 +123,10 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedDate: null,
|
selectedDate: null,
|
||||||
|
selectedTimeSlotId: null,
|
||||||
selectableDatesData: [],
|
selectableDatesData: [],
|
||||||
weatherAlerts: [],
|
weatherAlerts: [],
|
||||||
|
mobileEarlyBirdFee: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async beforeRouteEnter(to, from, next) {
|
async beforeRouteEnter(to, from, next) {
|
||||||
|
|
@ -135,6 +155,18 @@ export default {
|
||||||
*/
|
*/
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Price EARLY BIRD pre-emptively to allow for asynchronous call to pricing
|
||||||
|
const pricingPromise = baseMixin.methods.dispatchStoreAction(
|
||||||
|
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
|
||||||
|
{
|
||||||
|
availableLineItems: [{ partNumber: "EARLY BIRD" }],
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const earlyBirdPromise = baseMixin.methods.dispatchStoreAction(
|
||||||
|
storeActions.GET_MOBILE_EARLY_BIRD_FEE
|
||||||
|
);
|
||||||
const alertReasonsPromise = getAlertReasons(store.getters.order.serviceLocation.zipCodeCtu);
|
const alertReasonsPromise = getAlertReasons(store.getters.order.serviceLocation.zipCodeCtu);
|
||||||
|
|
||||||
// Settle promises and get results
|
// Settle promises and get results
|
||||||
|
|
@ -151,6 +183,14 @@ export default {
|
||||||
resultKey: "datePickerInitialData",
|
resultKey: "datePickerInitialData",
|
||||||
promise: datePickerInitialDataPromise,
|
promise: datePickerInitialDataPromise,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
resultKey: "earlyBird",
|
||||||
|
promise: earlyBirdPromise,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resultKey: "pricingResults",
|
||||||
|
promise: pricingPromise,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const resultMap = await settleAllPromises(promiseResultMap);
|
const resultMap = await settleAllPromises(promiseResultMap);
|
||||||
|
|
@ -160,6 +200,10 @@ export default {
|
||||||
vm.setCmsContent(resultMap.cmsContent);
|
vm.setCmsContent(resultMap.cmsContent);
|
||||||
vm.setData(resultMap.alertReasons);
|
vm.setData(resultMap.alertReasons);
|
||||||
vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData);
|
vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData);
|
||||||
|
vm.selectableDatesData = resultMap.datePickerInitialData.initialShopTimeSlotsResponse;
|
||||||
|
if (resultMap.earlyBird) {
|
||||||
|
vm.mobileEarlyBirdFee = resultMap.pricingResults[0];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -173,6 +217,35 @@ export default {
|
||||||
displayWeatherAlert() {
|
displayWeatherAlert() {
|
||||||
return this.weatherAlerts.length > 0;
|
return this.weatherAlerts.length > 0;
|
||||||
},
|
},
|
||||||
|
appointmentType() {
|
||||||
|
return this.$store.getters.order.serviceLocation.appointmentType;
|
||||||
|
},
|
||||||
|
timeSlotsForSelectedDate() {
|
||||||
|
if (this.selectedDate === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.selectableDatesData.days.find(
|
||||||
|
(selectableDate) => selectableDate.dateString === this.selectedDate.dateString
|
||||||
|
);
|
||||||
|
},
|
||||||
|
appointmentDateAndTime() {
|
||||||
|
if (!this.selectedTimeSlotId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const timeSlotSelectedObject = this.getTimeSlotObjectFromTimeSlotId(
|
||||||
|
this.selectedTimeSlotId
|
||||||
|
);
|
||||||
|
if (timeSlotSelectedObject) {
|
||||||
|
return {
|
||||||
|
date: this.selectedDate.dateString,
|
||||||
|
startTime: timeSlotSelectedObject.startTime,
|
||||||
|
endTime: timeSlotSelectedObject.endTime,
|
||||||
|
id: this.selectedTimeSlotId,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
doesCopyContainRouterLink,
|
doesCopyContainRouterLink,
|
||||||
|
|
@ -184,20 +257,12 @@ export default {
|
||||||
// NEED TODO - WHAT ARE PAGE REQ'S FOR THIS PAGE?
|
// NEED TODO - WHAT ARE PAGE REQ'S FOR THIS PAGE?
|
||||||
},
|
},
|
||||||
async getAvailableDatesMethod(startDate, endDate) {
|
async getAvailableDatesMethod(startDate, endDate) {
|
||||||
/* TODO - DO WE NEED TO KEEP AN AGGREGATE OF ALL DATES RETURNED
|
const newShopTimeSlots = await getAvailableDates(startDate, endDate);
|
||||||
FOR TIMESLOTS in this.selectableDatesData? IS selectableDatesData EVEN NEEDED?
|
// ADD API CALL RESULTS TO EXISTING DATE DATA
|
||||||
|
this.selectableDatesData.days = this.selectableDatesData.days.concat(
|
||||||
// // ADD API CALL RESULTS TO EXISTING DATE DATA
|
newShopTimeSlots.days
|
||||||
// this.selectableDatesData = this.selectableDatesData.concat(
|
);
|
||||||
// this.convertApiResponse(newShopTimeSlots.days)
|
return newShopTimeSlots;
|
||||||
// );
|
|
||||||
// // console.log("this.selectableDatesData is now: ", this.selectableDatesData);
|
|
||||||
|
|
||||||
// // RETURN AGGREGATE DATE DATA
|
|
||||||
// return this.selectableDatesData;
|
|
||||||
*/
|
|
||||||
|
|
||||||
return await getAvailableDates(startDate, endDate);
|
|
||||||
},
|
},
|
||||||
setData(alertReasonsData) {
|
setData(alertReasonsData) {
|
||||||
if (alertReasonsData) {
|
if (alertReasonsData) {
|
||||||
|
|
@ -230,6 +295,21 @@ export default {
|
||||||
getServiceZipCtuCodeFromStore() {
|
getServiceZipCtuCodeFromStore() {
|
||||||
return store.getters.order.serviceLocation.zipCodeCtu;
|
return store.getters.order.serviceLocation.zipCodeCtu;
|
||||||
},
|
},
|
||||||
|
openInshopTimeSlotsModal() {
|
||||||
|
this.$refs["timeSlotModalQuestion"].openModal();
|
||||||
|
},
|
||||||
|
getTimeSlotObjectFromTimeSlotId(timeSlotId) {
|
||||||
|
const timeSlots = this.selectableDatesData.days.find(
|
||||||
|
(selectableDate) => selectableDate.dateString === this.selectedDate.dateString
|
||||||
|
).timeSlots;
|
||||||
|
return timeSlots.find((timeSlot) => timeSlot.id === timeSlotId);
|
||||||
|
},
|
||||||
|
timeSlotModalClosed() {
|
||||||
|
// Clear the selectedDate if no timeslot has been selected
|
||||||
|
if (!this.selectedTimeSlotId) {
|
||||||
|
this.selectedDate = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
backButtonAction() {
|
backButtonAction() {
|
||||||
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
|
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||||
},
|
},
|
||||||
|
|
@ -250,10 +330,11 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
selectedDate(date) {
|
selectedDate(newValue, oldValue) {
|
||||||
/* EXAMPLE RETURNED date OBJECT:
|
// Clear time slot selection if date selected changes
|
||||||
{ "year": 2023, "month": 5, "date": 25, "dateString": "2023-05-25" }
|
if (newValue !== oldValue) {
|
||||||
*/
|
this.selectedTimeSlotId = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -264,6 +345,7 @@ export default {
|
||||||
Form,
|
Form,
|
||||||
loadingModal,
|
loadingModal,
|
||||||
datePicker,
|
datePicker,
|
||||||
|
timeSlotModalQuestion,
|
||||||
},
|
},
|
||||||
mounted() {},
|
mounted() {},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,264 @@
|
||||||
|
<template>
|
||||||
|
<modal
|
||||||
|
ref="timeSlots"
|
||||||
|
class="time-slots-modal"
|
||||||
|
:headerText="dateSelectedReadableDate"
|
||||||
|
:footerButtonText="footerCloseButtonText"
|
||||||
|
:onModalOpenedCallback="onModalOpened"
|
||||||
|
:onModalClosedCallback="onModalClosed"
|
||||||
|
@footer-button-event="closeModal">
|
||||||
|
<textBlock
|
||||||
|
v-show="inshopAppointmentDurationTextWithTime"
|
||||||
|
:customText="inshopAppointmentDurationTextWithTime"
|
||||||
|
justifyText="center"
|
||||||
|
typeStyle="small"
|
||||||
|
class="duration-text-block" />
|
||||||
|
<buttonQuestion
|
||||||
|
buttonTypeString="timeslotModalListButton"
|
||||||
|
:buttonTypeObject="timeslotModalListButton"
|
||||||
|
:answers="availableTimeSlots"
|
||||||
|
groupName="ChooseTimeSlot"
|
||||||
|
textPosition="text-center"
|
||||||
|
v-model="selectedTimeSlot"
|
||||||
|
isRequired
|
||||||
|
validationRules="time-slot-required"
|
||||||
|
class="mt-5" />
|
||||||
|
<div
|
||||||
|
class="mt-1 mb-2"
|
||||||
|
v-if="supplementalInformationBlock"
|
||||||
|
v-html="supplementalInformationBlock"></div>
|
||||||
|
<textBlock
|
||||||
|
v-show="shouldShowDropoffDisclaimerText"
|
||||||
|
:customText="dropoffDisclaimerText"
|
||||||
|
justifyText="left"
|
||||||
|
typeStyle="caption"
|
||||||
|
class="mb-2" />
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Components
|
||||||
|
import modal from "@/digital-components/modal/modal";
|
||||||
|
import textBlock from "@/digital-components/text-block/text-block";
|
||||||
|
import buttonQuestion from "@/digital-components/button-question/button-question";
|
||||||
|
import timeslotModalListButton from "./timeslot-modal-list-button/timeslot-modal-list-button";
|
||||||
|
|
||||||
|
// TODO: Move this somewhere more global
|
||||||
|
import { DAYS_OF_WEEK, MONTHS_OF_YEAR } from "@/digital-components/date-picker/mixins/constants.js";
|
||||||
|
import { defineRule, useField } from "vee-validate";
|
||||||
|
import { errorMessages } from "@/constants/error-messages";
|
||||||
|
import { required } from "@/helpers/validation-rules";
|
||||||
|
|
||||||
|
// Validation for the modal button
|
||||||
|
defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED));
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const AppointmentTypeStrings = {
|
||||||
|
IN_SHOP: "Inshop",
|
||||||
|
MOBILE: "Mobile",
|
||||||
|
DROP_OFF: "Dropoff",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "timeSlotModalQuestion",
|
||||||
|
props: {
|
||||||
|
modelValue: Object,
|
||||||
|
cmsWidgetName: String,
|
||||||
|
mobileCmsWidgetName: String,
|
||||||
|
earlyBirdCmsWidgetName: String,
|
||||||
|
dropoffCmsWidgetName: String,
|
||||||
|
appointmentType: String,
|
||||||
|
dateAndTimeSlotData: Object,
|
||||||
|
mobileEarlyBirdFee: Object,
|
||||||
|
estimatedServiceMinutesMinimum: Number,
|
||||||
|
estimatedServiceMinutesMaximum: Number,
|
||||||
|
validationRules: String,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedTimeSlot: null,
|
||||||
|
timeslotModalListButton: timeslotModalListButton,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const { handleChange } = useField("time-slot-modal-question", props.validationRules);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleChange,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
modelValue() {
|
||||||
|
this.selectedTimeSlot = this.modelValue;
|
||||||
|
this.handleChange(this.modelValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
appointmentDurationTextFromCms() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "SubheaderText");
|
||||||
|
},
|
||||||
|
supplementalInformationBlock() {
|
||||||
|
let appointmentTypeCmsWidgetName;
|
||||||
|
let cmsFieldName = "BodyText";
|
||||||
|
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
|
||||||
|
return null;
|
||||||
|
} else if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||||
|
appointmentTypeCmsWidgetName =
|
||||||
|
this.selectedTimeSlot === this.earlyBirdButtonText
|
||||||
|
? this.earlyBirdCmsWidgetName
|
||||||
|
: this.mobileCmsWidgetName;
|
||||||
|
} else {
|
||||||
|
appointmentTypeCmsWidgetName = this.dropoffCmsWidgetName;
|
||||||
|
if (this.isSameDay) {
|
||||||
|
cmsFieldName = "BodyText2";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.getCmsContent(appointmentTypeCmsWidgetName, cmsFieldName);
|
||||||
|
},
|
||||||
|
footerCloseButtonText() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
||||||
|
},
|
||||||
|
earlyBirdButtonText() {
|
||||||
|
return this.getCmsContent(this.earlyBirdCmsWidgetName, "HeaderText");
|
||||||
|
},
|
||||||
|
dropoffButtonText() {
|
||||||
|
return this.getCmsContent(this.dropoffCmsWidgetName, "HeaderText");
|
||||||
|
},
|
||||||
|
dropoffDisclaimerText() {
|
||||||
|
return this.getCmsContent(this.dropoffCmsWidgetName, "FooterText");
|
||||||
|
},
|
||||||
|
shouldShowDropoffDisclaimerText() {
|
||||||
|
return this.appointmentType === AppointmentTypeStrings.DROP_OFF && !this.isSameDay;
|
||||||
|
},
|
||||||
|
isSameDay() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
dateSelectedReadableDate() {
|
||||||
|
if (this.dateAndTimeSlotData === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// This conversion ensures we don't get get GMT induced date changes
|
||||||
|
const dateObject = new Date(`${this.dateAndTimeSlotData.dateString}T00:00:00`);
|
||||||
|
const weekdayName = DAYS_OF_WEEK[dateObject.getDay()];
|
||||||
|
const month = MONTHS_OF_YEAR[dateObject.getMonth()];
|
||||||
|
const numberDayOfMonth = dateObject.getDate();
|
||||||
|
// Ex. Tuesday, April 23
|
||||||
|
return `${weekdayName}, ${month} ${numberDayOfMonth}`;
|
||||||
|
},
|
||||||
|
inshopAppointmentDurationTextWithTime() {
|
||||||
|
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let durationSubHeaderText = this.appointmentDurationTextFromCms + " ";
|
||||||
|
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
|
||||||
|
durationSubHeaderText += "All day";
|
||||||
|
} else {
|
||||||
|
durationSubHeaderText += this.getDisplayTextForDurationLength(
|
||||||
|
this.estimatedServiceMinutesMinimum,
|
||||||
|
this.estimatedServiceMinutesMaximum
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return durationSubHeaderText;
|
||||||
|
},
|
||||||
|
availableTimeSlots() {
|
||||||
|
if (this.dateAndTimeSlotData === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let availableTimeSlots;
|
||||||
|
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
|
||||||
|
availableTimeSlots = [
|
||||||
|
{
|
||||||
|
value: this.dateAndTimeSlotData.timeSlots[0],
|
||||||
|
buttonLabel: this.dropoffButtonText,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
availableTimeSlots = this.dateAndTimeSlotData.timeSlots.map((timeslot) => {
|
||||||
|
let readableTime;
|
||||||
|
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
|
||||||
|
readableTime = this.getDisplayTextForMilitaryTime(timeslot.startTime);
|
||||||
|
} else if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||||
|
readableTime = `${this.getDisplayTextForMilitaryTime(
|
||||||
|
timeslot.startTime
|
||||||
|
)} - ${this.getDisplayTextForMilitaryTime(timeslot.endTime)}`;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: timeslot.id,
|
||||||
|
buttonLabel: readableTime,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||||
|
const offerPremium = this.dateAndTimeSlotData.timeSlots[0].offerPremium;
|
||||||
|
const hasEarlyBird = this.mobileEarlyBirdFee?.partType === "EARLY BIRD";
|
||||||
|
if (offerPremium && hasEarlyBird) {
|
||||||
|
availableTimeSlots.unshift({
|
||||||
|
value: this.dateAndTimeSlotData.timeSlots[0].id + "-earlybird",
|
||||||
|
buttonLabel: this.earlyBirdButtonText,
|
||||||
|
buttonLabelSubCopy: this.getTotalLineItemPrice(this.mobileEarlyBirdFee),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return availableTimeSlots;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openModal() {
|
||||||
|
this.$refs["timeSlots"].openModal();
|
||||||
|
},
|
||||||
|
// fires any time the footer button is used, is fired before "onModalClosed"
|
||||||
|
closeModal() {
|
||||||
|
this.$emit("update:modelValue", this.selectedTimeSlot);
|
||||||
|
this.$refs["timeSlots"].closeModal();
|
||||||
|
},
|
||||||
|
// fires any time the modal is closed, AFTER "closeModal" fires if footer button is used
|
||||||
|
onModalClosed() {
|
||||||
|
this.selectedTimeSlot = this.modelValue;
|
||||||
|
this.$emit("time-slot-modal-closed");
|
||||||
|
},
|
||||||
|
// Expected input: "HH:MM:SS"
|
||||||
|
getDisplayTextForMilitaryTime(militaryTimeInput) {
|
||||||
|
let hours = parseInt(militaryTimeInput.split(":")[0]);
|
||||||
|
const minutes = militaryTimeInput.split(":")[1];
|
||||||
|
let meridianNotation = "AM";
|
||||||
|
if (militaryTimeInput.split(":")[0] > 12) {
|
||||||
|
hours -= 12;
|
||||||
|
meridianNotation = "PM";
|
||||||
|
}
|
||||||
|
return `${hours}:${minutes} ${meridianNotation}`;
|
||||||
|
},
|
||||||
|
getDisplayTextForDurationLength(durationMinimum, durationMaximum) {
|
||||||
|
let displayTextForDurationLength;
|
||||||
|
if (durationMaximum >= 120) {
|
||||||
|
displayTextForDurationLength = `${durationMinimum / 60} - ${
|
||||||
|
durationMaximum / 60
|
||||||
|
} hours`;
|
||||||
|
} else {
|
||||||
|
displayTextForDurationLength = `${durationMinimum} - ${durationMaximum} minutes`;
|
||||||
|
}
|
||||||
|
return displayTextForDurationLength;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
modal,
|
||||||
|
textBlock,
|
||||||
|
buttonQuestion,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.time-slots-modal.modal.modal-component {
|
||||||
|
> .modal-dialog > .modal-content {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
.modal-header {
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
.text-block.duration-text-block {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
<template>
|
||||||
|
<baseInputButton
|
||||||
|
v-bind="$props"
|
||||||
|
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2"
|
||||||
|
v-model="selectedValue">
|
||||||
|
<div
|
||||||
|
:aria-label="buttonLabel"
|
||||||
|
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
|
||||||
|
<span class="m-0" :class="textPosition">
|
||||||
|
{{ buttonLabel }}
|
||||||
|
</span>
|
||||||
|
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition">
|
||||||
|
{{ formattedButtonLabelSubCopy }}
|
||||||
|
</span>
|
||||||
|
<span v-if="screenReaderOnlyText" class="sr-only">
|
||||||
|
{{ screenReaderOnlyText }}
|
||||||
|
</span>
|
||||||
|
<loader
|
||||||
|
v-if="isLoaderDisplayed && selectingInitiatesLoad"
|
||||||
|
:class="[this.loaderColor, this.loaderPosition]" />
|
||||||
|
</div>
|
||||||
|
</baseInputButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import loader from "@/ux-components/loader/loader";
|
||||||
|
import baseInputButton from "@/digital-components/base-input-button/base-input-button";
|
||||||
|
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "timeslotMOdalListButton",
|
||||||
|
mixins: [inputButtonWrapperMixin],
|
||||||
|
props: {
|
||||||
|
loaderColor: String,
|
||||||
|
loaderPosition: {
|
||||||
|
type: String,
|
||||||
|
default: "right",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoaderDisplayed: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
formattedButtonLabelSubCopy() {
|
||||||
|
return this.buttonLabelSubCopy?.toFixed(2);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
displayLoader() {
|
||||||
|
this.isLoaderDisplayed = true;
|
||||||
|
},
|
||||||
|
preHandleAnswerChange() {
|
||||||
|
if (this.selectingInitiatesLoad) {
|
||||||
|
this.displayLoader();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
loader,
|
||||||
|
baseInputButton,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.loader {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.list-button {
|
||||||
|
outline: none;
|
||||||
|
input[type="radio"],
|
||||||
|
input[type="checkbox"] {
|
||||||
|
position: static; //override bootstrap
|
||||||
|
|
||||||
|
&:focus-visible + .list-button-content {
|
||||||
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
|
}
|
||||||
|
&:focus + .list-button-content {
|
||||||
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
|
}
|
||||||
|
&:checked + .list-button-content {
|
||||||
|
color: $black;
|
||||||
|
font-weight: 500;
|
||||||
|
background: $blue-100;
|
||||||
|
box-shadow: 0 0 0 1px $blue;
|
||||||
|
}
|
||||||
|
&:checked:focus + .list-button-content {
|
||||||
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
|
}
|
||||||
|
&:checked + .list-button-content p,
|
||||||
|
&:checked + .list-button-content span {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
&:checked + .list-button-content span:nth-child(2) {
|
||||||
|
font-weight: 400;
|
||||||
|
color: $gray-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.list-button-content {
|
||||||
|
color: $gray-600;
|
||||||
|
position: relative;
|
||||||
|
background: $white;
|
||||||
|
transition: all 150ms linear;
|
||||||
|
border-radius: $border-radius-lg;
|
||||||
|
border: 1px solid $gray-500;
|
||||||
|
width: 100%;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
span {
|
||||||
|
&.small {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: $gray-550;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
:answers="answers"
|
:answers="answers"
|
||||||
groupName="chooseShop"
|
groupName="chooseShop"
|
||||||
textPosition="text-start"
|
textPosition="text-start"
|
||||||
v-model="selectedValue"
|
v-model="selectedProviderNumber"
|
||||||
isRequired
|
isRequired
|
||||||
validationRules="option-required" />
|
validationRules="option-required" />
|
||||||
<textLink
|
<textLink
|
||||||
|
|
@ -81,12 +81,19 @@ export default {
|
||||||
},
|
},
|
||||||
selectedValue: {
|
selectedValue: {
|
||||||
get: function () {
|
get: function () {
|
||||||
return this.modelValue?.providerNumber;
|
return this.modelValue;
|
||||||
},
|
},
|
||||||
set: function (newValue) {
|
set: function (newValue) {
|
||||||
// Button Question only supports Number, String data types so we must get the full object to emit
|
this.$emit("update:modelValue", newValue);
|
||||||
const provider = this.getSelectedProviderObject(newValue);
|
},
|
||||||
this.$emit("update:modelValue", provider);
|
},
|
||||||
|
selectedProviderNumber: {
|
||||||
|
get: function () {
|
||||||
|
return this.selectedValue?.providerNumber;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
// Button Question only supports Number, or String data types so we must get the full object to emit
|
||||||
|
this.selectedValue = this.getSelectedProviderObject(newValue);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
displayDropoffInformation() {
|
displayDropoffInformation() {
|
||||||
|
|
@ -198,7 +205,7 @@ export default {
|
||||||
|
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
|
|
||||||
this.selectedValue = null;
|
this.selectedProviderNumber = null;
|
||||||
|
|
||||||
this.$refs.buttonQuestion?.resetField();
|
this.$refs.buttonQuestion?.resetField();
|
||||||
|
|
||||||
|
|
@ -216,7 +223,7 @@ export default {
|
||||||
if (this.selectedAppointmentType) {
|
if (this.selectedAppointmentType) {
|
||||||
const selectedShopIndex = this.getSelectedProviderIndex(
|
const selectedShopIndex = this.getSelectedProviderIndex(
|
||||||
newValue,
|
newValue,
|
||||||
this.modelValue?.providerNumber
|
this.selectedProviderNumber
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedShopIndex >= 3) {
|
if (selectedShopIndex >= 3) {
|
||||||
|
|
|
||||||
|
|
@ -1150,7 +1150,18 @@ export const actions = {
|
||||||
// logApiCall: false,
|
// logApiCall: false,
|
||||||
// });
|
// });
|
||||||
},
|
},
|
||||||
|
getMobileEarlyBirdFee(context) {
|
||||||
|
const damageType = context.getters.damage.isRepair ? "Repair" : "Replace";
|
||||||
|
const paymentType = context.getters.order.payment.isInsurance ? "Insurance" : "Cash";
|
||||||
|
return globalMethods.callMockHttpClient({
|
||||||
|
method: endpoints.GetMobileEarlyBirdFee.method,
|
||||||
|
endpoint: `${endpoints.GetMobileEarlyBirdFee.mockUrl}/Cash/Replace`,
|
||||||
|
});
|
||||||
|
// return globalMethods.callHttpClient({
|
||||||
|
// method: endpoints.GetMobileEarlyBirdFee.method,
|
||||||
|
// endpoint: `${endpoints.GetMobileEarlyBirdFee.url}/${paymentType}/${damageType}`,
|
||||||
|
// });
|
||||||
|
},
|
||||||
// Session API Actions
|
// Session API Actions
|
||||||
saveSession(context) {
|
saveSession(context) {
|
||||||
const vehicle = context.getters.vehicle;
|
const vehicle = context.getters.vehicle;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue