DigitalConsumer.FixMyGlass/src/layouts/scheduling/mobile-scheduling-card/mobile-scheduling-card.vue
scottkiener-at-safelite cd83276dad CASH-2880 | Format
2026-06-24 09:20:10 -04:00

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>