Merge remote-tracking branch 'origin/release/2025.07.10' into feature/CASH-816
This commit is contained in:
commit
adfb325fae
80 changed files with 1913 additions and 933 deletions
BIN
src/assets/fonts/Urbanist-Bold.woff
Normal file
BIN
src/assets/fonts/Urbanist-Bold.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/Urbanist-Bold.woff2
Normal file
BIN
src/assets/fonts/Urbanist-Bold.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/Urbanist-BoldItalic.woff
Normal file
BIN
src/assets/fonts/Urbanist-BoldItalic.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/Urbanist-BoldItalic.woff2
Normal file
BIN
src/assets/fonts/Urbanist-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/Urbanist-ExtraBold.woff
Normal file
BIN
src/assets/fonts/Urbanist-ExtraBold.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/Urbanist-ExtraBold.woff2
Normal file
BIN
src/assets/fonts/Urbanist-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/Urbanist-MediumItalic.woff
Normal file
BIN
src/assets/fonts/Urbanist-MediumItalic.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/Urbanist-MediumItalic.woff2
Normal file
BIN
src/assets/fonts/Urbanist-MediumItalic.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/Urbanist-Regular.woff
Normal file
BIN
src/assets/fonts/Urbanist-Regular.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/Urbanist-Regular.woff2
Normal file
BIN
src/assets/fonts/Urbanist-Regular.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/Urbanist-SemiBold.woff
Normal file
BIN
src/assets/fonts/Urbanist-SemiBold.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/Urbanist-SemiBold.woff2
Normal file
BIN
src/assets/fonts/Urbanist-SemiBold.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -176,6 +176,10 @@ const endpoints = {
|
|||
url: "/analytics/api/v1/analytics/log-part-questions",
|
||||
method: "POST",
|
||||
},
|
||||
LogDigitalConsumer: {
|
||||
url: "/analytics/api/v1/analytics/digitalconsumer-log",
|
||||
method: "POST",
|
||||
},
|
||||
GetExperimentsByUser: {
|
||||
url: "/analytics/api/v1/analytics/get-experiments",
|
||||
method: "GET",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ const experimentSettings = {
|
|||
FOSTER_LOVE: "DisplayFosterLove",
|
||||
SKIP_TO_INSURANCE: "SkipToInsurance",
|
||||
DISPLAY_AFTERPAY_BREAKOUT_DISPLAY: "Display_AfterpayBreakoutDisplay",
|
||||
AFTERPAY_EXTENDED_PAY_OPTION_THRESHOLD: "AfterPayExtendedPayOptionThreshold",
|
||||
DYNAMO_LOGGING: "DynamoLogging",
|
||||
};
|
||||
|
||||
const experimentTriggers = {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const pagePercentageMapper = {
|
|||
"insurance-company": 52,
|
||||
"service-location": 60,
|
||||
schedule: 72,
|
||||
"mobile-details": 76,
|
||||
"customer-details": 84,
|
||||
"payment-method": 92,
|
||||
payment: 96,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const AppointmentTypeStrings = {
|
|||
IN_SHOP: "Inshop",
|
||||
MOBILE: "Mobile",
|
||||
DROP_OFF: "Dropoff",
|
||||
IN_SHOP_OR_DROP_OFF: "InshopOrDropoff",
|
||||
};
|
||||
|
||||
const PREMIUM_TIME_SLOT_ID_FLAG = "-PREMIUM";
|
||||
|
|
|
|||
54
src/constants/state-options.js
Normal file
54
src/constants/state-options.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
const stateOptions = {
|
||||
AL: "Alabama",
|
||||
AK: "Alaska",
|
||||
AZ: "Arizona",
|
||||
AR: "Arkansas",
|
||||
CA: "California",
|
||||
CO: "Colorado",
|
||||
CT: "Connecticut",
|
||||
DE: "Delaware",
|
||||
DC: "District Of Columbia",
|
||||
FL: "Florida",
|
||||
GA: "Georgia",
|
||||
HI: "Hawaii",
|
||||
ID: "Idaho",
|
||||
IL: "Illinois",
|
||||
IN: "Indiana",
|
||||
IA: "Iowa",
|
||||
KS: "Kansas",
|
||||
KY: "Kentucky",
|
||||
LA: "Louisiana",
|
||||
ME: "Maine",
|
||||
MD: "Maryland",
|
||||
MA: "Massachusetts",
|
||||
MI: "Michigan",
|
||||
MN: "Minnesota",
|
||||
MS: "Mississippi",
|
||||
MO: "Missouri",
|
||||
MT: "Montana",
|
||||
NE: "Nebraska",
|
||||
NV: "Nevada",
|
||||
NH: "New Hampshire",
|
||||
NJ: "New Jersey",
|
||||
NM: "New Mexico",
|
||||
NY: "New York",
|
||||
NC: "North Carolina",
|
||||
ND: "North Dakota",
|
||||
OH: "Ohio",
|
||||
OK: "Oklahoma",
|
||||
OR: "Oregon",
|
||||
PA: "Pennsylvania",
|
||||
RI: "Rhode Island",
|
||||
SC: "South Carolina",
|
||||
SD: "South Dakota",
|
||||
TN: "Tennessee",
|
||||
TX: "Texas",
|
||||
UT: "Utah",
|
||||
VT: "Vermont",
|
||||
VA: "Virginia",
|
||||
WA: "Washington",
|
||||
WV: "West Virginia",
|
||||
WI: "Wisconsin",
|
||||
WY: "Wyoming",
|
||||
};
|
||||
export { stateOptions };
|
||||
|
|
@ -63,6 +63,7 @@ const storeActions = {
|
|||
LOG_CUSTOM_EVENT: "logCustomEvent",
|
||||
INITIALIZE_SESSION: "initializeSession",
|
||||
LOG_PART_QUESTIONS: "logPartQuestions",
|
||||
LOG_DIGITALCONSUMER: "logDigitalConsumer",
|
||||
|
||||
// DEPENDENCY MUTATIONS
|
||||
RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleStateAndDependencies",
|
||||
|
|
@ -78,6 +79,7 @@ const storeActions = {
|
|||
SAVE_VIN_LOOKUP: "saveVinLookup",
|
||||
SAVE_SERVICE_ZIP_CODE_INFO: "saveServiceZipCodeInfo",
|
||||
SAVE_SERVICE_LOCATION: "saveServiceLocation",
|
||||
SAVE_MOBILE_DETAILS: "saveMobileDetails",
|
||||
SAVE_SCHEDULE: "saveSchedule",
|
||||
SAVE_EMAIL: "saveEmail",
|
||||
SAVE_PHONE_NUMBER: "savePhoneNumber",
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ const storeMutations = {
|
|||
|
||||
UPDATE_SERVICE_ZIP: "updateServiceZip",
|
||||
UPDATE_SERVICE_LOCATION: "updateServiceLocation",
|
||||
UPDATE_MOBILE_DETAILS: "updateMobileDetails",
|
||||
UPDATE_SERVICE_LOCATION_TECH_NOTES: "updateServiceLocationTechNotes",
|
||||
UPDATE_SCHEDULE: "updateSchedule",
|
||||
|
||||
|
|
|
|||
|
|
@ -351,6 +351,7 @@ export default {
|
|||
}
|
||||
}
|
||||
.button-question {
|
||||
font-family: UrbanistSemibold;
|
||||
color: $black;
|
||||
|
||||
.radio-button-container {
|
||||
|
|
|
|||
|
|
@ -42,19 +42,36 @@ const mockValuesForSetDate01 = {
|
|||
initialViewEndDate: "2023-12-30",
|
||||
calendarViewDirection: "future",
|
||||
initialShopTimeSlotsResponse: {
|
||||
days: [
|
||||
{
|
||||
date: "2023-12-30",
|
||||
timeSlots: [
|
||||
{
|
||||
id: "06747-01820-S-B*20453*7 AM",
|
||||
startTime: "07:00",
|
||||
endTime: "08:00",
|
||||
offerPremium: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
inshopTimeSlotsData: {
|
||||
days: [
|
||||
{
|
||||
date: "2023-12-30",
|
||||
timeSlots: [
|
||||
{
|
||||
id: "06747-01820-S-B*20453*7 AM",
|
||||
startTime: "07:00",
|
||||
endTime: "08:00",
|
||||
offerPremium: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
mobileTimeSlotsData: {
|
||||
days: [
|
||||
{
|
||||
date: "2023-12-30",
|
||||
timeSlots: [
|
||||
{
|
||||
id: "06747-01820-S-B*20453*7 AM",
|
||||
startTime: "07:00",
|
||||
endTime: "08:00",
|
||||
offerPremium: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
estimatedServiceMinutesMinimum: 90,
|
||||
estimatedServiceMinutesMaximum: 120,
|
||||
},
|
||||
|
|
@ -68,194 +85,195 @@ const mockValuesForSetDate01 = {
|
|||
monthIndex: 12,
|
||||
monthString: "December-2023",
|
||||
yearNum: 2023,
|
||||
dates: [
|
||||
datesInshop: [
|
||||
{
|
||||
dateNum: 1,
|
||||
dayClasses: " current-day",
|
||||
inputValue: "2023-12-01",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 2,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-02",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 3,
|
||||
dayClasses: " sunday",
|
||||
inputValue: "2023-12-03",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 4,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-04",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 5,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-05",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 6,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-06",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 7,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-07",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 8,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-08",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 9,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-09",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 10,
|
||||
dayClasses: " sunday",
|
||||
inputValue: "2023-12-10",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 11,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-11",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 12,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-12",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 13,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-13",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 14,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-14",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 15,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-15",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 16,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-16",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 17,
|
||||
dayClasses: " sunday",
|
||||
inputValue: "2023-12-17",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 18,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-18",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 19,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-19",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 20,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-20",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 21,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-21",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 22,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-22",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 23,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-23",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 24,
|
||||
dayClasses: " sunday",
|
||||
inputValue: "2023-12-24",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 25,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-25",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 26,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-26",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 27,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-27",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 28,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-28",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 29,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-29",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 30,
|
||||
dayClasses: "",
|
||||
inputValue: "2023-12-30",
|
||||
isSelectable: true,
|
||||
isAvailable: true,
|
||||
},
|
||||
{
|
||||
dateNum: 31,
|
||||
dayClasses: " sunday day-hidden",
|
||||
inputValue: "2023-12-31",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
],
|
||||
datesMobile: [],
|
||||
startDateDayIndex: 5,
|
||||
monthClass: "",
|
||||
isMonthThatHidesSomeDaysForInitialView: true,
|
||||
|
|
@ -265,32 +283,33 @@ const mockValuesForSetDate01 = {
|
|||
monthIndex: 1,
|
||||
monthString: "January-2024",
|
||||
yearNum: 2024,
|
||||
dates: [
|
||||
datesInshop: [
|
||||
{
|
||||
dateNum: 1,
|
||||
dayClasses: "",
|
||||
inputValue: "2024-01-01",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 2,
|
||||
dayClasses: "",
|
||||
inputValue: "2024-01-02",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 30,
|
||||
dayClasses: "",
|
||||
inputValue: "2024-01-30",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
{
|
||||
dateNum: 31,
|
||||
dayClasses: "",
|
||||
inputValue: "2024-01-31",
|
||||
isSelectable: false,
|
||||
isAvailable: false,
|
||||
},
|
||||
],
|
||||
datesMobile: [],
|
||||
startDateDayIndex: 1,
|
||||
monthClass: " month-hidden",
|
||||
},
|
||||
|
|
@ -303,36 +322,70 @@ const mockInitialData = {
|
|||
initialViewEndDate: "2023-08-19",
|
||||
calendarViewDirection: "future",
|
||||
initialShopTimeSlotsResponse: {
|
||||
days: [
|
||||
{
|
||||
date: "2023-07-25",
|
||||
timeSlots: [
|
||||
{
|
||||
id: "06747-01820-S-B*20295*7 AM",
|
||||
startTime: "07:00",
|
||||
endTime: "08:00",
|
||||
offerPremium: false,
|
||||
},
|
||||
{
|
||||
id: "06747-01820-S-B*20295*730 AM",
|
||||
startTime: "07:30",
|
||||
endTime: "08:30",
|
||||
offerPremium: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
date: "2023-07-26",
|
||||
timeSlots: [
|
||||
{
|
||||
id: "06747-01820-S-B*20296*7 AM",
|
||||
startTime: "07:00",
|
||||
endTime: "08:00",
|
||||
offerPremium: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
inshopTimeSlotsData: {
|
||||
days: [
|
||||
{
|
||||
date: "2023-07-25",
|
||||
timeSlots: [
|
||||
{
|
||||
id: "06747-01820-S-B*20295*7 AM",
|
||||
startTime: "07:00",
|
||||
endTime: "08:00",
|
||||
offerPremium: false,
|
||||
},
|
||||
{
|
||||
id: "06747-01820-S-B*20295*730 AM",
|
||||
startTime: "07:30",
|
||||
endTime: "08:30",
|
||||
offerPremium: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
date: "2023-07-26",
|
||||
timeSlots: [
|
||||
{
|
||||
id: "06747-01820-S-B*20296*7 AM",
|
||||
startTime: "07:00",
|
||||
endTime: "08:00",
|
||||
offerPremium: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
mobileTimeSlotsData: {
|
||||
days: [
|
||||
{
|
||||
date: "2023-07-25",
|
||||
timeSlots: [
|
||||
{
|
||||
id: "06747-01820-S-B*20295*7 AM",
|
||||
startTime: "07:00",
|
||||
endTime: "08:00",
|
||||
offerPremium: false,
|
||||
},
|
||||
{
|
||||
id: "06747-01820-S-B*20295*730 AM",
|
||||
startTime: "07:30",
|
||||
endTime: "08:30",
|
||||
offerPremium: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
date: "2023-07-26",
|
||||
timeSlots: [
|
||||
{
|
||||
id: "06747-01820-S-B*20296*7 AM",
|
||||
startTime: "07:00",
|
||||
endTime: "08:00",
|
||||
offerPremium: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
estimatedServiceMinutesMinimum: 90,
|
||||
estimatedServiceMinutesMaximum: 120,
|
||||
},
|
||||
|
|
@ -342,30 +395,59 @@ const mockInitialData = {
|
|||
};
|
||||
|
||||
const mockCallbackResponse = {
|
||||
days: [
|
||||
{
|
||||
date: "2024-12-30",
|
||||
timeSlots: [
|
||||
{
|
||||
id: "06747-01820-S-B*20453*7 AM",
|
||||
startTime: "07:00",
|
||||
endTime: "08:00",
|
||||
offerPremium: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
date: "2024-01-02",
|
||||
timeSlots: [
|
||||
{
|
||||
id: "06747-01820-S-B*20453*7 AM",
|
||||
startTime: "07:00",
|
||||
endTime: "08:00",
|
||||
offerPremium: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
inshopTimeSlotsData: {
|
||||
days: [
|
||||
{
|
||||
date: "2024-12-30",
|
||||
timeSlots: [
|
||||
{
|
||||
id: "06747-01820-S-B*20453*7 AM",
|
||||
startTime: "07:00",
|
||||
endTime: "08:00",
|
||||
offerPremium: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
date: "2024-01-02",
|
||||
timeSlots: [
|
||||
{
|
||||
id: "06747-01820-S-B*20453*7 AM",
|
||||
startTime: "07:00",
|
||||
endTime: "08:00",
|
||||
offerPremium: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
mobileTimeSlotsData: {
|
||||
days: [
|
||||
{
|
||||
date: "2024-12-30",
|
||||
timeSlots: [
|
||||
{
|
||||
id: "06747-01820-S-B*20453*7 AM",
|
||||
startTime: "07:00",
|
||||
endTime: "08:00",
|
||||
offerPremium: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
date: "2024-01-02",
|
||||
timeSlots: [
|
||||
{
|
||||
id: "06747-01820-S-B*20453*7 AM",
|
||||
startTime: "07:00",
|
||||
endTime: "08:00",
|
||||
offerPremium: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
estimatedServiceMinutesMinimum: 90,
|
||||
estimatedServiceMinutesMaximum: 120,
|
||||
};
|
||||
|
|
@ -429,7 +511,7 @@ describe("date-picker.vue", () => {
|
|||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test("If prop customSelectableDatesCallback is not supplied, expect the default function will return empty array", () => {
|
||||
test("If prop getMoreDatesCallback is not supplied, expect the default function will return empty array", () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: {
|
||||
|
|
@ -438,7 +520,7 @@ describe("date-picker.vue", () => {
|
|||
});
|
||||
|
||||
// Act
|
||||
const callbackResponse = wrapper.vm.customSelectableDatesCallback(
|
||||
const callbackResponse = wrapper.vm.getMoreDatesCallback(
|
||||
"2023-07-24",
|
||||
"2023-08-19",
|
||||
"Inshop",
|
||||
|
|
@ -608,7 +690,7 @@ describe("date-picker.vue", () => {
|
|||
const testResult = await wrapper.vm.loadInitialData({
|
||||
selectableDatesSetting: "custom",
|
||||
initialViewRowsToShow: 5,
|
||||
customSelectableDatesCallback: mockPromiseResolve,
|
||||
getSelectableDatesCallback: mockPromiseResolve,
|
||||
preSelectedDate: null,
|
||||
});
|
||||
|
||||
|
|
@ -640,7 +722,7 @@ describe("date-picker.vue", () => {
|
|||
const testResult = await datePicker.methods.loadInitialData.call(localThis, {
|
||||
selectableDatesSetting: "custom",
|
||||
initialViewRowsToShow: 5,
|
||||
customSelectableDatesCallback: mockPromiseResolve,
|
||||
getSelectableDatesCallback: mockPromiseResolve,
|
||||
preSelectedDate: null,
|
||||
|
||||
todayOverrideDateString: overrideDate,
|
||||
|
|
@ -676,7 +758,7 @@ describe("date-picker.vue", () => {
|
|||
const testResult = await datePicker.methods.loadInitialData.call(localThis, {
|
||||
selectableDatesSetting: "custom",
|
||||
initialViewRowsToShow: 5,
|
||||
customSelectableDatesCallback: mockPromiseResolve,
|
||||
getSelectableDatesCallback: mockPromiseResolve,
|
||||
preSelectedDate: null,
|
||||
});
|
||||
|
||||
|
|
@ -696,7 +778,7 @@ describe("date-picker.vue", () => {
|
|||
const testResult = await wrapper.vm.loadInitialData({
|
||||
selectableDatesSetting: "past",
|
||||
initialViewRowsToShow: 5,
|
||||
customSelectableDatesCallback: mockPromiseResolve,
|
||||
getSelectableDatesCallback: mockPromiseResolve,
|
||||
});
|
||||
|
||||
// Assert
|
||||
|
|
@ -715,7 +797,7 @@ describe("date-picker.vue", () => {
|
|||
const testResult = await wrapper.vm.loadInitialData({
|
||||
selectableDatesSetting: "custom",
|
||||
initialViewRowsToShow: 5,
|
||||
customSelectableDatesCallback: mockPromiseResolve,
|
||||
getSelectableDatesCallback: mockPromiseResolve,
|
||||
});
|
||||
|
||||
// Assert
|
||||
|
|
@ -742,10 +824,10 @@ describe("date-picker.vue", () => {
|
|||
|
||||
// Assert
|
||||
expect(wrapper.vm.hideSomeDaysForInitialView).toBe(true);
|
||||
expect(wrapper.vm.selectableDatesData[0]).toMatchObject({
|
||||
expect(wrapper.vm.selectableDatesInshop[0]).toMatchObject({
|
||||
date: "2023-12-30",
|
||||
});
|
||||
expect(wrapper.vm.selectableDatesData[0].timeSlots.length).toBeGreaterThan(0);
|
||||
expect(wrapper.vm.selectableDatesInshop[0].timeSlots.length).toBeGreaterThan(0);
|
||||
expect(wrapper.vm.months.length).toBe(7);
|
||||
expect(wrapper.vm.months[0]).toMatchObject({
|
||||
monthLabel: "December",
|
||||
|
|
@ -992,7 +1074,7 @@ describe("date-picker.vue", () => {
|
|||
const testResult = await datePicker.methods.loadInitialData.call(localThis, {
|
||||
selectableDatesSetting: "custom",
|
||||
initialViewRowsToShow: 5,
|
||||
customSelectableDatesCallback: mockPromiseResolve,
|
||||
getSelectableDatesCallback: mockPromiseResolve,
|
||||
preSelectedDate: "2024-01-01",
|
||||
todayOverrideDateString: overrideDate,
|
||||
});
|
||||
|
|
@ -1025,14 +1107,14 @@ describe("date-picker.vue", () => {
|
|||
// Act
|
||||
await wrapper.vm.showAnotherMonth();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.updateSelectableDates).toHaveBeenCalledWith(
|
||||
"2023-12-31",
|
||||
"2023-12-31"
|
||||
);
|
||||
expect(wrapper.vm.scrollToElement).toHaveBeenCalledWith("December-2023");
|
||||
// // Assert
|
||||
// expect(wrapper.vm.updateSelectableDates).toHaveBeenCalledWith(
|
||||
// "2023-12-31",
|
||||
// "2023-12-31"
|
||||
// );
|
||||
// expect(wrapper.vm.scrollToElement).toHaveBeenCalledWith("December-2023");
|
||||
|
||||
wrapper.unmount();
|
||||
// wrapper.unmount();
|
||||
});
|
||||
|
||||
test("expect that when hideSomeDaysForInitialView is false and direction is FUTURE that it scrolls to correct month", async () => {
|
||||
|
|
@ -1115,16 +1197,16 @@ describe("date-picker.vue", () => {
|
|||
});
|
||||
|
||||
describe("updateSelectableDates method...", () => {
|
||||
test("expect that new dates from callback are added to selectableDatesData", async () => {
|
||||
test("expect that new dates from callback are added to selectableDatesInshop", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: {
|
||||
selectableDatesSetting: "custom",
|
||||
todayOverrideDateString: mockValuesForSetDate01.overrideDate,
|
||||
customSelectableDatesCallback: jest.fn(() => mockCallbackResponse),
|
||||
getMoreDatesCallback: jest.fn(() => mockCallbackResponse),
|
||||
},
|
||||
});
|
||||
wrapper.vm.selectableDatesData = [
|
||||
wrapper.vm.selectableDatesInshop = [
|
||||
{
|
||||
date: "2023-12-30",
|
||||
timeSlots: [
|
||||
|
|
@ -1141,7 +1223,8 @@ describe("date-picker.vue", () => {
|
|||
|
||||
// Act
|
||||
await wrapper.vm.updateSelectableDates("2024-01-01", "2024-01-05");
|
||||
const isFoundInArray = wrapper.vm.selectableDatesData.find((obj) => {
|
||||
|
||||
const isFoundInArray = wrapper.vm.selectableDatesInshop.find((obj) => {
|
||||
return obj.date === "2024-01-02";
|
||||
});
|
||||
|
||||
|
|
@ -1151,16 +1234,16 @@ describe("date-picker.vue", () => {
|
|||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test("expect that isSelectable flag on selectable dates is set to true", async () => {
|
||||
test("expect that isAvailable flag on selectable dates is set to true", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: {
|
||||
selectableDatesSetting: "custom",
|
||||
todayOverrideDateString: mockValuesForSetDate01.overrideDate,
|
||||
customSelectableDatesCallback: jest.fn(() => mockCallbackResponse),
|
||||
getMoreDatesCallback: jest.fn(() => mockCallbackResponse),
|
||||
},
|
||||
});
|
||||
wrapper.vm.selectableDatesData = [
|
||||
wrapper.vm.selectableDatesInshop = [
|
||||
{
|
||||
date: "2023-12-30",
|
||||
timeSlots: [
|
||||
|
|
@ -1180,13 +1263,16 @@ describe("date-picker.vue", () => {
|
|||
const targetMonth = wrapper.vm.months.find((month) => {
|
||||
return month.monthString === "January-2024";
|
||||
});
|
||||
const dateInArray = targetMonth.dates.find((date) => {
|
||||
|
||||
console.log("TEST targetMonth: ", targetMonth);
|
||||
|
||||
const dateInArray = targetMonth.datesInshop.find((date) => {
|
||||
return date.inputValue === "2024-01-02";
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(dateInArray).toMatchObject({
|
||||
isSelectable: true,
|
||||
isAvailable: true,
|
||||
});
|
||||
|
||||
wrapper.unmount();
|
||||
|
|
@ -1194,7 +1280,7 @@ describe("date-picker.vue", () => {
|
|||
});
|
||||
|
||||
describe("scrollToElement method...", () => {
|
||||
test("expect that new dates from callback are added to selectableDatesData", () => {
|
||||
test("expect that new dates from callback are added to selectableDatesInshop", () => {
|
||||
// Arrange
|
||||
const container = document.createElement("div");
|
||||
container.classList.add("page-container-grouped-styles");
|
||||
|
|
|
|||
|
|
@ -53,18 +53,19 @@
|
|||
SAT
|
||||
</div>
|
||||
<div
|
||||
v-for="date in month.dates"
|
||||
v-show="!isMobileSelected"
|
||||
v-for="date in month.datesInshop"
|
||||
:key="date.inputValue"
|
||||
:id="date.inputValue"
|
||||
class="grid-item radio-wrapper"
|
||||
class="grid-item radio-wrapper inshop-version"
|
||||
:class="[
|
||||
date.dateNum === 1 ? 'first-day-' + month.startDateDayIndex : '',
|
||||
date.dayClasses,
|
||||
date.isSelectable ? 'selectable-day' : '',
|
||||
date.isAvailable ? 'selectable-day' : '',
|
||||
date.isPricingByDayUpchargeDay ? 'upch-day' : '',
|
||||
]">
|
||||
<input
|
||||
:disabled="!date.isSelectable"
|
||||
:disabled="!date.isAvailable"
|
||||
type="radio"
|
||||
name="day-of-month"
|
||||
v-model="selectedDate"
|
||||
|
|
@ -75,7 +76,37 @@
|
|||
<label :for="`${month.monthLabel}-${date.dateNum.toString()}`">
|
||||
<span>{{ date.dateNum.toString() }}</span>
|
||||
<span
|
||||
v-if="isPricingByDayExperiment && showPricingByDay && date.isSelectable"
|
||||
v-if="isPricingByDayExperiment && showPricingByDay && date.isAvailable"
|
||||
class="price">
|
||||
{{ date.priceString }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-show="isMobileSelected"
|
||||
v-for="date in month.datesMobile"
|
||||
:key="`${date.inputValue}-mobile`"
|
||||
:id="`${date.inputValue}-mobile`"
|
||||
class="grid-item radio-wrapper mobile-version"
|
||||
:class="[
|
||||
date.dateNum === 1 ? 'first-day-' + month.startDateDayIndex : '',
|
||||
date.dayClasses,
|
||||
date.isAvailable ? 'selectable-day' : '',
|
||||
date.isPricingByDayUpchargeDay ? 'upch-day' : '',
|
||||
]">
|
||||
<input
|
||||
:disabled="!date.isAvailable"
|
||||
type="radio"
|
||||
name="day-of-month"
|
||||
v-model="selectedDate"
|
||||
@click="fireDateSelectedEvent($event, date)"
|
||||
@keypress.enter="fireDateSelectedEvent($event, date)"
|
||||
:value="`${date.inputValue}-mobile`"
|
||||
:id="`${month.monthLabel}-${date.dateNum.toString()}-mobile`" />
|
||||
<label :for="`${month.monthLabel}-${date.dateNum.toString()}-mobile`">
|
||||
<span>{{ date.dateNum.toString() }}</span>
|
||||
<span
|
||||
v-if="isPricingByDayExperiment && showPricingByDay && date.isAvailable"
|
||||
class="price">
|
||||
{{ date.priceString }}
|
||||
</span>
|
||||
|
|
@ -134,21 +165,16 @@ import {
|
|||
MONTHS_OF_YEAR,
|
||||
DAYS_OF_WEEK,
|
||||
} from "@/digital-components/date-picker/mixins/constants";
|
||||
import {
|
||||
AppointmentTypeStrings,
|
||||
PREMIUM_FEE_PART_TYPE,
|
||||
PRICING_BY_DAY_PART_TYPE,
|
||||
} from "@/constants/schedule-constants";
|
||||
import { PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
|
||||
import {
|
||||
selectableDaysOptions,
|
||||
requiredParameter,
|
||||
forceTwoDigitString,
|
||||
} from "@/digital-components/date-picker/mixins/helpers";
|
||||
import {
|
||||
convertDateToDateString,
|
||||
convertDateStringToDate,
|
||||
} from "@/layouts/schedule/helpers/schedule-helper";
|
||||
import { useField, ErrorMessage, defineRule } from "vee-validate";
|
||||
import { useField, ErrorMessage } from "vee-validate";
|
||||
import { deepClone } from "@/helpers/object-helper";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
|
|
@ -159,10 +185,10 @@ export default {
|
|||
isLoading: true,
|
||||
months: null,
|
||||
disableViewMoreDatesButton: false,
|
||||
selectableDatesData: [], // NOTE: uses monthNum (1-based), NOT monthIndex (0-based)
|
||||
hideSomeDaysForInitialView: null,
|
||||
|
||||
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
|
||||
selectableDatesInshop: [], // NOTE: this and the mobile version below use monthNum (1-based), NOT monthIndex (0-based)
|
||||
selectableDatesMobile: [],
|
||||
};
|
||||
},
|
||||
props: {
|
||||
|
|
@ -182,7 +208,7 @@ export default {
|
|||
type: String,
|
||||
default: null,
|
||||
},
|
||||
customSelectableDatesCallback: {
|
||||
getMoreDatesCallback: {
|
||||
type: Function,
|
||||
default() {
|
||||
return [];
|
||||
|
|
@ -202,6 +228,7 @@ export default {
|
|||
estimatedServiceMinutesMinimum: Number,
|
||||
estimatedServiceMinutesMaximum: Number,
|
||||
displayWaitList: Boolean,
|
||||
isMobileSelected: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const uuid = uuidv4();
|
||||
|
|
@ -304,14 +331,16 @@ export default {
|
|||
const date = new Date(dateStr.split("-")[0], parseInt(dateStr.split("-")[1]), 0);
|
||||
return convertDateToDateString(date);
|
||||
},
|
||||
getInitialViewWeeks(todayString, initialViewRowsToShow, preSelectedDateString) {
|
||||
getInitialViewWeeks(todayString, initialViewRowsToShow, preSelectedDate) {
|
||||
// TODO: this only is for future direction; need to create logic for past direction
|
||||
const weeks = [];
|
||||
let weekStartDate = this.getWeekStartDate(todayString);
|
||||
let weekEndDate = this.getWeekEndDate(todayString);
|
||||
|
||||
if (preSelectedDateString) {
|
||||
let preSelectedDateMonthEnd = this.getMonthEnd(preSelectedDateString);
|
||||
if (preSelectedDate) {
|
||||
let preSelectedDateMonthEnd = this.getMonthEnd(
|
||||
preSelectedDate.replace("-mobile", "")
|
||||
);
|
||||
let weekIncludesPreSelectedMonthEnd = false;
|
||||
let i = 0;
|
||||
while (!weekIncludesPreSelectedMonthEnd) {
|
||||
|
|
@ -402,7 +431,7 @@ export default {
|
|||
},
|
||||
async loadInitialData(config) {
|
||||
/*
|
||||
** NOTE: this _could_ be called by a parent before fully loaded, so data or computeds might not be available
|
||||
** NOTE: this _could_ be called by a parent before fully loaded, so FYI component data or computeds might not be available
|
||||
*/
|
||||
let todayDateString;
|
||||
let calendarViewDirection = "none";
|
||||
|
|
@ -421,7 +450,7 @@ export default {
|
|||
|
||||
const currentMonthEnd = this.getMonthEnd(todayDateString);
|
||||
// TODO - set up currentMonthStart if direction is PAST:
|
||||
// let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1);
|
||||
// let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1);
|
||||
const initialViewWeeks = this.getInitialViewWeeks(
|
||||
todayDateString,
|
||||
config.initialViewRowsToShow,
|
||||
|
|
@ -451,10 +480,9 @@ export default {
|
|||
}
|
||||
|
||||
const loadInitialDataPromise = new Promise((resolve, reject) => {
|
||||
const response = config.customSelectableDatesCallback(
|
||||
const response = config.getSelectableDatesCallback(
|
||||
initialViewStartDate,
|
||||
initialViewEndDate,
|
||||
store.getters.order.serviceLocation.appointmentType,
|
||||
store.getters.order.serviceLocation.provider.providerNumber
|
||||
);
|
||||
resolve(response);
|
||||
|
|
@ -480,9 +508,17 @@ export default {
|
|||
const direction = config.calendarViewDirection;
|
||||
const monthsAfterToLoadOffset = 6;
|
||||
const monthsBeforeToLoadOffset = 36;
|
||||
config.initialShopTimeSlotsResponse.days.forEach((selectableDate) => {
|
||||
this.selectableDatesData.push(selectableDate);
|
||||
});
|
||||
|
||||
config.initialShopTimeSlotsResponse.inshopTimeSlotsData.days.forEach(
|
||||
(selectableDate) => {
|
||||
this.selectableDatesInshop.push(selectableDate);
|
||||
}
|
||||
);
|
||||
config.initialShopTimeSlotsResponse.mobileTimeSlotsData.days.forEach(
|
||||
(selectableDate) => {
|
||||
this.selectableDatesMobile.push(selectableDate);
|
||||
}
|
||||
);
|
||||
|
||||
// GENERATE MONTHS AND PUSH THEM INTO ARRAY
|
||||
const months = [];
|
||||
|
|
@ -534,7 +570,7 @@ export default {
|
|||
}
|
||||
},
|
||||
async getMonthData(offset = requiredParameter(), options) {
|
||||
/* options will contain:
|
||||
/* options contains:
|
||||
calendarViewDirection (string)
|
||||
initialViewStartDateNum (number)
|
||||
initialViewEndDateNum (number)
|
||||
|
|
@ -543,9 +579,14 @@ export default {
|
|||
hideSecondMonth (boolean),
|
||||
preSelectedDate (string)
|
||||
|
||||
data used:
|
||||
todayDate (date object)
|
||||
data variables used:
|
||||
this.todayString (date string)
|
||||
this.currentWeekStartDateNum
|
||||
this.currentWeekEndDateNum
|
||||
this.todayDateNum
|
||||
this.hideSomeDaysForInitialView (string)
|
||||
this.selectableDatesInshop
|
||||
this.selectableDatesMobile
|
||||
*/
|
||||
|
||||
let monthNum = convertDateStringToDate(this.todayString).getMonth() + offset + 1;
|
||||
|
|
@ -568,7 +609,8 @@ export default {
|
|||
const preSelectedDateObj = options.preSelectedDate
|
||||
? convertDateStringToDate(options.preSelectedDate)
|
||||
: null;
|
||||
const dates = [];
|
||||
let datesInshop = [];
|
||||
let datesMobile = [];
|
||||
const monthEndDate = new Date(yearNum, monthNum, 0);
|
||||
const monthStartDateNum =
|
||||
offset === 0 && calendarViewDirection === "future"
|
||||
|
|
@ -620,64 +662,74 @@ export default {
|
|||
monthClass = monthClass + " last-available-month";
|
||||
}
|
||||
|
||||
// populate dates array
|
||||
for (let i = monthStartDateNum; i <= monthEndDateNum; i++) {
|
||||
let dayClasses = "";
|
||||
const dateString =
|
||||
yearNum.toString() +
|
||||
"-" +
|
||||
("0" + monthNum).slice(-2) +
|
||||
"-" +
|
||||
("0" + i).slice(-2);
|
||||
const dayIndex = convertDateStringToDate(dateString).getDay();
|
||||
const dayObject = DAYS_OF_WEEK[dayIndex];
|
||||
const isSelectable =
|
||||
this.selectableDatesData.findIndex((date) => date.date === dateString) > -1
|
||||
? true
|
||||
: false;
|
||||
const isPricingByDayUpchargeDay = dayObject.isPricingByDayUpchargeDay;
|
||||
const displayPrice = isPricingByDayUpchargeDay
|
||||
? options.pricingByDayBasePrice + options.pricingByDayUpcharge
|
||||
: options.pricingByDayBasePrice;
|
||||
const priceString = "$" + displayPrice;
|
||||
const getDates = (selectableDates, datesArray) => {
|
||||
// populate dates array
|
||||
for (let i = monthStartDateNum; i <= monthEndDateNum; i++) {
|
||||
let dayClasses = "";
|
||||
const dateString =
|
||||
yearNum.toString() +
|
||||
"-" +
|
||||
("0" + monthNum).slice(-2) +
|
||||
"-" +
|
||||
("0" + i).slice(-2);
|
||||
const dayIndex = convertDateStringToDate(dateString).getDay();
|
||||
const dayObject = DAYS_OF_WEEK[dayIndex];
|
||||
const isAvailable =
|
||||
selectableDates.findIndex((date) => date.date === dateString) > -1
|
||||
? true
|
||||
: false;
|
||||
const isPricingByDayUpchargeDay = dayObject.isPricingByDayUpchargeDay;
|
||||
const displayPrice = isPricingByDayUpchargeDay
|
||||
? options.pricingByDayBasePrice + options.pricingByDayUpcharge
|
||||
: options.pricingByDayBasePrice;
|
||||
const priceString = "$" + displayPrice;
|
||||
|
||||
if (offset === 0 && i === this.todayDateNum) {
|
||||
dayClasses += " current-day";
|
||||
if (offset === 0 && i === this.todayDateNum) {
|
||||
dayClasses += " current-day";
|
||||
}
|
||||
if (
|
||||
offset === 0 &&
|
||||
i < this.todayDateNum &&
|
||||
calendarViewDirection === "future"
|
||||
) {
|
||||
dayClasses += " unavailable-day";
|
||||
}
|
||||
if (offset === 0 && i > this.todayDateNum && calendarViewDirection === "past") {
|
||||
dayClasses += " unavailable-day";
|
||||
}
|
||||
if (dayIndex === 0) {
|
||||
dayClasses += " " + dayObject.cssClass;
|
||||
}
|
||||
if (
|
||||
this.hideSomeDaysForInitialView &&
|
||||
convertDateStringToDate(initialViewEndDate).getMonth() + 1 === monthNum &&
|
||||
convertDateStringToDate(initialViewEndDate).getDate() < i
|
||||
) {
|
||||
dayClasses += " day-hidden";
|
||||
isMonthThatHidesSomeDaysForInitialView = true;
|
||||
}
|
||||
const dateObject = {
|
||||
dateNum: i,
|
||||
dayClasses: dayClasses,
|
||||
inputValue: dateString,
|
||||
priceString: priceString,
|
||||
isAvailable: isAvailable,
|
||||
isPricingByDayUpchargeDay: isPricingByDayUpchargeDay,
|
||||
};
|
||||
datesArray.push(dateObject);
|
||||
}
|
||||
if (offset === 0 && i < this.todayDateNum && calendarViewDirection === "future") {
|
||||
dayClasses += " unavailable-day";
|
||||
}
|
||||
if (offset === 0 && i > this.todayDateNum && calendarViewDirection === "past") {
|
||||
dayClasses += " unavailable-day";
|
||||
}
|
||||
if (dayIndex === 0) {
|
||||
dayClasses += " " + dayObject.cssClass;
|
||||
}
|
||||
if (
|
||||
this.hideSomeDaysForInitialView &&
|
||||
convertDateStringToDate(initialViewEndDate).getMonth() + 1 === monthNum &&
|
||||
convertDateStringToDate(initialViewEndDate).getDate() < i
|
||||
) {
|
||||
dayClasses += " day-hidden";
|
||||
isMonthThatHidesSomeDaysForInitialView = true;
|
||||
}
|
||||
const dateObject = {
|
||||
dateNum: i,
|
||||
dayClasses: dayClasses,
|
||||
inputValue: dateString,
|
||||
priceString: priceString,
|
||||
isSelectable: isSelectable,
|
||||
isPricingByDayUpchargeDay: isPricingByDayUpchargeDay,
|
||||
};
|
||||
dates.push(dateObject);
|
||||
}
|
||||
return datesArray;
|
||||
};
|
||||
datesInshop = getDates(this.selectableDatesInshop, datesInshop);
|
||||
datesMobile = getDates(this.selectableDatesMobile, datesMobile);
|
||||
|
||||
const monthToAdd = {
|
||||
monthLabel: MONTHS_OF_YEAR[monthNum - 1],
|
||||
monthIndex: monthNum,
|
||||
monthString: MONTHS_OF_YEAR[monthNum - 1] + "-" + yearNum?.toString(),
|
||||
yearNum: yearNum,
|
||||
dates: dates,
|
||||
datesInshop: datesInshop,
|
||||
datesMobile: datesMobile,
|
||||
startDateDayIndex: startDateDayIndex,
|
||||
monthClass: monthClass,
|
||||
isMonthThatHidesSomeDaysForInitialView: isMonthThatHidesSomeDaysForInitialView,
|
||||
|
|
@ -697,9 +749,13 @@ export default {
|
|||
);
|
||||
|
||||
// find the first day-hidden to become the next api call start date
|
||||
monthStartDate = monthToShow.dates.find(({ dayClasses }) =>
|
||||
dayClasses.includes("day-hidden")
|
||||
);
|
||||
monthStartDate = this.isMobileSelected
|
||||
? monthToShow.datesMobile.find(({ dayClasses }) =>
|
||||
dayClasses.includes("day-hidden")
|
||||
)
|
||||
: monthToShow.datesInshop.find(({ dayClasses }) =>
|
||||
dayClasses.includes("day-hidden")
|
||||
);
|
||||
} else {
|
||||
if (this.calendarViewDirection === "future") {
|
||||
monthToShow = this.months.find((month) =>
|
||||
|
|
@ -714,15 +770,19 @@ export default {
|
|||
}
|
||||
}
|
||||
if (monthToShow) {
|
||||
const monthToShowDates = this.isMobileSelected
|
||||
? monthToShow.datesMobile
|
||||
: monthToShow.datesInshop;
|
||||
const monthStartDateString = monthStartDate
|
||||
? monthStartDate.inputValue
|
||||
: monthToShow.dates[0].inputValue;
|
||||
: monthToShowDates[0].inputValue;
|
||||
|
||||
// make new API call with this month's start and end dates
|
||||
const moreSelectableDatesData = await this.updateSelectableDates(
|
||||
monthStartDateString,
|
||||
monthToShow.dates[monthToShow.dates.length - 1].inputValue
|
||||
monthToShowDates[monthToShowDates.length - 1].inputValue
|
||||
);
|
||||
|
||||
this.isLoading = false;
|
||||
this.hideSomeDaysForInitialView = false; // if hid days on initial partial view, this will reveal those days
|
||||
monthToShow.monthClass = monthToShow.monthClass.replace(" month-hidden", "");
|
||||
|
|
@ -735,27 +795,37 @@ export default {
|
|||
}
|
||||
},
|
||||
async updateSelectableDates(monthStart, monthEnd) {
|
||||
const moreSelectableDates = await this.customSelectableDatesCallback(
|
||||
monthStart,
|
||||
monthEnd,
|
||||
this.$store.getters.order.serviceLocation.appointmentType,
|
||||
this.$store.getters.order.serviceLocation.provider.providerNumber
|
||||
);
|
||||
const moreSelectableDates = await this.getMoreDatesCallback(monthStart, monthEnd);
|
||||
|
||||
moreSelectableDates.days.forEach((selectableDate) => {
|
||||
const index = this.selectableDatesData.findIndex(
|
||||
moreSelectableDates.inshopTimeSlotsData.days.forEach((selectableDate) => {
|
||||
const index = this.selectableDatesInshop.findIndex(
|
||||
(dateObj) => dateObj.date === selectableDate.date
|
||||
);
|
||||
if (index === -1) this.selectableDatesData.push(selectableDate);
|
||||
if (index === -1) this.selectableDatesInshop.push(selectableDate);
|
||||
this.months.forEach((month) => {
|
||||
// TODO: avoid checking all calendar dates; maybe only ones between monthStart and monthEnd as defined above?
|
||||
month.dates.forEach((date) => {
|
||||
month.datesInshop.forEach((date) => {
|
||||
if (date.inputValue === selectableDate.date) {
|
||||
date["isSelectable"] = true;
|
||||
date["isAvailable"] = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
moreSelectableDates.mobileTimeSlotsData.days.forEach((selectableDate) => {
|
||||
const index = this.selectableDatesMobile.findIndex(
|
||||
(dateObj) => dateObj.date === selectableDate.date
|
||||
);
|
||||
if (index === -1) this.selectableDatesMobile.push(selectableDate);
|
||||
this.months.forEach((month) => {
|
||||
// TODO: avoid checking all calendar dates; maybe only ones between monthStart and monthEnd as defined above?
|
||||
month.datesMobile.forEach((date) => {
|
||||
if (date.inputValue === selectableDate.date) {
|
||||
date["isAvailable"] = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return moreSelectableDates;
|
||||
},
|
||||
scrollToElement(elementId, duration = 800, easing = "ease-in-out") {
|
||||
|
|
@ -913,8 +983,7 @@ export default {
|
|||
|
||||
.month-year {
|
||||
grid-area: 1 / 1 / 2 / 5;
|
||||
font-family: AvertaBold;
|
||||
letter-spacing: 0.75px;
|
||||
font-family: UrbanistBold;
|
||||
color: $black;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.5rem;
|
||||
|
|
@ -1209,7 +1278,7 @@ export default {
|
|||
border-radius: 0.25rem;
|
||||
color: $black;
|
||||
span {
|
||||
font-family: AvertaSemibold;
|
||||
font-family: UrbanistSemibold;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
|
@ -1240,7 +1309,7 @@ export default {
|
|||
&.current-day {
|
||||
label {
|
||||
font-weight: 600;
|
||||
font-family: "AvertaSemibold";
|
||||
font-family: "UrbanistSemibold";
|
||||
color: $black;
|
||||
font-size-adjust: 0.5; // needed for Averta fonts baseline alignment
|
||||
}
|
||||
|
|
@ -1252,11 +1321,10 @@ export default {
|
|||
text-decoration: none;
|
||||
color: $black;
|
||||
font-weight: 400;
|
||||
font-family: "AvertaSemibold";
|
||||
font-family: "UrbanistSemibold";
|
||||
font-size-adjust: 0.5; // needed for Averta fonts baseline alignment
|
||||
|
||||
&.price {
|
||||
font-family: "AvertaRegular";
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
|
@ -1274,7 +1342,7 @@ export default {
|
|||
color: $blue;
|
||||
span {
|
||||
font-weight: 400;
|
||||
font-family: "AvertaSemibold";
|
||||
font-family: "UrbanistSemibold";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,8 +136,8 @@ export default {
|
|||
<style lang="scss">
|
||||
.dropdown-question {
|
||||
label {
|
||||
font-family: UrbanistSemibold;
|
||||
color: $black;
|
||||
font-weight: 500;
|
||||
}
|
||||
.label-bold {
|
||||
color: $black;
|
||||
|
|
|
|||
|
|
@ -129,7 +129,6 @@ export default {
|
|||
<style lang="scss">
|
||||
.dockableContainer {
|
||||
&.showDockableContainer {
|
||||
font-family: AvertaRegular;
|
||||
color: $gray-600;
|
||||
.sidebarBody {
|
||||
button {
|
||||
|
|
|
|||
|
|
@ -256,8 +256,8 @@ export default {
|
|||
justify-content: center !important;
|
||||
}
|
||||
label {
|
||||
font-family: UrbanistSemibold;
|
||||
color: $black;
|
||||
font-weight: 500;
|
||||
}
|
||||
.label-bold {
|
||||
font-weight: 600;
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="row-align d-flex px-4">
|
||||
<div>
|
||||
<img class="w-100 mt-1 me-4" :src="LogoImage" />
|
||||
<div class="logo-block">
|
||||
<img class="w-100" :src="LogoImage" />
|
||||
<span>EIN: 26-3043727</span>
|
||||
<!-- This will likely never change so hard coded -->
|
||||
</div>
|
||||
<div>
|
||||
<p class="small subheadline mb-2" v-html="DonationSubHeadline"></p>
|
||||
|
|
@ -79,7 +81,8 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p class="mt-2 mb-4 mx-0 text-center caption" v-html="DonationFooterText"></p>
|
||||
<p class="mt-2 mb-2 mx-0 text-center caption" v-html="DonationFooterText"></p>
|
||||
<p class="mb-4 mx-0 text-center caption" v-html="DonationFooterText2"></p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -121,6 +124,9 @@ export default {
|
|||
DonationFooterText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
||||
},
|
||||
DonationFooterText2() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "FooterText2");
|
||||
},
|
||||
LogoImage() {
|
||||
return this.getCmsContent("DonationWidget", "Image");
|
||||
},
|
||||
|
|
@ -177,13 +183,13 @@ export default {
|
|||
|
||||
p {
|
||||
&.headline {
|
||||
font-family: AvertaBold;
|
||||
font-family: UrbanistBold;
|
||||
color: $red;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.subheadline {
|
||||
font-family: AvertaBold;
|
||||
font-family: UrbanistBold;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
|
@ -195,8 +201,26 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 5rem;
|
||||
.logo-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 0 0.5rem;
|
||||
margin-right: 1rem;
|
||||
height: max-content;
|
||||
|
||||
img {
|
||||
width: 88px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.625rem;
|
||||
margin: 0 auto;
|
||||
padding: 0.5rem 0;
|
||||
font-family: UrbanistSemibold;
|
||||
}
|
||||
}
|
||||
|
||||
.donation-slider {
|
||||
|
|
|
|||
|
|
@ -424,7 +424,6 @@ export default {
|
|||
text-transform: uppercase;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.75px;
|
||||
}
|
||||
.special-save-box {
|
||||
background-color: $green-100;
|
||||
|
|
|
|||
|
|
@ -476,7 +476,6 @@ export default {
|
|||
text-transform: uppercase;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.75px;
|
||||
}
|
||||
.special-save-box {
|
||||
background-color: $green-100;
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@
|
|||
</div>
|
||||
<div v-if="donationCartItem" class="donation-amount">
|
||||
<span>{{ donationCartItemName }}</span>
|
||||
<span>{{ getLineItemAmount(donationCartItem.subTotal) }}</span>
|
||||
<span>{{ getLineItemAmount(donationCartItem.sellingPrice) }}</span>
|
||||
</div>
|
||||
<div class="amount-due">
|
||||
<span>{{ amountDueText }}</span>
|
||||
|
|
@ -182,6 +182,7 @@ import {
|
|||
getAmountDue,
|
||||
getSubTotal,
|
||||
getSalesTax,
|
||||
getAmountDueWithDonation,
|
||||
} from "@/helpers/pricing-helper.js";
|
||||
|
||||
// Constants
|
||||
|
|
@ -211,6 +212,7 @@ export default {
|
|||
isNoComp: Boolean,
|
||||
isExpandedOnLoad: Boolean,
|
||||
isMSRFeeApplicable: Boolean,
|
||||
donationCartItem: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -1083,33 +1085,6 @@ export default {
|
|||
donationCartItemName() {
|
||||
return this.getCmsContent("DonationCartTextWidget", "Text");
|
||||
},
|
||||
donationCartItem() {
|
||||
if (!this.lineItems || !this.lineItems.supportingItems) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const donationLineItem = this.lineItems.supportingItems.find(
|
||||
(item) => item.partType === partTypeStrings.DONATION
|
||||
);
|
||||
|
||||
if (donationLineItem) {
|
||||
return {
|
||||
name: this.donationCartItemName,
|
||||
category: cartItemCategories.SUPPORTING_ITEMS,
|
||||
cartItemType: cartItemTypes.DONATION,
|
||||
isDisplayed: true,
|
||||
isRemovable: false,
|
||||
subTotal: donationLineItem.sellingPrice,
|
||||
salesTax: 0,
|
||||
lineItems: [],
|
||||
isCoveredByInsurance: cartItemTypesCoveredByInsurance.includes(
|
||||
cartItemTypes.DONATION
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
removeLinkText() {
|
||||
return this.getCmsContent("RemoveCartItemTextWidget", "Text");
|
||||
},
|
||||
|
|
@ -1156,9 +1131,15 @@ export default {
|
|||
amountDue() {
|
||||
if (!this.lineItems || this.lineItems.length < 1) return;
|
||||
if (this.showAsPaid) return 0;
|
||||
return this.isInsurance || !this.shouldHideRecalibration
|
||||
? getAmountDue(this.lineItems) // calculate with recal (if on order)
|
||||
: getAmountDue(this.lineItemsWithoutRecal); // calculated without recal
|
||||
if (this.isInsurance || !this.shouldHideRecalibration) {
|
||||
return this.donationCartItem
|
||||
? getAmountDueWithDonation(this.lineItems, this.donationCartItem)
|
||||
: getAmountDue(this.lineItems);
|
||||
} else {
|
||||
return this.donationCartItem
|
||||
? getAmountDueWithDonation(this.lineItemsWithoutRecal, this.donationCartItem)
|
||||
: getAmountDue(this.lineItemsWithoutRecal);
|
||||
}
|
||||
},
|
||||
amountPaid() {
|
||||
if (!this.showAsPaid) {
|
||||
|
|
@ -1311,8 +1292,7 @@ export default {
|
|||
.amount-due,
|
||||
.amount-paid,
|
||||
.donation-amount {
|
||||
font-family:
|
||||
UrbanistSemibold, AvertaSemibold; //Okay to remove AvertaSemibold after 2025.06.19 merge/release
|
||||
font-family: UrbanistSemibold;
|
||||
color: $black;
|
||||
}
|
||||
.sub-total {
|
||||
|
|
@ -1358,6 +1338,7 @@ export default {
|
|||
visibility: visible;
|
||||
}
|
||||
a {
|
||||
font-family: UrbanistSemibold;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { GaCategories, GaActions, GaLabels } from "@/constants/analytics";
|
|||
import { getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper";
|
||||
import { headerKeys } from "@/constants/header-keys";
|
||||
import axiosResponseInterceptorMessages from "@/constants/axios-response-interceptor-messages.js";
|
||||
import { endpoints } from "@/constants/endpoints";
|
||||
|
||||
// Add a response interceptor for global axios error handing.
|
||||
axios.interceptors.response.use(
|
||||
|
|
@ -100,34 +101,38 @@ export default {
|
|||
return resolve(response);
|
||||
},
|
||||
(error) => {
|
||||
if (logApiCall) {
|
||||
analyticsMixIn.methods.pushEventToGA(
|
||||
GaCategories.API_RESPONSE,
|
||||
GaActions.RESULT,
|
||||
`${GaLabels.ERROR}_${endpoint}`,
|
||||
true
|
||||
);
|
||||
if (endpoint.toLowerCase().includes(endpoints.LogDigitalConsumer.url)) {
|
||||
return resolve({ data: null, error: "Ignore errors when logging" });
|
||||
} else {
|
||||
if (logApiCall) {
|
||||
analyticsMixIn.methods.pushEventToGA(
|
||||
GaCategories.API_RESPONSE,
|
||||
GaActions.RESULT,
|
||||
`${GaLabels.ERROR}_${endpoint}`,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
if (error.response.status && error.response.status != "404") {
|
||||
// Do not route to error logic when no wipers found or no promo found (404s)
|
||||
const errorPayload = {
|
||||
cause: `Response error ${error.response.status}`,
|
||||
currentPage: pageNameToLog,
|
||||
endpoint: endpoint,
|
||||
};
|
||||
|
||||
router.bailout(errorPayload);
|
||||
|
||||
// do not log 404 errors from services because we return NotFound
|
||||
// when a service doesn't return an object
|
||||
global.$logger.logError(
|
||||
`${method}: ${endpoint}: ${error.message}`,
|
||||
error.response
|
||||
);
|
||||
}
|
||||
|
||||
return reject(error.response);
|
||||
}
|
||||
|
||||
if (error.response.status && error.response.status != "404") {
|
||||
// Do not route to error logic when no wipers found or no promo found (404s)
|
||||
const errorPayload = {
|
||||
cause: `Response error ${error.response.status}`,
|
||||
currentPage: pageNameToLog,
|
||||
endpoint: endpoint,
|
||||
};
|
||||
|
||||
router.bailout(errorPayload);
|
||||
|
||||
// do not log 404 errors from services because we return NotFound
|
||||
// when a service doesn't return an object
|
||||
global.$logger.logError(
|
||||
`${method}: ${endpoint}: ${error.message}`,
|
||||
error.response
|
||||
);
|
||||
}
|
||||
|
||||
return reject(error.response);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -339,3 +339,7 @@ export function splitCMSCopyOnParagraphTag(copy) {
|
|||
// filter removes empty strings that are a result of string.split with regex
|
||||
return copy.split(/(?:<p(?:.*?)>)|(?:<\/p>)/g).filter((paragraph) => paragraph !== "");
|
||||
}
|
||||
|
||||
export function splitCMSCopyOnBR(copy) {
|
||||
return copy.split("<br>");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import store from "@/store";
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import { deepClone } from "@/helpers/object-helper";
|
||||
|
||||
export function getDisplayAmountDue(lineItemsObject, includeTax = true) {
|
||||
return getAmountDue(lineItemsObject, includeTax).toLocaleString("en-US", {
|
||||
|
|
@ -62,6 +63,13 @@ export function getSalesTax(lineItemsObject) {
|
|||
);
|
||||
}
|
||||
|
||||
export function getAmountDueWithDonation(lineItemsObject, donationLineItem) {
|
||||
// this is amount due with Donation added
|
||||
const lineItemsCloneWithDonation = deepClone(lineItemsObject);
|
||||
lineItemsCloneWithDonation.supportingItems.push(donationLineItem);
|
||||
return getAmountDue(lineItemsCloneWithDonation);
|
||||
}
|
||||
|
||||
export async function getPricingByDayPartWithPrice(pageNameToLog) {
|
||||
// Get the Pricing By Day Part
|
||||
const basePriceByDayPart = await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@
|
|||
:damage="damageInfo"
|
||||
:availableVaps="vaps"
|
||||
:allowItemRemoval="false"
|
||||
v-model="lineItems"
|
||||
v-model="lineItemsWithoutDonation"
|
||||
:donationCartItem="donationLineItem"
|
||||
:showAsPaid="isPia"
|
||||
servicePackageOptionsCmsName="ServicePackageTitle"
|
||||
:isInsurance="isInsurance"
|
||||
|
|
@ -123,6 +124,7 @@ import experimentMixin from "@/mixins/experiment-mixin.js";
|
|||
import { containsRecalParts } from "@/helpers/recal-helper.js";
|
||||
import donationBlock from "@/experiment-components/donation-block.vue";
|
||||
import { partNumberStrings } from "@/constants/part-number-strings";
|
||||
import { partTypeStrings } from "@/constants/part-type-strings";
|
||||
import analyticsMixin from "@/mixins/analytics-mixin";
|
||||
|
||||
export default {
|
||||
|
|
@ -229,7 +231,7 @@ export default {
|
|||
};
|
||||
|
||||
const donationItems = lineItemsFromSubmittedOrder.supportingItems?.filter(
|
||||
(item) => item.partNumber === partNumberStrings.DONATION
|
||||
(item) => item.partType === partTypeStrings.DONATION
|
||||
);
|
||||
const donationAmount = donationItems?.length > 0 ? donationItems[0].sellingPrice : 0;
|
||||
|
||||
|
|
@ -243,7 +245,7 @@ export default {
|
|||
vm.shouldDisplayFosterLove = hasFosterLoveExperiment;
|
||||
vm.donationAmount = donationAmount;
|
||||
vm.showDonationSuccess = lineItemsFromSubmittedOrder.supportingItems?.some(
|
||||
(item) => item.partType === partNumberStrings.DONATION
|
||||
(item) => item.partType === partTypeStrings.DONATION
|
||||
);
|
||||
|
||||
vm.pushAnalytics();
|
||||
|
|
@ -490,6 +492,22 @@ export default {
|
|||
donationValues() {
|
||||
return [1, 3, 5];
|
||||
},
|
||||
lineItemsWithoutDonation() {
|
||||
const lineItemsClone = deepClone(this.lineItems);
|
||||
lineItemsClone.supportingItems = lineItemsClone.supportingItems?.filter(
|
||||
(item) => item.partType !== partTypeStrings.DONATION
|
||||
);
|
||||
return lineItemsClone;
|
||||
},
|
||||
donationLineItem() {
|
||||
if (!this.lineItems) return null;
|
||||
const donationLineItems = this.lineItems.supportingItems?.filter(
|
||||
(item) => item.partType === partTypeStrings.DONATION
|
||||
);
|
||||
let output =
|
||||
donationLineItems && donationLineItems.length > 0 ? donationLineItems[0] : null;
|
||||
return output;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
arePagePrerequisitesValid() {
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ import { Form, defineRule } from "vee-validate";
|
|||
import store from "@/store";
|
||||
import { paymentMethods } from "@/constants/payment-method-constants";
|
||||
import { flushSaveSessionQueue } from "@/helpers/heritage-integration/order-helper";
|
||||
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED));
|
||||
|
|
@ -157,11 +158,21 @@ export default {
|
|||
getTechNotesFromStore() {
|
||||
return store.getters.order.serviceLocation.techNotes;
|
||||
},
|
||||
getSelectedAppointmentType() {
|
||||
return store.getters.order.serviceLocation.appointmentType;
|
||||
},
|
||||
backButtonAction() {
|
||||
this.$router.navigateWithoutSaving(
|
||||
this.navigationScenarios.CLICKED_BACK,
|
||||
this.pageName
|
||||
);
|
||||
if (this.getSelectedAppointmentType() == AppointmentTypeStrings.MOBILE) {
|
||||
this.$router.navigateWithoutSaving(
|
||||
this.navigationScenarios.CLICKED_BACK_WITH_MOBILE_SERVICE,
|
||||
this.pageName
|
||||
);
|
||||
} else {
|
||||
this.$router.navigateWithoutSaving(
|
||||
this.navigationScenarios.CLICKED_BACK,
|
||||
this.pageName
|
||||
);
|
||||
}
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
this.dispatchStoreAction(
|
||||
|
|
|
|||
|
|
@ -99,9 +99,7 @@ export default {
|
|||
lowerCaseParams.append(name.toLowerCase(), value);
|
||||
}
|
||||
|
||||
const zip =
|
||||
peekQueryFromStash(queryStrings.ZIP_CODE) ??
|
||||
store.getters.order.serviceLocation.zipCode;
|
||||
const zip = store.getters.order.serviceLocation.zipCode;
|
||||
|
||||
var vinByAddressPromise;
|
||||
if (zip) {
|
||||
|
|
|
|||
|
|
@ -118,8 +118,6 @@ import baseMixin from "@/mixins/base-mixin.js";
|
|||
import store from "@/store";
|
||||
import vinPagesMixin from "@/mixins/vin-pages-mixin";
|
||||
import { routeData } from "@/router/constants/routes";
|
||||
import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stash";
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule("license-plate-required", required(errorMessages.LICENSE_PLATE_REQUIRED));
|
||||
|
|
@ -164,8 +162,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
licensePlate: this.getLicensePlateFromStore(),
|
||||
registrationZipCode:
|
||||
this.getRegistrationZipFromStore() ?? consumeQueryFromStash(queryStrings.ZIP_CODE),
|
||||
registrationZipCode: this.getRegistrationZipFromStore(),
|
||||
emailOrSms: this.getEmailOrSmsFromStore(),
|
||||
serviceZipCode: this.getServiceZipFromStore(),
|
||||
carId: this.getCarIdfromStore(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
// Components
|
||||
import mobileAddressQuestions from "@/layouts/mobile-details/mobile-address-questions/mobile-address-questions.vue";
|
||||
|
||||
// Supporting Files
|
||||
import { mount, shallowMount } from "@vue/test-utils";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
import { storeMutations } from "@/constants/store-mutations";
|
||||
import store from "@/store";
|
||||
|
||||
describe("initial state", () => {
|
||||
test("Should show all the address field", () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
// Assert
|
||||
const streetAddressField = wrapper.findComponent({ ref: "streetAddress" });
|
||||
const cityField = wrapper.findComponent({ ref: "city" });
|
||||
const stateField = wrapper.findComponent({ ref: "state" });
|
||||
const zipCodeField = wrapper.findComponent({ ref: "zipCode" });
|
||||
|
||||
expect(streetAddressField.exists()).toBe(true);
|
||||
expect(streetAddressField.isVisible()).toBe(true);
|
||||
expect(cityField.exists()).toBe(true);
|
||||
expect(cityField.isVisible()).toBe(true);
|
||||
expect(stateField.exists()).toBe(true);
|
||||
expect(stateField.isVisible()).toBe(true);
|
||||
expect(zipCodeField.exists()).toBe(true);
|
||||
expect(zipCodeField.isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
test("Should render mobileAddressQuestions sub-components (textbox-questions and dropdown-questions)", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
// Act
|
||||
const streetAddress = wrapper.findComponent({ ref: "streetAddress" });
|
||||
const city = wrapper.findComponent({ ref: "city" });
|
||||
const state = wrapper.findComponent({ ref: "state" });
|
||||
const zipCode = wrapper.findComponent({ ref: "zipCode" });
|
||||
|
||||
// Assert
|
||||
expect(streetAddress.exists()).toBe(true);
|
||||
expect(city.exists()).toBe(true);
|
||||
expect(state.exists()).toBe(true);
|
||||
expect(zipCode.exists()).toBe(true);
|
||||
});
|
||||
|
||||
test("Should render apartmentNumberOrBusinessName textbox-question when captureApartmentNumberOrBusinessName = true", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({
|
||||
props: {
|
||||
modelValue: {
|
||||
streetAddress: "",
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
},
|
||||
captureApartmentNumberOrBusinessName: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
const streetAddress = wrapper.findComponent({ ref: "streetAddress" });
|
||||
const apartmentNumberOrBusinessName = wrapper.findComponent({
|
||||
ref: "apartmentNumberOrBusinessName",
|
||||
});
|
||||
const city = wrapper.findComponent({ ref: "city" });
|
||||
const state = wrapper.findComponent({ ref: "state" });
|
||||
const zipCode = wrapper.findComponent({ ref: "zipCode" });
|
||||
|
||||
// Assert
|
||||
expect(streetAddress.exists()).toBe(true);
|
||||
expect(apartmentNumberOrBusinessName.exists()).toBe(true);
|
||||
expect(apartmentNumberOrBusinessName.isVisible()).toBe(true);
|
||||
expect(city.exists()).toBe(true);
|
||||
expect(state.exists()).toBe(true);
|
||||
expect(zipCode.exists()).toBe(true);
|
||||
});
|
||||
|
||||
test("Should *not* render apartmentNumberOrBusinessName textbox-question when captureApartmentNumberOrBusinessName = false", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({
|
||||
props: {
|
||||
modelValue: {
|
||||
streetAddress: "",
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
},
|
||||
captureApartmentNumberOrBusinessName: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
const streetAddress = wrapper.findComponent({ ref: "streetAddress" });
|
||||
const apartmentNumberOrBusinessName = wrapper.findComponent({
|
||||
ref: "apartmentNumberOrBusinessName",
|
||||
});
|
||||
const city = wrapper.findComponent({ ref: "city" });
|
||||
const state = wrapper.findComponent({ ref: "state" });
|
||||
const zipCode = wrapper.findComponent({ ref: "zipCode" });
|
||||
|
||||
// Assert
|
||||
expect(streetAddress.exists()).toBe(true);
|
||||
expect(apartmentNumberOrBusinessName.exists()).toBe(true);
|
||||
expect(apartmentNumberOrBusinessName.isVisible()).toBe(false);
|
||||
expect(city.exists()).toBe(true);
|
||||
expect(state.exists()).toBe(true);
|
||||
expect(zipCode.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("happy paths", () => {
|
||||
test("full street address is passed in => address fields are displayed", async () => {
|
||||
// Arrange/Act
|
||||
const { wrapper } = setupMocks({
|
||||
props: {
|
||||
modelValue: {
|
||||
streetAddress: "12345 Test Road",
|
||||
city: "Tests",
|
||||
state: "OH",
|
||||
zipCode: "12312",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Assert
|
||||
const cityField = wrapper.findComponent({ ref: "city" });
|
||||
const stateField = wrapper.findComponent({ ref: "state" });
|
||||
const zipField = wrapper.findComponent({ ref: "zipCode" });
|
||||
expect(cityField.exists()).toBeTruthy();
|
||||
expect(cityField.isVisible()).toBeTruthy();
|
||||
expect(stateField.exists()).toBeTruthy();
|
||||
expect(cityField.isVisible()).toBeTruthy();
|
||||
expect(zipField.exists()).toBeTruthy();
|
||||
expect(cityField.isVisible()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocks({
|
||||
mountOptions,
|
||||
props,
|
||||
isShallowMount = true,
|
||||
querySelectorFunction,
|
||||
geocoderResult = ["1234 Test Street"],
|
||||
matchFound = false,
|
||||
}) {
|
||||
store.commit(storeMutations.RESET_STATE);
|
||||
|
||||
const resultingMountOptions = getMountOptions({
|
||||
...mountOptions,
|
||||
router: {
|
||||
navigate: jest.fn(),
|
||||
navigate: jest.fn(),
|
||||
},
|
||||
loadScript: jest.fn().mockResolvedValue(),
|
||||
});
|
||||
|
||||
if (props) resultingMountOptions.propsData = props;
|
||||
|
||||
if (isShallowMount) {
|
||||
resultingMountOptions.global.stubs = {
|
||||
...(resultingMountOptions.global.stubs ?? {}),
|
||||
textboxQuestion: {
|
||||
template: `<span></span>`,
|
||||
methods: {
|
||||
handleChange: jest.fn(),
|
||||
},
|
||||
},
|
||||
dropdownQuestion: {
|
||||
template: `<span></span>`,
|
||||
methods: {
|
||||
handleChange: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const wrapper = isShallowMount
|
||||
? shallowMount(mobileAddressQuestions, resultingMountOptions)
|
||||
: mount(mobileAddressQuestions, resultingMountOptions);
|
||||
|
||||
document.querySelector = jest.fn().mockImplementation((query) => {
|
||||
let result = null;
|
||||
if (query == ".pac-container") result = document.createElement("div");
|
||||
else if (querySelectorFunction) {
|
||||
result = querySelectorFunction(query);
|
||||
}
|
||||
|
||||
return result ?? null;
|
||||
});
|
||||
|
||||
return { wrapper };
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
<template>
|
||||
<div class="address-questions">
|
||||
<div class="row mb-4" aria-live="polite">
|
||||
<div class="col">
|
||||
<!-- Temporarily changing id from autocomplete to streetAddress -->
|
||||
<textboxQuestion
|
||||
customInputId="streetAddress"
|
||||
cmsWidgetName="StreetAddressQuestionWidget"
|
||||
v-model="addressModel.streetAddress"
|
||||
validationRules="street-address-required"
|
||||
ref="streetAddress"
|
||||
:labelBold="labelBold"
|
||||
autocomplete="streetAddress" />
|
||||
</div>
|
||||
</div>
|
||||
<transition name="fade" mode="out-in">
|
||||
<div
|
||||
class="row mb-4"
|
||||
v-show="showApartmentNumberOrBusinessNameField"
|
||||
aria-live="polite">
|
||||
<div class="col">
|
||||
<textboxQuestion
|
||||
customInputId="apartmentNumberOrBusinessName"
|
||||
cmsWidgetName="ApartmentNumberOrBusinessNameQuestionWidget"
|
||||
v-model="addressModel.apartmentNumberOrBusinessName"
|
||||
ref="apartmentNumberOrBusinessName"
|
||||
:labelBold="labelBold" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="fade" mode="out-in">
|
||||
<div class="row mb-4" aria-live="polite">
|
||||
<div class="col">
|
||||
<textboxQuestion
|
||||
customInputId="city"
|
||||
cmsWidgetName="CityQuestionWidget"
|
||||
v-model="addressModel.city"
|
||||
ref="city"
|
||||
validationRules="city-required"
|
||||
autocomplete="address-level2"
|
||||
:labelBold="labelBold" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="fade" mode="out-in">
|
||||
<div class="row mb-4" aria-live="polite">
|
||||
<div class="col">
|
||||
<dropdownQuestion
|
||||
customDropdownId="state"
|
||||
cmsWidgetName="StateQuestionWidget"
|
||||
v-model="addressModel.state"
|
||||
ref="state"
|
||||
:options="stateOptions"
|
||||
validationRules="state-required"
|
||||
autocomplete="address-level1"
|
||||
:labelBold="labelBold"
|
||||
:isDisabled="this.isStateDisabled" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<textboxQuestion
|
||||
customInputId="zipCode"
|
||||
cmsWidgetName="ZipQuestionWidget"
|
||||
v-model="addressModel.zipCode"
|
||||
ref="zipCode"
|
||||
mask="#####"
|
||||
validationRules="zip-code-required|zip-code-format"
|
||||
autocomplete="postal-code"
|
||||
:labelBold="labelBold"
|
||||
:isDisabled="this.isZipCodeDisabled" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
|
||||
import dropdownQuestion from "@/digital-components/dropdown-question/dropdown-question";
|
||||
import { defineRule } from "vee-validate";
|
||||
import { required, regex } from "@/helpers/validation-rules";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
import { stateOptions } from "@/constants/state-options";
|
||||
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule("street-address-required", required(errorMessages.STREET_ADDRESS_REQUIRED));
|
||||
defineRule("city-required", required(errorMessages.CITY_REQUIRED));
|
||||
defineRule("state-required", required(errorMessages.STATE_REQUIRED));
|
||||
defineRule("zip-code-required", required(errorMessages.ZIP_REQUIRED));
|
||||
defineRule("zip-code-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.ZIP_FORMAT));
|
||||
|
||||
export default {
|
||||
name: "mobile-address-questions",
|
||||
emits: ["update:modelValue"], // The component emits an event
|
||||
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
streetAddress: "",
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
}),
|
||||
},
|
||||
validationRules: String,
|
||||
captureApartmentNumberOrBusinessName: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
preserveCityAndStateOnReset: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
labelBold: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isStateDisabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isZipCodeDisabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
stateOptions() {
|
||||
return stateOptions;
|
||||
},
|
||||
addressModel: {
|
||||
get: function () {
|
||||
return this.modelValue;
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.$emit("update:modelValue", newValue);
|
||||
},
|
||||
},
|
||||
showApartmentNumberOrBusinessNameField: {
|
||||
get: function () {
|
||||
return this.captureApartmentNumberOrBusinessName;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
textboxQuestion,
|
||||
dropdownQuestion,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.address-questions {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
#streetAddressField {
|
||||
position: relative;
|
||||
|
||||
.pac-container {
|
||||
top: 76px !important; // Height of #streetAddressField
|
||||
left: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
139
src/layouts/mobile-details/mobile-details.spec.js
Normal file
139
src/layouts/mobile-details/mobile-details.spec.js
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import mobileDetails from "@/layouts/mobile-details/mobile-details.vue";
|
||||
import { mount, shallowMount } from "@vue/test-utils";
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
import store from "@/store";
|
||||
|
||||
const linkWidgetName = "linkWidgetName";
|
||||
const modalWidgetName = "modalWidgetName";
|
||||
|
||||
const mockLinkCmsContent = {
|
||||
BodyText: "Sample link body text here.",
|
||||
};
|
||||
|
||||
const mockModalCmsContent = {
|
||||
FooterText: "Sample modal footer text here.",
|
||||
};
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
|
||||
if (widgetName === linkWidgetName) {
|
||||
return mockLinkCmsContent[cmsFieldName];
|
||||
}
|
||||
|
||||
if (widgetName === modalWidgetName) {
|
||||
return mockModalCmsContent[cmsFieldName];
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
dispatchStoreAction: jest.fn((actionName) => {
|
||||
if (actionName === storeActions.SAVE_MOBILE_DETAILS) {
|
||||
return true;
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store.getters = {
|
||||
order: {
|
||||
serviceLocation: {
|
||||
zipCode: "43235",
|
||||
zipCodeCtu: "00000",
|
||||
state: "OH",
|
||||
appointmentType: "Mobile",
|
||||
city: "Columbus",
|
||||
address: "123 Main St",
|
||||
address2: "Suite 100",
|
||||
isVehicleProtected: true,
|
||||
},
|
||||
schedule: {
|
||||
date: "2023-10-10",
|
||||
endTime: "14:00",
|
||||
startTime: "13:00",
|
||||
routeCode: "R1",
|
||||
jobMaxMinutes: "60",
|
||||
jobMinMinutes: "30",
|
||||
},
|
||||
},
|
||||
payment: {
|
||||
isInsurance: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("mobile-details.vue", () => {
|
||||
test("if the continue button is clicked, navigate forward", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks(mobileDetails, {
|
||||
mixins: [mockMixin],
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
wrapper.setData({
|
||||
addressQuestions: {
|
||||
streetAddress: "Test Street",
|
||||
apartmentNumberOrBusinessName: "Test Apartment",
|
||||
city: "Test City",
|
||||
state: "TS",
|
||||
zipCode: "12345",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
});
|
||||
|
||||
// Act
|
||||
await wrapper.vm.forwardButtonAction();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled();
|
||||
});
|
||||
test("arePagePrerequisitesValid should be true ", async () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks(mobileDetails, {
|
||||
mixins: [mockMixin],
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
//Act
|
||||
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
//Assert
|
||||
expect(arePagePrerequisitesValid).toBe(true);
|
||||
});
|
||||
|
||||
test("if the back button is clicked, navigate back", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks(mobileDetails, {
|
||||
mixins: [mockMixin],
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
// Act
|
||||
await wrapper.vm.backButtonAction();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocks({ mountOptions, mixins, props, isShallowMount = true }) {
|
||||
const resultingMountOptions = getMountOptions({
|
||||
...mountOptions,
|
||||
mixins,
|
||||
router: {
|
||||
navigate: jest.fn(),
|
||||
navigateWithSaving: jest.fn(),
|
||||
navigateWithoutSaving: jest.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
if (props) resultingMountOptions.propsData = props;
|
||||
resultingMountOptions["attachTo"] = document.body;
|
||||
const wrapper = isShallowMount
|
||||
? shallowMount(mobileDetails, resultingMountOptions)
|
||||
: mount(mobileDetails, resultingMountOptions);
|
||||
|
||||
return { wrapper };
|
||||
}
|
||||
168
src/layouts/mobile-details/mobile-details.vue
Normal file
168
src/layouts/mobile-details/mobile-details.vue
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
<template>
|
||||
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
|
||||
<loadingModal ref="loadingModal" />
|
||||
<div class="container-fluid page-container-grouped-styles">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-xl-4 mt-4">
|
||||
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
|
||||
<div class="mobile-location-questions">
|
||||
<div class="address-questions-container">
|
||||
<mobileAddressQuestions
|
||||
ref="addressQuestions"
|
||||
v-model="this.addressQuestions"
|
||||
captureApartmentNumberOrBusinessName="true"
|
||||
preserveCityAndStateOnReset="true"
|
||||
labelBold="true"
|
||||
isStateDisabled="true"
|
||||
isZipCodeDisabled="true" />
|
||||
<vehicleProtectedQuestion
|
||||
ref="vehicleProtectedQuestion"
|
||||
v-model="this.isVehicleProtected"
|
||||
cmsWidgetName="VehicleProtectedQuestionWidget"
|
||||
labelBold="true" />
|
||||
<textBlock
|
||||
cmsWidgetName="WorkspaceRequirementsWidget"
|
||||
typeStyle="caption" />
|
||||
<navbar
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
ref="navbar"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
@back-clicked="backButtonAction"
|
||||
@ForwardClicked="forwardButtonAction" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
<script>
|
||||
//Components
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
||||
import navbar from "@/fmg-components/nav-bar/nav-bar";
|
||||
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
||||
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
||||
import { Form } from "vee-validate";
|
||||
import mobileAddressQuestions from "@/layouts/mobile-details/mobile-address-questions/mobile-address-questions";
|
||||
import vehicleProtectedQuestion from "@/layouts/service-location/mobile-location-modal-questions/vehicle-protected-question/vehicle-protected-question";
|
||||
import store from "@/store";
|
||||
//Supporting files
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||
export default {
|
||||
name: "MobileDetails",
|
||||
data() {
|
||||
return {
|
||||
addressQuestions: {
|
||||
streetAddress: this.getServiceAddressFromStore(),
|
||||
apartmentNumberOrBusinessName: this.getServiceAddress2FromStore(),
|
||||
city: this.getServiceCityFromStore(),
|
||||
state: this.getServiceStateFromStore(),
|
||||
zipCode: this.getServiceZipCodeFromStore(),
|
||||
},
|
||||
isVehicleProtected: this.getIsVehicleProtectedFromStore(),
|
||||
};
|
||||
},
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
// Call APIs
|
||||
const cmsContentPromise = fetchCmsContentForPage(to.name);
|
||||
// Settle promises and get results
|
||||
const promiseResultMap = [
|
||||
{
|
||||
resultKey: "cmsContent",
|
||||
promise: cmsContentPromise,
|
||||
},
|
||||
];
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
// Call the "next" function to complete the transition to this page.
|
||||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
arePagePrerequisitesValid() {
|
||||
const serviceLocation = store.getters.order.serviceLocation;
|
||||
const serviceLocationPreReqs =
|
||||
serviceLocation.zipCode &&
|
||||
serviceLocation.zipCodeCtu &&
|
||||
serviceLocation.appointmentType &&
|
||||
serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
|
||||
|
||||
// Schedule
|
||||
const schedule = store.getters.order.schedule;
|
||||
const scheduleReqs = !!(
|
||||
schedule.date &&
|
||||
schedule.startTime &&
|
||||
schedule.endTime &&
|
||||
schedule.jobMaxMinutes &&
|
||||
schedule.jobMinMinutes
|
||||
);
|
||||
|
||||
const preReqResult = serviceLocationPreReqs && scheduleReqs;
|
||||
return preReqResult;
|
||||
},
|
||||
getServiceAddressFromStore() {
|
||||
return store.getters.order.serviceLocation.address;
|
||||
},
|
||||
getServiceAddress2FromStore() {
|
||||
return store.getters.order.serviceLocation.address2;
|
||||
},
|
||||
getServiceCityFromStore() {
|
||||
return store.getters.order.serviceLocation.city;
|
||||
},
|
||||
getServiceStateFromStore() {
|
||||
return store.getters.order.serviceLocation.state;
|
||||
},
|
||||
getServiceZipCodeFromStore() {
|
||||
return store.getters.order.serviceLocation.zipCode;
|
||||
},
|
||||
getIsVehicleProtectedFromStore() {
|
||||
return store.getters.order.serviceLocation.isVehicleProtected;
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
await this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_MOBILE_DETAILS,
|
||||
{
|
||||
address: this.addressQuestions.streetAddress,
|
||||
address2: this.addressQuestions.apartmentNumberOrBusinessName,
|
||||
city: this.addressQuestions.city,
|
||||
isVehicleProtected: this.isVehicleProtected,
|
||||
},
|
||||
false
|
||||
);
|
||||
this.$router.navigateWithSaving(
|
||||
this.navigationScenarios.CLICKED_FORWARD,
|
||||
this.pageName
|
||||
);
|
||||
},
|
||||
backButtonAction() {
|
||||
this.$router.navigateWithoutSaving(
|
||||
this.navigationScenarios.CLICKED_BACK,
|
||||
this.pageName
|
||||
);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
headerText() {
|
||||
return this.getCmsContent("MobileLocationModalWidget", "HeaderText");
|
||||
},
|
||||
},
|
||||
components: {
|
||||
mobileAddressQuestions,
|
||||
vehicleProtectedQuestion,
|
||||
textBlock,
|
||||
funnelHeader,
|
||||
navbar,
|
||||
funnelSubHeader,
|
||||
Form,
|
||||
loadingModal,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -82,6 +82,8 @@
|
|||
<span>Amount Due</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>6 or 12 monthly payment plans available</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -208,6 +210,12 @@ export default {
|
|||
overflow: hidden;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
> p {
|
||||
padding-top: 1rem;
|
||||
font-size: $font-size-12;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.payment-section {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<textBlock
|
||||
:cmsWidgetName="headerCmsWidgetName"
|
||||
:customText="customHeaderText"
|
||||
typeStyle="body small bold dark"
|
||||
typeStyle="body small dark"
|
||||
marginTopSizeOverride="0" />
|
||||
</div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -145,6 +145,10 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
:deep(.review-table .text-block span) {
|
||||
font-family: UrbanistSemibold;
|
||||
}
|
||||
|
||||
.review-toggle {
|
||||
&.expanded {
|
||||
margin-bottom: 0;
|
||||
|
|
@ -177,8 +181,8 @@ export default {
|
|||
text-decoration: none;
|
||||
}
|
||||
.label {
|
||||
font-family: UrbanistSemibold;
|
||||
color: $black;
|
||||
font-weight: 500;
|
||||
line-height: 1.625;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
<template>
|
||||
<div class="alert fade show my-2 py-2 border-0 alert-info" role="alert">
|
||||
<div id="afterpay-banner" role="alert">
|
||||
<component
|
||||
:is="'script'"
|
||||
src="https://js.squarecdn.com/square-marketplace.js"
|
||||
async></component>
|
||||
|
||||
<div
|
||||
class="mx-4 my-0 alert-heading text-center"
|
||||
:class="[isAfterpayBreakoutDisplay ? 'fw-bolder' : 'fw-bold']">
|
||||
<div>
|
||||
<span v-for="token in headerCopyTokens" :key="token" v-html="token"></span>
|
||||
</div>
|
||||
<div :id="showAfterpayExtendedPayOption ? 'extended-pay-option' : ''">
|
||||
<span v-for="token in afterpayCopyTokens" :key="token">
|
||||
<span v-if="isAfterpayPriceToken(token)">{{ afterpayPrice }}</span>
|
||||
<span v-else-if="isInlineImageToken(token)">
|
||||
|
|
@ -29,7 +30,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
|
||||
import { splitCopyOnCMSPlaceHolder, splitCMSCopyOnBR } from "@/helpers/cms-content-helper";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import { getPromosThatMatchLineItemsOnOrder } from "@/helpers/promotions-helper";
|
||||
import { getArrayOfAllLineItemsAndChildParts } from "@/store";
|
||||
|
|
@ -55,8 +56,8 @@ export default {
|
|||
props: {
|
||||
cmsWidgetName: String,
|
||||
lineItems: Array,
|
||||
isAfterpayBreakoutDisplay: Boolean,
|
||||
isInsuranceSelected: Boolean,
|
||||
afterpayExtendedPayOptionThreshold: Number,
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
|
|
@ -73,27 +74,24 @@ export default {
|
|||
hasImage() {
|
||||
return !!this.imageUrl;
|
||||
},
|
||||
headerCopyTokens() {
|
||||
return splitCMSCopyOnBR(this.getCmsContent(this.cmsWidgetName, "HeaderText"));
|
||||
},
|
||||
afterpayCopyTokens() {
|
||||
if (this.isAfterpayBreakoutDisplay) {
|
||||
// Text for Afterpay Breakout experiment
|
||||
var afterpayBannerCopy = this.getCmsContent(this.cmsWidgetName, "BodyText2");
|
||||
afterpayBannerCopy =
|
||||
afterpayBannerCopy &&
|
||||
afterpayBannerCopy.replaceAll(
|
||||
"{custom:paymentType}",
|
||||
this.isInsuranceSelected ? "deductible" : "spending"
|
||||
);
|
||||
return splitCopyOnCMSPlaceHolder(afterpayBannerCopy);
|
||||
if (this.showAfterpayExtendedPayOption) {
|
||||
return splitCopyOnCMSPlaceHolder(
|
||||
this.getCmsContent(this.cmsWidgetName, "BodyText")
|
||||
);
|
||||
} else {
|
||||
return splitCopyOnCMSPlaceHolder(
|
||||
this.getCmsContent(this.cmsWidgetName, "HeaderText")
|
||||
this.getCmsContent(this.cmsWidgetName, "BodyText2")
|
||||
);
|
||||
}
|
||||
},
|
||||
modalCopy() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "BodyText");
|
||||
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
||||
},
|
||||
afterpayPrice() {
|
||||
getTierOnePackagePrice() {
|
||||
let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems);
|
||||
if (this.lineItems.promos) {
|
||||
allLineItems = allLineItems?.filter((item) => item.partType !== "PROMO_DISCOUNT");
|
||||
|
|
@ -103,11 +101,6 @@ export default {
|
|||
baseMixin.methods.filterOutFees(allLineItems)
|
||||
);
|
||||
|
||||
let vapsLineItemsForSelectedPackage = this.lineItems.vaps;
|
||||
vapsLineItemsForSelectedPackage.forEach((lineItem) => {
|
||||
price += baseMixin.methods.getTotalLineItemPrice(lineItem);
|
||||
});
|
||||
|
||||
if (this.lineItems.promos) {
|
||||
let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems);
|
||||
const promos = getPromosThatMatchLineItemsOnOrder(
|
||||
|
|
@ -119,9 +112,13 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
const adjusted = (price / 4).toFixed(2);
|
||||
|
||||
return `$${adjusted}`;
|
||||
return price;
|
||||
},
|
||||
showAfterpayExtendedPayOption() {
|
||||
return (
|
||||
this.afterpayExtendedPayOptionThreshold &&
|
||||
this.getTierOnePackagePrice >= this.afterpayExtendedPayOptionThreshold
|
||||
);
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
|
|
@ -129,28 +126,56 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.alert {
|
||||
padding: 0.675rem 0;
|
||||
&.alert-info {
|
||||
background-color: $blue-100;
|
||||
.alert-heading {
|
||||
color: $blue-700;
|
||||
#afterpay-banner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: $blue-150;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-radius: 1rem;
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
> div:first-of-type {
|
||||
display: flex;
|
||||
color: $black;
|
||||
font-size: $font-size-20;
|
||||
font-weight: $font-weight-600;
|
||||
line-height: 2rem;
|
||||
@include media-breakpoint-down(md) {
|
||||
border-bottom: 1px solid;
|
||||
padding-bottom: 1rem;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
|
||||
> span:first-of-type {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
}
|
||||
&.fw-bolder {
|
||||
font-weight: 600;
|
||||
}
|
||||
img {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
svg {
|
||||
fill: $blue-700;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: column;
|
||||
border-right: 1px solid;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
& .alert-heading {
|
||||
font-size: 0.875rem;
|
||||
|
||||
> div:last-of-type {
|
||||
color: $gray-650;
|
||||
text-align: left;
|
||||
@include media-breakpoint-down(md) {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-left: 1rem;
|
||||
|
||||
&#extended-pay-option {
|
||||
max-width: 28rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#afterpay-learnmore {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
<afterpayModalBanner
|
||||
v-if="showAfterpayBanner"
|
||||
cmsWidgetName="AfterpayModalWidget"
|
||||
:isAfterpayBreakoutDisplay="isAfterpayBreakoutDisplay"
|
||||
:afterpayExtendedPayOptionThreshold="afterpayExtendedPayOptionThreshold"
|
||||
:isInsuranceSelected="isInsuranceSelected"
|
||||
:lineItems="lineItems" />
|
||||
|
||||
|
|
@ -591,10 +591,7 @@ export default {
|
|||
);
|
||||
},
|
||||
showAfterpayBanner() {
|
||||
return (
|
||||
(this.isAfterpayBreakoutDisplay || !this.isInsuranceSelected) &&
|
||||
(!this.isRecalibrationOnOrder || !this.shouldHideRecalibration)
|
||||
);
|
||||
return !this.isRecalibrationOnOrder || !this.shouldHideRecalibration;
|
||||
},
|
||||
isRecalPriceRemove() {
|
||||
return (
|
||||
|
|
@ -606,14 +603,12 @@ export default {
|
|||
showRecalDisclaimer() {
|
||||
return this.isRecalibrationOnOrder && this.isRecalPriceRemove;
|
||||
},
|
||||
isAfterpayBreakoutDisplay() {
|
||||
return (
|
||||
(!this.isRecalPriceRemove || !this.isRecalibrationOnOrder) &&
|
||||
experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY,
|
||||
"true"
|
||||
)
|
||||
afterpayExtendedPayOptionThreshold() {
|
||||
let thresholdAmount = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.AFTERPAY_EXTENDED_PAY_OPTION_THRESHOLD
|
||||
);
|
||||
thresholdAmount && (thresholdAmount = parseInt(thresholdAmount));
|
||||
return thresholdAmount;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -405,7 +405,6 @@ export default {
|
|||
text-transform: uppercase;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.75px;
|
||||
}
|
||||
.special-save-box {
|
||||
background-color: $green-100;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import baseMixin from "../../mixins/base-mixin";
|
|||
jest.mock("@/mixins/base-mixin.js", () => ({
|
||||
methods: {
|
||||
dispatchStoreAction: jest.fn().mockImplementation((storeAction) => {
|
||||
if (storeAction === "getShopTimeSlots") {
|
||||
if (storeAction === "getShopTimeSlots" || storeAction === "getMobileTimeSlots") {
|
||||
return {
|
||||
data: {
|
||||
estimatedServiceMinutesMinimum: 90,
|
||||
|
|
@ -71,7 +71,7 @@ jest.mock("@/mixins/base-mixin.js", () => ({
|
|||
}
|
||||
}),
|
||||
dispatchStoreActionWithLogging: jest.fn().mockImplementation((storeAction) => {
|
||||
if (storeAction === "getShopTimeSlots") {
|
||||
if (storeAction === "getShopTimeSlots" || storeAction === "getMobileTimeSlots") {
|
||||
return {
|
||||
data: {
|
||||
estimatedServiceMinutesMinimum: 90,
|
||||
|
|
@ -243,48 +243,73 @@ describe("schedule.vue...", () => {
|
|||
expect(arePagePrerequisitesValid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return newShopTimeSlots when getAvailableDatesMethod is called", async () => {
|
||||
test("should return timeslots when getMoreScheduleData is called", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
wrapper.vm.selectableDatesData = {
|
||||
wrapper.vm.selectableDatesInshop = {
|
||||
days: [],
|
||||
};
|
||||
wrapper.vm.selectableDatesMobile = {
|
||||
days: [],
|
||||
};
|
||||
|
||||
// Act
|
||||
const newShopTimeSlots = await wrapper.vm.getAvailableDatesMethod(
|
||||
const newShopTimeSlots = await wrapper.vm.getMoreScheduleData(
|
||||
"2023-01-01",
|
||||
"2023-01-31"
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(newShopTimeSlots).toStrictEqual({
|
||||
days: [
|
||||
{
|
||||
date: "2023-12-01",
|
||||
timeSlots: [
|
||||
{
|
||||
endTime: "08:00",
|
||||
id: "06747-01820-S-B*20424*7 AM",
|
||||
offerPremium: false,
|
||||
startTime: "07:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
estimatedServiceMinutesMinimum: 90,
|
||||
estimatedServiceMinutesMaximum: 120,
|
||||
inshopTimeSlotsData: {
|
||||
days: [
|
||||
{
|
||||
date: "2023-12-01",
|
||||
timeSlots: [
|
||||
{
|
||||
endTime: "08:00",
|
||||
id: "06747-01820-S-B*20424*7 AM",
|
||||
offerPremium: false,
|
||||
startTime: "07:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
estimatedServiceMinutesMinimum: 90,
|
||||
estimatedServiceMinutesMaximum: 120,
|
||||
},
|
||||
mobileTimeSlotsData: {
|
||||
days: [
|
||||
{
|
||||
date: "2023-12-01",
|
||||
timeSlots: [
|
||||
{
|
||||
endTime: "08:00",
|
||||
id: "06747-01820-S-B*20424*7 AM",
|
||||
offerPremium: false,
|
||||
startTime: "07:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
estimatedServiceMinutesMinimum: 90,
|
||||
estimatedServiceMinutesMaximum: 120,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should call API service in day ranges of 34 or less when getAvailableDatesMethod is called with large date ranges", async () => {
|
||||
test("should call API service in day ranges of 34 or less when getMoreScheduleData is called with large date ranges", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
wrapper.vm.selectableDatesData = {
|
||||
wrapper.vm.selectableDatesInshop = {
|
||||
days: [],
|
||||
};
|
||||
wrapper.vm.selectableDatesMobile = {
|
||||
days: [],
|
||||
};
|
||||
|
||||
// Act
|
||||
await wrapper.vm.getAvailableDates.call(
|
||||
await wrapper.vm.getMoreScheduleData.call(
|
||||
wrapper.vm,
|
||||
"2023-01-01",
|
||||
"2023-03-31",
|
||||
|
|
@ -293,7 +318,7 @@ describe("schedule.vue...", () => {
|
|||
);
|
||||
|
||||
// Assert
|
||||
expect(baseMixin.methods.dispatchStoreActionWithLogging).toHaveBeenCalledTimes(3);
|
||||
expect(baseMixin.methods.dispatchStoreActionWithLogging).toHaveBeenCalledTimes(6);
|
||||
expect(baseMixin.methods.dispatchStoreActionWithLogging).toHaveBeenCalledWith(
|
||||
"getShopTimeSlots",
|
||||
expect.anything(),
|
||||
|
|
@ -306,7 +331,7 @@ describe("schedule.vue...", () => {
|
|||
test("should call next() and call all functions within next", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
wrapper.vm.selectableDatesData = {
|
||||
wrapper.vm.selectableDatesInshop = {
|
||||
days: [],
|
||||
};
|
||||
wrapper.vm.updateFooterButtonText = jest.fn();
|
||||
|
|
@ -332,7 +357,7 @@ describe("schedule.vue...", () => {
|
|||
})
|
||||
);
|
||||
expect(wrapper.vm.$refs.locationAlerts.initializeComponent).toHaveBeenCalled();
|
||||
expect(wrapper.vm.selectableDatesData).toStrictEqual(
|
||||
expect(wrapper.vm.selectableDatesInshop).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
days: expect.any(Array),
|
||||
estimatedServiceMinutesMaximum: expect.any(Number),
|
||||
|
|
@ -353,7 +378,7 @@ describe("schedule.vue...", () => {
|
|||
test("timeSlotsForSelectedDate should return timeslots if selected date is available", () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
wrapper.vm.selectableDatesData = {
|
||||
wrapper.vm.selectableDatesInshop = {
|
||||
days: [
|
||||
{
|
||||
date: "2022-11-11",
|
||||
|
|
@ -392,7 +417,7 @@ describe("schedule.vue...", () => {
|
|||
test("timeSlotsForSelectedDate should be null if no date has been selected", () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
wrapper.vm.selectableDatesData = {
|
||||
wrapper.vm.selectableDatesInshop = {
|
||||
days: [
|
||||
{
|
||||
date: "2022-11-11",
|
||||
|
|
@ -430,7 +455,7 @@ describe("schedule.vue...", () => {
|
|||
test("getServiceZipCtuCodeFromStore should return zipCodeCtu", () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
wrapper.vm.selectableDatesData = {
|
||||
wrapper.vm.selectableDatesInshop = {
|
||||
days: [],
|
||||
};
|
||||
|
||||
|
|
@ -444,7 +469,7 @@ describe("schedule.vue...", () => {
|
|||
test("getDisplayTextForMilitaryTime should return the correctly formatted string", () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
wrapper.vm.selectableDatesData = {
|
||||
wrapper.vm.selectableDatesInshop = {
|
||||
days: [],
|
||||
};
|
||||
const timeInput1 = "15:00";
|
||||
|
|
@ -466,7 +491,7 @@ describe("schedule.vue...", () => {
|
|||
test("Clicking back should fire correct navigation", () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
wrapper.vm.selectableDatesData = {
|
||||
wrapper.vm.selectableDatesInshop = {
|
||||
days: [],
|
||||
};
|
||||
wrapper.vm.$router.navigateWithoutSaving = jest.fn();
|
||||
|
|
@ -501,51 +526,6 @@ describe("schedule.vue...", () => {
|
|||
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("for mobile appts, updateSupportingItems should call store action to save supporting items", async () => {
|
||||
// Arrange
|
||||
store.getters.order.serviceLocation.appointmentType = "Mobile";
|
||||
store.getters.lineItems.supportingItems = [
|
||||
{
|
||||
partNumber: "EARLY BIRD",
|
||||
description: null,
|
||||
partType: "EARLY BIRD",
|
||||
laborAmount: 0,
|
||||
sellingPrice: 0,
|
||||
kitPrice: 0,
|
||||
},
|
||||
];
|
||||
const { wrapper } = setupMocks({});
|
||||
wrapper.vm.dispatchStoreAction = jest.fn(() => {
|
||||
return {
|
||||
data: [],
|
||||
};
|
||||
});
|
||||
wrapper.vm.mobilePremiumAppointmentFee = 14.99;
|
||||
wrapper.setData({
|
||||
selectedTimeSlot: {
|
||||
date: "2019-01-01",
|
||||
startTime: "09:00",
|
||||
endTime: "10:00",
|
||||
routeCode: null,
|
||||
isPremiumAppointment: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
await wrapper.vm.updateSupportingItems();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.dispatchStoreAction).toBeCalledWith(
|
||||
"saveSupportingItemsSuppressingStateResetting",
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
partType: "EARLY BIRD",
|
||||
}),
|
||||
]),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
test("for Inshop appts, updateSupportingItems should call store action to save supporting items WITHOUT the EARLY BIRD supporting item", async () => {
|
||||
// Arrange
|
||||
store.getters.order.serviceLocation.appointmentType = "Inshop";
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@
|
|||
selectableDatesSetting="custom"
|
||||
ref="datePicker"
|
||||
v-model="selectedDate"
|
||||
:isMobileSelected="isMobileSelected"
|
||||
class="text-link-small"
|
||||
:customSelectableDatesCallback="getAvailableDatesMethod"
|
||||
:getMoreDatesCallback="getMoreScheduleData"
|
||||
validationRules="date-required"
|
||||
@date-clicked="handleDateClicked"
|
||||
:pricingByDayBasePrice="pricingByDayBasePrice"
|
||||
|
|
@ -34,12 +35,8 @@
|
|||
:timeSlotsForSelectedDate="timeSlotsForSelectedDate"
|
||||
:appointmentType="appointmentType"
|
||||
:premiumAppointmentFee="mobilePremiumAppointmentFee"
|
||||
:estimatedServiceMinutesMinimum="
|
||||
selectableDatesData.estimatedServiceMinutesMinimum
|
||||
"
|
||||
:estimatedServiceMinutesMaximum="
|
||||
selectableDatesData.estimatedServiceMinutesMaximum
|
||||
"
|
||||
:estimatedServiceMinutesMinimum="getServiceMinutesMin"
|
||||
:estimatedServiceMinutesMaximum="getServiceMinutesMax"
|
||||
@TimeSlotSelected="updateTimeSlot"
|
||||
:displayWaitList="displayWaitList"
|
||||
@waitListRequested="handleWaitListRequested" />
|
||||
|
|
@ -67,8 +64,6 @@ import locationAlerts from "@/layouts/schedule/location-alerts/location-alerts";
|
|||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
|
||||
// Supporting files
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
import { getQuerystringParameter } from "@/helpers/querystring-helper";
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
|
|
@ -81,6 +76,7 @@ import {
|
|||
} from "@/layouts/schedule/helpers/schedule-helper";
|
||||
import {
|
||||
AppointmentTypeStrings,
|
||||
RouteCodeFlags,
|
||||
PREMIUM_FEE_PART_TYPE,
|
||||
PRICING_BY_DAY_PART_TYPE,
|
||||
} from "@/constants/schedule-constants";
|
||||
|
|
@ -101,24 +97,19 @@ defineRule("date-required", required(errorMessages.DATE_REQUIRED));
|
|||
|
||||
// Define constants
|
||||
const TIME_SLOTS_CALL_DAYS_LIMIT = 34; // needs to be 34 for API limits (35 does not consistently work)
|
||||
const NUMBER_OF_CALENDAR_ROWS_TO_SHOW_FOR_INITIAL_VIEW = 2;
|
||||
|
||||
const getAvailableDates = async (
|
||||
startDateString,
|
||||
endDateString,
|
||||
appointmentType,
|
||||
providerNumber
|
||||
) => {
|
||||
const getScheduleApiResponse = async (startDateString, endDateString, 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;
|
||||
let storeActionConfigInshop;
|
||||
let storeActionConfigMobile;
|
||||
|
||||
if (i > 1) {
|
||||
apiStartDate = sumDateString(apiEndDate, 1);
|
||||
|
|
@ -133,29 +124,32 @@ const getAvailableDates = async (
|
|||
}
|
||||
}
|
||||
|
||||
if (appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||
storeActionConfig = {
|
||||
storeAction: storeActions.GET_MOBILE_TIME_SLOTS,
|
||||
payload: {
|
||||
startDate: apiStartDate,
|
||||
endDate: apiEndDate,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
storeActionConfig = {
|
||||
storeAction: storeActions.GET_SHOP_TIME_SLOTS,
|
||||
payload: {
|
||||
startDate: apiStartDate,
|
||||
endDate: apiEndDate,
|
||||
shopAppointmentType: appointmentType,
|
||||
providerNumber: providerNumber,
|
||||
},
|
||||
};
|
||||
storeActionConfigMobile = {
|
||||
storeAction: storeActions.GET_MOBILE_TIME_SLOTS,
|
||||
payload: {
|
||||
startDate: apiStartDate,
|
||||
endDate: apiEndDate,
|
||||
},
|
||||
};
|
||||
storeActionConfigInshop = {
|
||||
storeAction: storeActions.GET_SHOP_TIME_SLOTS,
|
||||
payload: {
|
||||
startDate: apiStartDate,
|
||||
endDate: apiEndDate,
|
||||
shopAppointmentType: "InshopOrDropoff",
|
||||
providerNumber: providerNumber,
|
||||
},
|
||||
};
|
||||
if (apiStartDate < apiEndDate) {
|
||||
storeActionConfigs.push(storeActionConfigInshop);
|
||||
storeActionConfigs.push(storeActionConfigMobile);
|
||||
}
|
||||
if (apiStartDate < apiEndDate) storeActionConfigs.push(storeActionConfig);
|
||||
}
|
||||
|
||||
const timeSlotsResponsesData = {
|
||||
const inshopTimeSlotsData = {
|
||||
days: [],
|
||||
};
|
||||
const mobileTimeSlotsData = {
|
||||
days: [],
|
||||
};
|
||||
|
||||
|
|
@ -167,39 +161,71 @@ const getAvailableDates = async (
|
|||
|
||||
const makeParallelCalls = async () => {
|
||||
await Promise.all(
|
||||
storeActionConfigs.map(async (storeAction) => {
|
||||
const timeSlotsResponse = await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeAction.storeAction,
|
||||
storeAction.payload,
|
||||
"schedule",
|
||||
false
|
||||
);
|
||||
timeSlotsResponsesData.estimatedServiceMinutesMinimum =
|
||||
timeSlotsResponse.data.estimatedServiceMinutesMinimum;
|
||||
timeSlotsResponsesData.estimatedServiceMinutesMaximum =
|
||||
timeSlotsResponse.data.estimatedServiceMinutesMaximum;
|
||||
timeSlotsResponsesData.days = [
|
||||
...timeSlotsResponsesData.days,
|
||||
...timeSlotsResponse.data.days,
|
||||
];
|
||||
storeActionConfigs.map(async (storeActionConfig) => {
|
||||
if (storeActionConfig.storeAction.indexOf("Mobile") > -1) {
|
||||
const mobileTimeSlotsResponse =
|
||||
await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActionConfig.storeAction,
|
||||
storeActionConfig.payload,
|
||||
"schedule",
|
||||
false
|
||||
);
|
||||
mobileTimeSlotsData.estimatedServiceMinutesMinimum =
|
||||
mobileTimeSlotsResponse.data.estimatedServiceMinutesMinimum;
|
||||
mobileTimeSlotsData.estimatedServiceMinutesMaximum =
|
||||
mobileTimeSlotsResponse.data.estimatedServiceMinutesMaximum;
|
||||
mobileTimeSlotsData.days = [
|
||||
...mobileTimeSlotsData.days,
|
||||
...mobileTimeSlotsResponse.data.days,
|
||||
];
|
||||
} else {
|
||||
const inshopTimeSlotsResponse =
|
||||
await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActionConfig.storeAction,
|
||||
storeActionConfig.payload,
|
||||
"schedule",
|
||||
false
|
||||
);
|
||||
|
||||
inshopTimeSlotsData.estimatedServiceMinutesMinimum =
|
||||
inshopTimeSlotsResponse.data.estimatedServiceMinutesMinimum;
|
||||
inshopTimeSlotsData.estimatedServiceMinutesMaximum =
|
||||
inshopTimeSlotsResponse.data.estimatedServiceMinutesMaximum;
|
||||
inshopTimeSlotsData.days = [
|
||||
...inshopTimeSlotsData.days,
|
||||
...inshopTimeSlotsResponse.data.days,
|
||||
];
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return makeParallelCalls().then(() => {
|
||||
// sort days chronologically
|
||||
timeSlotsResponsesData.days.sort(compareDayStrings);
|
||||
return timeSlotsResponsesData;
|
||||
inshopTimeSlotsData.days.sort(compareDayStrings);
|
||||
mobileTimeSlotsData.days.sort(compareDayStrings);
|
||||
|
||||
return {
|
||||
inshopTimeSlotsData: inshopTimeSlotsData,
|
||||
mobileTimeSlotsData: mobileTimeSlotsData,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const returnFirstDate = (dates) => {
|
||||
if (dates.days?.length > 0) {
|
||||
return dates.days[0].date;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "schedule",
|
||||
data() {
|
||||
return {
|
||||
selectedDate: this.getSelectedDate(),
|
||||
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
|
||||
selectableDatesData: [],
|
||||
mobilePremiumAppointmentFee: null,
|
||||
waitListRequested: null,
|
||||
displayWaitList: null,
|
||||
|
|
@ -209,6 +235,9 @@ export default {
|
|||
pricingByDayBasePrice: null,
|
||||
pricingByDayUpcharge: null,
|
||||
showPricingByDay: null,
|
||||
selectableDatesInshop: [],
|
||||
selectableDatesMobile: [],
|
||||
isMobileSelected: false,
|
||||
};
|
||||
},
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
|
|
@ -230,7 +259,6 @@ export default {
|
|||
isRecalibrationOnOrder && shouldHideRecalibration
|
||||
? getItemsWithoutRecalParts(lineItems.glassParts)
|
||||
: (lineItems.glassParts ?? []);
|
||||
const supportingItemsFromStore = lineItems?.supportingItems;
|
||||
const supportingItemsWithoutFees = baseMixin.methods.filterOutCertainPartTypesOrNumbers(
|
||||
lineItems.supportingItems,
|
||||
{
|
||||
|
|
@ -249,10 +277,14 @@ export default {
|
|||
const priceString = getAmountDue(lineItemsToBePriced, false); // pass the IncludeTax param as false
|
||||
const priceStringIntegerRoundedDown = priceString?.split(".")[0]; // same method used as getDisplayPrice() in service-package-radio used on /quote
|
||||
const pricingByDayBasePrice = parseInt(priceStringIntegerRoundedDown);
|
||||
const isMobileSelected =
|
||||
store.getters.order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
|
||||
let includePricingByDayUpcharge = false;
|
||||
|
||||
// Check to see if date should be pre-selected
|
||||
let preSelectedSlot = await store.getters.order.schedule;
|
||||
let preSelectedDate = preSelectedSlot ? preSelectedSlot.date : null;
|
||||
if (isMobileSelected && preSelectedDate) preSelectedDate += "-mobile";
|
||||
if (!preSelectedSlot.date || preSelectedSlot?.date?.length < 1) {
|
||||
preSelectedSlot = null;
|
||||
} else {
|
||||
|
|
@ -279,9 +311,9 @@ export default {
|
|||
const datePickerInitialDataPromise = await datePicker.methods.loadInitialData({
|
||||
// setup config options for date-picker
|
||||
selectableDatesSetting: "custom",
|
||||
initialViewRowsToShow: 2,
|
||||
customSelectableDatesCallback: getAvailableDates,
|
||||
preSelectedDate: preSelectedSlot ? preSelectedSlot.date : preSelectedSlot,
|
||||
initialViewRowsToShow: NUMBER_OF_CALENDAR_ROWS_TO_SHOW_FOR_INITIAL_VIEW,
|
||||
getSelectableDatesCallback: getScheduleApiResponse,
|
||||
preSelectedDate: preSelectedDate,
|
||||
});
|
||||
|
||||
// Get pricingByDayUpcharge needed for Pricing By Day
|
||||
|
|
@ -351,7 +383,10 @@ export default {
|
|||
vm.setCmsContent(resultMap.cmsContent);
|
||||
vm.$refs.datePicker.initializeComponent(datePickerInitialData);
|
||||
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
|
||||
vm.selectableDatesData = datePickerInitialData.initialShopTimeSlotsResponse;
|
||||
vm.selectableDatesInshop =
|
||||
datePickerInitialData.initialShopTimeSlotsResponse.inshopTimeSlotsData;
|
||||
vm.selectableDatesMobile =
|
||||
datePickerInitialData.initialShopTimeSlotsResponse.mobileTimeSlotsData;
|
||||
vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice
|
||||
? resultMap.premiumFeeWithPrice[0]
|
||||
: null;
|
||||
|
|
@ -363,21 +398,33 @@ export default {
|
|||
vm.pricingByDayBasePrice = pricingByDayBasePrice;
|
||||
vm.pricingByDayUpcharge = pricingByDayUpcharge;
|
||||
vm.showPricingByDay = showPricingByDay;
|
||||
vm.isMobileSelected = isMobileSelected;
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
const selectableDatesData = this.selectableDatesData;
|
||||
// if no date selected on load
|
||||
if (!this.selectedDate) {
|
||||
if (selectableDatesData?.days?.length > 0) {
|
||||
this.selectedDate = selectableDatesData.days[0].date;
|
||||
// if no date is preselected on load, then select the first available
|
||||
if (this.isMobileSelected) {
|
||||
this.selectedDate = returnFirstDate(this.selectableDatesMobile);
|
||||
} else {
|
||||
this.selectedDate = returnFirstDate(this.selectableDatesInshop);
|
||||
}
|
||||
|
||||
// if there is still no selected date, then load more and try again
|
||||
if (!this.selectedDate) {
|
||||
setTimeout(() => {
|
||||
this.$refs.datePicker.showAnotherMonth().then((moreSelectableDatesData) => {
|
||||
this.selectableDatesData = moreSelectableDatesData;
|
||||
if (selectableDatesData?.days?.length > 0) {
|
||||
this.selectedDate = selectableDatesData.days[0].date;
|
||||
this.$refs.datePicker.showAnotherMonth().then((moreSelectableDates) => {
|
||||
// update global
|
||||
this.selectableDatesInshop.days =
|
||||
moreSelectableDates.inshopTimeSlotsData.days;
|
||||
this.selectableDatesMobile.days =
|
||||
moreSelectableDates.mobileTimeSlotsData.days;
|
||||
|
||||
if (this.isMobileSelected) {
|
||||
this.selectedDate = returnFirstDate(this.selectableDatesMobile);
|
||||
} else {
|
||||
this.selectedDate = returnFirstDate(this.selectableDatesInshop);
|
||||
}
|
||||
|
||||
this.setDisplayWaitList();
|
||||
|
|
@ -396,27 +443,46 @@ export default {
|
|||
return this.splitCopyOnCMSPlaceHolder(this.ChangeShopLinkText);
|
||||
},
|
||||
appointmentType() {
|
||||
return this.$store.getters.order.serviceLocation.appointmentType;
|
||||
// TODO - THIS HAS TO BE CHANGED. APPOINTMENT TYPE CAN NOW CHANGE ON THE FLY, NOT FROM STORE ANYMORE.
|
||||
// return this.$store.getters.order.serviceLocation.appointmentType;
|
||||
if (this.isMobileSelected) {
|
||||
return "Mobile";
|
||||
} else {
|
||||
return "InshopOrDropoff";
|
||||
}
|
||||
},
|
||||
getServiceMinutesMin() {
|
||||
return this.isMobileSelected
|
||||
? this.selectableDatesMobile.estimatedServiceMinutesMinimum
|
||||
: this.selectableDatesInshop.estimatedServiceMinutesMinimum;
|
||||
},
|
||||
getServiceMinutesMax() {
|
||||
return this.isMobileSelected
|
||||
? this.selectableDatesMobile.estimatedServiceMinutesMaximum
|
||||
: this.selectableDatesInshop.estimatedServiceMinutesMaximum;
|
||||
},
|
||||
timeSlotsForSelectedDate() {
|
||||
if (!this.selectedDate) return null;
|
||||
const selectedDate = this.selectedDate;
|
||||
|
||||
return this.selectableDatesData.days?.find(
|
||||
(selectableDate) => selectableDate.date === this.selectedDate
|
||||
);
|
||||
if (this.isMobileSelected) {
|
||||
const selectedDateString = selectedDate.replace("-mobile", "");
|
||||
return this.selectableDatesMobile.days?.find(
|
||||
(selectableDate) => selectableDate.date === selectedDateString
|
||||
);
|
||||
} else {
|
||||
return this.selectableDatesInshop.days?.find(
|
||||
(selectableDate) => selectableDate.date === this.selectedDate
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
arePagePrerequisitesValid() {
|
||||
const serviceLocation = store.getters.order.serviceLocation;
|
||||
const serviceLocation = store.getters.order.serviceLocation; // TODO - MAY NEED TO REMOVE THIS AS A PREREQ
|
||||
|
||||
const serviceLocationPreReqs =
|
||||
serviceLocation.zipCode &&
|
||||
serviceLocation.zipCodeCtu &&
|
||||
serviceLocation.appointmentType &&
|
||||
(serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE ||
|
||||
serviceLocation.provider.providerNumber);
|
||||
const serviceLocationPreReqs = serviceLocation.zipCode && serviceLocation.zipCodeCtu; // TODO - MAY NEED TO ADD IN SERVICE LOCATION PREREQS HERE TOO
|
||||
|
||||
const paymentInfo = store.getters.payment.isInsurance !== null;
|
||||
|
||||
|
|
@ -441,26 +507,38 @@ export default {
|
|||
}
|
||||
return preReqResult;
|
||||
},
|
||||
async getAvailableDatesMethod(startDate, endDate) {
|
||||
const newShopTimeSlots = await getAvailableDates(
|
||||
async getMoreScheduleData(startDate, endDate) {
|
||||
// called only when "View more dates" is clicked; not on initial page load
|
||||
const moreShopTimeSlots = await getScheduleApiResponse(
|
||||
startDate,
|
||||
endDate,
|
||||
this.appointmentType,
|
||||
this.$store.getters.order.serviceLocation.provider.providerNumber
|
||||
);
|
||||
|
||||
// ADD API CALL RESULTS TO EXISTING DATE DATA
|
||||
this.selectableDatesData.days = this.selectableDatesData.days.concat(
|
||||
newShopTimeSlots.days
|
||||
this.selectableDatesInshop.days = this.selectableDatesInshop.days.concat(
|
||||
moreShopTimeSlots.inshopTimeSlotsData.days
|
||||
);
|
||||
return newShopTimeSlots;
|
||||
this.selectableDatesMobile.days = this.selectableDatesMobile.days.concat(
|
||||
moreShopTimeSlots.mobileTimeSlotsData.days
|
||||
);
|
||||
|
||||
return moreShopTimeSlots;
|
||||
},
|
||||
getAvailableDates,
|
||||
getScheduleApiResponse,
|
||||
getServiceZipCtuCodeFromStore() {
|
||||
return store.getters.order.serviceLocation.zipCodeCtu;
|
||||
},
|
||||
getSelectedDate() {
|
||||
return store.getters.order.schedule.date;
|
||||
let isMobileSelected = this.isMobileSelected;
|
||||
if (isMobileSelected === undefined) {
|
||||
isMobileSelected =
|
||||
store.getters.order.serviceLocation.appointmentType ===
|
||||
AppointmentTypeStrings.MOBILE;
|
||||
}
|
||||
let storedDate = store.getters.order.schedule.date;
|
||||
if (isMobileSelected) storedDate += "-mobile";
|
||||
return storedDate;
|
||||
},
|
||||
getSelectedTimeSlotInfo() {
|
||||
const supportingItems = this.getSupportingItems();
|
||||
|
|
@ -492,10 +570,14 @@ export default {
|
|||
timeSlotInfo.timeSlot.date
|
||||
)}`;
|
||||
|
||||
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
|
||||
navbarButtonText += ` at ${this.getDisplayTextForMilitaryTime(
|
||||
timeSlotInfo.timeSlot.startTime
|
||||
)}`;
|
||||
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF) {
|
||||
if (
|
||||
!timeSlotInfo.timeSlot.routeCode.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)
|
||||
) {
|
||||
navbarButtonText += ` at ${this.getDisplayTextForMilitaryTime(
|
||||
timeSlotInfo.timeSlot.startTime
|
||||
)}`;
|
||||
}
|
||||
} else if (
|
||||
this.appointmentType === AppointmentTypeStrings.MOBILE &&
|
||||
!timeSlotInfo.isPremiumAppointment
|
||||
|
|
@ -513,8 +595,7 @@ export default {
|
|||
},
|
||||
convertSelectedDateToShortMonthAndDay(selectedDate) {
|
||||
// This conversion ensures we don't get get GMT induced date changes
|
||||
const dateObject = convertDateStringToDate(selectedDate);
|
||||
// Ex: April 25
|
||||
const dateObject = convertDateStringToDate(selectedDate.replace("-mobile", ""));
|
||||
return dateObject.toLocaleDateString("en-us", { month: "short", day: "numeric" });
|
||||
},
|
||||
getDisplayTextForMilitaryTime(militaryTimeInput, shouldTrimMinutesIfEmpty = false) {
|
||||
|
|
@ -523,10 +604,7 @@ export default {
|
|||
const minutes = militaryTimeInput.split(":")[1];
|
||||
const meridianNotation = hours > 11 ? "PM" : "AM";
|
||||
|
||||
if (hours > 12) {
|
||||
hours -= 12;
|
||||
}
|
||||
|
||||
if (hours > 12) hours -= 12;
|
||||
if (shouldTrimMinutesIfEmpty && minutes === "00") {
|
||||
return `${hours} ${meridianNotation}`;
|
||||
} else {
|
||||
|
|
@ -540,16 +618,25 @@ export default {
|
|||
);
|
||||
},
|
||||
forwardButtonAction() {
|
||||
// TODO - WILL NEED TO SAVE THE APPOINTMENT TYPE ALSO (AND ANYTHING ELSE THAT SERVICE-LOCATION DID TOO)
|
||||
this.updateSupportingItems();
|
||||
|
||||
if (this.displayWaitList) {
|
||||
var gaLabel = "";
|
||||
const status = this.waitListRequested ? "checked" : "unchecked";
|
||||
let gaLabel = "";
|
||||
let status = this.waitListRequested ? "checked" : "unchecked";
|
||||
if (
|
||||
this.waitListRequested === null &&
|
||||
this.$store.state.order.customer.waitListRequested === true
|
||||
) {
|
||||
status = "checked";
|
||||
}
|
||||
const type = store.getters.isMobileAppointment ? "mobile" : "inshop";
|
||||
gaLabel = `${status}_${type}`;
|
||||
|
||||
const currentDate = new Date();
|
||||
const dateString = this.selectableDatesData.days[0].date;
|
||||
const dateString = this.isMobileSelected
|
||||
? this.selectableDatesMobile.days[0].date
|
||||
: this.selectableDatesInshop.days[0].date;
|
||||
const [year, month, day] = dateString.split("-").map(Number);
|
||||
const appointmentDate = new Date(year, month - 1, day);
|
||||
const timeDifference = appointmentDate - currentDate;
|
||||
|
|
@ -566,11 +653,14 @@ export default {
|
|||
store.getters.order.serviceLocation.zipCodeCtu;
|
||||
this.pushEventToGA("waitlist", "add_to_waitlist_check_box_status", gaLabel, true);
|
||||
}
|
||||
|
||||
if (this.selectedTimeSlotInfo.timeSlot.jobMinMinutes == null) {
|
||||
this.selectedTimeSlotInfo.timeSlot.jobMinMinutes =
|
||||
this.selectableDatesData?.estimatedServiceMinutesMinimum?.toString();
|
||||
this.selectedTimeSlotInfo.timeSlot.jobMaxMinutes =
|
||||
this.selectableDatesData?.estimatedServiceMinutesMaximum?.toString();
|
||||
this.selectedTimeSlotInfo.timeSlot.jobMinMinutes = this.isMobileSelected
|
||||
? this.selectableDatesMobile.estimatedServiceMinutesMinimum?.toString()
|
||||
: this.selectableDatesInshop.estimatedServiceMinutesMinimum?.toString();
|
||||
this.selectedTimeSlotInfo.timeSlot.jobMaxMinutes = this.isMobileSelected
|
||||
? this.selectableDatesMobile.estimatedServiceMinutesMaximum?.toString()
|
||||
: this.selectableDatesInshop.estimatedServiceMinutesMaximum?.toString();
|
||||
}
|
||||
|
||||
this.dispatchStoreAction(
|
||||
|
|
@ -598,9 +688,12 @@ export default {
|
|||
experimentSettings.DISPLAY_WAITLIST,
|
||||
"true"
|
||||
) &&
|
||||
this.selectableDatesData.days[0]
|
||||
this.selectableDatesInshop.days[0] && // TODO - ASK BUSINESS... WHAT LOGIC SHOULD WE USE HERE FOR WHEN TO DISPLAY THE WAIT LIST?
|
||||
this.selectableDatesMobile.days[0]
|
||||
) {
|
||||
const dateString = this.selectableDatesData.days[0].date;
|
||||
const dateString = this.isMobileSelected
|
||||
? this.selectableDatesMobile.days[0].date
|
||||
: this.selectableDatesInshop.days[0].date;
|
||||
const [year, month, day] = dateString.split("-").map(Number);
|
||||
const targetDate = new Date(year, month - 1, day);
|
||||
const currentDate = new Date();
|
||||
|
|
@ -766,7 +859,7 @@ export default {
|
|||
}
|
||||
}
|
||||
.change-location a {
|
||||
font-family: AvertaSemibold;
|
||||
font-family: UrbanistSemibold;
|
||||
}
|
||||
.time-slots-question {
|
||||
padding: 0 0.75rem;
|
||||
|
|
|
|||
|
|
@ -631,7 +631,6 @@ export default {
|
|||
margin-left: 0.5rem;
|
||||
strong {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0%;
|
||||
}
|
||||
}
|
||||
.success-image {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
class="mt-4"
|
||||
:class="[
|
||||
isMobileAppointment ? 'is-mobile-appointment' : '',
|
||||
isDropOffAppointment ? 'is-drop-off-appointment' : '',
|
||||
isInshopOrDropOffAppointment ? 'is-inshop-or-drop-off-appointment' : '',
|
||||
]"
|
||||
:answers="availableTimeSlots"
|
||||
groupName="chooseTimeSlot"
|
||||
|
|
@ -329,14 +329,12 @@ export default {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
|
||||
return this.getAvailableTimeSlotsForDropOff(
|
||||
this.timeSlotsForSelectedDate.timeSlots
|
||||
);
|
||||
} else if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||
return this.getAvailableTimeSlotsForMobile(this.timeSlotsForSelectedDate.timeSlots);
|
||||
} else {
|
||||
return this.getAvailableTimeSlotsForInshop(this.timeSlotsForSelectedDate.timeSlots);
|
||||
return this.getAvailableTimeSlotsForInshopOrDropOff(
|
||||
this.timeSlotsForSelectedDate.timeSlots
|
||||
);
|
||||
}
|
||||
},
|
||||
hasWaitListExperiment() {
|
||||
|
|
@ -357,8 +355,8 @@ export default {
|
|||
isMobileAppointment() {
|
||||
return this.appointmentType === AppointmentTypeStrings.MOBILE;
|
||||
},
|
||||
isDropOffAppointment() {
|
||||
return this.appointmentType === AppointmentTypeStrings.DROP_OFF;
|
||||
isInshopOrDropOffAppointment() {
|
||||
return this.appointmentType === AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -408,6 +406,26 @@ export default {
|
|||
|
||||
return availableTimeSlots;
|
||||
},
|
||||
getAvailableTimeSlotsForInshopOrDropOff(timeSlotsForSelectedDate) {
|
||||
const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot) => {
|
||||
let buttonLabelValue;
|
||||
if (timeSlot.id.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
||||
buttonLabelValue = this.dropoffButtonText;
|
||||
} else if (timeSlot.id.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||
buttonLabelValue = this.overnightDropoffButtonText;
|
||||
} else if (this.isSameDay) {
|
||||
buttonLabelValue = this.sameDayDropoffButtonText;
|
||||
} else {
|
||||
buttonLabelValue = militaryToTwelveHourTime(timeSlot.startTime);
|
||||
}
|
||||
return {
|
||||
value: timeSlot.id,
|
||||
buttonLabel: buttonLabelValue,
|
||||
};
|
||||
});
|
||||
|
||||
return availableTimeSlots;
|
||||
},
|
||||
getAvailableTimeSlotsForMobile(timeSlotsForSelectedDate) {
|
||||
const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot) => {
|
||||
const readableTime = `${militaryToTwelveHourTime(timeSlot.startTime)} - ${militaryToTwelveHourTime(
|
||||
|
|
@ -561,7 +579,7 @@ export default {
|
|||
margin-top: 1.5rem;
|
||||
|
||||
.time-slot-header {
|
||||
font-family: AvertaBold;
|
||||
font-family: UrbanistBold;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.text-block.duration-text-block {
|
||||
|
|
|
|||
|
|
@ -314,57 +314,6 @@ describe("service-location.vue", () => {
|
|||
expect(wrapper.vm.serviceZipCodeQuestion).toStrictEqual(newServiceZipCodeQuestion);
|
||||
});
|
||||
|
||||
test("resets mobile location when service zip code is updated", () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
const mobileLocationQuestionsComponent = wrapper.findComponent({
|
||||
ref: "mobileLocationQuestions",
|
||||
});
|
||||
mobileLocationQuestionsComponent.resetComponent = jest.fn();
|
||||
|
||||
const serviceZipCodeComponent = wrapper.findComponent({
|
||||
ref: "serviceZipCodeQuestion",
|
||||
});
|
||||
serviceZipCodeComponent.resetMobileFeePart = jest.fn();
|
||||
|
||||
const newServiceZipCodeQuestion = {
|
||||
zipCode: "61606",
|
||||
state: "IL",
|
||||
zipCodeCtu: "01526",
|
||||
};
|
||||
|
||||
const mobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "5555 Sulgrave Dr",
|
||||
apartmentNumberOrBusinessName: "Apt 1",
|
||||
city: "New Albany",
|
||||
state: "OH",
|
||||
zipCode: "43054",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
};
|
||||
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
|
||||
|
||||
const newMobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "",
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: "",
|
||||
state: "IL",
|
||||
zipCode: "61606",
|
||||
},
|
||||
isVehicleProtected: null,
|
||||
isMobileSelected: false,
|
||||
};
|
||||
|
||||
// Act
|
||||
serviceZipCodeComponent.vm.$emit("update:modelValue", newServiceZipCodeQuestion);
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.mobileLocationQuestions).toStrictEqual(newMobileLocationQuestions);
|
||||
});
|
||||
|
||||
test("resets appointment type selection when service zip code is updated by service zip modal", () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
|
@ -424,190 +373,6 @@ describe("service-location.vue", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("updating mobile location", () => {
|
||||
test("updates the page model after providing the mobile location", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
await wrapper.setData({
|
||||
shopProviderData: {
|
||||
mobileProviderNumber: "001820",
|
||||
},
|
||||
selectedAppointmentType: "Mobile",
|
||||
providerData: { mobileProviderNumber: "01820" },
|
||||
isGlassServiceableMobile: true,
|
||||
});
|
||||
|
||||
const mobileLocationQuestionsComponent = wrapper.findComponent({
|
||||
ref: "mobileLocationQuestions",
|
||||
});
|
||||
mobileLocationQuestionsComponent.resetComponent = jest.fn();
|
||||
|
||||
const serviceZipCodeComponent = wrapper.findComponent({
|
||||
ref: "serviceZipCodeQuestion",
|
||||
});
|
||||
serviceZipCodeComponent.resetMobileFeePart = jest.fn();
|
||||
wrapper.vm.forwardButtonAction = jest.fn();
|
||||
|
||||
const mobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "",
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
},
|
||||
isVehicleProtected: null,
|
||||
};
|
||||
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
|
||||
const newMobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "5555 Sulgrave Dr",
|
||||
apartmentNumberOrBusinessName: "Apt 1",
|
||||
city: "New Albany",
|
||||
state: "OH",
|
||||
zipCode: "43054",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
isMobileSelected: true,
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
// Trigger the event
|
||||
await mobileLocationQuestionsComponent.vm.$emit(
|
||||
"setMobileLocation",
|
||||
newMobileLocationQuestions
|
||||
);
|
||||
|
||||
await wrapper.vm.$nextTick(); // Wait for DOM updates
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.mobileLocationQuestions).toStrictEqual(newMobileLocationQuestions);
|
||||
});
|
||||
|
||||
test("resets service zip code when mobile location is updated", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
await wrapper.setData({
|
||||
shopProviderData: {
|
||||
mobileProviderNumber: "001820",
|
||||
},
|
||||
selectedAppointmentType: "Mobile",
|
||||
providerData: { mobileProviderNumber: "01820" },
|
||||
isGlassServiceableMobile: true,
|
||||
});
|
||||
|
||||
const mobileLocationQuestionsComponent = wrapper.findComponent({
|
||||
ref: "mobileLocationQuestions",
|
||||
});
|
||||
mobileLocationQuestionsComponent.resetComponent = jest.fn();
|
||||
|
||||
const serviceZipCodeComponent = wrapper.findComponent({
|
||||
ref: "serviceZipCodeQuestion",
|
||||
});
|
||||
serviceZipCodeComponent.resetMobileFeePart = jest.fn();
|
||||
wrapper.vm.forwardButtonAction = jest.fn();
|
||||
const mobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "",
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
},
|
||||
isVehicleProtected: null,
|
||||
isMobileSelectedAndModalClosed: false,
|
||||
mobileFeePart: null,
|
||||
};
|
||||
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
|
||||
|
||||
const newMobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "5555 Rustic Dr",
|
||||
apartmentNumberOrBusinessName: "Apt 1",
|
||||
city: "Westerville",
|
||||
state: "OH",
|
||||
zipCode: "43081",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
isMobileSelectedAndModalClosed: true,
|
||||
mobileFeePart: null,
|
||||
};
|
||||
|
||||
wrapper.vm.serviceZipCodeQuestion = {
|
||||
zipCode: "61606",
|
||||
state: "IL",
|
||||
zipCodeCtu: "01526",
|
||||
};
|
||||
|
||||
const newServiceZipCodeInfo = {
|
||||
state: "OH",
|
||||
zipCode: "43081",
|
||||
zipCodeCtu: "01526",
|
||||
};
|
||||
|
||||
wrapper.vm.closeModalAction = jest.fn();
|
||||
// Act
|
||||
|
||||
// Trigger the event
|
||||
await mobileLocationQuestionsComponent.vm.$emit(
|
||||
"setMobileLocation",
|
||||
newMobileLocationQuestions
|
||||
);
|
||||
|
||||
await wrapper.vm.$nextTick(); // Wait for DOM updates
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.serviceZipCodeQuestion).toStrictEqual(newServiceZipCodeInfo);
|
||||
});
|
||||
|
||||
test("does not reset appointment type selection when service zip code is updated by mobile location modal when Mobile is selected", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
await wrapper.setData({
|
||||
shopProviderData: {
|
||||
mobileProviderNumber: "001820",
|
||||
},
|
||||
selectedAppointmentType: "Mobile",
|
||||
providerData: { mobileProviderNumber: "01820" },
|
||||
});
|
||||
|
||||
const mobileLocationQuestionsComponent = wrapper.findComponent({
|
||||
ref: "mobileLocationQuestions",
|
||||
});
|
||||
mobileLocationQuestionsComponent.resetComponent = jest.fn();
|
||||
wrapper.vm.forwardButtonAction = jest.fn();
|
||||
wrapper.vm.isServiceableMobile = true;
|
||||
|
||||
const serviceZipCodeComponent = wrapper.findComponent({
|
||||
ref: "serviceZipCodeQuestion",
|
||||
});
|
||||
serviceZipCodeComponent.resetMobileFeePart = jest.fn();
|
||||
|
||||
const mobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "555 Some Street",
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: "Westerville",
|
||||
state: "OH",
|
||||
zipCode: "43081",
|
||||
},
|
||||
isVehicleProtected: "YesAnswer",
|
||||
isMobileSelectedAndModalClosed: true,
|
||||
};
|
||||
|
||||
wrapper.vm.closeModalAction = jest.fn();
|
||||
// Act
|
||||
mobileLocationQuestionsComponent.vm.$emit("setMobileLocation", mobileLocationQuestions);
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.selectedAppointmentType).toStrictEqual("Mobile");
|
||||
});
|
||||
});
|
||||
|
||||
describe("serviceability logic", () => {
|
||||
describe("should be logical AND when recalibration is defined.", () => {
|
||||
test("T & T => T", async () => {
|
||||
|
|
@ -1434,8 +1199,5 @@ function setupMocks({ mountOptionsMockData = {} }) {
|
|||
const mountOptions = getMountOptions(mountOptionsMockData);
|
||||
const wrapper = shallowMount(serviceLocation, mountOptions);
|
||||
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
|
||||
wrapper.vm.$refs.mobileLocationQuestions.isMobileAddressComplete = jest.fn();
|
||||
wrapper.vm.$refs.mobileLocationQuestions.resetAlerts = jest.fn();
|
||||
wrapper.vm.$refs.mobileLocationQuestions.openModal = jest.fn();
|
||||
return { wrapper, apiPromise };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,24 +90,6 @@
|
|||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-xl-4">
|
||||
<mobileLocationModalQuestions
|
||||
customComponentId="mobileLocationQuestions"
|
||||
v-show="selectedAppointmentType === 'Mobile'"
|
||||
v-model="mobileLocationQuestions"
|
||||
:mobileFeePart="mobileFeePart"
|
||||
:mobileFeeApplies="mobileFeeApplies"
|
||||
@updated-mobile-fee-part="setMobileFeePart"
|
||||
@updated-recycle-fee-part="setRecycleFeePart"
|
||||
@updated-serviceability="setServiceabilityDetails"
|
||||
@updated-contains-military-base="setContainsMilitaryBase"
|
||||
@updated-mobile-ctu="setCtuForMobile"
|
||||
validationRules="mobile-location-required"
|
||||
ref="mobileLocationQuestions"
|
||||
linkWidgetName="MobileLocationLinkWidget"
|
||||
modalWidgetName="MobileLocationModalWidget"
|
||||
@setMobileLocation="setMobileLocationQuestions"
|
||||
@set-mobile-location-invalid="setMobileLocationInValid" />
|
||||
|
||||
<button-question
|
||||
v-if="selectedAppointmentType === 'Inshop'"
|
||||
class="shop-question-button"
|
||||
|
|
@ -121,7 +103,14 @@
|
|||
:buttonTypeObject="shopListButton"
|
||||
groupName="chooseShop"
|
||||
textPosition="text-start" />
|
||||
|
||||
<div class="text-center">
|
||||
<textBlock
|
||||
v-if="mobileFeeApplies && isMobileSelected"
|
||||
:customText="mobileFeeText"
|
||||
cmsWidgetName="MobileFeeDisclaimerWidget"
|
||||
typeStyle="caption"
|
||||
class="ps-4 pe-4 mt-4" />
|
||||
</div>
|
||||
<shopQuestionPopup
|
||||
modalWidgetName="ShopQuestionPopupWidget"
|
||||
:selectedAppointmentType="selectedAppointmentType"
|
||||
|
|
@ -159,7 +148,6 @@
|
|||
// Components
|
||||
import alert from "@/ux-components/alert/alert";
|
||||
import serviceZipModalQuestion from "@/layouts/service-location/service-zip-modal-question/service-zip-modal-question";
|
||||
import mobileLocationModalQuestions from "@/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions";
|
||||
import appointmentTypeQuestion from "@/layouts/service-location/appointment-type-question/appointment-type-question";
|
||||
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
|
||||
import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup";
|
||||
|
|
@ -173,6 +161,7 @@ import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-heade
|
|||
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
||||
import { Form } from "vee-validate";
|
||||
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
|
||||
// Supporting files
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
|
|
@ -331,20 +320,8 @@ export default {
|
|||
this.$nextTick();
|
||||
},
|
||||
},
|
||||
mobileLocationQuestions: {
|
||||
get: function () {
|
||||
return {
|
||||
addressQuestions: {
|
||||
streetAddress: this.streetAddress,
|
||||
apartmentNumberOrBusinessName: this.apartmentNumberOrBusinessName,
|
||||
city: this.city,
|
||||
state: this.state,
|
||||
zipCode: this.zipCode,
|
||||
},
|
||||
isVehicleProtected: this.isVehicleProtected,
|
||||
isMobileSelected: this.selectedAppointmentType == AppointmentTypeStrings.MOBILE,
|
||||
};
|
||||
},
|
||||
isMobileSelected() {
|
||||
return this.selectedAppointmentType == AppointmentTypeStrings.MOBILE;
|
||||
},
|
||||
isServiceableMobile() {
|
||||
if (this.isRecalibrationServiceableMobile !== null) {
|
||||
|
|
@ -387,6 +364,20 @@ export default {
|
|||
return this.mobileFeeHasPrice;
|
||||
}
|
||||
},
|
||||
mobileFeeText() {
|
||||
const cmsContentText = this.getCmsContent("MobileFeeDisclaimerWidget", "Text");
|
||||
return cmsContentText.replaceAll("{custom:mobileFee}", this.mobileFee);
|
||||
},
|
||||
mobileFee() {
|
||||
if (!this.mobileFeePart) {
|
||||
return 0;
|
||||
}
|
||||
return (
|
||||
this.mobileFeePart.laborAmount +
|
||||
this.mobileFeePart.sellingPrice +
|
||||
this.mobileFeePart.kitPrice
|
||||
);
|
||||
},
|
||||
showMobileFreeAlert() {
|
||||
if (this.isServiceableMobile && !this.isInsurance && !this.mobileFeeHasPrice) {
|
||||
return true;
|
||||
|
|
@ -594,32 +585,6 @@ export default {
|
|||
setRecycleFeePart(recycleFeePart) {
|
||||
this.recycleFeePart = recycleFeePart;
|
||||
},
|
||||
//creating async function as the computed property can not directly handle asynchronous operations or promises.
|
||||
async setMobileLocationQuestions(newValue) {
|
||||
var newZipCode = newValue.addressQuestions.zipCode;
|
||||
if (newZipCode !== this.zipCode) {
|
||||
await getShopProviderData(newZipCode).then((result) => {
|
||||
if (this.isServiceableMobile) {
|
||||
this.shopProviderData = result.data;
|
||||
this.selectedProvider = new Provider(
|
||||
this.shopProviderData.mobileProviderNumber
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (this.isServiceableMobile) {
|
||||
this.setMobileLocation(newValue);
|
||||
this.setMobileLocationInValid(false);
|
||||
} else {
|
||||
await getZipCodeData(newZipCode).then((zipCodeData) => {
|
||||
this.serviceZipCodeQuestion = {
|
||||
state: zipCodeData.state,
|
||||
zipCode: newZipCode,
|
||||
zipCodeCtu: zipCodeData.zipCodeCtu,
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
getCarIdfromStore() {
|
||||
return store.getters.vehicle.carId;
|
||||
},
|
||||
|
|
@ -668,15 +633,6 @@ export default {
|
|||
this.isRecalibrationServiceableMobile =
|
||||
serviceabilityDetails.isRecalibrationServiceableMobile;
|
||||
},
|
||||
setMobileLocation(mobileLocation) {
|
||||
this.streetAddress = mobileLocation.addressQuestions.streetAddress;
|
||||
this.apartmentNumberOrBusinessName =
|
||||
mobileLocation.addressQuestions.apartmentNumberOrBusinessName;
|
||||
this.city = mobileLocation.addressQuestions.city;
|
||||
this.state = mobileLocation.addressQuestions.state;
|
||||
this.zipCode = mobileLocation.addressQuestions.zipCode;
|
||||
this.isVehicleProtected = mobileLocation.isVehicleProtected;
|
||||
},
|
||||
async reloadShopData(zipCode) {
|
||||
await this.$refs.shopQuestion.reloadShopData(zipCode);
|
||||
},
|
||||
|
|
@ -831,13 +787,49 @@ export default {
|
|||
|
||||
this.updateAndSaveSupportingItems();
|
||||
this.updateAndSaveIsMSRFeeApplicable();
|
||||
this.$router.navigateWithSaving(
|
||||
this.navigationScenarios.CLICKED_FORWARD,
|
||||
this.pageName
|
||||
|
||||
//mocked schedule and should be clear when implement schedule page.
|
||||
this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_SCHEDULE,
|
||||
this.getTimeSlotInfo(),
|
||||
false
|
||||
);
|
||||
|
||||
if (this.selectedAppointmentType == AppointmentTypeStrings.MOBILE) {
|
||||
//navigate to mobile details page
|
||||
this.$router.navigateWithSaving(
|
||||
this.navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE,
|
||||
this.pageName
|
||||
);
|
||||
} else {
|
||||
this.$router.navigateWithSaving(
|
||||
this.navigationScenarios.CLICKED_FORWARD,
|
||||
this.pageName
|
||||
);
|
||||
}
|
||||
},
|
||||
setMobileLocationInValid(isMobileLocationInValid) {
|
||||
this.isMobileAddressValid = !isMobileLocationInValid;
|
||||
getTimeSlotInfo() {
|
||||
// Get the current date
|
||||
const currentDate = new Date();
|
||||
|
||||
// Add 10 days to the current date
|
||||
currentDate.setDate(currentDate.getDate() + 10);
|
||||
|
||||
// Format the date as yyyy-mm-dd
|
||||
const year = currentDate.getFullYear();
|
||||
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(currentDate.getDate()).padStart(2, "0");
|
||||
|
||||
const formattedDate = `${year}-${month}-${day}`;
|
||||
|
||||
return {
|
||||
date: formattedDate,
|
||||
endTime: "12:00",
|
||||
jobMaxMinutes: "180",
|
||||
jobMinMinutes: "120",
|
||||
routeCode: "3357I-03357-M-I*20988*AM",
|
||||
startTime: "08:00",
|
||||
};
|
||||
},
|
||||
onShopSelected(providerObject) {
|
||||
const current = this.shopProviderData?.shopProviders?.find(
|
||||
|
|
@ -917,16 +909,6 @@ export default {
|
|||
this.selectedProvider = new Provider(
|
||||
this.shopProviderData.mobileProviderNumber
|
||||
);
|
||||
if (this.zipCode == this.getServiceZipCodeFromStore()) {
|
||||
//restore mobile address from store in case user make changes to address but not commit it.
|
||||
this.streetAddress = this.getServiceAddressFromStore();
|
||||
this.apartmentNumberOrBusinessName = this.getServiceAddress2FromStore();
|
||||
this.city = this.getServiceCityFromStore();
|
||||
this.isVehicleProtected = this.getIsVehicleProtectedFromStore();
|
||||
} else {
|
||||
this.resetMobileLocation();
|
||||
}
|
||||
this.$refs.mobileLocationQuestions.resetAlerts();
|
||||
} else {
|
||||
this.selectedProvider = this.shopProviderData.shopProviders[0];
|
||||
}
|
||||
|
|
@ -937,7 +919,6 @@ export default {
|
|||
alert,
|
||||
serviceZipModalQuestion,
|
||||
appointmentTypeQuestion,
|
||||
mobileLocationModalQuestions,
|
||||
funnelHeader,
|
||||
navbar,
|
||||
funnelSubHeader,
|
||||
|
|
@ -946,6 +927,7 @@ export default {
|
|||
contentGroupModal,
|
||||
shopQuestionPopup,
|
||||
buttonQuestion,
|
||||
textBlock,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -96,7 +96,6 @@ import { saveSession } from "@/helpers/heritage-integration/order-helper.js";
|
|||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
import { routeData } from "@/router/constants/routes";
|
||||
import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stash";
|
||||
|
||||
// Define Validation Rules
|
||||
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
|
||||
|
|
@ -116,7 +115,7 @@ export default {
|
|||
mixins: [vinPagesMixin],
|
||||
data() {
|
||||
return {
|
||||
serviceZipCode: this.getZipFromStore() ?? consumeQueryFromStash(queryStrings.ZIP_CODE),
|
||||
serviceZipCode: this.getZipFromStore(),
|
||||
emailOrSms: this.getEmailOrSmsFromStore(),
|
||||
carId: this.getCarIdfromStore(),
|
||||
isHeavyTruck: this.getIsVehicleHeavyTruckServiceableFromStore(),
|
||||
|
|
|
|||
|
|
@ -110,10 +110,6 @@ import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
|
|||
import { applicationConfig } from "../../constants/application-config";
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
import { getQuerystringParameter } from "@/helpers/querystring-helper";
|
||||
import {
|
||||
consumeQueryFromStash,
|
||||
peekQueryFromStash,
|
||||
} from "@/router/methods/helpers/querystring-stash";
|
||||
import { debugLog } from "@/helpers/debug-log-helper";
|
||||
|
||||
//define validation rules
|
||||
|
|
@ -138,7 +134,7 @@ export default {
|
|||
imageUrl: this.getImagefromStore(),
|
||||
imageVifNumber: this.getImageVifNumberfromStore(),
|
||||
imageVifColor: this.getImageVifColorfromStore(),
|
||||
serviceZipCode: this.getZipFromStore() ?? peekQueryFromStash(queryStrings.ZIP_CODE),
|
||||
serviceZipCode: this.getZipFromStore(),
|
||||
yearOptions: [],
|
||||
makeOptions: [],
|
||||
modelOptions: [],
|
||||
|
|
|
|||
|
|
@ -159,8 +159,6 @@ import baseMixin from "@/mixins/base-mixin.js";
|
|||
import store from "@/store";
|
||||
import vinPagesMixin from "@/mixins/vin-pages-mixin";
|
||||
import { routeData } from "@/router/constants/routes";
|
||||
import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stash";
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
|
||||
|
|
@ -204,7 +202,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
vin: this.getVinFromStore(),
|
||||
serviceZipCode: this.getZipFromStore() ?? consumeQueryFromStash(queryStrings.ZIP_CODE),
|
||||
serviceZipCode: this.getZipFromStore(),
|
||||
emailOrSms: this.getEmailOrSmsFromStore(),
|
||||
carId: this.getCarIdfromStore(),
|
||||
isHeavyTruck: this.getIsVehicleHeavyTruckServiceableFromStore(),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import {
|
|||
setSessionKeyIfUnset,
|
||||
} from "@/helpers/heritage-integration/cookie-helper";
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
import { experimentSettings } from "@/constants/experiments";
|
||||
import { experimentSettings, experimentUniverses } from "@/constants/experiments";
|
||||
import experimentMixin from "@/mixins/experiment-mixin";
|
||||
import {
|
||||
analyticsPageEvents,
|
||||
GaCategories,
|
||||
|
|
@ -128,6 +129,60 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
async logDigitalConsumer() {
|
||||
const currentPageName = getPageNameFromRouter();
|
||||
const universes = store.getters.applicationUser.experiments;
|
||||
|
||||
let hasDynamoLogging = experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.DYNAMO_LOGGING,
|
||||
"true",
|
||||
universes
|
||||
);
|
||||
if (!hasDynamoLogging) {
|
||||
return;
|
||||
}
|
||||
|
||||
const variationNames = universes
|
||||
.filter((item) => item.universeName === experimentUniverses.CONCEPT_FUNNEL)
|
||||
.map((item) => item.variationName)
|
||||
.filter(Boolean); // removes undefined/null
|
||||
|
||||
const conceptVariation = variationNames.length > 0 ? variationNames[0] : "";
|
||||
|
||||
const isConceptExposed = universes.find(
|
||||
(item) => item.universeName === experimentUniverses.CONCEPT_FUNNEL
|
||||
)?.isExposed;
|
||||
|
||||
const submittedOrder = baseMixin.methods.getSubmittedOrder();
|
||||
const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder();
|
||||
const hasSubmittedOrderAtConfirmationPage =
|
||||
hasSubmittedOrder && currentPageName?.toLowerCase() == routeData.CONFIRMATION.name;
|
||||
|
||||
var payload = {
|
||||
actionName: `Browser page:${currentPageName}`,
|
||||
referralSequenceNumber: hasSubmittedOrderAtConfirmationPage
|
||||
? submittedOrder.referralSequenceNumber
|
||||
: store.getters.order.referralSequenceNumber,
|
||||
referralNumber: hasSubmittedOrderAtConfirmationPage
|
||||
? submittedOrder.referralNumber
|
||||
: store.getters.order.referralNumber,
|
||||
workOrderId: hasSubmittedOrderAtConfirmationPage
|
||||
? submittedOrder.workOrderId
|
||||
: store.getters.order.workOrderId,
|
||||
workOrderNumber: hasSubmittedOrderAtConfirmationPage
|
||||
? submittedOrder.workOrderNumber
|
||||
: store.getters.order.workOrderNumber,
|
||||
conceptVariation: conceptVariation,
|
||||
isConceptExposed: isConceptExposed,
|
||||
};
|
||||
|
||||
await baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.LOG_DIGITALCONSUMER,
|
||||
payload,
|
||||
false
|
||||
);
|
||||
},
|
||||
|
||||
async pushEventForChatsToGA(category, action, label, pushToLogApp = false) {
|
||||
const currentPageName = getPageNameFromRouter();
|
||||
const value = `2.0_${currentPageName}`;
|
||||
|
|
|
|||
|
|
@ -197,6 +197,18 @@ export default {
|
|||
return [parseInt(v[1], 10), parseInt(v[2], 10), parseInt(v[3] || 0, 10)];
|
||||
}
|
||||
},
|
||||
isMobileDevice() {
|
||||
const userAgent = navigator.userAgent;
|
||||
return (
|
||||
userAgent.includes("Android") ||
|
||||
userAgent.includes("Mobile") ||
|
||||
userAgent.includes("iPod") ||
|
||||
userAgent.includes("iPhone") ||
|
||||
userAgent.includes("IEMobile") ||
|
||||
userAgent.includes("BlackBerry") ||
|
||||
userAgent.includes("webOS")
|
||||
);
|
||||
},
|
||||
getSubmittedOrder() {
|
||||
return JSON.parse(
|
||||
window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE)
|
||||
|
|
|
|||
|
|
@ -49,6 +49,12 @@ const navigationScenarios = {
|
|||
// Insurance-Company
|
||||
CLICKED_PAY_ON_MY_OWN: "CLICKED_PAY_ON_MY_OWN",
|
||||
|
||||
//Service-Location
|
||||
CLICKED_FORWARD_WITH_MOBILE_SERVICE: "CLICKED_FORWARD_WITH_MOBILE_SERVICE",
|
||||
|
||||
//Customer-details
|
||||
CLICKED_BACK_WITH_MOBILE_SERVICE: "CLICKED_BACK_WITH_MOBILE_SERVICE",
|
||||
|
||||
// Schedule
|
||||
CLICKED_CHANGE_LOCATION: "CLICKED_CHANGE_LOCATION",
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,10 @@ export const routeData = {
|
|||
name: "schedule",
|
||||
path: "/schedule",
|
||||
},
|
||||
MOBILE_DETAILS: {
|
||||
name: "mobile-details",
|
||||
path: "/mobile-details",
|
||||
},
|
||||
CUSTOMER_DETAILS: {
|
||||
name: "customer-details",
|
||||
path: "/customer-details",
|
||||
|
|
|
|||
|
|
@ -460,6 +460,10 @@ const routingTable = function () {
|
|||
scenario: navigationScenarios.CLICKED_FORWARD,
|
||||
destinationPageData: routeData.SCHEDULE,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE,
|
||||
destinationPageData: routeData.MOBILE_DETAILS,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -479,6 +483,19 @@ const routingTable = function () {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pageName: routeData.MOBILE_DETAILS.name,
|
||||
maps: [
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_BACK,
|
||||
destinationPageData: routeData.SERVICE_LOCATION,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_FORWARD,
|
||||
destinationPageData: routeData.CUSTOMER_DETAILS,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pageName: routeData.CUSTOMER_DETAILS.name,
|
||||
maps: [
|
||||
|
|
@ -486,6 +503,10 @@ const routingTable = function () {
|
|||
scenario: navigationScenarios.CLICKED_BACK,
|
||||
destinationPageData: routeData.SCHEDULE,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_BACK_WITH_MOBILE_SERVICE,
|
||||
destinationPageData: routeData.MOBILE_DETAILS,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_FORWARD,
|
||||
destinationPageData: routeData.PAYMENT_METHOD,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import analyticsMixin from "@/mixins/analytics-mixin";
|
||||
|
||||
export async function afterEach(to, from) {
|
||||
// digital consumer logging
|
||||
analyticsMixin.methods.logDigitalConsumer();
|
||||
|
||||
// Push page view to GA
|
||||
analyticsMixin.methods.pushPageViewToGA();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
|
|||
import { routeData } from "@/router/constants/routes";
|
||||
import { isVerifiedInsurance } from "@/router/methods/helpers/requires-verified-redirect";
|
||||
import store from "@/store";
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
import baseMixin from "@/mixins/base-mixin";
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stash";
|
||||
|
||||
export async function vehicleBeforeEnter(to, from) {
|
||||
// If cookie indicates that vehicle has changed during insurance flow, reset parts info.
|
||||
|
|
@ -12,6 +16,23 @@ export async function vehicleBeforeEnter(to, from) {
|
|||
store.commit(storeMutations.RESET_GLASS_PARTS_STATE);
|
||||
}
|
||||
|
||||
// If there is a zip querystring, consume & commit it here.
|
||||
const newZipFromQuerystring = consumeQueryFromStash(queryStrings.ZIP_CODE);
|
||||
if (newZipFromQuerystring) {
|
||||
const zipData = await baseMixin.methods.getZipCodeData(
|
||||
newZipFromQuerystring,
|
||||
routeData.VEHICLE.name
|
||||
);
|
||||
|
||||
if (zipData.isValid) {
|
||||
store.dispatch(storeActions.SAVE_SERVICE_ZIP_CODE_INFO, {
|
||||
zipCode: newZipFromQuerystring,
|
||||
state: zipData.state.state,
|
||||
zipCodeCtu: zipData.zipCodeCtu,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isVerifiedInsurance()) {
|
||||
return {
|
||||
name: routeData.VEHICLE_DAMAGE.name,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export const routes = [
|
|||
createRoute(routeData.PAYMENT_PIA_RETURN),
|
||||
createRoute(routeData.CONFIRMATION),
|
||||
createRoute(routeData.RETURN_USER),
|
||||
createRoute(routeData.MOBILE_DETAILS),
|
||||
|
||||
// Virtual pages (resolve to a non-virtual page.)
|
||||
createVirtualRoute(routeData.LANDING, landingBeforeEnter),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ import { experimentTriggers } from "@/constants/experiments";
|
|||
import { damageLocationsSelected } from "@/constants/damage-locations-selected";
|
||||
import { singleWindshieldCarIds } from "@/constants/single-windshield-carids";
|
||||
import { routeData } from "@/router/constants/routes";
|
||||
import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper.js";
|
||||
import {
|
||||
deleteFunnelCookie,
|
||||
getSessionIdValue,
|
||||
getDeviceIdValue,
|
||||
} from "@/helpers/heritage-integration/cookie-helper.js";
|
||||
import { deepEqual } from "@/helpers/object-helper";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import {
|
||||
|
|
@ -421,6 +425,12 @@ export const mutations = {
|
|||
},
|
||||
};
|
||||
},
|
||||
updateMobileDetails(state, mobileDetails) {
|
||||
state.order.serviceLocation.address = mobileDetails.address;
|
||||
state.order.serviceLocation.address2 = mobileDetails.address2;
|
||||
state.order.serviceLocation.city = mobileDetails.city;
|
||||
state.order.serviceLocation.isVehicleProtected = mobileDetails.isVehicleProtected;
|
||||
},
|
||||
updateServiceLocationTechNotes(state, techNotes) {
|
||||
state.order.serviceLocation.techNotes = techNotes;
|
||||
},
|
||||
|
|
@ -1551,6 +1561,38 @@ export const actions = {
|
|||
});
|
||||
},
|
||||
|
||||
logDigitalConsumer(
|
||||
context,
|
||||
{
|
||||
actionName,
|
||||
referralSequenceNumber,
|
||||
referralNumber,
|
||||
workOrderId,
|
||||
workOrderNumber,
|
||||
conceptVariation,
|
||||
isConceptExposed,
|
||||
}
|
||||
) {
|
||||
var payload = {
|
||||
sessionId: getSessionIdValue(),
|
||||
deviceId: getDeviceIdValue(),
|
||||
actionName: actionName ?? "",
|
||||
referralSequenceNumber: referralSequenceNumber ?? "",
|
||||
referralNumber: referralNumber ?? "",
|
||||
applicationName: baseMixin.methods.isMobileDevice() ? "2.0 Mobile" : "2.0",
|
||||
workOrderId: workOrderId ?? "",
|
||||
workOrderNumber: workOrderNumber ?? "",
|
||||
conceptVariation: conceptVariation,
|
||||
isConceptExposed: isConceptExposed,
|
||||
};
|
||||
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.LogDigitalConsumer.method,
|
||||
endpoint: endpoints.LogDigitalConsumer.url,
|
||||
payload: payload,
|
||||
});
|
||||
},
|
||||
|
||||
// Misc Actions
|
||||
setReferralInformation(context, { referralNumber, referralDate, referralCorrelationId, eon }) {
|
||||
context.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber);
|
||||
|
|
@ -3204,6 +3246,9 @@ export const actions = {
|
|||
|
||||
context.commit(storeMutations.UPDATE_SERVICE_LOCATION, serviceLocationInfo);
|
||||
},
|
||||
saveMobileDetails(context, mobileDetails) {
|
||||
context.commit(storeMutations.UPDATE_MOBILE_DETAILS, mobileDetails);
|
||||
},
|
||||
|
||||
saveServiceLocationTechNotes(context, techNotesInfo) {
|
||||
context.commit(storeMutations.UPDATE_SERVICE_LOCATION_TECH_NOTES, techNotesInfo);
|
||||
|
|
|
|||
|
|
@ -97,3 +97,11 @@ body {
|
|||
}
|
||||
}
|
||||
}
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
option,
|
||||
.btn {
|
||||
letter-spacing: 0.03rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ body {
|
|||
font-size: 1rem;
|
||||
line-height: 1.625;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.03rem;
|
||||
}
|
||||
|
||||
//Headings
|
||||
|
|
@ -43,7 +44,7 @@ h5,
|
|||
h6,
|
||||
.h6 {
|
||||
line-height: 1.7;
|
||||
letter-spacing: 0.75px;
|
||||
letter-spacing: 0.03rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
|
|
@ -78,58 +79,58 @@ caption,
|
|||
font-weight: 500;
|
||||
}
|
||||
|
||||
// Averta Fonts
|
||||
// Urbanist Fonts
|
||||
|
||||
@font-face {
|
||||
font-family: "AvertaBold";
|
||||
font-family: "UrbanistRegular";
|
||||
src:
|
||||
url("@/assets/fonts/averta-bold-webfont.woff2") format("woff2"),
|
||||
url("@/assets/fonts/averta-bold-webfont.woff") format("woff");
|
||||
url("@/assets/fonts/Urbanist-Regular.woff") format("woff2"),
|
||||
url("@/assets/fonts/Urbanist-Regular.woff2") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AvertaBold_Italic";
|
||||
font-family: "UrbanistSemibold";
|
||||
src:
|
||||
url("@/assets/fonts/averta-bolditalic-webfont.woff2") format("woff2"),
|
||||
url("@/assets/fonts/averta-bolditalic-webfont.woff") format("woff");
|
||||
url("@/assets/fonts/Urbanist-SemiBold.woff") format("woff2"),
|
||||
url("@/assets/fonts/Urbanist-SemiBold.woff2") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AvertaExtraBold";
|
||||
font-family: "UrbanistBold";
|
||||
src:
|
||||
url("@/assets/fonts/averta-extrabold-webfont.woff2") format("woff2"),
|
||||
url("@/assets/fonts/averta-extrabold-webfont.woff") format("woff");
|
||||
url("@/assets/fonts/Urbanist-Bold.woff") format("woff2"),
|
||||
url("@/assets/fonts/Urbanist-Bold.woff2") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AvertaRegular";
|
||||
font-family: "UrbanistExtraBold";
|
||||
src:
|
||||
url("@/assets/fonts/averta-regular-webfont.woff2") format("woff2"),
|
||||
url("@/assets/fonts/averta-regular-webfont.woff") format("woff");
|
||||
url("@/assets/fonts/Urbanist-ExtraBold.woff") format("woff2"),
|
||||
url("@/assets/fonts/Urbanist-ExtraBold.woff2") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AvertaRegular_Italic";
|
||||
font-family: "UrbanistMedium_Italic";
|
||||
src:
|
||||
url("@/assets/fonts/averta-regularitalic-webfont.woff2") format("woff2"),
|
||||
url("@/assets/fonts/averta-regularitalic-webfont.woff") format("woff");
|
||||
url("@/assets/fonts/Urbanist-MediumItalic.woff") format("woff2"),
|
||||
url("@/assets/fonts/Urbanist-MediumItalic.woff2") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AvertaSemibold";
|
||||
font-family: "UrbanistBold_Italic";
|
||||
src:
|
||||
url("@/assets/fonts/averta-semibold-webfont.woff2") format("woff2"),
|
||||
url("@/assets/fonts/averta-semibold-webfont.woff") format("woff");
|
||||
url("@/assets/fonts/Urbanist-BoldItalic.woff") format("woff2"),
|
||||
url("@/assets/fonts/Urbanist-BoldItalic.woff2") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ $black-100: #0a0a0a;
|
|||
|
||||
// Blues
|
||||
$blue-100: #e4f1f7; // Used in theme
|
||||
$blue-150: #e5f1fa;
|
||||
$blue-200: #c1dfee;
|
||||
$blue-300: #9fcee6;
|
||||
$blue-400: #69adcf; // Used in theme
|
||||
|
|
@ -59,6 +60,7 @@ $gray: #b0b3b3; // Default Gray
|
|||
$gray-500: #8e9292; // Used in theme
|
||||
$gray-550: #727676; // Used in theme
|
||||
$gray-600: #4d5151; // Used in theme
|
||||
$gray-650: #525656;
|
||||
$gray-700: #303333; // Used in theme
|
||||
$gray-800: #222424; // Used in theme
|
||||
$gray-900: #181a1a; // Used in theme
|
||||
|
|
@ -120,14 +122,17 @@ $theme-colors: (
|
|||
$body-color: $gray-600;
|
||||
|
||||
//Fonts
|
||||
$font-family-sans-serif: AvertaRegular, Arial, Helvetica, sans-serif;
|
||||
$font-family-sans-serif: UrbanistRegular, Arial, Helvetica, sans-serif;
|
||||
$font-family-monospace:
|
||||
SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
// stylelint-enable value-keyword-case
|
||||
$font-family-base: $font-family-sans-serif;
|
||||
$font-family-code: $font-family-monospace;
|
||||
$font-size-base: 1rem; // Assumes the browser default, typically `16px`
|
||||
|
||||
$font-size-12: $font-size-base * 0.75; // 12px
|
||||
$font-size-20: $font-size-base * 1.25; // 20px
|
||||
|
||||
//Custom Font size (extra small)
|
||||
$font-size-xsm: $font-size-base * 0.75;
|
||||
$font-sizes: (
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ export default {
|
|||
justify-content: flex-start;
|
||||
|
||||
p {
|
||||
font-family: UrbanistRegular;
|
||||
color: $gray-600;
|
||||
text-align: center;
|
||||
|
||||
|
|
@ -123,15 +124,16 @@ export default {
|
|||
&:checked + .list-card-content {
|
||||
p {
|
||||
color: $black;
|
||||
font-weight: 500;
|
||||
font-family: UrbanistSemibold;
|
||||
}
|
||||
.p-bold {
|
||||
font-family: UrbanistSemibold;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sub-copy {
|
||||
font-family: UrbanistSemibold;
|
||||
color: $gray-600;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue