Merge pull request #1090 from Safelite/feature/CSR-1137
CSR-1137 | Time slot selection modal for in shop
This commit is contained in:
commit
ee4e25c62b
11 changed files with 516 additions and 28 deletions
|
|
@ -26,7 +26,7 @@ module.exports = {
|
|||
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
|
||||
coverageThreshold: {
|
||||
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
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -111,6 +111,11 @@ const endpoints = {
|
|||
mockUrl: "https://mockey.qa.sagaws.net/service/shop-time-slots", // TODO: REMOVE MOCKURL
|
||||
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: {
|
||||
url: "/order/api/v1/order/save-session",
|
||||
method: "POST",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ const storeActions = {
|
|||
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
|
||||
GET_SHOP_TIME_SLOTS: "getShopTimeSlots",
|
||||
GET_PROVIDERS: "getProviders",
|
||||
GET_MOBILE_EARLY_BIRD_FEE: "getMobileEarlyBirdFee",
|
||||
SAVE_SESSION: "saveSession",
|
||||
LOAD_SESSION: "loadSession",
|
||||
UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE: "updateStoreWithSaveSessionResponse",
|
||||
|
|
|
|||
|
|
@ -120,7 +120,6 @@ export default {
|
|||
isRequired: Boolean,
|
||||
isOverflowScrollable: Boolean,
|
||||
isWide: Boolean,
|
||||
isCashOrInsurance: Boolean,
|
||||
modelValue: [Array, Number, String],
|
||||
value: [Number, String],
|
||||
validationRules: String,
|
||||
|
|
@ -182,6 +181,7 @@ export default {
|
|||
getComponentLoopWrapperClasses() {
|
||||
let classes;
|
||||
switch (this.buttonTypeString) {
|
||||
case "timeSlotModalListButton":
|
||||
case "listButton":
|
||||
classes = "w-100";
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@
|
|||
type="radio"
|
||||
name="day-of-month"
|
||||
v-model="selectedDate"
|
||||
@click="fireDateClickedEvent"
|
||||
:value="date.inputValue"
|
||||
:id="`${month.monthLabel}-${date.dateNum.toString()}`" />
|
||||
<label :for="`${month.monthLabel}-${date.dateNum.toString()}`">
|
||||
|
|
@ -145,6 +146,9 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
fireDateClickedEvent() {
|
||||
this.$emit("date-clicked");
|
||||
},
|
||||
getWeekStartDate(date) {
|
||||
// Get the day of the week for date
|
||||
let dayOfWeek = date.getDay();
|
||||
|
|
@ -306,8 +310,7 @@ export default {
|
|||
|
||||
const monthsAfterToLoadOffset = 12; // TO BE MADE "CONSTANTS"
|
||||
const monthsBeforeToLoadOffset = 36; // TO BE MADE "CONSTANTS"
|
||||
|
||||
config.initialShopTimeSlotsResponse.forEach((selectableDate) => {
|
||||
config.initialShopTimeSlotsResponse.days.forEach((selectableDate) => {
|
||||
this.selectableDatesData.push(selectableDate);
|
||||
});
|
||||
|
||||
|
|
@ -522,7 +525,7 @@ export default {
|
|||
monthStart,
|
||||
monthEnd
|
||||
);
|
||||
moreSelectableDates.forEach((selectableDate) => {
|
||||
moreSelectableDates.days.forEach((selectableDate) => {
|
||||
const index = this.selectableDatesData.findIndex(
|
||||
(obj) => obj.dateString === selectableDate.dateString
|
||||
);
|
||||
|
|
|
|||
|
|
@ -21,4 +21,6 @@ const MONTHS_OF_YEAR = [
|
|||
"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>
|
||||
.text-block {
|
||||
display: flex;
|
||||
&.left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,23 @@
|
|||
selectableDatesSetting="custom"
|
||||
ref="datePicker"
|
||||
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
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
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 { Form, defineRule } from "vee-validate";
|
||||
import datePicker from "@/digital-components/date-picker/date-picker";
|
||||
import timeSlotModalQuestion from "./time-slot-modal-question/time-slot-modal-question";
|
||||
|
||||
// Supporting files
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
|
|
@ -72,6 +89,7 @@ import { required } from "@/helpers/validation-rules";
|
|||
import store from "@/store";
|
||||
|
||||
defineRule("date-required", required(errorMessages.DATE_REQUIRED));
|
||||
defineRule("time-slot-selection-required", required(errorMessages.DATE_REQUIRED));
|
||||
|
||||
const getAvailableDates = async (startDate, endDate) => {
|
||||
// USING DATES PASSED, MAKE AN API CALL
|
||||
|
|
@ -85,11 +103,11 @@ const getAvailableDates = async (startDate, endDate) => {
|
|||
false
|
||||
);
|
||||
const newShopTimeSlots = newShopTimeSlotsResponse.data;
|
||||
return convertApiResponse(newShopTimeSlots.days);
|
||||
return convertApiResponse(newShopTimeSlots);
|
||||
};
|
||||
const convertApiResponse = (responseData) => {
|
||||
// DATA CONVERSION
|
||||
responseData?.forEach((date) => {
|
||||
responseData?.days.forEach((date) => {
|
||||
const dateString = date.date;
|
||||
date.dateString = dateString;
|
||||
const dateStringPieces = dateString.split("-");
|
||||
|
|
@ -105,8 +123,10 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
selectedDate: null,
|
||||
selectedTimeSlotId: null,
|
||||
selectableDatesData: [],
|
||||
weatherAlerts: [],
|
||||
mobileEarlyBirdFee: null,
|
||||
};
|
||||
},
|
||||
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);
|
||||
|
||||
// Settle promises and get results
|
||||
|
|
@ -151,6 +183,14 @@ export default {
|
|||
resultKey: "datePickerInitialData",
|
||||
promise: datePickerInitialDataPromise,
|
||||
},
|
||||
{
|
||||
resultKey: "earlyBird",
|
||||
promise: earlyBirdPromise,
|
||||
},
|
||||
{
|
||||
resultKey: "pricingResults",
|
||||
promise: pricingPromise,
|
||||
},
|
||||
];
|
||||
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
|
|
@ -160,6 +200,10 @@ export default {
|
|||
vm.setCmsContent(resultMap.cmsContent);
|
||||
vm.setData(resultMap.alertReasons);
|
||||
vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData);
|
||||
vm.selectableDatesData = resultMap.datePickerInitialData.initialShopTimeSlotsResponse;
|
||||
if (resultMap.earlyBird) {
|
||||
vm.mobileEarlyBirdFee = resultMap.pricingResults[0];
|
||||
}
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -173,6 +217,35 @@ export default {
|
|||
displayWeatherAlert() {
|
||||
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: {
|
||||
doesCopyContainRouterLink,
|
||||
|
|
@ -184,20 +257,12 @@ export default {
|
|||
// NEED TODO - WHAT ARE PAGE REQ'S FOR THIS PAGE?
|
||||
},
|
||||
async getAvailableDatesMethod(startDate, endDate) {
|
||||
/* TODO - DO WE NEED TO KEEP AN AGGREGATE OF ALL DATES RETURNED
|
||||
FOR TIMESLOTS in this.selectableDatesData? IS selectableDatesData EVEN NEEDED?
|
||||
|
||||
// // ADD API CALL RESULTS TO EXISTING DATE DATA
|
||||
// this.selectableDatesData = this.selectableDatesData.concat(
|
||||
// this.convertApiResponse(newShopTimeSlots.days)
|
||||
// );
|
||||
// // console.log("this.selectableDatesData is now: ", this.selectableDatesData);
|
||||
|
||||
// // RETURN AGGREGATE DATE DATA
|
||||
// return this.selectableDatesData;
|
||||
*/
|
||||
|
||||
return await getAvailableDates(startDate, endDate);
|
||||
const newShopTimeSlots = await getAvailableDates(startDate, endDate);
|
||||
// ADD API CALL RESULTS TO EXISTING DATE DATA
|
||||
this.selectableDatesData.days = this.selectableDatesData.days.concat(
|
||||
newShopTimeSlots.days
|
||||
);
|
||||
return newShopTimeSlots;
|
||||
},
|
||||
setData(alertReasonsData) {
|
||||
if (alertReasonsData) {
|
||||
|
|
@ -230,6 +295,21 @@ export default {
|
|||
getServiceZipCtuCodeFromStore() {
|
||||
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() {
|
||||
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||
},
|
||||
|
|
@ -250,10 +330,11 @@ export default {
|
|||
},
|
||||
},
|
||||
watch: {
|
||||
selectedDate(date) {
|
||||
/* EXAMPLE RETURNED date OBJECT:
|
||||
{ "year": 2023, "month": 5, "date": 25, "dateString": "2023-05-25" }
|
||||
*/
|
||||
selectedDate(newValue, oldValue) {
|
||||
// Clear time slot selection if date selected changes
|
||||
if (newValue !== oldValue) {
|
||||
this.selectedTimeSlotId = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
|
|
@ -264,6 +345,7 @@ export default {
|
|||
Form,
|
||||
loadingModal,
|
||||
datePicker,
|
||||
timeSlotModalQuestion,
|
||||
},
|
||||
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>
|
||||
|
|
@ -1150,7 +1150,18 @@ export const actions = {
|
|||
// 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
|
||||
saveSession(context) {
|
||||
const vehicle = context.getters.vehicle;
|
||||
|
|
|
|||
Loading…
Reference in a new issue