426 lines
12 KiB
Vue
426 lines
12 KiB
Vue
<template>
|
|
<div class="mobile-scheduling-card">
|
|
<div v-if="showFreeFlag && !isLoading" class="mobile-scheduling-card__free-flag">
|
|
<img
|
|
class="mobile-scheduling-card__free-flag-icon"
|
|
src="@/assets/img/party.svg"
|
|
alt=""
|
|
aria-hidden="true" />
|
|
<span class="mobile-scheduling-card__free-flag-text">{{ freeFlagText }}</span>
|
|
</div>
|
|
<div v-if="isLoading" class="mobile-scheduling-card__panel">
|
|
<schedulingCardLoader />
|
|
</div>
|
|
<div
|
|
v-else
|
|
class="mobile-scheduling-card__panel"
|
|
:class="{ 'mobile-scheduling-card__panel--with-free-flag': showFreeFlag }">
|
|
<button
|
|
type="button"
|
|
class="mobile-scheduling-card__header"
|
|
:aria-expanded="isExpanded"
|
|
@click="toggleExpanded">
|
|
<img
|
|
class="mobile-scheduling-card__van-icon"
|
|
src="@/assets/img/van.svg"
|
|
alt=""
|
|
aria-hidden="true" />
|
|
<span class="mobile-scheduling-card__header-title">{{ headerTitle }}</span>
|
|
<span
|
|
class="mobile-scheduling-card__appt-badge"
|
|
:class="{
|
|
'mobile-scheduling-card__appt-badge--empty': appointmentCount === 0,
|
|
}">
|
|
{{ appointmentCountLabel }}
|
|
</span>
|
|
<img
|
|
class="mobile-scheduling-card__chevron"
|
|
:class="{ 'mobile-scheduling-card__chevron--expanded': isExpanded }"
|
|
src="@/assets/img/icons/chevron-no-background.svg"
|
|
alt=""
|
|
aria-hidden="true" />
|
|
</button>
|
|
<div class="mobile-scheduling-card__location-bar">
|
|
<button
|
|
type="button"
|
|
class="mobile-scheduling-card__zip-link"
|
|
@click.stop="onZipCodeClick">
|
|
{{ zipCode }}
|
|
</button>
|
|
<span class="mobile-scheduling-card__location-divider" aria-hidden="true">|</span>
|
|
<span class="mobile-scheduling-card__address-hint">{{ addressHintText }}</span>
|
|
</div>
|
|
<div v-show="isExpanded" class="mobile-scheduling-card__body">
|
|
<fieldset class="mobile-scheduling-card__time-slots">
|
|
<legend class="sr-only">{{ headerTitle }}</legend>
|
|
<label
|
|
v-for="slot in displayTimeSlots"
|
|
:key="slot.value"
|
|
class="mobile-scheduling-card__slot"
|
|
:class="{
|
|
'mobile-scheduling-card__slot--selected': isSlotSelected(slot),
|
|
}">
|
|
<input
|
|
type="radio"
|
|
class="mobile-scheduling-card__slot-input"
|
|
:name="radioGroupName"
|
|
:value="slot.value"
|
|
:checked="isSlotSelected(slot)"
|
|
@change="onTimeSlotSelected(slot)" />
|
|
<span class="mobile-scheduling-card__slot-label">
|
|
<span class="mobile-scheduling-card__slot-label-primary">
|
|
{{ slot.label }}
|
|
</span>
|
|
<span
|
|
v-if="slot.subLabel && isSlotSelected(slot)"
|
|
class="mobile-scheduling-card__slot-label-secondary">
|
|
{{ slot.subLabel }}
|
|
</span>
|
|
</span>
|
|
<span v-if="slot.priceLabel" class="mobile-scheduling-card__slot-price">
|
|
{{ slot.priceLabel }}
|
|
</span>
|
|
</label>
|
|
</fieldset>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { PREMIUM_TIME_SLOT_ID_FLAG, AppointmentTypeStrings } from "@/constants/schedule-constants";
|
|
import { militaryToTwelveHourTime } from "@/layouts/schedule/helpers/schedule-helper";
|
|
import schedulingCardLoader from "@/layouts/scheduling/scheduling-card-loader/scheduling-card-loader.vue";
|
|
export default {
|
|
name: "mobile-scheduling-card",
|
|
emits: ["update:modelValue", "zip-code-clicked"],
|
|
props: {
|
|
modelValue: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
timeSlots: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
premiumTimeSlotPrice: {
|
|
type: Number,
|
|
default: null,
|
|
},
|
|
zipCode: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
providerNumber: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
radioGroupName: {
|
|
type: String,
|
|
default: "schedulingTimeSlot",
|
|
},
|
|
showFreeFlag: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
cmsWidgetName: {
|
|
type: String,
|
|
default: "MobileCardWidget",
|
|
},
|
|
isLoading: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
isExpanded: true,
|
|
};
|
|
},
|
|
computed: {
|
|
headerTitle() {
|
|
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
|
|
},
|
|
freeFlagText() {
|
|
return this.getCmsContent(this.cmsWidgetName, "SubheaderText");
|
|
},
|
|
addressHintText() {
|
|
return this.getCmsContent(this.cmsWidgetName, "BodyText");
|
|
},
|
|
premiumTimeSlotLabel() {
|
|
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
|
},
|
|
premiumTimeSlotSubLabel() {
|
|
return this.getCmsContent(this.cmsWidgetName, "FooterText2");
|
|
},
|
|
appointmentCount() {
|
|
return this.displayTimeSlots.length;
|
|
},
|
|
appointmentCountLabel() {
|
|
return `${this.appointmentCount} appt${this.appointmentCount === 1 ? "" : "s"}`;
|
|
},
|
|
hasPremiumTimeSlot() {
|
|
return this.timeSlots?.[0]?.offerPremium === true && this.premiumTimeSlotPrice != null;
|
|
},
|
|
displayTimeSlots() {
|
|
if (!this.timeSlots?.length) {
|
|
return [];
|
|
}
|
|
|
|
const slots = this.timeSlots.map((timeSlot) => ({
|
|
value: timeSlot.id,
|
|
label: this.formatArrivalWindow(timeSlot.startTime, timeSlot.endTime),
|
|
priceLabel: null,
|
|
isPremiumAppointment: false,
|
|
routeCodeId: timeSlot.id,
|
|
}));
|
|
|
|
if (this.hasPremiumTimeSlot) {
|
|
slots.unshift({
|
|
value: this.addPremiumFlagToInput(this.timeSlots[0].id),
|
|
label: this.premiumTimeSlotLabel,
|
|
subLabel: this.premiumTimeSlotSubLabel || null,
|
|
priceLabel: this.formattedPremiumPrice,
|
|
isPremiumAppointment: true,
|
|
routeCodeId: this.timeSlots[0].id,
|
|
});
|
|
}
|
|
|
|
return slots;
|
|
},
|
|
formattedPremiumPrice() {
|
|
if (this.premiumTimeSlotPrice == null) {
|
|
return null;
|
|
}
|
|
return `+$${this.premiumTimeSlotPrice.toFixed(2)}`;
|
|
},
|
|
selectedRouteCodeValue() {
|
|
if (
|
|
this.modelValue?.appointmentType !== AppointmentTypeStrings.MOBILE ||
|
|
this.modelValue?.providerNumber !== this.providerNumber
|
|
) {
|
|
return null;
|
|
}
|
|
if (!this.modelValue?.routeCodeId) {
|
|
return null;
|
|
}
|
|
return this.modelValue.isPremiumAppointment
|
|
? this.addPremiumFlagToInput(this.modelValue.routeCodeId)
|
|
: this.modelValue.routeCodeId;
|
|
},
|
|
},
|
|
methods: {
|
|
toggleExpanded() {
|
|
this.isExpanded = !this.isExpanded;
|
|
},
|
|
onZipCodeClick() {
|
|
this.$emit("zip-code-clicked");
|
|
},
|
|
formatArrivalWindow(startTime, endTime) {
|
|
return `${militaryToTwelveHourTime(startTime)} - ${militaryToTwelveHourTime(
|
|
endTime
|
|
)} arrival window`;
|
|
},
|
|
isSlotSelected(slot) {
|
|
return this.selectedRouteCodeValue === slot.value;
|
|
},
|
|
onTimeSlotSelected(slot) {
|
|
this.$emit("update:modelValue", {
|
|
appointmentType: AppointmentTypeStrings.MOBILE,
|
|
providerNumber: this.providerNumber,
|
|
routeCodeId: slot.routeCodeId,
|
|
isPremiumAppointment: slot.isPremiumAppointment,
|
|
});
|
|
},
|
|
addPremiumFlagToInput(routeCode) {
|
|
return routeCode + PREMIUM_TIME_SLOT_ID_FLAG;
|
|
},
|
|
},
|
|
components: {
|
|
schedulingCardLoader,
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.mobile-scheduling-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
|
|
&__free-flag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 2px;
|
|
background: $green;
|
|
color: $green-600;
|
|
border-radius: 8px 8px 0 0;
|
|
padding: 2px 8px 2px 4px;
|
|
font-size: 0.75rem;
|
|
font-weight: 400;
|
|
line-height: 1.25rem;
|
|
}
|
|
|
|
&__free-flag-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
&__panel {
|
|
width: 100%;
|
|
border: 1px solid $gray-1000;
|
|
border-radius: 16px;
|
|
padding: 16px;
|
|
background: $white;
|
|
|
|
&--with-free-flag {
|
|
border-radius: 0 16px 16px 16px;
|
|
}
|
|
}
|
|
|
|
&__header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
width: 100%;
|
|
border: 0;
|
|
background: transparent;
|
|
padding: 0;
|
|
text-align: left;
|
|
}
|
|
|
|
&__van-icon {
|
|
width: 24px;
|
|
height: 24px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
&__header-title {
|
|
flex: 1;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
line-height: 1.5rem;
|
|
color: $black;
|
|
}
|
|
|
|
&__appt-badge {
|
|
background: $blue-150;
|
|
color: $blue;
|
|
border-radius: 72px;
|
|
padding: 2px 8px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
line-height: 1.25rem;
|
|
white-space: nowrap;
|
|
|
|
&--empty {
|
|
background: $gray-100;
|
|
color: $gray-500;
|
|
}
|
|
}
|
|
|
|
&__chevron {
|
|
width: 16px;
|
|
height: 16px;
|
|
flex-shrink: 0;
|
|
transform: rotate(180deg);
|
|
transition: transform 150ms linear;
|
|
|
|
&--expanded {
|
|
transform: rotate(0deg);
|
|
}
|
|
}
|
|
|
|
&__body {
|
|
margin-top: 8px;
|
|
}
|
|
|
|
&__location-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 0.875rem;
|
|
line-height: 1.5rem;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
&__zip-link {
|
|
border: 0;
|
|
background: transparent;
|
|
padding: 0;
|
|
color: $blue;
|
|
font-weight: 600;
|
|
text-decoration: underline;
|
|
cursor: pointer;
|
|
}
|
|
|
|
&__location-divider,
|
|
&__address-hint {
|
|
color: $gray-600;
|
|
}
|
|
|
|
&__time-slots {
|
|
border: 0;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
&__slot {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
border: 1px solid $gray-1000;
|
|
border-radius: 8px;
|
|
padding: 12px 8px;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
cursor: pointer;
|
|
background: $white;
|
|
|
|
&--selected {
|
|
border: 2px solid $blue;
|
|
background: $blue-150;
|
|
}
|
|
}
|
|
|
|
&__slot-input {
|
|
margin-top: 4px;
|
|
accent-color: $blue;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
&__slot-label {
|
|
display: flex;
|
|
flex: 1;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
&__slot-label-primary,
|
|
&__slot-label-secondary {
|
|
font-size: 1rem;
|
|
line-height: 1.5rem;
|
|
color: $gray-600;
|
|
}
|
|
|
|
&__slot--selected &__slot-label-primary {
|
|
color: $black;
|
|
font-weight: 600;
|
|
}
|
|
|
|
&__slot-price {
|
|
background: $green;
|
|
color: $green-600;
|
|
border-radius: 72px;
|
|
padding: 2px 8px;
|
|
font-size: 0.75rem;
|
|
font-weight: 400;
|
|
line-height: 1.25rem;
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
}
|
|
</style>
|