1331 lines
50 KiB
Vue
1331 lines
50 KiB
Vue
<template>
|
|
<div
|
|
class="date-picker text-center"
|
|
:class="`${calendarViewDirection} calendar-2025 ${showPricingByDayClass}`">
|
|
<fieldset id="date-picker-fieldset" ref="datePickerFieldset">
|
|
<legend class="sr-only">Select a day and time</legend>
|
|
<div
|
|
v-for="month in months"
|
|
:key="`${month.monthLabel}-${month.yearNum?.toString()}`"
|
|
:id="`${month.monthLabel}-${month.yearNum?.toString()}`"
|
|
class="calendar-grid-container"
|
|
:class="[
|
|
hideSomeDaysForInitialView ? 'partial-month-initial-view' : '',
|
|
month.monthClass,
|
|
]">
|
|
<div class="month-year body-small d-flex align-items-center">
|
|
{{ month.monthLabel }} {{ month.yearNum?.toString() }}
|
|
</div>
|
|
<div
|
|
v-if="calendarViewDirection === 'future'"
|
|
class="legend caption d-flex align-items-center justify-content-end">
|
|
<span class="legend-circle me-1"></span>
|
|
= Available
|
|
</div>
|
|
<div class="nav-back ps-3"><button></button></div>
|
|
<div class="nav-forward pe-3"><button></button></div>
|
|
<div class="grid-item caption">
|
|
<span class="sr-only">Sunday</span>
|
|
SUN
|
|
</div>
|
|
<div class="grid-item caption">
|
|
<span class="sr-only">Monday</span>
|
|
MON
|
|
</div>
|
|
<div class="grid-item caption">
|
|
<span class="sr-only">Tuesday</span>
|
|
TUE
|
|
</div>
|
|
<div class="grid-item caption">
|
|
<span class="sr-only">Wednesday</span>
|
|
WED
|
|
</div>
|
|
<div class="grid-item caption">
|
|
<span class="sr-only">Thursday</span>
|
|
THU
|
|
</div>
|
|
<div class="grid-item caption">
|
|
<span class="sr-only">Friday</span>
|
|
FRI
|
|
</div>
|
|
<div class="grid-item caption">
|
|
<span class="sr-only">Saturday</span>
|
|
SAT
|
|
</div>
|
|
<div
|
|
v-for="date in month.dates"
|
|
:key="date.inputValue"
|
|
:id="date.inputValue"
|
|
class="grid-item radio-wrapper"
|
|
:class="[
|
|
date.dateNum === 1 ? 'first-day-' + month.startDateDayIndex : '',
|
|
date.dayClasses,
|
|
date.isSelectable ? 'selectable-day' : '',
|
|
date.isPricingByDayUpchargeDay ? 'upch-day' : '',
|
|
]">
|
|
<input
|
|
:disabled="!date.isSelectable"
|
|
type="radio"
|
|
name="day-of-month"
|
|
v-model="selectedDate"
|
|
@click="fireDateSelectedEvent($event, date)"
|
|
@keypress.enter="fireDateSelectedEvent($event, date)"
|
|
:value="date.inputValue"
|
|
:id="`${month.monthLabel}-${date.dateNum.toString()}`" />
|
|
<label :for="`${month.monthLabel}-${date.dateNum.toString()}`">
|
|
<span>{{ date.dateNum.toString() }}</span>
|
|
<span
|
|
v-if="isPricingByDayExperiment && showPricingByDay && date.isSelectable"
|
|
class="price">
|
|
{{ date.priceString }}
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<loader
|
|
:class="[!isLoading ? 'date-picker-hidden' : '']"
|
|
loaderColor="blue"
|
|
loaderPosition="center" />
|
|
<div class="row form-test-error" id="date-of-month-error">
|
|
<ErrorMessage :name="customComponentId" class="small mt-1"></ErrorMessage>
|
|
</div>
|
|
<button
|
|
v-if="calendarViewDirection === 'future' && !disableViewMoreDatesButton"
|
|
type="button"
|
|
class="btn btn-link view-more-dates"
|
|
id="viewMoreDates"
|
|
:disabled="isLoading"
|
|
@click="showAnotherMonth">
|
|
View more dates
|
|
</button>
|
|
<timeSlotQuestion
|
|
ref="timeSlotModalQuestion"
|
|
v-model="selectedTimeSlotInfo"
|
|
cmsWidgetName="TimeSlotModalQuestion"
|
|
waitListLabelWidget="WaitListLabelWidget"
|
|
waitListQuestionWidget="WaitListQuestionWidget"
|
|
mobilePremiumCmsWidgetName="MobilePremiumTimeSlotModal"
|
|
mobileCmsWidgetName="MobileTimeSlotModal"
|
|
dropoffCmsWidgetName="DropOffTimeSlotModal"
|
|
sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal"
|
|
overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal"
|
|
:selectedDate="selectedDate"
|
|
:appointmentType="appointmentType"
|
|
:premiumAppointmentFee="premiumAppointmentFee"
|
|
:displayWaitList="displayWaitList"
|
|
:timeSlotsForSelectedDate="timeSlotsForSelectedDate"
|
|
:estimatedServiceMinutesMinimum="estimatedServiceMinutesMinimum"
|
|
:estimatedServiceMinutesMaximum="estimatedServiceMinutesMaximum"
|
|
@waitListRequested="handleWaitListRequested"
|
|
@time-slot-modal-closed="timeSlotModalClosed" />
|
|
</fieldset>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue";
|
|
|
|
// Supporting files
|
|
import loader from "@/ux-components/loader/loader";
|
|
import store from "@/store";
|
|
import {
|
|
TIMINGFUNC_MAP,
|
|
BUFFER_OFFSET,
|
|
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 {
|
|
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 { deepClone } from "@/helpers/object-helper";
|
|
import { v4 as uuidv4 } from "uuid";
|
|
|
|
export default {
|
|
name: "datePicker",
|
|
data() {
|
|
return {
|
|
isLoading: true,
|
|
months: null,
|
|
disableViewMoreDatesButton: false,
|
|
selectableDatesData: [], // NOTE: uses monthNum (1-based), NOT monthIndex (0-based)
|
|
hideSomeDaysForInitialView: null,
|
|
|
|
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
|
|
};
|
|
},
|
|
props: {
|
|
customComponentId: String,
|
|
selectableDatesSetting: {
|
|
type: String,
|
|
validator(value) {
|
|
return Object.values(selectableDaysOptions).includes(value);
|
|
},
|
|
default: selectableDaysOptions.PAST,
|
|
},
|
|
modelValue: {
|
|
type: Object,
|
|
},
|
|
todayOverrideDateString: {
|
|
// keep for use in unit tests to override today's date
|
|
type: String,
|
|
default: null,
|
|
},
|
|
customSelectableDatesCallback: {
|
|
type: Function,
|
|
default() {
|
|
return [];
|
|
},
|
|
},
|
|
validationRules: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
showPricingByDay: Boolean,
|
|
pricingByDayBasePrice: Number,
|
|
pricingByDayUpcharge: Number,
|
|
isPricingByDayExperiment: Boolean,
|
|
timeSlotsForSelectedDate: Object,
|
|
appointmentType: String,
|
|
premiumAppointmentFee: Object,
|
|
estimatedServiceMinutesMinimum: Number,
|
|
estimatedServiceMinutesMaximum: Number,
|
|
displayWaitList: Boolean,
|
|
},
|
|
setup(props) {
|
|
const uuid = uuidv4();
|
|
const componentId = !props.customComponentId
|
|
? `component-${uuid}`
|
|
: props.customComponentId;
|
|
const modelValue = deepClone(props).modelValue;
|
|
const initialValue = modelValue;
|
|
const fieldOptions = {
|
|
value: modelValue,
|
|
initialValue: initialValue,
|
|
};
|
|
const { errorMessage, handleBlur, handleChange, meta, validate, errors, resetField } =
|
|
useField(componentId, props.validationRules, fieldOptions);
|
|
|
|
return {
|
|
componentId,
|
|
errorMessage,
|
|
handleBlur,
|
|
handleChange,
|
|
validate,
|
|
meta,
|
|
errors,
|
|
resetField,
|
|
};
|
|
},
|
|
computed: {
|
|
todayString() {
|
|
return this.todayOverrideDateString || convertDateToDateString(new Date());
|
|
},
|
|
todayDayIndex() {
|
|
return convertDateStringToDate(this.todayString).getDay();
|
|
},
|
|
todayDateNum() {
|
|
return convertDateStringToDate(this.todayString).getDate();
|
|
},
|
|
currentWeekStartDateNum() {
|
|
return this.todayDayIndex >= this.todayDateNum
|
|
? 1
|
|
: this.todayDateNum - this.todayDayIndex;
|
|
},
|
|
currentWeekEndDateNum() {
|
|
return this.todayDateNum + (6 - this.todayDayIndex);
|
|
},
|
|
calendarViewDirection() {
|
|
if (this.selectableDatesSetting === "custom") return "future";
|
|
return "past";
|
|
},
|
|
showPricingByDayClass() {
|
|
return this.showPricingByDay ? "show-pricing-by-day" : "";
|
|
},
|
|
selectedDate: {
|
|
get() {
|
|
return this.modelValue;
|
|
},
|
|
set(newSelectedDate) {
|
|
this.$emit("update:modelValue", newSelectedDate);
|
|
},
|
|
},
|
|
},
|
|
methods: {
|
|
initializeComponent(initialData) {
|
|
this.setCalendarData(initialData);
|
|
},
|
|
fireDateSelectedEvent(event, date) {
|
|
// Ignore if arrow key selected radioButton
|
|
if (event.screenX === 0 && event.screenY === 0) {
|
|
return;
|
|
}
|
|
this.$emit("date-clicked", date);
|
|
},
|
|
getWeekStartDate(dateString) {
|
|
const date = convertDateStringToDate(dateString);
|
|
const dayOfWeek = date.getDay();
|
|
// Subtract the day of the week from date to get the date of Sunday
|
|
const sunday = new Date(date);
|
|
sunday.setDate(sunday.getDate() - dayOfWeek);
|
|
return convertDateToDateString(sunday);
|
|
},
|
|
getWeekEndDate(dateString) {
|
|
const date = convertDateStringToDate(dateString);
|
|
const dayOfWeek = date.getDay();
|
|
const daysUntilSaturday = 6 - dayOfWeek; // Calculate the number of days until Saturday
|
|
// Clone the given date and add the remaining days until Saturday
|
|
const saturday = new Date(date);
|
|
saturday.setDate(date.getDate() + daysUntilSaturday);
|
|
return convertDateToDateString(saturday);
|
|
},
|
|
getNextWeekSunday(dateString) {
|
|
const date = convertDateStringToDate(dateString);
|
|
const dayOfWeek = date.getDay();
|
|
const daysUntilNextSunday = 7 - dayOfWeek; // Calculate the number of days until the next Sunday
|
|
// Clone the given date and add the remaining days until Sunday
|
|
const nextSunday = new Date(date);
|
|
nextSunday.setDate(date.getDate() + daysUntilNextSunday);
|
|
return convertDateToDateString(nextSunday);
|
|
},
|
|
getMonthEnd(dateStr) {
|
|
// convert string to date, do date calc, then return a string back
|
|
const date = new Date(dateStr.split("-")[0], parseInt(dateStr.split("-")[1]), 0);
|
|
return convertDateToDateString(date);
|
|
},
|
|
getInitialViewWeeks(todayString, initialViewRowsToShow, preSelectedDateString) {
|
|
// 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);
|
|
let weekIncludesPreSelectedMonthEnd = false;
|
|
let i = 0;
|
|
while (!weekIncludesPreSelectedMonthEnd) {
|
|
if (i > 0) {
|
|
weekStartDate = this.getNextWeekSunday(weekEndDate);
|
|
weekEndDate = this.getWeekEndDate(weekStartDate);
|
|
|
|
if (
|
|
(preSelectedDateMonthEnd > weekStartDate &&
|
|
preSelectedDateMonthEnd < weekEndDate) ||
|
|
preSelectedDateMonthEnd === weekStartDate ||
|
|
preSelectedDateMonthEnd === weekEndDate
|
|
) {
|
|
weekEndDate = preSelectedDateMonthEnd;
|
|
weekIncludesPreSelectedMonthEnd = true;
|
|
}
|
|
}
|
|
weeks.push({
|
|
weekNum: i + 1,
|
|
weekStartDate: weekStartDate,
|
|
weekEndDate: weekEndDate,
|
|
});
|
|
i++;
|
|
}
|
|
} else {
|
|
for (let i = 0; i < initialViewRowsToShow; i++) {
|
|
if (i > 0) {
|
|
weekStartDate = this.getNextWeekSunday(weekEndDate);
|
|
weekEndDate = this.getWeekEndDate(weekStartDate);
|
|
}
|
|
weeks.push({
|
|
weekNum: i + 1,
|
|
weekStartDate: weekStartDate,
|
|
weekEndDate: weekEndDate,
|
|
});
|
|
}
|
|
// If any of these weeks is split between two months, then make them 2 separate "weeks"
|
|
// (a week split between two months is considered 2 weeks per business requirements)
|
|
const hasSplitWeek = (week) => {
|
|
return week.weekStartDate.split("-")[1] !== week.weekEndDate.split("-")[1]
|
|
? true
|
|
: false;
|
|
};
|
|
const splitWeekIndex = weeks.findIndex(hasSplitWeek);
|
|
|
|
if (splitWeekIndex > -1) {
|
|
const week1 = [];
|
|
const week2 = [];
|
|
let switchToWeek2 = false;
|
|
|
|
for (let j = 0; j < 7; j++) {
|
|
const newDate = convertDateStringToDate(
|
|
weeks[splitWeekIndex].weekStartDate
|
|
);
|
|
newDate.setDate(newDate.getDate() + j);
|
|
if (newDate.getDate() === 1) switchToWeek2 = true;
|
|
if (switchToWeek2) {
|
|
week2.push(convertDateToDateString(newDate));
|
|
} else {
|
|
week1.push(convertDateToDateString(newDate));
|
|
}
|
|
}
|
|
|
|
const week1EndDate = week1[week1.length - 1];
|
|
const week2StartDate = week2[0];
|
|
|
|
if (week1EndDate < todayString) {
|
|
// replace week 1 with week 2
|
|
weeks[splitWeekIndex].weekStartDate = week2StartDate;
|
|
} else {
|
|
const newWeek = {
|
|
weekNum: weeks[splitWeekIndex].weekNum,
|
|
weekStartDate: week2StartDate,
|
|
weekEndDate: weeks[splitWeekIndex].weekEndDate,
|
|
};
|
|
weeks[splitWeekIndex].weekEndDate = week1EndDate;
|
|
weeks.splice(splitWeekIndex + 1, 0, newWeek);
|
|
weeks.pop();
|
|
weeks.forEach((item, index) => {
|
|
if (index > splitWeekIndex) {
|
|
item.weekNum = item.weekNum + 1;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return weeks;
|
|
},
|
|
async loadInitialData(config) {
|
|
/*
|
|
** NOTE: this _could_ be called by a parent before fully loaded, so data or computeds might not be available
|
|
*/
|
|
let todayDateString;
|
|
let calendarViewDirection = "none";
|
|
let hideSomeDaysForInitialView = false;
|
|
let hideSecondMonth = false;
|
|
|
|
if (this.todayString) {
|
|
todayDateString = this.todayString;
|
|
} else if (config.todayOverrideDateString) {
|
|
todayDateString = config.todayOverrideDateString;
|
|
} else {
|
|
todayDateString = convertDateToDateString(new Date());
|
|
}
|
|
if (config.selectableDatesSetting === "past") calendarViewDirection = "past";
|
|
if (config.selectableDatesSetting === "custom") calendarViewDirection = "future";
|
|
|
|
const currentMonthEnd = this.getMonthEnd(todayDateString);
|
|
// TODO - set up currentMonthStart if direction is PAST:
|
|
// let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1);
|
|
const initialViewWeeks = this.getInitialViewWeeks(
|
|
todayDateString,
|
|
config.initialViewRowsToShow,
|
|
config.preSelectedDate
|
|
);
|
|
const initialViewStartDate = todayDateString;
|
|
const initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate;
|
|
|
|
if (!config.preSelectedDate) {
|
|
const firstSaturdayMonth = initialViewWeeks[0].weekEndDate.split("-")[1];
|
|
const lastSundayMonth =
|
|
initialViewWeeks[initialViewWeeks.length - 1].weekStartDate.split("-")[1];
|
|
|
|
if (calendarViewDirection === "future") {
|
|
if (firstSaturdayMonth !== lastSundayMonth) {
|
|
hideSomeDaysForInitialView = true;
|
|
}
|
|
if (initialViewStartDate.split("-")[1] === lastSundayMonth) {
|
|
hideSecondMonth = true;
|
|
if (currentMonthEnd > initialViewEndDate) {
|
|
// should part of 1st month be hidden?
|
|
hideSomeDaysForInitialView = true;
|
|
}
|
|
}
|
|
}
|
|
// TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE THE ABOVE ^ ^ ^
|
|
}
|
|
|
|
const loadInitialDataPromise = new Promise((resolve, reject) => {
|
|
const response = config.customSelectableDatesCallback(
|
|
initialViewStartDate,
|
|
initialViewEndDate,
|
|
store.getters.order.serviceLocation.appointmentType,
|
|
store.getters.order.serviceLocation.provider.providerNumber
|
|
);
|
|
resolve(response);
|
|
});
|
|
|
|
return loadInitialDataPromise.then((response) => {
|
|
const initialData = {
|
|
todayDate: todayDateString,
|
|
initialViewStartDate: initialViewStartDate,
|
|
initialViewEndDate: initialViewEndDate,
|
|
calendarViewDirection: calendarViewDirection,
|
|
initialShopTimeSlotsResponse: response,
|
|
hideSomeDaysForInitialView: hideSomeDaysForInitialView,
|
|
hideSecondMonth: hideSecondMonth,
|
|
preSelectedDate: config.preSelectedDate,
|
|
};
|
|
return initialData;
|
|
});
|
|
},
|
|
async setCalendarData(config = {}) {
|
|
this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView;
|
|
const hideSecondMonth = config.hideSecondMonth;
|
|
const direction = config.calendarViewDirection;
|
|
const monthsAfterToLoadOffset = 6;
|
|
const monthsBeforeToLoadOffset = 36;
|
|
config.initialShopTimeSlotsResponse.days.forEach((selectableDate) => {
|
|
this.selectableDatesData.push(selectableDate);
|
|
});
|
|
|
|
// GENERATE MONTHS AND PUSH THEM INTO ARRAY
|
|
const months = [];
|
|
const options = {
|
|
calendarViewDirection: direction,
|
|
monthsBeforeToLoadOffset: monthsBeforeToLoadOffset,
|
|
monthsAfterToLoadOffset: monthsAfterToLoadOffset,
|
|
initialViewStartDate: config.initialViewStartDate,
|
|
initialViewEndDate: config.initialViewEndDate,
|
|
hideSecondMonth: hideSecondMonth,
|
|
preSelectedDate: config.preSelectedDate,
|
|
pricingByDayBasePrice: config.pricingByDayBasePrice,
|
|
pricingByDayUpcharge: config.pricingByDayUpcharge,
|
|
};
|
|
if (direction === "future") {
|
|
// first 0, then 1
|
|
for (let i = 0; i <= monthsAfterToLoadOffset; i++) {
|
|
months.push(await this.getMonthData(i, options));
|
|
}
|
|
} else if (direction === "past") {
|
|
// first 0, then -1
|
|
for (let i = 0; i >= 0 - monthsBeforeToLoadOffset; i--) {
|
|
months.unshift(await this.getMonthData(i, options));
|
|
}
|
|
} else {
|
|
// TODO - IF A CALENDAR WITH BOTH PAST AND FUTURE WAS EVER NEEDED
|
|
// for (let i = monthsAfterToLoadOffset; i >= monthsBeforeToLoadOffset; i--) {
|
|
// months.push(this.getMonthDataPAST(i));
|
|
// }
|
|
}
|
|
this.months = months;
|
|
this.isLoading = false;
|
|
|
|
if (config.preSelectedDate) {
|
|
this.$nextTick(() => {
|
|
//Advance to month
|
|
const monthToShow = this.months.find((month) =>
|
|
month.monthClass.includes("month-preselected")
|
|
);
|
|
if (
|
|
monthToShow.monthClass.includes("month-preselected") &&
|
|
monthToShow.monthClass.includes("last-available-month")
|
|
) {
|
|
// disable View More dates button if the preSelectedDate in the last available month
|
|
this.disableViewMoreDatesButton = true;
|
|
}
|
|
this.scrollToElement(monthToShow.monthString);
|
|
});
|
|
}
|
|
},
|
|
async getMonthData(offset = requiredParameter(), options) {
|
|
/* options will contain:
|
|
calendarViewDirection (string)
|
|
initialViewStartDateNum (number)
|
|
initialViewEndDateNum (number)
|
|
monthsBeforeToLoadOffset (number),
|
|
monthsAfterToLoadOffset (number),
|
|
hideSecondMonth (boolean),
|
|
preSelectedDate (string)
|
|
|
|
data used:
|
|
todayDate (date object)
|
|
this.hideSomeDaysForInitialView (string)
|
|
*/
|
|
|
|
let monthNum = convertDateStringToDate(this.todayString).getMonth() + offset + 1;
|
|
let yearNum = convertDateStringToDate(this.todayString).getFullYear();
|
|
const calendarViewDirection = options.calendarViewDirection;
|
|
if (calendarViewDirection === "future" && offset > 0) {
|
|
while (monthNum > 12) {
|
|
monthNum = monthNum - 12;
|
|
yearNum++;
|
|
}
|
|
} else if (calendarViewDirection === "past" && offset < 0) {
|
|
while (monthNum < 1) {
|
|
monthNum = 12 + monthNum;
|
|
yearNum--;
|
|
}
|
|
}
|
|
const initialViewEndDate = options.initialViewEndDate;
|
|
// const initialViewStartDate = options.initialViewStartDate; // TODO: to be used for past calendarViewDirection
|
|
const hideSecondMonth = options.hideSecondMonth;
|
|
const preSelectedDateObj = options.preSelectedDate
|
|
? convertDateStringToDate(options.preSelectedDate)
|
|
: null;
|
|
const dates = [];
|
|
const monthEndDate = new Date(yearNum, monthNum, 0);
|
|
const monthStartDateNum =
|
|
offset === 0 && calendarViewDirection === "future"
|
|
? this.currentWeekStartDateNum
|
|
: 1;
|
|
const monthStartDate = new Date(yearNum, monthNum - 1, monthStartDateNum);
|
|
const startDateDayIndex = monthStartDate.getDay();
|
|
|
|
let monthClass = "";
|
|
let isMonthThatHidesSomeDaysForInitialView;
|
|
let monthEndDateNum = monthEndDate.getDate();
|
|
|
|
if (
|
|
offset === 0 &&
|
|
calendarViewDirection === "past" &&
|
|
monthEndDateNum > this.currentWeekEndDateNum
|
|
) {
|
|
monthEndDateNum = this.currentWeekEndDateNum;
|
|
}
|
|
|
|
if (options.preSelectedDate) {
|
|
if (
|
|
monthStartDate.getFullYear() === preSelectedDateObj.getFullYear() &&
|
|
monthStartDate.getMonth() === preSelectedDateObj.getMonth()
|
|
) {
|
|
monthClass = monthClass + " month-preselected";
|
|
} else if (monthStartDate > preSelectedDateObj) {
|
|
monthClass = monthClass + " month-hidden";
|
|
}
|
|
} else {
|
|
if (Math.abs(offset) === 1 && hideSecondMonth) {
|
|
monthClass = monthClass + " month-hidden";
|
|
} else if (Math.abs(offset) > 1) {
|
|
monthClass = monthClass + " month-hidden";
|
|
}
|
|
}
|
|
|
|
if (
|
|
Math.abs(offset) === options.monthsAfterToLoadOffset &&
|
|
calendarViewDirection === "future"
|
|
) {
|
|
monthClass = monthClass + " last-available-month";
|
|
}
|
|
if (
|
|
Math.abs(offset) === options.monthsBeforeToLoadOffset &&
|
|
calendarViewDirection === "past"
|
|
) {
|
|
// TODO - re-check this logic if past direction
|
|
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;
|
|
|
|
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,
|
|
isSelectable: isSelectable,
|
|
isPricingByDayUpchargeDay: isPricingByDayUpchargeDay,
|
|
};
|
|
dates.push(dateObject);
|
|
}
|
|
|
|
const monthToAdd = {
|
|
monthLabel: MONTHS_OF_YEAR[monthNum - 1],
|
|
monthIndex: monthNum,
|
|
monthString: MONTHS_OF_YEAR[monthNum - 1] + "-" + yearNum?.toString(),
|
|
yearNum: yearNum,
|
|
dates: dates,
|
|
startDateDayIndex: startDateDayIndex,
|
|
monthClass: monthClass,
|
|
isMonthThatHidesSomeDaysForInitialView: isMonthThatHidesSomeDaysForInitialView,
|
|
};
|
|
return monthToAdd;
|
|
},
|
|
async showAnotherMonth() {
|
|
if (!this.months) return;
|
|
this.isLoading = true;
|
|
let monthToShow;
|
|
let monthStartDate;
|
|
|
|
if (this.hideSomeDaysForInitialView) {
|
|
monthToShow = this.months.find(
|
|
({ isMonthThatHidesSomeDaysForInitialView }) =>
|
|
isMonthThatHidesSomeDaysForInitialView
|
|
);
|
|
|
|
// find the first day-hidden to become the next api call start date
|
|
monthStartDate = monthToShow.dates.find(({ dayClasses }) =>
|
|
dayClasses.includes("day-hidden")
|
|
);
|
|
} else {
|
|
if (this.calendarViewDirection === "future") {
|
|
monthToShow = this.months.find((month) =>
|
|
month.monthClass.includes("month-hidden")
|
|
);
|
|
}
|
|
if (this.calendarViewDirection === "past") {
|
|
// TODO: UPDATE THIS WITH CORRECT PAST LOOKING LOGIC
|
|
monthToShow = this.months.find((month) =>
|
|
month.monthClass.includes("month-hidden")
|
|
);
|
|
}
|
|
}
|
|
if (monthToShow) {
|
|
const monthStartDateString = monthStartDate
|
|
? monthStartDate.inputValue
|
|
: monthToShow.dates[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
|
|
);
|
|
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", "");
|
|
this.scrollToElement(monthToShow.monthString);
|
|
|
|
if (monthToShow.monthClass.includes("last-available-month"))
|
|
this.disableViewMoreDatesButton = true;
|
|
|
|
return moreSelectableDatesData;
|
|
}
|
|
},
|
|
async updateSelectableDates(monthStart, monthEnd) {
|
|
const moreSelectableDates = await this.customSelectableDatesCallback(
|
|
monthStart,
|
|
monthEnd,
|
|
this.$store.getters.order.serviceLocation.appointmentType,
|
|
this.$store.getters.order.serviceLocation.provider.providerNumber
|
|
);
|
|
|
|
moreSelectableDates.days.forEach((selectableDate) => {
|
|
const index = this.selectableDatesData.findIndex(
|
|
(dateObj) => dateObj.date === selectableDate.date
|
|
);
|
|
if (index === -1) this.selectableDatesData.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) => {
|
|
if (date.inputValue === selectableDate.date) {
|
|
date["isSelectable"] = true;
|
|
}
|
|
});
|
|
});
|
|
});
|
|
return moreSelectableDates;
|
|
},
|
|
scrollToElement(elementId, duration = 800, easing = "ease-in-out") {
|
|
const wrapper = document.querySelector(".page-container-grouped-styles");
|
|
const target = document.getElementById(elementId);
|
|
const initY = wrapper.scrollTop;
|
|
const wrapperRect = wrapper.getBoundingClientRect();
|
|
const targetRect = target.getBoundingClientRect();
|
|
const targetY = targetRect.top - wrapperRect.top - BUFFER_OFFSET;
|
|
const timingFunc = TIMINGFUNC_MAP[easing];
|
|
let start = null;
|
|
const step = (timestamp) => {
|
|
start = start || timestamp;
|
|
const time = Math.min(1, (timestamp - start) / duration);
|
|
const percentageNew = timingFunc(time);
|
|
const distanceToGo = targetY;
|
|
const thisDistance = percentageNew * distanceToGo;
|
|
wrapper.scrollTo(0, initY + thisDistance);
|
|
if (percentageNew < 1) {
|
|
window.requestAnimationFrame(step);
|
|
}
|
|
};
|
|
window.requestAnimationFrame(step);
|
|
},
|
|
getSelectedTimeSlotInfo() {
|
|
const supportingItems = this.getSupportingItems();
|
|
|
|
var isPremiumAppointment = false;
|
|
if (supportingItems) {
|
|
isPremiumAppointment =
|
|
!!supportingItems.filter(
|
|
(lineItem) => lineItem.partType === PREMIUM_FEE_PART_TYPE
|
|
).length > 0;
|
|
}
|
|
|
|
const selectedTimeSlotInfo = {
|
|
timeSlot: store.getters.order.schedule,
|
|
isPremiumAppointment: isPremiumAppointment,
|
|
};
|
|
|
|
return selectedTimeSlotInfo;
|
|
},
|
|
getSupportingItems() {
|
|
return store.getters.lineItems.supportingItems;
|
|
},
|
|
handleWaitListRequested(value) {
|
|
this.$emit("waitListRequested", value);
|
|
},
|
|
},
|
|
watch: {
|
|
modelValue(newValue) {
|
|
this.resetField({
|
|
value: newValue,
|
|
});
|
|
},
|
|
errorMessage(newValue, oldValue) {
|
|
// Check for changing from no error message to some error message.
|
|
if (newValue && !oldValue) {
|
|
this.scrollToElement("date-of-month-error");
|
|
}
|
|
},
|
|
selectedTimeSlotInfo(newValue) {
|
|
this.$emit("TimeSlotSelected", newValue); // NEEDED ON SCHEDULE - to update footer button text and to save to Store correctly
|
|
},
|
|
},
|
|
components: {
|
|
loader,
|
|
ErrorMessage,
|
|
timeSlotQuestion,
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.date-picker-hidden {
|
|
opacity: 0;
|
|
max-height: 0;
|
|
}
|
|
.date-picker {
|
|
position: relative;
|
|
flex-grow: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
fieldset {
|
|
flex-grow: 1;
|
|
position: relative;
|
|
|
|
.view-more-dates {
|
|
color: $blue;
|
|
}
|
|
}
|
|
.loader {
|
|
height: 2rem;
|
|
width: calc(100% - 1.5rem);
|
|
|
|
&::after {
|
|
width: 1.5rem;
|
|
height: 1.5rem;
|
|
}
|
|
}
|
|
.calendar-grid-container {
|
|
margin: 0 auto 1rem auto;
|
|
max-width: 360px;
|
|
position: relative;
|
|
transition:
|
|
height ease 2s,
|
|
opacity ease 2s;
|
|
display: grid;
|
|
grid-template-columns: repeat(7, 1fr);
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: 0;
|
|
opacity: 1;
|
|
|
|
.grid-item {
|
|
text-align: center;
|
|
margin: 10px 3px;
|
|
font-size: 0.75rem;
|
|
line-height: 1.5;
|
|
|
|
&.caption {
|
|
color: $gray-550;
|
|
}
|
|
|
|
&.first-day-,
|
|
&.first-day-0 {
|
|
grid-column-start: 1;
|
|
}
|
|
&.first-day-1 {
|
|
grid-column-start: 2;
|
|
}
|
|
&.first-day-2 {
|
|
grid-column-start: 3;
|
|
}
|
|
&.first-day-3 {
|
|
grid-column-start: 4;
|
|
}
|
|
&.first-day-4 {
|
|
grid-column-start: 5;
|
|
}
|
|
&.first-day-5 {
|
|
grid-column-start: 6;
|
|
}
|
|
&.first-day-6 {
|
|
grid-column-start: 7;
|
|
}
|
|
}
|
|
|
|
.separator-line {
|
|
grid-area: 2/1/2/8;
|
|
border-top: 1px solid $gray-500;
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
.month-year {
|
|
grid-area: 1 / 1 / 2 / 5;
|
|
font-family: AvertaBold;
|
|
letter-spacing: 0.75px;
|
|
color: $black;
|
|
justify-content: center;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.legend {
|
|
grid-area: 1 / 5 / 2 / 8;
|
|
.legend-circle {
|
|
border-radius: 50%;
|
|
width: 16px;
|
|
height: 16px;
|
|
background-color: $blue-100;
|
|
border: 1px solid $blue;
|
|
}
|
|
}
|
|
.nav-back,
|
|
.nav-forward {
|
|
display: none;
|
|
}
|
|
|
|
.radio-wrapper {
|
|
position: relative;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
outline: none;
|
|
height: 1.5rem;
|
|
opacity: 1;
|
|
transition:
|
|
height ease 250ms,
|
|
opacity ease 250ms;
|
|
|
|
input[type="radio"] {
|
|
position: absolute; //override bootstrap
|
|
height: 0;
|
|
opacity: 0;
|
|
|
|
&:focus-visible + label {
|
|
box-shadow: 0 0 0 2.5px $blue;
|
|
}
|
|
|
|
&:focus + label,
|
|
&:checked:focus + label {
|
|
box-shadow:
|
|
0 0 0 3px #fff,
|
|
0 0 0 5.5px #1574a1;
|
|
background-color: $blue;
|
|
color: $white;
|
|
&.current-day {
|
|
&:after {
|
|
background-color: $white;
|
|
}
|
|
.first-day {
|
|
color: $white;
|
|
}
|
|
}
|
|
}
|
|
|
|
&:checked + label {
|
|
color: $white;
|
|
background: $blue;
|
|
&:after {
|
|
background-color: $white;
|
|
}
|
|
.first-day {
|
|
color: $white;
|
|
}
|
|
}
|
|
}
|
|
|
|
.first-day {
|
|
position: absolute;
|
|
font-size: 9px;
|
|
font-weight: 500;
|
|
color: $gray-600;
|
|
top: 0;
|
|
z-index: 1;
|
|
}
|
|
|
|
label {
|
|
position: relative;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
min-width: 2.5rem;
|
|
width: 2.5rem;
|
|
height: 2.5rem;
|
|
border: 2px solid transparent;
|
|
|
|
span {
|
|
&.small {
|
|
font-size: 0.75rem;
|
|
color: $gray-550;
|
|
}
|
|
}
|
|
|
|
&:checked {
|
|
@include media-breakpoint-up(sm) {
|
|
box-shadow: 0 0 0 4px transparent;
|
|
background-color: $blue;
|
|
color: $white;
|
|
&:after {
|
|
background-color: $white;
|
|
}
|
|
}
|
|
cursor: pointer;
|
|
&.current-day {
|
|
+ .first-day {
|
|
color: $white;
|
|
}
|
|
}
|
|
.first-day {
|
|
color: $white;
|
|
}
|
|
}
|
|
+ p {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
&.selectable-day {
|
|
label {
|
|
background-color: $blue-100;
|
|
min-width: 2.75rem;
|
|
width: 2.75rem;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
@include media-breakpoint-up(md) {
|
|
min-width: 3rem;
|
|
width: 3rem;
|
|
}
|
|
&:hover {
|
|
border: 2px solid $blue;
|
|
}
|
|
}
|
|
}
|
|
&.unavailable-day {
|
|
label {
|
|
border: none;
|
|
pointer-events: none;
|
|
}
|
|
}
|
|
//NEEDED?
|
|
&.unavailable-day:not(&.sunday) {
|
|
label {
|
|
&::before {
|
|
content: "";
|
|
display: inline-block;
|
|
position: absolute;
|
|
left: -1rem;
|
|
width: 1rem;
|
|
height: 2.5rem;
|
|
}
|
|
}
|
|
}
|
|
&.current-day {
|
|
label {
|
|
font-weight: 500;
|
|
color: $black;
|
|
|
|
&:after {
|
|
content: "";
|
|
width: 0.25rem;
|
|
height: 0.25rem;
|
|
border-radius: 50%;
|
|
background-color: $black;
|
|
position: absolute;
|
|
top: 1.875rem;
|
|
}
|
|
.first-day {
|
|
color: $blue;
|
|
}
|
|
}
|
|
input[type="radio"]:checked + label:after {
|
|
background-color: $black;
|
|
}
|
|
&.selectable-day {
|
|
label {
|
|
&:after {
|
|
display: none;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
&.partial-month-initial-view {
|
|
.day-hidden {
|
|
opacity: 0;
|
|
overflow: hidden;
|
|
display: flex;
|
|
margin: 0;
|
|
max-height: 0;
|
|
}
|
|
}
|
|
&.month-hidden {
|
|
opacity: 0;
|
|
max-height: 0;
|
|
margin-bottom: 0;
|
|
overflow: hidden;
|
|
}
|
|
}
|
|
|
|
.btn-link {
|
|
font-weight: 500;
|
|
text-underline-offset: 4px;
|
|
flex-grow: 0;
|
|
&:focus {
|
|
box-shadow: none;
|
|
}
|
|
}
|
|
|
|
.past {
|
|
.calendar-grid-container {
|
|
.month-year {
|
|
grid-area: 1 / 1 / 2 / 6;
|
|
}
|
|
.nav-forward {
|
|
grid-area: 1 / 7 / 2 / 8;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
button {
|
|
&:after {
|
|
content: "";
|
|
position: absolute;
|
|
width: 7px;
|
|
height: 12px;
|
|
background-image: url("data:image/svg+xml,%3Csvg width='7' height='12' viewBox='0 0 7 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.66831 5.99879C6.66955 6.17554 6.60074 6.34558 6.47695 6.47168L1.15501 11.8017C1.02812 11.9287 0.85603 12 0.676587 12C0.497145 12 0.325053 11.9287 0.198168 11.8017C0.0712831 11.6748 7.268e-09 11.5026 8.07183e-09 11.3231C8.87567e-09 11.1436 0.0712831 10.9714 0.198168 10.8445L5.05126 5.99879L0.198168 1.14736C0.0725521 1.02042 0.00248585 0.848755 0.00338306 0.670131C0.00428028 0.491506 0.0760674 0.320554 0.202952 0.194881C0.329837 0.0692091 0.501426 -0.000888818 0.679971 9.54485e-06C0.858515 0.000906955 1.02939 0.0727263 1.15501 0.199668L6.47312 5.52399C6.59843 5.65017 6.66862 5.82092 6.66831 5.99879Z' fill='%231574A1'/%3E%3C/svg%3E%0A");
|
|
}
|
|
}
|
|
}
|
|
.nav-back {
|
|
grid-area: 1 / 6 / 2 / 7;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
button {
|
|
&:after {
|
|
content: "";
|
|
position: absolute;
|
|
width: 7px;
|
|
height: 12px;
|
|
background-image: url("data:image/svg+xml,%3Csvg width='7' height='12' viewBox='0 0 7 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0.331685 6.00121C0.330445 5.82446 0.399256 5.65442 0.523053 5.52832L5.84499 0.198256C5.97188 0.0713149 6.14397 -8.63821e-08 6.32341 -6.72174e-08C6.50285 -4.80527e-08 6.67495 0.071315 6.80183 0.198256C6.92872 0.325198 7 0.497368 7 0.67689C7 0.856412 6.92872 1.02858 6.80183 1.15552L1.94874 6.00121L6.80183 10.8526C6.92745 10.9796 6.99751 11.1512 6.99662 11.3299C6.99572 11.5085 6.92393 11.6794 6.79705 11.8051C6.67016 11.9308 6.49857 12.0009 6.32003 12C6.14148 11.9991 5.97061 11.9273 5.84499 11.8003L0.526881 6.47601C0.401568 6.34983 0.331375 6.17908 0.331685 6.00121Z' fill='%231574A1'/%3E%3C/svg%3E%0A");
|
|
}
|
|
}
|
|
}
|
|
.nav-back,
|
|
.nav-forward {
|
|
button {
|
|
position: relative;
|
|
border-radius: 50%;
|
|
width: 28px;
|
|
height: 28px;
|
|
background-color: $blue-100;
|
|
border: 1px solid $blue;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
&:focus,
|
|
&:focus-visible {
|
|
border: 2.5px solid $blue;
|
|
box-shadow: none;
|
|
background-color: $blue-100;
|
|
color: $white;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// new calendar styling overrides
|
|
&.calendar-2025 {
|
|
.calendar-grid-container .grid-item {
|
|
margin: 0 0 0.15rem 0;
|
|
}
|
|
.month-year {
|
|
grid-area: 1 / 1 / 2 / 8;
|
|
}
|
|
.legend {
|
|
display: none;
|
|
}
|
|
.radio-wrapper {
|
|
align-items: flex-start;
|
|
width: 100%;
|
|
height: 2.5rem;
|
|
color: $black;
|
|
|
|
input[type="radio"] {
|
|
&:focus-visible + label {
|
|
box-shadow: none;
|
|
}
|
|
|
|
&:focus + label,
|
|
&:checked:focus + label {
|
|
box-shadow: none;
|
|
background-color: $blue-100;
|
|
color: $black;
|
|
}
|
|
&:checked + label {
|
|
background: $blue-100;
|
|
border: 2px solid $blue;
|
|
border-radius: 0.25rem;
|
|
color: $black;
|
|
span {
|
|
font-family: AvertaSemibold;
|
|
font-weight: 400;
|
|
}
|
|
}
|
|
}
|
|
label {
|
|
color: $gray-500;
|
|
flex-direction: column;
|
|
height: 2.5rem;
|
|
width: 100%;
|
|
font-size: 0.75rem;
|
|
line-height: 1.2;
|
|
justify-content: center;
|
|
padding: 0.25rem;
|
|
}
|
|
&.selectable-day {
|
|
label {
|
|
color: $black;
|
|
background-color: $blue-100;
|
|
min-width: 2.75rem;
|
|
width: 2.75rem;
|
|
border-radius: 0.25rem;
|
|
@include media-breakpoint-up(md) {
|
|
min-width: 3rem;
|
|
width: 3rem;
|
|
}
|
|
}
|
|
}
|
|
&.current-day {
|
|
label {
|
|
font-weight: 600;
|
|
font-family: "AvertaSemibold";
|
|
color: $black;
|
|
font-size-adjust: 0.5; // needed for Averta fonts baseline alignment
|
|
}
|
|
}
|
|
}
|
|
&.show-pricing-by-day {
|
|
.radio-wrapper {
|
|
&.selectable-day label span {
|
|
text-decoration: none;
|
|
color: $black;
|
|
font-weight: 400;
|
|
font-family: "AvertaSemibold";
|
|
font-size-adjust: 0.5; // needed for Averta fonts baseline alignment
|
|
|
|
&.price {
|
|
font-family: "AvertaRegular";
|
|
font-weight: 400;
|
|
}
|
|
}
|
|
&.upch-day label span.price {
|
|
color: $gray-600;
|
|
font-weight: 400;
|
|
font-family: $font-family-sans-serif;
|
|
}
|
|
input[type="radio"] {
|
|
&:focus + label,
|
|
&:checked:focus + label {
|
|
color: $gray-600;
|
|
}
|
|
&:checked + label {
|
|
color: $blue;
|
|
span {
|
|
font-weight: 400;
|
|
font-family: "AvertaSemibold";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
:deep(.button-inner-wrapper) {
|
|
display: flex;
|
|
max-width: 100%;
|
|
flex-wrap: wrap;
|
|
gap: 0 0.5rem;
|
|
|
|
& > div {
|
|
flex: 0 0 calc(50% - 0.25rem);
|
|
}
|
|
}
|
|
:deep(.button-question.is-mobile-appointment) {
|
|
.button-inner-wrapper > div {
|
|
flex: 0 0 100%;
|
|
}
|
|
}
|
|
:deep(.button-question.is-drop-off-appointment) {
|
|
.button-inner-wrapper > div {
|
|
flex: 0 0 100%;
|
|
}
|
|
}
|
|
}
|
|
|
|
.btn-link {
|
|
display: block;
|
|
position: relative;
|
|
height: 2rem;
|
|
width: 100%;
|
|
justify-content: center;
|
|
background: transparent;
|
|
border: none;
|
|
font-weight: 500;
|
|
text-underline-offset: 4px;
|
|
z-index: 2;
|
|
}
|
|
|
|
.form-test-error {
|
|
margin: 0 auto;
|
|
max-width: 414px;
|
|
text-align: left;
|
|
}
|
|
</style>
|