PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.8
Booking for Appointments and Events Calendar – Amelia v2.4.8
2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / v3 / src / views / _components / advanced-slot-calendar / AmAdvancedSlotCalendar.vue
ameliabooking / v3 / src / views / _components / advanced-slot-calendar Last commit date
AmAdvancedSlotCalendar.vue 1 month ago
AmAdvancedSlotCalendar.vue
1935 lines
1 <template>
2 <div class="am-advsc__wrapper" :style="cssVars">
3 <div
4 v-show="headerMonthOptions.length && calendarStartDate"
5 class="am-advsc__header"
6 :class="[
7 { 'am-advsc__header-mobile': checkScreen },
8 { 'am-advsc__header-mobile-s': mobileS },
9 ]"
10 >
11 <AmSelect
12 v-model="calMonth"
13 size="medium"
14 aria-label="month selection"
15 @change="changeMonth(calYear + '-' + calMonth)"
16 >
17 <AmOption
18 v-for="month in headerMonthOptions"
19 v-show="month.available"
20 :key="month.value"
21 :value="month.value"
22 :label="month.label"
23 />
24 </AmSelect>
25 <AmSelect v-model="calYear" size="medium" aria-label="year selection" @change="changeYear()">
26 <AmOption v-for="year in headerYearOption" :key="year" :value="year" :label="year" />
27 </AmSelect>
28
29 <AmButtonGroup>
30 <AmButton
31 aria-label="previous month"
32 category="secondary"
33 type="plain"
34 size="medium"
35 icon-only
36 :icon="AmIconArrowLeft"
37 @click="prevMonth"
38 />
39 <AmButton
40 aria-label="next month"
41 category="secondary"
42 type="plain"
43 size="medium"
44 icon-only
45 :icon="AmIconArrowRight"
46 @click="nextMonth"
47 />
48 </AmButtonGroup>
49 </div>
50
51 <div class="am-advsc__duration">
52 <AmSelect
53 v-if="calendarServiceDurations.length > 1"
54 v-model="calendarServiceDuration"
55 @change="selectDuration"
56 >
57 <AmOption
58 v-for="item in calendarServiceDurations"
59 :key="item.duration"
60 :label="
61 useSecondsToDuration(item.duration, amLabels.h, amLabels.min) +
62 (item.priceLabel === '' ? '' : '(' + item.priceLabel + ')' + taxText)
63 "
64 :value="item.duration"
65 >
66 <AmOptionTemplate1
67 :identifier="item.duration"
68 :label="useSecondsToDuration(item.duration, amLabels.h, amLabels.min)"
69 :price-string="`${item.priceLabel} ${taxText}`"
70 icon-string="clock"
71 ></AmOptionTemplate1>
72 </AmOption>
73 </AmSelect>
74 </div>
75
76 <div v-if="props.timeZone" class="am-advsc__time-zone">
77 <span>{{ timeZoneString }}</span>
78 </div>
79
80 <div
81 v-if="props.periodPricing && props.periodPricing.price && props.showEstimatedPricing"
82 class="am-advsc__period-pricing"
83 >
84 <span v-if="props.periodPricing.price.low !== null" class="period-pricing-low"
85 >{{ useFormattedPrice(props.periodPricing.price.low)
86 }}{{ !props.periodPricing.price.uniqueMin ? '+' : '' }}</span
87 >
88 <span v-if="props.periodPricing.price.mid !== null" class="period-pricing-mid"
89 >{{ useFormattedPrice(props.periodPricing.price.mid)
90 }}{{ !props.periodPricing.price.uniqueMid ? '+' : '' }}</span
91 >
92 <span v-if="props.periodPricing.price.high !== null" class="period-pricing-high"
93 >{{ useFormattedPrice(props.periodPricing.price.high)
94 }}{{ !props.periodPricing.price.uniqueMax ? '+' : '' }}</span
95 >
96 </div>
97
98 <FullCalendar
99 ref="advCalendarRef"
100 class="am-advsc"
101 :am-mobile="checkScreen"
102 :am-mobile-s="mobileS"
103 :options="options"
104 ></FullCalendar>
105
106 <div v-if="shouldShowSlots" ref="slotsWrapper" class="am-advsc__slots-wrapper">
107 <div class="am-advsc__slots-heading">
108 {{ slotsHeading }}
109 </div>
110 <div class="am-advsc__slots">
111 <!-- Unified chronological list (regular + waiting) -->
112 <div
113 v-for="slotObj in mergedSlots"
114 :key="(slotObj.waiting ? 'wl-' : 'std-') + slotObj.time"
115 class="am-advsc__slots-item"
116 :class="[
117 {
118 'am-advsc__slots-item__selected': calendarEventSlot === slotObj.time,
119 'am-advsc__slots-item__low':
120 calendarEventDate &&
121 props.periodPricing &&
122 props.periodPricing.dates &&
123 calendarEventDate in props.periodPricing.dates &&
124 slotObj.time in props.periodPricing.dates[calendarEventDate].slots &&
125 props.periodPricing.dates[calendarEventDate].slots[slotObj.time].type === 'low',
126 'am-advsc__slots-item__high':
127 calendarEventDate &&
128 props.periodPricing &&
129 props.periodPricing.dates &&
130 calendarEventDate in props.periodPricing.dates &&
131 slotObj.time in props.periodPricing.dates[calendarEventDate].slots &&
132 props.periodPricing.dates[calendarEventDate].slots[slotObj.time].type === 'high',
133 },
134 { 'am-advsc__slots-item-mobile': checkScreen },
135 {
136 'am-advsc__slots-item-disabled':
137 calendarEventBusySlots.includes(slotObj.time) &&
138 props.showBusySlots &&
139 !calendarEventSlots.includes(slotObj.time) &&
140 !slotObj.waiting,
141 },
142 { 'am-width-full': props.showSlotPricing },
143 { 'am-advsc__slots-item-waiting': slotObj.waiting },
144 ]"
145 tabindex="0"
146 @click="
147 !slotObj.waiting &&
148 props.showBusySlots &&
149 calendarEventBusySlots.includes(slotObj.time) &&
150 !calendarEventSlots.includes(slotObj.time)
151 ? null
152 : slotSelected(slotObj)
153 "
154 @keydown.enter="
155 !slotObj.waiting &&
156 props.showBusySlots &&
157 calendarEventBusySlots.includes(slotObj.time) &&
158 !calendarEventSlots.includes(slotObj.time)
159 ? null
160 : slotSelected(slotObj)
161 "
162 >
163 <div
164 class="am-advsc__slots-item__inner"
165 :class="{ 'am-contains-waiting-tag': slotObj.waiting && props.showPeopleWaiting }"
166 >
167 {{
168 `${getFrontedFormattedTime(slotObj.time)} ${
169 props.endTime
170 ? ' - ' + getFrontedFormattedTime(addSeconds(slotObj.time, calendarSlotDuration))
171 : ''
172 }`
173 }}
174
175 <span
176 v-if="
177 props.showSlotPricing &&
178 calendarEventDate &&
179 props.periodPricing &&
180 props.periodPricing.dates &&
181 calendarEventDate in props.periodPricing.dates &&
182 slotObj.time in props.periodPricing.dates[calendarEventDate].slots &&
183 props.periodPricing.dates[calendarEventDate].slots[slotObj.time].price !== null
184 "
185 >
186 {{
187 useFormattedPrice(
188 props.periodPricing.dates[calendarEventDate].slots[slotObj.time].price,
189 )
190 }}
191 </span>
192 <span
193 v-if="slotObj.waiting && props.showPeopleWaiting"
194 class="am-advsc__waiting-tag am-icon-clock"
195 >
196 {{ props.labelWaitingList + ' (' + slotObj.peopleWaiting + ')' }}
197 </span>
198 </div>
199 </div>
200 </div>
201 </div>
202 <div
203 v-if="isDateSelected && !calendarEventSlots.length && !waitingListDisplayTimes.length"
204 role="status"
205 aria-live="polite"
206 aria-atomic="true"
207 style="text-align: center"
208 >
209 {{ props.labelSlotsSelected }}
210 </div>
211 <div class="am-sr-only" role="status" aria-live="polite" aria-atomic="true">
212 {{ selectionAnnouncement }}
213 </div>
214 </div>
215 </template>
216
217 <script setup>
218 // * Import Libraries
219 import moment from 'moment'
220 import FullCalendar from '@fullcalendar/vue3'
221 import dayGridPlugin from '@fullcalendar/daygrid'
222 import interactionPlugin from '@fullcalendar/interaction'
223 import allLocales from '@fullcalendar/core/locales-all'
224
225 // * Import _components
226 import AmSelect from '../select/AmSelect.vue'
227 import AmOption from '../select/AmOption.vue'
228 import AmButtonGroup from '../button/AmButtonGroup'
229 import AmButton from '../button/AmButton.vue'
230 import AmIconArrowRight from '../icons/IconArrowRight.vue'
231 import AmIconArrowLeft from '../icons/IconArrowLeft.vue'
232 import AmOptionTemplate1 from '../../_components/select/parts/AmOptionTemplate1.vue'
233
234 // * Import from Vue
235 import { ref, reactive, onBeforeMount, inject, computed, watch, onMounted, nextTick } from 'vue'
236
237 // * Composables
238 import {
239 months,
240 getFrontedFormattedTime,
241 getFrontedFormattedDate,
242 addSeconds,
243 getFirstDayOfWeek,
244 useSecondsToDuration,
245 } from '../../../assets/js/common/date'
246 import { shortLocale } from '../../../plugins/settings.js'
247 import { useColorTransparency } from '../../../assets/js/common/colorManipulation'
248 import { useScrollTo } from '../../../assets/js/common/scrollElements.js'
249 import { useSortedTimeStrings } from '../../../assets/js/common/helper.js'
250 import { useFormattedPrice } from '../../../assets/js/common/formatting'
251
252 /**
253 * Component Props
254 * @type {Readonly<ExtractPropTypes<{}>>}
255 */
256 const props = defineProps({
257 initialView: {
258 type: String,
259 default: 'dayGridMonth',
260 },
261 periodPricing: {
262 type: Object,
263 default: () => {},
264 },
265 weekDaysVisibility: {
266 type: Boolean,
267 default: true,
268 },
269 weekDaysEnabled: {
270 type: Boolean,
271 default: true,
272 },
273 notMultiple: {
274 type: Boolean,
275 default: true,
276 },
277 calendarMinimumDate: {
278 type: String,
279 default: '',
280 },
281 calendarMaximumDate: {
282 type: String,
283 default: '',
284 },
285 id: {
286 type: Number,
287 default: 0,
288 },
289 serviceId: {
290 type: Number,
291 default: 0,
292 },
293 date: {
294 type: String,
295 default: '',
296 },
297 endTime: {
298 type: Boolean,
299 default: true,
300 },
301 timeZone: {
302 type: Boolean,
303 default: true,
304 },
305 showBusySlots: {
306 type: Boolean,
307 default: false,
308 },
309 showCalendarDateBusyness: {
310 type: Boolean,
311 default: true,
312 },
313 showEstimatedPricing: {
314 type: Boolean,
315 default: false,
316 },
317 showIndicatorPricing: {
318 type: Boolean,
319 default: false,
320 },
321 showSlotPricing: {
322 type: Boolean,
323 default: false,
324 },
325 showPeopleWaiting: {
326 type: Boolean,
327 default: true,
328 },
329 labelSlotsSelected: {
330 type: String,
331 default: '',
332 },
333 labelWaitingList: {
334 type: String,
335 default: '',
336 },
337 nestedItem: {
338 type: Object,
339 default: () => {
340 return {
341 inCollapse: false,
342 }
343 },
344 },
345 busyness: {
346 type: Object,
347 default: () => {
348 return {}
349 },
350 },
351 taxVisibility: {
352 type: Boolean,
353 default: false,
354 },
355 taxLabel: {
356 type: String,
357 default: '',
358 },
359 taxLabelIncl: {
360 type: String,
361 default: '',
362 },
363 })
364
365 let slotsWrapper = ref(null)
366 let containerWrapper = inject('formWrapper', ref(null))
367
368 // timeZoneString: this.$root.settings.general.showClientTimeZone ? Intl.DateTimeFormat().resolvedOptions().timeZone : this.$root.settings.wordpress.timezone
369 const amSettings = inject('settings')
370 let timeZoneString = ref(
371 amSettings.general.showClientTimeZone
372 ? Intl.DateTimeFormat().resolvedOptions().timeZone
373 : amSettings.wordpress.timezone,
374 )
375
376 let taxText = computed(() => {
377 if (props.taxVisibility) {
378 if (amSettings.payments.taxes.excluded) {
379 return props.taxLabel
380 } else {
381 return props.taxLabelIncl
382 }
383 }
384
385 return ''
386 })
387
388 // * Calendar Reference
389 const advCalendarRef = ref(null)
390
391 let isDateSelected = ref(false)
392
393 // * Duration
394 let calendarServiceDurations = inject('calendarServiceDurations', [])
395
396 // * Labels
397 let amLabels = inject('labels')
398
399 // * Calendar Header
400 // Month
401 let calMonth = ref('')
402
403 let headerMonthOptions = ref([])
404
405 // Year
406 let calYear = ref(1)
407 let headerYearOption = reactive([])
408
409 function selectDuration(duration) {
410 emits('selectedDuration', duration)
411 }
412
413 function getYearMonthString() {
414 return moment(advCalendarRef.value.getApi().getDate()).format('YYYY-MM')
415 }
416
417 function getHeaderMonths(year) {
418 let minimumDate = moment(props.calendarMinimumDate, 'YYYY-MM-DD HH:mm')
419
420 let maximumDate = moment(props.calendarMaximumDate, 'YYYY-MM-DD HH:mm')
421
422 let availableMonths = []
423
424 for (let i = 1; i <= 12; i++) {
425 let monthString = i < 10 ? '0'.concat(i.toString()) : i.toString()
426
427 let monthStartDate = moment(year + '-' + monthString + '-01', 'YYYY-MM-DD')
428 let monthEndDate = moment(year + '-' + monthString + '-01', 'YYYY-MM-DD').endOf('month')
429
430 availableMonths.push({
431 value: monthString,
432 label: months[i - 1],
433 available:
434 minimumDate.isSameOrBefore(monthEndDate) && maximumDate.isSameOrAfter(monthStartDate),
435 })
436 }
437
438 return availableMonths
439 }
440
441 function changeYear() {
442 let availableMonths = getHeaderMonths(calYear.value)
443
444 advCalendarRef.value
445 .getApi()
446 .gotoDate(calYear.value + '-' + availableMonths.filter((i) => i.available)[0].value + '-01')
447
448 emits('changedMonth', getYearMonthString())
449 }
450
451 function changeMonth() {
452 advCalendarRef.value.getApi().gotoDate(calYear.value + '-' + calMonth.value + '-01')
453
454 emits('changedMonth', getYearMonthString())
455 }
456
457 function prevMonth() {
458 advCalendarRef.value.getApi().prev()
459
460 emits('changedMonth', getYearMonthString())
461 }
462
463 function nextMonth() {
464 advCalendarRef.value.getApi().next()
465
466 emits('changedMonth', getYearMonthString())
467 }
468
469 function renderedMonth(data) {
470 emits('renderedMonth', {
471 start: moment(data.start).format('YYYY-MM-DD'),
472 end: moment(data.end).format('YYYY-MM-DD'),
473 yearMonth: getYearMonthString(),
474 })
475 }
476
477 let calendarStartDate = inject('calendarStartDate')
478
479 // * Amelia Calendar Options
480 const options = ref({
481 initialDate: calendarStartDate.value,
482 plugins: [dayGridPlugin, interactionPlugin],
483 initialView: props.initialView,
484 dayCellDidMount: function (info) {
485 if (!info.el.classList.contains('am-advsc__dayGridMonth-disabled')) {
486 info.el.setAttribute('tabindex', '0')
487 info.el.setAttribute('aria-label', `Select date ${info.date.toDateString()}`)
488 info.el.setAttribute('role', 'button')
489 info.el.setAttribute('aria-describedby', 'calendar-instructions')
490 }
491
492 // Enhanced keyboard navigation
493 info.el.addEventListener('keydown', (e) => {
494 const calendarApi = info.view.calendar
495
496 if (e.key === 'Enter' || e.keyCode === 13 || e.key === ' ') {
497 e.preventDefault()
498
499 // Check if this date cell is disabled
500 if (info.el.classList.contains('am-advsc__dayGridMonth-disabled')) {
501 return
502 }
503
504 // Use moment to format the date the same way as mouse clicks do
505 // This avoids timezone issues with toISOString()
506 const localDateStr = moment(info.date).format('YYYY-MM-DD')
507
508 // Build the event object manually (as FullCalendar would)
509 const dateClickEvent = {
510 date: info.date,
511 dateStr: localDateStr, // Use local date string instead of ISO string
512 allDay: true,
513 dayEl: info.el,
514 jsEvent: e,
515 view: info.view,
516 }
517
518 // First trigger dateClick
519 calendarApi.trigger('dateClick', dateClickEvent)
520
521 // Then check for events on this date and trigger eventClick
522 const events = calendarApi.getEvents()
523
524 // Find events that occur on this specific date
525 const matchingEvents = events.filter((event) => {
526 if (!event.start) return false
527 // Use moment to format the event start date in the same way
528 const eventStartDate = moment(event.start).format('YYYY-MM-DD')
529 return eventStartDate === localDateStr
530 })
531
532 // If there are events on this date, trigger eventClick for the first one
533 if (matchingEvents.length > 0) {
534 const eventClickData = {
535 el: info.el,
536 event: matchingEvents[0],
537 jsEvent: e,
538 view: info.view,
539 }
540
541 // Trigger the eventClick handler
542 calendarApi.trigger('eventClick', eventClickData)
543 }
544 }
545 })
546
547 // Add focus and blur event handlers for better visual feedback
548 info.el.addEventListener('focus', () => {
549 if (!info.el.classList.contains('am-advsc__dayGridMonth-disabled')) {
550 info.el.style.outline = '1px solid var(--am-c-cal-init)'
551 info.el.style.outlineOffset = '-2px'
552 info.el.style.borderRadius = '6px'
553 }
554 })
555
556 info.el.addEventListener('blur', () => {
557 info.el.style.outline = ''
558 info.el.style.outlineOffset = ''
559 info.el.style.borderRadius = ''
560 })
561 },
562 headerToolbar: false,
563 views: {
564 dayGridMonth: {},
565 dayGridWeek: {},
566 },
567 slotLabelFormat: {
568 hour: 'numeric',
569 minute: '2-digit',
570 hour12: false,
571 },
572 eventTimeFormat: {
573 hour: 'numeric',
574 minute: '2-digit',
575 hour12: false,
576 },
577 aspectRatio: 1.45,
578 firstDay: getFirstDayOfWeek(),
579 dayMaxEvents: true,
580 selectLongPressDelay: 0,
581 dayHeaderClassNames: calendarDayHeaderClassBuilder,
582 dayCellClassNames: calendarDayClassBuilder,
583 // dayCellContent: calendarDayCellContent,
584 dateClick: calendarDateClick,
585 eventClassNames: 'am-advsc__occupancy',
586 eventContent: calendarEventContent,
587 eventClick: calendarEventClick,
588 // weekends: true,
589 // hiddenDays: [0, 6],
590 weekends: props.weekDaysVisibility,
591 events: inject('calendarEvents'),
592 datesSet: renderedMonth,
593 locales: allLocales,
594 locale: shortLocale,
595 })
596
597 let cWidth = inject('containerWidth', 0)
598 let checkScreen = computed(() => cWidth.value < 560 || (cWidth.value > 560 && cWidth.value < 640))
599 let mobileS = computed(() => cWidth.value < 340)
600
601 const emits = defineEmits([
602 'selectedDate',
603 'selectedTime',
604 'renderedMonth',
605 'changedMonth',
606 'selectedDuration',
607 'unselectDate',
608 'waitingListSlot',
609 ])
610
611 let calendarEventSlots = inject('calendarEventSlots')
612
613 let calendarEventDate = inject('calendarEventDate')
614
615 let calendarEventBusySlots = inject('calendarEventBusySlots', [])
616
617 // Waiting list times for selected date
618 let calendarWaitingListTimes = inject('calendarWaitingListTimes', ref([]))
619
620 let calendarEventSlot = inject('calendarEventSlot')
621
622 let calendarChangeSideBar = inject('calendarChangeSideBar', ref({}))
623
624 let calendarSlotDuration = inject('calendarSlotDuration')
625
626 let calendarServiceDuration = inject('calendarServiceDuration')
627
628 let calendarWaitingListSlots = inject('calendarWaitingListSlots')
629
630 // * Slots heading
631 let slotsHeading = ref(props.date ? getFrontedFormattedDate(props.date) : '')
632
633 const selectionAnnouncement = computed(() => {
634 if (!isDateSelected.value) {
635 return ''
636 }
637
638 if (slotsHeading.value) {
639 return slotsHeading.value
640 }
641
642 if (calendarEventDate?.value && calendarEventSlot?.value) {
643 return `${getFrontedFormattedDate(calendarEventDate.value)} - ${getFrontedFormattedTime(calendarEventSlot.value)}`
644 }
645
646 if (calendarEventDate?.value) {
647 return getFrontedFormattedDate(calendarEventDate.value)
648 }
649
650 return ''
651 })
652
653 // Computed arrays for display
654 const combinedStandardSlots = computed(() =>
655 useSortedTimeStrings([
656 ...new Set(
657 calendarEventSlots.value.concat(props.showBusySlots ? calendarEventBusySlots.value : []),
658 ),
659 ]).filter((s) => !calendarWaitingListTimes.value.includes(s)),
660 )
661
662 const waitingListDisplayTimes = computed(() => useSortedTimeStrings(calendarWaitingListTimes.value))
663
664 // Merge standard and waiting list slots chronologically while preserving status
665 const mergedSlots = computed(() => {
666 // Helper: extract people waiting for a given time on selected date
667 const getPeopleWaiting = (time) => {
668 try {
669 const dateKey = calendarEventDate?.value
670 ? moment(calendarEventDate.value).format('YYYY-MM-DD')
671 : null
672 if (!dateKey) return 0
673
674 const dayObj = calendarWaitingListSlots?.value?.[dateKey]
675 if (!dayObj) return 0
676
677 const slotArr = dayObj[time]
678 if (!Array.isArray(slotArr) || !slotArr.length) return 0
679
680 const rec = slotArr[0]
681 const w = rec && (typeof rec.w === 'number' ? rec.w : Number(rec.w))
682 return Number.isFinite(w) ? w : 0
683 } catch (e) {
684 return 0
685 }
686 }
687
688 const std = combinedStandardSlots.value.map((t) => ({
689 time: t,
690 waiting: false,
691 }))
692
693 const wl = waitingListDisplayTimes.value.map((t) => ({
694 time: t,
695 waiting: true,
696 peopleWaiting: getPeopleWaiting(t),
697 }))
698
699 return [...std, ...wl].sort((a, b) => a.time.localeCompare(b.time))
700 })
701
702 // Only show slots on iOS when a date is currently selected; other platforms unchanged
703 const shouldShowSlots = computed(() => {
704 if (iOS()) {
705 return (
706 isDateSelected.value &&
707 (calendarEventSlots.value.length || waitingListDisplayTimes.value.length)
708 )
709 }
710
711 return calendarEventSlots.value.length || waitingListDisplayTimes.value.length
712 })
713
714 /**
715 * Html class builder for calendar header day cell
716 * @param data
717 * @returns {string[]}
718 */
719 function calendarDayHeaderClassBuilder(data) {
720 let classCollector = [`am-advsc__${data.view.type}-header-cell`]
721
722 // * Week days class
723 if (data.date.getDay() === 0 || data.date.getDay() === 6) {
724 classCollector.push(`am-advsc__${data.view.type}-header-weekend`)
725 }
726
727 return classCollector
728 }
729
730 watch(calendarStartDate, () => {
731 const advCalendar = advCalendarRef.value.getApi()
732
733 advCalendar.gotoDate(calendarStartDate.value)
734 })
735
736 // iOS: ensure first available slot auto-selects once slots load, and keep heading in sync
737 watch(calendarEventSlots, (val) => {
738 if (!iOS() || !isDateSelected.value) return
739
740 const hasSlots = Array.isArray(val) && val.length > 0
741
742 if (!hasSlots) {
743 slotsHeading.value = ''
744 return
745 }
746
747 const currentDate = calendarEventDate?.value || null
748 const needsSelection = !calendarEventSlot.value || !val.includes(calendarEventSlot.value)
749
750 if (needsSelection) {
751 calendarEventSlot.value = val[0]
752 emits('selectedTime', calendarEventSlot.value)
753 }
754
755 const headingDate = currentDate ? getFrontedFormattedDate(currentDate) : currentDate
756 slotsHeading.value =
757 headingDate +
758 (calendarEventSlot.value && val.includes(calendarEventSlot.value)
759 ? ' - ' + getFrontedFormattedTime(calendarEventSlot.value)
760 : '')
761 })
762
763 watch(
764 () => ({
765 showCalendarDateBusyness: props.showCalendarDateBusyness,
766 busyness: props.busyness,
767 }),
768 () => {
769 nextTick(() => {
770 advCalendarRef.value?.getApi()?.render()
771 })
772 },
773 { deep: true },
774 )
775
776 /**
777 * Html class builder for calendar day cell
778 * @param data
779 * @returns {*[]}
780 */
781 function calendarDayClassBuilder(data) {
782 let classCollector = [`am-advsc__${data.view.type}-cell`]
783
784 // * Week days class
785 if (!props.weekDaysEnabled && (data.date.getDay() === 0 || data.date.getDay() === 6)) {
786 classCollector.push(`am-advsc__${data.view.type}-weekend`)
787 }
788
789 let dateString = moment(data.date).format('YYYY-MM-DD')
790
791 // * Determine which day has slots => [] - no slots || [...] has slots
792 let eventIdentifier = options.value.events.filter(
793 (item) => moment(item.start).format('YYYY-MM-DD') === dateString,
794 )
795
796 // TODO - this condition should be refactorized after implementation in the system
797 // * Disabled class
798 if (
799 (eventIdentifier.length &&
800 'slotsAvailable' in eventIdentifier[0].extendedProps &&
801 !eventIdentifier[0].extendedProps.slotsAvailable) ||
802 !eventIdentifier.length
803 ) {
804 classCollector.push(`am-advsc__${data.view.type}-disabled`)
805 } else if (
806 props.periodPricing &&
807 props.periodPricing.dates &&
808 dateString in props.periodPricing.dates &&
809 (props.periodPricing.dates[dateString].type === 'low' ||
810 props.periodPricing.dates[dateString].type === 'high')
811 ) {
812 classCollector.push(`am-advsc__${data.view.type}-` + props.periodPricing.dates[dateString].type)
813 }
814
815 if (props.date && props.date === moment(data.date).format('YYYY-MM-DD')) {
816 classCollector.push(`am-advsc__${data.view.type}-selected`)
817 }
818
819 return classCollector
820 }
821
822 /**
823 * Click on Day Cell handler
824 * @param data
825 */
826
827 // * Guard repeated iOS date-clicks fired twice for a single tap
828 let lastIOSDateClick = ref({ date: '', ts: 0 })
829
830 // * this function must be removed and its content should be placed inside the calendarDateClick function after releasing the event calendar
831 function calendarDateClickFunctionality(data) {
832 const advCalendar = advCalendarRef.value.getApi()
833 let advCalendarNode = advCalendarRef.value.$el
834
835 const advCalendarType = advCalendar.currentData.currentViewType
836
837 // * Adding or Removing html class that determines selection state
838 let selectedDayClass = `am-advsc__${advCalendarType}-selected`
839 let disabledDayClass = `am-advsc__${advCalendarType}-disabled`
840
841 if (data.dayEl.classList.contains(disabledDayClass)) {
842 return
843 }
844
845 if (data.dayEl.classList.contains(selectedDayClass)) {
846 isDateSelected.value = false
847
848 data.dayEl.classList.remove(selectedDayClass)
849
850 slotsHeading.value = ''
851
852 // Only force-clear slots immediately on iOS; other platforms keep existing flow
853 if (iOS()) {
854 calendarEventSlots.value = []
855 calendarEventSlot.value = ''
856 calendarEventBusySlots.value = []
857 }
858
859 emits('unselectDate')
860 } else {
861 isDateSelected.value = true
862
863 emits('selectedDate', data.dateStr)
864
865 if (calendarEventSlot.value && !calendarEventSlots.value.includes(calendarEventSlot.value)) {
866 calendarEventSlot.value = calendarEventSlots.value[0]
867 emits('selectedTime', calendarEventSlot.value)
868 } else if (
869 calendarEventSlot.value &&
870 calendarEventSlots.value.includes(calendarEventSlot.value)
871 ) {
872 } else if (calendarEventSlots.value.length) {
873 calendarEventSlot.value = calendarEventSlots.value[0]
874 emits('selectedTime', calendarEventSlot.value)
875 }
876
877 slotsHeading.value =
878 (data.dateStr ? getFrontedFormattedDate(data.dateStr) : data.dateStr) +
879 (calendarEventSlots.value.includes(calendarEventSlot.value)
880 ? ' - ' + getFrontedFormattedTime(calendarEventSlot.value)
881 : '')
882
883 if (advCalendarNode.querySelectorAll(`.${selectedDayClass}`).length) {
884 for (let i = 0; i < advCalendarNode.querySelectorAll(`.${selectedDayClass}`).length; i++) {
885 advCalendarNode
886 .querySelectorAll(`.${selectedDayClass}`)
887 [i].classList.remove(selectedDayClass)
888 }
889 }
890
891 if (!data.dayEl.classList.contains(disabledDayClass)) {
892 data.dayEl.classList.add(selectedDayClass)
893 }
894
895 if (iOS()) {
896 setTimeout(() => {
897 if (containerWrapper.value && calendarEventSlots.value.length && slotsWrapper.value) {
898 useScrollTo(containerWrapper.value, slotsWrapper.value, 20, 300, props.nestedItem)
899 }
900 }, 500)
901 } else {
902 if (containerWrapper.value && calendarEventSlots.value.length && slotsWrapper.value) {
903 useScrollTo(containerWrapper.value, slotsWrapper.value, 20, 300, props.nestedItem)
904 }
905 }
906 }
907
908 let selectedSlot = {
909 reference: 'slot',
910 position: 1,
911 value: '',
912 }
913 selectedSlot.value = slotsHeading.value ? `${slotsHeading.value}` : ''
914 if (!props.notMultiple && props.date) {
915 selectedSlot.reference = 'package-slot ' + props.id + ' ' + props.serviceId
916 }
917
918 let isWaiting = waitingListDisplayTimes.value.includes(selectedSlot.value)
919 emits('waitingListSlot', isWaiting)
920
921 sidebarDataCollector(selectedSlot)
922 }
923
924 // * this function had to return to its previous state after event calendar release
925 function calendarDateClick(data) {
926 if (iOS()) {
927 const now = Date.now()
928 const last = lastIOSDateClick.value
929
930 // Ignore the second click if iOS fires two events for a single tap
931 if (last.date === data.dateStr && now - last.ts < 250) {
932 return
933 }
934
935 lastIOSDateClick.value = { date: data.dateStr, ts: now }
936
937 // Give eventClick/emit time to propagate state before painting selection
938 return setTimeout(() => {
939 calendarDateClickFunctionality(data)
940 }, 50)
941 }
942
943 setTimeout(() => calendarDateClickFunctionality(data), 50)
944 }
945
946 function iOS() {
947 return (
948 ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'].includes(
949 navigator.platform,
950 ) ||
951 // iPad on iOS 13 detection
952 (navigator.userAgent.includes('Mac') && 'ontouchend' in document)
953 )
954 }
955
956 /**
957 * Click on Event handler
958 * @param eventData
959 */
960 function calendarEventClick(eventData) {
961 emits('selectedDate', moment(eventData.event.start).format('YYYY-MM-DD'))
962 }
963
964 /**
965 * Calendar Event Content Block render function
966 * @param eventCalendarData
967 * @returns {{html: string}}
968 */
969 function calendarEventContent(eventCalendarData) {
970 if (!props.showCalendarDateBusyness) {
971 return { html: '' }
972 }
973
974 const slotsAvailablePercentage =
975 props.busyness[moment(eventCalendarData.event.start).format('YYYY-MM-DD')]
976
977 if (slotsAvailablePercentage === undefined || slotsAvailablePercentage === null) {
978 return { html: '' }
979 }
980
981 const eventContent = `<div class="am-advsc__slot-wrapper" style="height: ${slotsAvailablePercentage}%"></div>`
982
983 return { html: eventContent }
984 }
985
986 const { sidebarDataCollector } = inject('sidebarStepsFunctions', {
987 sidebarDataCollector: () => {},
988 })
989
990 // * Click on slot
991 function slotSelected(slot) {
992 if (calendarChangeSideBar.value) {
993 let selectedSlot = {
994 reference: 'slot',
995 // position will depend on fields order
996 position: 1,
997 value: '',
998 }
999
1000 selectedSlot.value = !slotsHeading.value.includes(' - ')
1001 ? slotsHeading.value + ' - ' + getFrontedFormattedTime(slot.time)
1002 : slotsHeading.value.split(' - ')[0] + ' - ' + getFrontedFormattedTime(slot.time)
1003
1004 slotsHeading.value = selectedSlot.value
1005
1006 if (!props.notMultiple && props.date) {
1007 selectedSlot.reference = 'package-slot ' + props.id + ' ' + props.serviceId
1008 }
1009
1010 sidebarDataCollector(selectedSlot)
1011 }
1012
1013 calendarEventSlot.value = slot.time
1014
1015 let isWaiting = waitingListDisplayTimes.value.includes(slot.time)
1016
1017 // Get the first qualifying provider ID for waiting list slot
1018 let selectedProviderId = null
1019 if (isWaiting && slot.waiting) {
1020 const dateKey = calendarEventDate?.value
1021 ? moment(calendarEventDate.value).format('YYYY-MM-DD')
1022 : null
1023 if (dateKey && calendarWaitingListSlots?.value?.[dateKey]?.[slot.time]) {
1024 const slotProviders = calendarWaitingListSlots.value[dateKey][slot.time]
1025 if (slotProviders && slotProviders.length > 0) {
1026 // Get first qualifying provider
1027 const qualifyingProvider = slotProviders.find((p) => {
1028 const isFullyBooked = p.c <= 0
1029 const hasWaitingSpace = !p.maxWaitingCapacity || (p.w || 0) < p.maxWaitingCapacity
1030 return isFullyBooked && hasWaitingSpace
1031 })
1032 if (qualifyingProvider) {
1033 selectedProviderId = qualifyingProvider.e
1034 }
1035 }
1036 }
1037 }
1038
1039 emits('waitingListSlot', isWaiting, slot.waiting ? slot.peopleWaiting : 0, selectedProviderId)
1040
1041 emits('selectedTime', slot.time)
1042 }
1043
1044 window.addEventListener('resize', resize)
1045 // * resize function
1046 function resize() {
1047 if (advCalendarRef.value) {
1048 advCalendarRef.value.options.aspectRatio = mobileS.value ? 1 : checkScreen.value ? 1.2 : 1.45
1049 advCalendarRef.value.getApi().render()
1050
1051 if (shortLocale === 'zh' || shortLocale === 'ja') {
1052 document.querySelectorAll('.fc-daygrid-day-number').forEach((el) => {
1053 const ariaLabel = el.getAttribute('aria-label')
1054 if (ariaLabel) {
1055 // Match all digit groups: ["2025", "9", "18"]
1056 const numbers = ariaLabel.match(/\d+/g)
1057 if (numbers && numbers.length) {
1058 const day = numbers[numbers.length - 1] // take the last one → "18"
1059 setTimeout(() => {
1060 el.textContent = day
1061 }, 500)
1062 }
1063 }
1064 })
1065 }
1066 }
1067 }
1068
1069 // * Color Vars
1070 let amColors = inject('amColors', {
1071 amColors: {
1072 value: {
1073 colorPrimary: '#1246D6',
1074 colorSuccess: '#019719',
1075 colorError: '#B4190F',
1076 colorWarning: '#CCA20C',
1077 colorMainBgr: '#FFFFFF',
1078 colorMainHeadingText: '#33434C',
1079 colorMainText: '#1A2C37',
1080 colorSbBgr: '#17295A',
1081 colorSbText: '#FFFFFF',
1082 colorInpBgr: '#FFFFFF',
1083 colorInpBorder: '#D1D5D7',
1084 colorInpText: '#1A2C37',
1085 colorInpPlaceHolder: '#1A2C37',
1086 colorDropBgr: '#FFFFFF',
1087 colorDropBorder: '#D1D5D7',
1088 colorDropText: '#0E1920',
1089 colorCalCell: '#1246D6',
1090 colorCalCellText: '#1246D6',
1091 colorCalCellLow: '#1246D6',
1092 colorCalCellLowText: '#1246D6',
1093 colorCalCellHigh: '#1246D6',
1094 colorCalCellHighText: '#1246D6',
1095 colorCalCellSelected: '#1246D6',
1096 colorCalCellSelectedText: '#FFFFFF',
1097 colorCalCellDisabled: '#B4190F',
1098 colorCalCellDisabledText: '#1A2C37',
1099 colorBtnPrim: '#265CF2',
1100 colorBtnPrimText: '#FFFFFF',
1101 colorBtnSec: '#1A2C37',
1102 colorBtnSecText: '#FFFFFF',
1103 },
1104 },
1105 })
1106
1107 const cssVars = computed(() => {
1108 return {
1109 '--am-c-cal-indicator':
1110 'showIndicatorPricing' in props && props.showIndicatorPricing ? 'visible' : 'hidden',
1111 '--am-c-cal-low':
1112 'colorCalCellLow' in amColors.value
1113 ? amColors.value.colorCalCellLow
1114 : amColors.value.colorCalCell,
1115 '--am-c-cal-high':
1116 'colorCalCellHigh' in amColors.value
1117 ? amColors.value.colorCalCellHigh
1118 : amColors.value.colorCalCell,
1119 '--am-c-cal-low-text':
1120 'colorCalCellLowText' in amColors.value
1121 ? amColors.value.colorCalCellLowText
1122 : amColors.value.colorCalCellText,
1123 '--am-c-cal-high-text':
1124 'colorCalCellHighText' in amColors.value
1125 ? amColors.value.colorCalCellHighText
1126 : amColors.value.colorCalCellText,
1127 '--am-c-cal-low-op10': useColorTransparency(
1128 'colorCalCellLow' in amColors.value
1129 ? amColors.value.colorCalCellLow
1130 : amColors.value.colorCalCell,
1131 0.1,
1132 ),
1133 '--am-c-cal-low-op20': useColorTransparency(
1134 'colorCalCellLow' in amColors.value
1135 ? amColors.value.colorCalCellLow
1136 : amColors.value.colorCalCell,
1137 0.2,
1138 ),
1139 '--am-c-cal-waiting':
1140 'colorCalCellWaiting' in amColors.value
1141 ? amColors.value.colorCalCellWaiting
1142 : amColors.value.colorMainBgr,
1143 '--am-c-cal-waiting-text':
1144 'colorCalCellWaitingText' in amColors.value
1145 ? amColors.value.colorCalCellWaitingText
1146 : amColors.value.colorMainText,
1147 '--am-c-cal-waiting-op40':
1148 'colorCalCellWaitingText' in amColors.value
1149 ? useColorTransparency(amColors.value.colorCalCellWaitingText, 0.4)
1150 : useColorTransparency(amColors.value.colorMainText, 0.4),
1151 '--am-c-cal-low-op30': useColorTransparency(
1152 'colorCalCellLow' in amColors.value
1153 ? amColors.value.colorCalCellLow
1154 : amColors.value.colorCalCell,
1155 0.3,
1156 ),
1157 '--am-c-cal-low-op60': useColorTransparency(
1158 'colorCalCellLow' in amColors.value
1159 ? amColors.value.colorCalCellLow
1160 : amColors.value.colorCalCell,
1161 0.6,
1162 ),
1163 '--am-c-cal-low-op80': useColorTransparency(
1164 'colorCalCellLow' in amColors.value
1165 ? amColors.value.colorCalCellLow
1166 : amColors.value.colorCalCell,
1167 0.8,
1168 ),
1169 '--am-c-cal-high-op10': useColorTransparency(
1170 'colorCalCellHigh' in amColors.value
1171 ? amColors.value.colorCalCellHigh
1172 : amColors.value.colorCalCell,
1173 0.1,
1174 ),
1175 '--am-c-cal-high-op20': useColorTransparency(
1176 'colorCalCellHigh' in amColors.value
1177 ? amColors.value.colorCalCellHigh
1178 : amColors.value.colorCalCell,
1179 0.2,
1180 ),
1181 '--am-c-cal-high-op30': useColorTransparency(
1182 'colorCalCellHigh' in amColors.value
1183 ? amColors.value.colorCalCellHigh
1184 : amColors.value.colorCalCell,
1185 0.3,
1186 ),
1187 '--am-c-cal-high-op60': useColorTransparency(
1188 'colorCalCellHigh' in amColors.value
1189 ? amColors.value.colorCalCellHigh
1190 : amColors.value.colorCalCell,
1191 0.6,
1192 ),
1193 '--am-c-cal-high-op80': useColorTransparency(
1194 'colorCalCellHigh' in amColors.value
1195 ? amColors.value.colorCalCellHigh
1196 : amColors.value.colorCalCell,
1197 0.8,
1198 ),
1199 '--am-c-advsc-bgr-op10': useColorTransparency(amColors.value.colorMainText, 0.1),
1200 '--am-c-cal-init': amColors.value.colorCalCell,
1201 '--am-c-cal-init-text': amColors.value.colorCalCellText,
1202 '--am-c-cal-init-op10': useColorTransparency(amColors.value.colorCalCell, 0.1),
1203 '--am-c-cal-init-op20': useColorTransparency(amColors.value.colorCalCell, 0.2),
1204 '--am-c-cal-init-op30': useColorTransparency(amColors.value.colorCalCell, 0.3),
1205 '--am-c-cal-init-op60': useColorTransparency(amColors.value.colorCalCell, 0.6),
1206
1207 '--am-c-cal-selected': amColors.value.colorCalCellSelected,
1208 '--am-c-cal-selected-text': amColors.value.colorCalCellSelectedText,
1209 '--am-c-cal-selected-op80': useColorTransparency(amColors.value.colorCalCellSelected, 0.8),
1210
1211 '--am-c-cal-disabled': amColors.value.colorCalCellDisabled,
1212 '--am-c-cal-disabled-text': amColors.value.colorCalCellDisabledText,
1213 '--am-c-cal-disabled-op10': useColorTransparency(amColors.value.colorCalCellDisabled, 0.1),
1214 '--am-c-cal-disabled-op60': useColorTransparency(amColors.value.colorCalCellDisabled, 0.4),
1215 }
1216 })
1217
1218 /**
1219 * Lifecycle Hooks
1220 */
1221 onBeforeMount(() => {
1222 let minimumDate = moment(props.calendarMinimumDate, 'YYYY-MM-DD HH:mm')
1223 let maximumDate = moment(props.calendarMaximumDate, 'YYYY-MM-DD HH:mm')
1224
1225 let pastYears = parseInt(minimumDate.format('YYYY'))
1226 let futureYears = parseInt(maximumDate.format('YYYY'))
1227
1228 for (pastYears; pastYears <= futureYears; pastYears++) {
1229 headerYearOption.push(pastYears)
1230 }
1231
1232 let date = moment(calendarStartDate.value, 'YYYY-MM-DD')
1233
1234 calMonth.value = date.format('MM')
1235
1236 let year = parseInt(date.format('YYYY'))
1237
1238 headerMonthOptions.value = getHeaderMonths(year)
1239
1240 calYear.value = year
1241 })
1242
1243 onMounted(() => {
1244 if (advCalendarRef.value) {
1245 advCalendarRef.value.options.aspectRatio = mobileS.value ? 1 : checkScreen.value ? 1.2 : 1.45
1246
1247 if (shortLocale === 'zh' || shortLocale === 'ja') {
1248 document.querySelectorAll('.fc-daygrid-day-number').forEach((el) => {
1249 const ariaLabel = el.getAttribute('aria-label')
1250 if (ariaLabel) {
1251 // Match all digit groups: ["2025", "9", "18"]
1252 const numbers = ariaLabel.match(/\d+/g)
1253 if (numbers && numbers.length) {
1254 const day = numbers[numbers.length - 1] // take the last one → "18"
1255 setTimeout(() => {
1256 el.textContent = day
1257 }, 500)
1258 }
1259 }
1260 })
1261 }
1262 }
1263
1264 if (containerWrapper.value && calendarEventSlots.value.length && slotsWrapper.value) {
1265 let calendarHeight = advCalendarRef.value
1266 ? advCalendarRef.value.getApi().el.offsetHeight + 25
1267 : 20
1268 useScrollTo(containerWrapper.value, slotsWrapper.value, calendarHeight, 300, props.nestedItem)
1269 }
1270 })
1271 </script>
1272
1273 <script>
1274 export default {
1275 name: 'AmAdvancedCalendar',
1276 }
1277 </script>
1278
1279 <style lang="scss">
1280 //Amelia Calendar
1281 $amCalClass: am-advsc;
1282 @mixin am-advsc-block {
1283 .am-advsc {
1284 margin-bottom: 24px;
1285
1286 &__wrapper {
1287 --am-fs-advsc: 15px;
1288 --am-ln-advsc: 1.6;
1289 --am-c-advsc-text: var(--am-c-main-text);
1290 // Calendar cell
1291 --am-c-advsc-cell-bgr: var(--am-c-cal-init-op10);
1292 --am-c-advsc-cell-busy: var(--am-c-cal-init-op30);
1293 --am-c-advsc-cell-border: var(--am-c-cal-init-op60);
1294 --am-c-advsc-cell-text: var(--am-c-cal-init-text);
1295 // Slot
1296 --am-fs-asdvsc-slot: 15px;
1297 --am-c-advsc-slot-text: var(--am-c-cal-init-text);
1298 --am-rad-advsc-slot: 4px;
1299
1300 * {
1301 font-family: var(--am-font-family);
1302 box-sizing: border-box;
1303 }
1304
1305 & > div {
1306 $count: 5;
1307 @for $i from 0 through $count {
1308 &:nth-child(#{$i + 1}) {
1309 animation: 600ms cubic-bezier(0.45, 1, 0.4, 1.2) #{$i * 100}ms am-animation-slide-up;
1310 animation-fill-mode: both;
1311 }
1312 }
1313 }
1314 }
1315
1316 &__time-zone {
1317 display: flex;
1318 justify-content: center;
1319 align-items: center;
1320 margin: 12px 0 4px;
1321
1322 span {
1323 font-size: 15px;
1324 font-weight: 400;
1325 line-height: 1;
1326 background-color: var(--am-c-advsc-cell-bgr);
1327 border-radius: 8px;
1328 color: var(--am-c-advsc-cell-text);
1329 padding: 4px 8px;
1330 }
1331 }
1332
1333 &__period-pricing {
1334 display: flex;
1335 justify-content: right;
1336 align-items: center;
1337 flex-wrap: wrap;
1338 gap: 10px;
1339 margin: 12px 0 4px;
1340
1341 span {
1342 font-size: 15px;
1343 font-weight: 400;
1344 line-height: 1;
1345 border-radius: 8px;
1346 padding: 4px 8px;
1347 }
1348
1349 span.period-pricing-low {
1350 background-color: var(--am-c-cal-low-op10);
1351 color: var(--am-c-cal-low-text);
1352 }
1353
1354 span.period-pricing-mid {
1355 background-color: var(--am-c-advsc-cell-bgr);
1356 color: var(--am-c-advsc-cell-text);
1357 }
1358
1359 span.period-pricing-high {
1360 background-color: var(--am-c-cal-high-op10);
1361 color: var(--am-c-cal-high-text);
1362 }
1363 }
1364
1365 &__duration {
1366 margin-top: 10px;
1367 }
1368
1369 table,
1370 tr,
1371 th {
1372 background-color: transparent;
1373 }
1374
1375 &.fc {
1376 &-theme-standard {
1377 .fc {
1378 &-scrollgrid {
1379 border: none;
1380 }
1381 }
1382
1383 th,
1384 td {
1385 border: none;
1386 }
1387
1388 // Calendar header cell
1389 th.#{$amCalClass} {
1390 // Month View
1391 &__dayGridMonth {
1392 &-header {
1393 &-cell {
1394 font-size: 16px;
1395 line-height: 1.5;
1396 color: #8c959a;
1397 padding: 4px 6px;
1398 .fc-col-header-cell-cushion {
1399 font-size: var(--am-fs-advsc);
1400 text-transform: initial;
1401 text-decoration: none;
1402 line-height: 1;
1403 letter-spacing: 0;
1404 color: var(--am-c-advsc-text);
1405 padding: 12px 0;
1406 white-space: nowrap;
1407 }
1408 }
1409
1410 &-weekend {
1411 color: #d1d5d7;
1412 }
1413 }
1414 }
1415 }
1416
1417 // Calendar cell
1418 td.#{$amCalClass} {
1419 // Month view
1420 &__dayGridMonth {
1421 // Calendar cell
1422 &-cell {
1423 position: relative;
1424 border: none;
1425 padding: 4px 6px;
1426
1427 // Calendar today day
1428 &.fc-day-today {
1429 position: relative;
1430 background: none;
1431
1432 // Calendar cell state
1433 &.#{$amCalClass} {
1434 // Selected cell
1435 &__dayGridMonth-selected {
1436 .fc-daygrid-day-frame {
1437 background-color: var(--am-c-advsc-cell-bgr);
1438 border-color: var(--am-c-advsc-cell-border);
1439
1440 &:after {
1441 background-color: var(--am-c-advsc-cell-text);
1442 }
1443 }
1444 }
1445
1446 // Disabled cell
1447 &__dayGridMonth-disabled {
1448 .fc-daygrid-day {
1449 &-frame {
1450 --am-c-advsc-cell-bgr: var(--am-c-cal-disabled-op10);
1451 --am-c-advsc-cell-border: transparent;
1452
1453 &:hover {
1454 --am-c-advsc-cell-bgr: var(--am-c-cal-disabled-op10);
1455 --am-c-advsc-cell-border: transparent;
1456 cursor: not-allowed;
1457 }
1458 }
1459
1460 &-number {
1461 --am-c-advsc-cell-text: var(--am-c-cal-disabled-text);
1462 }
1463 }
1464 }
1465 }
1466
1467 .fc-daygrid-day-frame {
1468 // Today marker - dot
1469 &:after {
1470 content: '';
1471 display: block;
1472 position: absolute;
1473 top: 4px;
1474 right: 4px;
1475 width: 4px;
1476 height: 4px;
1477 border-radius: 50%;
1478 background-color: var(--am-c-advsc-cell-text);
1479 }
1480 }
1481 }
1482
1483 // Calendar cell state
1484 &.#{$amCalClass} {
1485 // Disabled cell
1486 &__dayGridMonth-disabled {
1487 .fc-daygrid-day {
1488 &-frame {
1489 --am-c-advsc-cell-bgr: var(--am-c-cal-disabled-op10);
1490 --am-c-advsc-cell-border: transparent;
1491
1492 &:hover {
1493 --am-c-advsc-cell-bgr: var(--am-c-cal-disabled-op10);
1494 --am-c-advsc-cell-border: transparent;
1495 cursor: not-allowed;
1496 }
1497 }
1498
1499 &-number {
1500 --am-c-advsc-cell-text: var(--am-c-cal-disabled-text);
1501 }
1502 }
1503 }
1504
1505 // Pricing cell
1506 &__dayGridMonth-low {
1507 .fc-daygrid-day {
1508 &-frame {
1509 --am-c-advsc-cell-bgr: var(--am-c-cal-low-op10);
1510 --am-c-advsc-cell-border: var(--am-c-cal-low-op60);
1511 --am-c-advsc-cell-text: var(--am-c-cal-low-text);
1512 --am-c-advsc-cell-busy: var(--am-c-cal-low-op30);
1513
1514 &:hover {
1515 --am-c-advsc-cell-bgr: var(--am-c-cal-low-op30);
1516 }
1517 }
1518
1519 &-frame::before {
1520 visibility: var(--am-c-cal-indicator);
1521 content: '↘';
1522 position: absolute;
1523 top: -2px;
1524 left: 2px;
1525 font-size: 10px;
1526 color: var(--am-c-cal-low-op80);
1527 font-weight: bold;
1528 }
1529 }
1530 }
1531
1532 &__dayGridMonth-high {
1533 .fc-daygrid-day {
1534 &-frame {
1535 --am-c-advsc-cell-bgr: var(--am-c-cal-high-op10);
1536 --am-c-advsc-cell-border: var(--am-c-cal-high-op60);
1537 --am-c-advsc-cell-text: var(--am-c-cal-high-text);
1538 --am-c-advsc-cell-busy: var(--am-c-cal-high-op30);
1539
1540 &:hover {
1541 --am-c-advsc-cell-bgr: var(--am-c-cal-high-op30);
1542 }
1543 }
1544
1545 &-frame::before {
1546 visibility: var(--am-c-cal-indicator);
1547 content: '↗';
1548 position: absolute;
1549 top: -2px;
1550 left: 2px;
1551 font-size: 10px;
1552 color: var(--am-c-cal-high-op80);
1553 font-weight: bold;
1554 }
1555 }
1556 }
1557
1558 // Selected cell
1559 &__dayGridMonth-selected {
1560 .fc-daygrid-day-frame {
1561 --am-c-advsc-cell-text: var(--am-c-cal-selected-text);
1562 --am-c-advsc-cell-bgr: var(--am-c-cal-selected);
1563 --am-c-advsc-cell-border: var(--am-c-cal-selected);
1564
1565 &:hover {
1566 --am-c-advsc-cell-bgr: var(--am-c-cal-selected-op80);
1567 --am-c-advsc-cell-border: var(--am-c-cal-selected-op80);
1568 }
1569
1570 .#{$amCalClass}__slot-wrapper {
1571 display: none;
1572 }
1573 }
1574 }
1575
1576 // Weekend days cell
1577 &__dayGridMonth-weekend {
1578 .fc-daygrid-day {
1579 &-frame {
1580 background-color: transparent;
1581 border-color: transparent;
1582
1583 &:hover {
1584 border-color: transparent;
1585 cursor: not-allowed;
1586 }
1587 }
1588
1589 &-number {
1590 color: #d1d5d7;
1591 }
1592 }
1593 }
1594 }
1595
1596 // Calendar inner cell items
1597 .fc-daygrid-day {
1598 // Calendar inner cell wrapper
1599 &-frame {
1600 position: absolute;
1601 width: calc(100% - 12px);
1602 height: calc(100% - 8px);
1603 min-height: auto;
1604 top: 4px;
1605 left: 6px;
1606 background-color: var(--am-c-advsc-cell-bgr);
1607 border: 1px solid var(--am-c-advsc-cell-border);
1608 border-radius: 4px;
1609 cursor: pointer;
1610
1611 &:hover {
1612 --am-c-advsc-cell-bgr: var(--am-c-cal-init-op20);
1613 --am-c-advsc-cell-border: var(--am-c-cal-init);
1614 transition: all 0.3s ease-in-out;
1615 }
1616 }
1617
1618 // Calendar date slot availability wrapper
1619 &-bg {
1620 .fc-bg-event {
1621 background: none;
1622 opacity: 1;
1623
1624 // Calendar date slot availability
1625 .#{$amCalClass}__slot-wrapper {
1626 position: absolute;
1627 width: 100%;
1628 bottom: 0;
1629 left: 0;
1630 background-color: var(--am-c-advsc-cell-busy);
1631 }
1632 }
1633 }
1634
1635 // Inner cell date wrapper
1636 &-top {
1637 position: absolute;
1638 top: 50%;
1639 left: 50%;
1640 transform: translate(-50%, -50%);
1641 }
1642
1643 // Inner cell date holder
1644 &-number {
1645 color: var(--am-c-advsc-cell-text);
1646 text-decoration: none;
1647 line-height: 1;
1648 padding: 0;
1649 white-space: nowrap;
1650 }
1651 }
1652 }
1653 }
1654 }
1655
1656 // Tighter inset for small screens so the selection frame doesn't hug edges
1657 @media (max-width: 560px) {
1658 &-frame {
1659 width: calc(100% - 6px);
1660 height: calc(100% - 6px);
1661 top: 3px;
1662 left: 3px;
1663 }
1664 }
1665 }
1666 }
1667
1668 // Mobile Calendar cell
1669 &[am-mobile='true'] {
1670 &.fc {
1671 &-theme-standard {
1672 td.#{$amCalClass} {
1673 // Month view
1674 &__dayGridMonth {
1675 // Calendar cell TODO
1676 &-cell {
1677 padding: 3px;
1678 .fc-daygrid-day {
1679 &-frame {
1680 top: 3px;
1681 left: 4px;
1682 width: calc(100% - 8px);
1683 height: calc(100% - 6px);
1684 }
1685 }
1686 }
1687 }
1688 }
1689 }
1690 }
1691 }
1692 // Mobile S (<340px) Calendar cell
1693 &[am-mobile-s='true'] {
1694 &.fc {
1695 &-theme-standard {
1696 height: 230px;
1697 th.#{$amCalClass} {
1698 // Month view
1699 &__dayGridMonth {
1700 &-header-cell {
1701 --am-fs-advsc: 12px;
1702 padding: 2px 3px;
1703 line-height: 1.2;
1704 }
1705 }
1706 }
1707 td.#{$amCalClass} {
1708 // Month view
1709 &__dayGridMonth {
1710 // Calendar cell TODO
1711 &-cell {
1712 padding: 2px;
1713 .fc-daygrid-day {
1714 &-frame {
1715 top: 2px;
1716 left: 2px;
1717 &:after {
1718 right: 1px;
1719 top: 1px;
1720 }
1721 }
1722 }
1723 }
1724 }
1725 }
1726 }
1727 }
1728 }
1729
1730 &__header {
1731 display: flex;
1732 gap: 24px;
1733
1734 .am-button {
1735 font-size: 18px;
1736 padding: 8px;
1737 }
1738
1739 &-mobile {
1740 gap: 8px;
1741 }
1742
1743 &-mobile-s {
1744 flex-direction: column;
1745 align-items: center;
1746 gap: 8px;
1747 }
1748 }
1749
1750 &__slots {
1751 display: flex;
1752 flex-direction: row;
1753 align-items: center;
1754 justify-content: center;
1755 flex-wrap: wrap;
1756 gap: 8px 12px;
1757
1758 &-heading {
1759 font-size: var(--am-fs-advsc);
1760 line-height: var(--am-ln-advsc);
1761 color: var(--am-c-advsc-text);
1762 margin-bottom: 8px;
1763 padding: 0 6px;
1764 }
1765
1766 &-item {
1767 --am-c-advsc-slot-bgr: var(--am-c-cal-init-op10);
1768 --am-c-advsc-slot-border: var(--am-c-cal-init-op60);
1769 display: inline-flex;
1770 width: calc(50% - 6px);
1771
1772 &.am-width-full {
1773 width: 100%;
1774
1775 &:last-child {
1776 max-width: 100%;
1777 }
1778
1779 .am-advsc__waiting-tag {
1780 margin-top: -4px;
1781
1782 &:before {
1783 margin-top: 2px;
1784 }
1785 }
1786 }
1787
1788 &:focus {
1789 .am-advsc__slots-item__inner {
1790 --am-c-advsc-slot-border: var(--am-c-cal-selected);
1791 }
1792 }
1793
1794 span {
1795 background-color: var(--am-c-cal-init-op10);
1796 padding: 0 4px 0 4px;
1797 border-radius: 4px;
1798 }
1799
1800 $count: 1000;
1801 @for $i from 0 through $count {
1802 &:nth-child(#{$i + 1}) {
1803 animation: 600ms cubic-bezier(0.45, 1, 0.4, 1.2) #{$i * 100}ms am-animation-slide-up;
1804 animation-fill-mode: both;
1805 }
1806 }
1807
1808 &:last-child {
1809 max-width: 50%;
1810 }
1811
1812 &__low {
1813 --am-c-advsc-slot-bgr: var(--am-c-cal-low-op10);
1814 --am-c-advsc-slot-border: var(--am-c-cal-low-op60);
1815 --am-c-advsc-slot-text: var(--am-c-cal-low-text);
1816
1817 span {
1818 background-color: var(--am-c-cal-low-op10);
1819 padding: 0 4px 0 4px;
1820 border-radius: 4px;
1821 }
1822 }
1823
1824 &__high {
1825 --am-c-advsc-slot-bgr: var(--am-c-cal-high-op10);
1826 --am-c-advsc-slot-border: var(--am-c-cal-high-op60);
1827 --am-c-advsc-slot-text: var(--am-c-cal-high-text);
1828
1829 span {
1830 background-color: var(--am-c-cal-high-op10);
1831 padding: 0 4px 0 4px;
1832 border-radius: 4px;
1833 }
1834 }
1835
1836 &__selected {
1837 --am-c-advsc-slot-bgr: var(--am-c-cal-selected);
1838 --am-c-advsc-slot-border: var(--am-c-cal-selected);
1839 --am-c-advsc-slot-text: var(--am-c-cal-selected-text);
1840 }
1841
1842 &__inner {
1843 display: flex;
1844 flex-wrap: wrap;
1845 align-items: center;
1846 justify-content: center;
1847 gap: 0 12px;
1848 width: 100%;
1849 font-size: var(--am-fs-asdvsc-slot);
1850 font-weight: 400;
1851 line-height: 1.6;
1852 color: var(--am-c-advsc-slot-text);
1853 padding: 7px;
1854 border: 1px solid var(--am-c-advsc-slot-border);
1855 border-radius: var(--am-rad-advsc-slot);
1856 background-color: var(--am-c-advsc-slot-bgr);
1857 cursor: pointer;
1858 }
1859
1860 &-mobile {
1861 // TODO - for time view
1862 padding: 0 4px;
1863 --am-fs-asdvsc-slot: 12px;
1864
1865 .am-advsc__slots-item__inner {
1866 padding: 4px;
1867 }
1868 }
1869
1870 &-disabled {
1871 .am-advsc__slots-item__inner {
1872 background: var(--am-c-cal-disabled-op10);
1873 border: transparent;
1874 cursor: not-allowed;
1875 color: var(--am-c-cal-disabled-text);
1876 }
1877 }
1878
1879 // Waiting list specific styling
1880 &-waiting {
1881 border-color: var(--am-c-rs-text-op60);
1882 .am-advsc__slots-item__inner {
1883 background: var(--am-c-cal-waiting);
1884 border: 1px solid var(--am-c-cal-waiting-op40);
1885 flex-direction: column;
1886 line-height: 1.6;
1887 color: var(--am-c-cal-waiting-text);
1888
1889 &.am-contains-waiting-tag {
1890 padding: 3px 12px;
1891 }
1892 }
1893 .am-advsc__waiting-tag {
1894 background: transparent;
1895 display: flex;
1896 font-size: 10px;
1897 line-height: 1;
1898 border-radius: 10px;
1899 text-transform: lowercase;
1900 margin-top: -3px;
1901
1902 &:before {
1903 margin-top: 1px;
1904 }
1905 }
1906
1907 &.am-advsc__slots-item-mobile {
1908 .am-advsc__slots-item__inner.am-contains-waiting-tag {
1909 line-height: 1.1;
1910 white-space: nowrap;
1911 }
1912 }
1913
1914 &.am-advsc__slots-item__selected {
1915 .am-advsc__slots-item__inner {
1916 background: var(--am-c-cal-selected);
1917 border: 1px solid var(--am-c-cal-selected);
1918 color: var(--am-c-cal-selected-text);
1919 }
1920 }
1921 }
1922 }
1923 }
1924 }
1925 }
1926
1927 .amelia-v2-booking #amelia-container {
1928 @include am-advsc-block;
1929 }
1930
1931 #amelia-app-backend-new {
1932 @include am-advsc-block;
1933 }
1934 </style>
1935