Files
mgm-daylight-mcrm-mobile-ios/pages/roomCalendar/roomCalendar.vue
T
2026-09-20 12:35:46 +08:00

780 lines
24 KiB
Vue

<template>
<view class="room-calendar-page">
<u-navbar
:safeAreaInsetTop="true"
:placeholder="true"
:title="title"
:fixed="true"
:autoBack="false"
leftIcon=""
leftIconColor="#C39A4B"
bgColor="#C39A4B"
titleStyle="font-weight:bold;color:#ffffff;"
>
<view slot="right" class="calendar-refresh-btn" @click.stop="refreshCalendar">
<u-icon name="reload" size="24" color="#ffffff"></u-icon>
</view>
</u-navbar>
<view class="calendar-shell">
<view class="property-tabs">
<u-tabs
:current="selectedPropertyIndex"
:scrollable="false"
:list="propertyTabList"
lineColor="#C39A4B"
:itemStyle="propertyTabItemStyle"
:activeStyle="propertyTabActiveStyle"
:inactiveStyle="propertyTabInactiveStyle"
@change="changePropertyTab"
></u-tabs>
</view>
<view class="room-type-grid">
<view
v-for="option in currentSelectorList"
:key="option.code"
class="room-type-btn"
:class="{ active: selectedSelectorCodes.indexOf(option.code) !== -1 }"
@click="toggleSelectorOption(option.code)"
>
{{ option.label }}
</view>
</view>
<view v-if="calendarNoPermission" class="calendar-empty-state">
<view class="hz-flex-row calendar-empty-image-row">
<image src="/static/images/datanull.png" class="calendar-empty-image"></image>
</view>
<view class="hz-flex-row hz-font-bolder hz-font-md calendar-empty-text">
{{$t("todo.noPermission")}}
</view>
</view>
<room-calendar-grid
v-else-if="calendarReady"
:key="calendarRenderKey"
mode="availability"
:months="calendarMonths"
:weekday-labels="weekdayLabels"
:room-types="selectedRoomTypes"
:data-map="availabilityMap"
:initial-week-id="todayWeekId"
></room-calendar-grid>
</view>
</view>
</template>
<script>
import dayjs from '../../uni_modules/uview-ui/libs/util/dayjs.js';
import {
buildCalendarDateRange,
getTodayWeekId,
mapRoomCalendarQualificationRecords,
} from '../../common/roomCalendarUi.js';
import { getPropertiesRoomTypes, requestRoomCalendar } from '../../api/roomCalendarApi.js';
import { getCalendarSetting as requestCalendarSetting } from '../../api/configApi.js';
export default {
data() {
return {
title: this.$t("roomCalendar.title"),
userData: null,
selectedPropertyCode: '',
selectedRoomTypeCodes: [],
selectedOtherPropertyCodes: [],
availabilityMap: {},
calendarPreviousDays: 0,
calendarVisibleDays: 90,
calendarYesterdayStart: '',
calendarYesterdayEnd: '',
maxSelectedRoomTypes: 0,
calendarToday: dayjs().format('YYYY-MM-DD'),
calendarStartDate: '',
calendarEndDate: '',
calendarMonths: [],
calendarReady: false,
calendarNoPermission: false,
calendarRenderKey: 0,
calendarRequestTask: null,
calendarRequestId: 0,
weekdayLabels: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
propertyList: [],
};
},
computed: {
currentProperty() {
return this.propertyList.find(item => item.code === this.selectedPropertyCode) || this.propertyList[0] || {
defaultRoomTypes: [],
roomTypes: [],
};
},
isOtherProperty() {
return this.selectedPropertyCode === 'OTHER';
},
currentRoomTypes() {
return this.currentProperty.roomTypes || [];
},
currentOtherProperties() {
return this.currentProperty.otherProperties || [];
},
currentSelectorList() {
if (this.isOtherProperty) {
return this.currentOtherProperties.map(item => ({
code: item.code,
label: item.code,
}));
}
return this.currentRoomTypes;
},
selectedSelectorCodes() {
return this.isOtherProperty ? this.selectedOtherPropertyCodes : this.selectedRoomTypeCodes;
},
selectedOtherProperties() {
return this.currentOtherProperties.filter(item => this.selectedOtherPropertyCodes.indexOf(item.code) !== -1);
},
selectedRoomTypes() {
if (this.isOtherProperty) {
return this.selectedOtherProperties.reduce((list, property) => {
(property.roomTypes || []).forEach(roomType => {
list.push({
...roomType,
code: this.getOtherPropertyRoomTypeCode(property.code, roomType.code),
displayName:roomType.label,
parentCode: property.code,
propertyCode: property.code,
requestRoomTypeCode: roomType.code,
});
});
return list;
}, []);
}
const selected = this.currentRoomTypes.filter(item => this.selectedRoomTypeCodes.indexOf(item.code) !== -1);
return selected;
},
selectedPropertyIndex() {
const index = this.propertyList.findIndex(item => item.code === this.selectedPropertyCode);
return index === -1 ? 0 : index;
},
propertyTabList() {
return this.propertyList.map(item => ({
code: item.code,
name: item.name,
}));
},
propertyTabItemStyle() {
return {
height: '78rpx',
padding: '0 24rpx',
flexShrink: 0,
};
},
propertyTabActiveStyle() {
return {
whiteSpace: 'nowrap',
color: '#C39A4B',
fontFamily: 'Inter-Bold',
fontSize: '30rpx',
fontWeight: 'bold',
};
},
propertyTabInactiveStyle() {
return {
whiteSpace: 'nowrap',
color: '#111111',
fontFamily: 'Inter-Bold',
fontSize: '30rpx',
fontWeight: 'bold',
};
},
todayWeekId() {
return getTodayWeekId(this.calendarMonths);
},
},
onLoad() {},
onShow() {
this.$i18n.locale = uni.getLocale();
this.title = this.$t("roomCalendar.title");
this.userData = uni.getStorageSync("userData");
this.showTabbarByPermission(this.userData);
if (!this.userData || this.userData.canViewCalendar !== true) {
this.calendarNoPermission = true;
this.calendarReady = false;
return;
}
this.calendarNoPermission = false;
this.loadPropertiesRoomTypes();
},
onUnload() {
this.calendarRequestId += 1;
this.abortCalendarRequest();
},
methods: {
async loadPropertiesRoomTypes() {
if (!this.userData || this.userData.canViewCalendar !== true) {
this.showCalendarNoPermission();
return;
}
try {
this.calendarReady = false;
uni.showLoading({
title: this.$t("common.loading"),
});
await this.loadCalendarSetting();
const res = await getPropertiesRoomTypes({});
if (this.isNoPermissionResponse(res)) {
this.showCalendarNoPermission();
return;
}
this.propertyList = Array.isArray(res && res.propertyList) ? res.propertyList : [];
if (!this.propertyList.length) throw new Error('No calendar properties configured');
this.selectedPropertyCode = this.propertyList[0].code;
this.selectDefaultOptions();
this.buildCalendar();
await this.loadAvailabilityData();
this.calendarRenderKey += 1;
this.$nextTick(() => {
this.calendarReady = !this.calendarNoPermission;
});
} catch (e) {
uni.$u.toast(e.message || e.errMsg || 'Load property room types failed');
} finally {
uni.hideLoading();
}
},
async loadCalendarSetting() {
try {
const res = await requestCalendarSetting({});
if (res && res.data) {
this.applyCalendarSettings(res.data);
}
} catch (e) {
console.log(e);
}
},
applyCalendarSettings(settings) {
const settingMap = settings.reduce((map, item) => {
map[item.keys] = item.keyValues;
return map;
}, {});
this.calendarPreviousDays = this.getNumberSetting(settingMap.CALENDAR_VIEW_PREVIOUS_DAYS, this.calendarPreviousDays);
this.calendarVisibleDays = this.getNumberSetting(settingMap.CALENDAR_VIEW_VISIBLE_DAYS, this.calendarVisibleDays);
this.calendarYesterdayStart = settingMap.CALENDAR_VIEW_YESTERDAY_START || this.calendarYesterdayStart;
this.calendarYesterdayEnd = settingMap.CALENDAR_VIEW_YESTERDAY_END || this.calendarYesterdayEnd;
this.maxSelectedRoomTypes = this.getPositiveIntegerSetting(settingMap.MAX_SELECTED_ROOM_TYPES, this.maxSelectedRoomTypes);
},
getNumberSetting(value, defaultValue) {
if (value === undefined || value === null || value === '') return defaultValue;
const numberValue = Number(value);
return Number.isNaN(numberValue) ? defaultValue : numberValue;
},
getPositiveIntegerSetting(value, defaultValue) {
const numberValue = this.getNumberSetting(value, defaultValue);
return numberValue > 0 ? Math.floor(numberValue) : 0;
},
buildCalendar() {
const effectiveToday = this.getEffectiveCalendarToday();
const startDate = dayjs(effectiveToday).subtract(this.calendarPreviousDays, 'day').format('YYYY-MM-DD');
const endDate = dayjs(effectiveToday).add(this.calendarVisibleDays + this.calendarPreviousDays, 'day').format('YYYY-MM-DD');
this.calendarToday = effectiveToday;
this.calendarStartDate = startDate;
this.calendarEndDate = endDate;
this.calendarMonths = buildCalendarDateRange({
today: effectiveToday,
startDate,
endDate,
weekdayLabels: this.weekdayLabels,
});
},
getEffectiveCalendarToday() {
const now = dayjs();
if (this.isInYesterdayWindow(now)) {
return now.subtract(1, 'day').format('YYYY-MM-DD');
}
return now.format('YYYY-MM-DD');
},
isInYesterdayWindow(now) {
const startMinutes = this.getTimeMinutes(this.calendarYesterdayStart);
const endMinutes = this.getTimeMinutes(this.calendarYesterdayEnd);
if (startMinutes === null || endMinutes === null) return false;
const currentMinutes = now.hour() * 60 + now.minute();
if (startMinutes <= endMinutes) {
return currentMinutes >= startMinutes && currentMinutes <= endMinutes;
}
return currentMinutes >= startMinutes || currentMinutes <= endMinutes;
},
getTimeMinutes(value) {
if (!value) return null;
const parts = String(value).split(':').map(Number);
if (parts.length < 2 || Number.isNaN(parts[0]) || Number.isNaN(parts[1])) return null;
return parts[0] * 60 + parts[1];
},
changePropertyTab(tab) {
const property = this.propertyList[tab.index];
if (!property) return;
this.selectProperty(property.code);
},
selectProperty(code) {
const isSameProperty = code === this.selectedPropertyCode;
this.selectedPropertyCode = code;
this.ensureSelectedOptionsForRefresh(isSameProperty);
this.loadAvailabilityData();
},
selectDefaultOptions() {
if (this.isOtherProperty) {
this.selectedOtherPropertyCodes = this.getDefaultOtherPropertyCodes();
} else {
this.selectedRoomTypeCodes = this.getDefaultRoomTypeCodes();
}
},
ensureSelectedOptionsForRefresh(isSameProperty = false) {
if (this.isOtherProperty) {
if (!isSameProperty) {
this.selectedOtherPropertyCodes = this.getValidOtherPropertyCodes(this.selectedOtherPropertyCodes);
}
if (!this.selectedOtherPropertyCodes.length) {
this.selectedOtherPropertyCodes = this.getDefaultOtherPropertyCodes();
}
return;
}
if (!isSameProperty) {
this.selectedRoomTypeCodes = this.getValidRoomTypeCodes(this.selectedRoomTypeCodes);
}
if (!this.selectedRoomTypeCodes.length) {
this.selectedRoomTypeCodes = this.getDefaultRoomTypeCodes();
}
},
getValidRoomTypeCodes(codes) {
const validCodes = this.currentRoomTypes.map(item => this.getRoomTypeCode(item));
return (codes || []).filter(code => validCodes.indexOf(code) !== -1);
},
getValidOtherPropertyCodes(codes) {
const validCodes = this.currentOtherProperties.map(item => this.getPropertyCode(item));
return (codes || []).filter(code => validCodes.indexOf(code) !== -1);
},
toggleSelectorOption(code) {
if (this.isOtherProperty) {
this.toggleOtherProperty(code);
return;
}
this.toggleRoomType(code);
},
toggleRoomType(code) {
const index = this.selectedRoomTypeCodes.indexOf(code);
if (index === -1) {
if (this.isMaxSelectedRoomTypesReached(this.selectedRoomTypeCodes.length)) {
this.showMaxSelectedRoomTypesToast();
return;
}
this.selectedRoomTypeCodes.push(code);
this.loadAvailabilityData();
return;
}
if (this.selectedRoomTypeCodes.length === 1) {
uni.$u.toast('At least one room type is required');
return;
}
this.selectedRoomTypeCodes.splice(index, 1);
this.loadAvailabilityData();
},
toggleOtherProperty(code) {
const index = this.selectedOtherPropertyCodes.indexOf(code);
if (index === -1) {
if (this.isMaxSelectedRoomTypesReached(this.selectedOtherPropertyCodes.length)) {
this.showMaxSelectedRoomTypesToast();
return;
}
this.selectedOtherPropertyCodes.push(code);
this.loadAvailabilityData();
return;
}
if (this.selectedOtherPropertyCodes.length === 1) {
uni.$u.toast('At least one property is required');
return;
}
this.selectedOtherPropertyCodes.splice(index, 1);
this.loadAvailabilityData();
},
limitRoomTypeCodes(codes) {
if (!this.maxSelectedRoomTypes) return codes;
return codes.slice(0, this.maxSelectedRoomTypes);
},
getDefaultRoomTypeCodes() {
const defaultRoomTypes = this.normalizeRoomTypeCodes(
this.currentProperty.defaultRoomTypes || this.currentProperty.DefaultRoomTypes
);
const firstRoomTypeCode = this.getRoomTypeCode(this.currentRoomTypes[0]);
const roomTypeCodes = defaultRoomTypes.length
? defaultRoomTypes
: (firstRoomTypeCode ? [firstRoomTypeCode] : []);
return this.limitRoomTypeCodes(roomTypeCodes);
},
normalizeRoomTypeCodes(roomTypes) {
if (!roomTypes) return [];
if (Array.isArray(roomTypes)) {
return roomTypes.reduce((list, item) => list.concat(this.normalizeRoomTypeCodes(item)), []);
}
if (typeof roomTypes === 'string') {
return roomTypes
.split(',')
.map(code => code.trim())
.filter(code => code);
}
const code = this.getRoomTypeCode(roomTypes);
return code ? [code] : [];
},
getRoomTypeCode(roomType) {
if (!roomType) return '';
if (typeof roomType === 'string') return roomType;
return roomType.code || roomType.Code || roomType.roomType || roomType.RoomType || roomType.roomTypeCode || roomType.RoomTypeCode || '';
},
isMaxSelectedRoomTypesReached(selectedCount) {
return this.maxSelectedRoomTypes > 0 && selectedCount >= this.maxSelectedRoomTypes;
},
showMaxSelectedRoomTypesToast() {
uni.$u.toast(this.$t("roomCalendar.maxSelectedRoomTypes", {
count: this.maxSelectedRoomTypes,
}));
},
refreshCalendar() {
if (!this.userData || this.userData.canViewCalendar !== true || this.calendarNoPermission) return;
this.ensureSelectedOptionsForRefresh(true);
this.loadAvailabilityData();
},
async loadAvailabilityData() {
const records = [];
const dateRange = this.getCalendarDateRange();
const requestId = this.startCalendarRequest();
this.hideCalendarForLoading();
try {
uni.showLoading({
title: this.$t("common.loading"),
});
if (this.isOtherProperty) {
const params = this.buildAvailabilityParams(dateRange);
if (!params.Property || !params.RoomType) {
this.availabilityMap = {};
return;
}
console.log('[RoomCalendar] request Property:', params.Property, 'RoomType:', params.RoomType);
const res = await requestRoomCalendar(params, this.getCalendarRequestConfig(requestId));
if (!this.isLatestCalendarRequest(requestId)) return;
if (this.isNoPermissionResponse(res)) {
this.showCalendarNoPermission();
return;
}
if (res.success !== true) {
throw new Error(res.message || 'Load room calendar failed');
}
records.push(...this.buildOtherPropertyRecords(res.data || []));
this.availabilityMap = mapRoomCalendarQualificationRecords(records);
return;
}
const allRoomType = this.selectedRoomTypes
.map(roomType => roomType.code)
.join(',');
console.log('[RoomCalendar] request RoomType:', allRoomType);
const params = {
Property: this.selectedPropertyCode,
RoomType: allRoomType,
StartDate: dateRange.startDate,
EndDate: dateRange.endDate,
};
const res = await requestRoomCalendar(params, this.getCalendarRequestConfig(requestId));
if (!this.isLatestCalendarRequest(requestId)) return;
if (this.isNoPermissionResponse(res)) {
this.showCalendarNoPermission();
return;
}
if (res.success !== true) {
throw new Error(res.message || 'Load room calendar failed');
}
records.push(...(res.data || []));
this.availabilityMap = mapRoomCalendarQualificationRecords(records);
} catch (e) {
if (!this.isLatestCalendarRequest(requestId) || this.isRequestAbortError(e)) return;
this.availabilityMap = {};
uni.$u.toast(e.message || e.errMsg || 'Load room calendar failed');
} finally {
if (this.isLatestCalendarRequest(requestId)) {
this.calendarRequestTask = null;
uni.hideLoading();
this.showCalendarAfterLoading(requestId);
}
}
},
hideCalendarForLoading() {
this.calendarReady = false;
this.calendarNoPermission = false;
},
showCalendarAfterLoading(requestId) {
this.$nextTick(() => {
if (this.isLatestCalendarRequest(requestId) && !this.calendarNoPermission) {
this.calendarReady = true;
}
});
},
isNoPermissionResponse(res) {
return res && res.success === false && (res.errorCode || res.code) === 'NO_PERMISSION';
},
showCalendarNoPermission() {
this.availabilityMap = {};
this.calendarReady = false;
this.calendarNoPermission = true;
},
startCalendarRequest() {
this.calendarRequestId += 1;
this.abortCalendarRequest();
this.calendarRequestTask = null;
return this.calendarRequestId;
},
abortCalendarRequest() {
if (this.calendarRequestTask && typeof this.calendarRequestTask.abort === 'function') {
this.calendarRequestTask.abort();
}
this.calendarRequestTask = null;
},
getCalendarRequestConfig(requestId) {
return {
getTask: task => {
if (this.isLatestCalendarRequest(requestId)) {
this.calendarRequestTask = task;
}
},
};
},
isLatestCalendarRequest(requestId) {
return requestId === this.calendarRequestId;
},
isRequestAbortError(error) {
const errMsg = error && error.errMsg ? String(error.errMsg) : '';
return errMsg.indexOf('abort') !== -1;
},
buildAvailabilityParams(dateRange) {
const allProperty = this.isOtherProperty
? this.selectedOtherPropertyCodes.join(',')
: this.selectedPropertyCode;
const allRoomType = this.getUniqueCodes(this.selectedRoomTypes.map(roomType => {
return roomType.requestRoomTypeCode || roomType.parentCode || roomType.code;
})).join(',');
return {
Property: allProperty,
RoomType: allRoomType,
StartDate: dateRange.startDate,
EndDate: dateRange.endDate,
};
},
getUniqueCodes(codes) {
return [...new Set(codes.filter(code => code))];
},
getOtherPropertyRoomTypeCode(propertyCode, roomTypeCode) {
return `${propertyCode}_${roomTypeCode}`;
},
getDefaultOtherPropertyCodes() {
const defaultPropertySource =
this.currentProperty.defaultProperty ||
this.currentProperty.defaultProperties ||
this.currentProperty.defaultOtherProperty ||
this.currentProperty.defaultOtherProperties ||
this.currentProperty.defaultPropertyList ||
this.currentProperty.defaultPropertyCodes ||
this.currentProperty.defaultRoomTypes;
const defaultProperties = this.normalizeDefaultProperties(defaultPropertySource);
const defaultCodes = defaultProperties
.map(item => this.getPropertyCode(item))
.filter(code => this.currentOtherProperties.some(property => property.code === code));
const uniqueDefaultCodes = [...new Set(defaultCodes)];
if (uniqueDefaultCodes.length) {
return this.limitRoomTypeCodes(uniqueDefaultCodes);
}
const firstOtherPropertyCode = this.getPropertyCode(this.currentOtherProperties[0]);
return firstOtherPropertyCode ? [firstOtherPropertyCode] : [];
},
normalizeDefaultProperties(defaultProperties) {
if (!defaultProperties) return [];
if (Array.isArray(defaultProperties)) {
return defaultProperties.reduce((list, item) => list.concat(this.normalizeDefaultProperties(item)), []);
}
if (typeof defaultProperties === 'string') {
return defaultProperties
.split(',')
.map(code => code.trim())
.filter(code => code);
}
return [defaultProperties];
},
getPropertyCode(property) {
if (typeof property === 'string') return property;
return property.code || property.Code || property.propertyCode || property.PropertyCode || property.Property || property.property || '';
},
getRecordRoomType(record) {
return record.roomType !== undefined ? record.roomType : record.RoomType;
},
getRecordProperty(record) {
return record.property !== undefined
? record.property
: record.Property || record.propertyCode || record.PropertyCode || '';
},
buildOtherPropertyRecords(records) {
return records.reduce((list, record) => {
this.getOtherPropertyRecordRoomTypeCodes(record).forEach(roomTypeCode => {
list.push({
...record,
roomType: roomTypeCode,
RoomType: roomTypeCode,
});
});
return list;
}, []);
},
getOtherPropertyRecordRoomTypeCodes(record) {
const recordRoomType = this.getRecordRoomType(record);
if (!recordRoomType) return [];
if (this.selectedRoomTypes.some(roomType => roomType.code === recordRoomType)) {
return [recordRoomType];
}
const recordPropertyCode = this.getSelectedOtherPropertyCode(this.getRecordProperty(record));
if (recordPropertyCode) {
return [this.getOtherPropertyRoomTypeCode(recordPropertyCode, recordRoomType)];
}
const matchedRoomTypes = this.selectedRoomTypes.filter(roomType => roomType.requestRoomTypeCode === recordRoomType);
return matchedRoomTypes.length ? matchedRoomTypes.map(roomType => roomType.code) : [recordRoomType];
},
getSelectedOtherPropertyCode(propertyValue) {
const normalizedValue = this.normalizeText(propertyValue);
if (!normalizedValue) return '';
const property = this.selectedOtherProperties.find(item => {
return this.getOtherPropertyAliases(item).some(value => this.normalizeText(value) === normalizedValue);
});
return property ? property.code : '';
},
getOtherPropertyAliases(property) {
const aliases = [
property.code,
property.name,
property.displayName,
property.propertyName,
property.hotelName,
];
if (property.code === 'MO') {
aliases.push('MGM Macau', 'Macau', 'MOGR');
}
return aliases;
},
normalizeText(value) {
return String(value || '').trim().toUpperCase();
},
getCalendarDateRange() {
const dates = [];
this.calendarMonths.forEach(month => {
month.weeks.forEach(week => {
week.days.forEach(day => {
if (!day.isOutsideMonth) {
dates.push(day.date);
}
});
});
});
return {
startDate: dates[0],
endDate: dates[dates.length - 1],
};
},
},
};
</script>
<style lang="scss">
page {
height: 100%;
overflow: hidden;
}
.room-calendar-page {
background-color: #ffffff;
display: flex;
flex-direction: column;
height: calc(100vh - var(--window-bottom));
overflow: hidden;
}
.calendar-shell {
box-sizing: border-box;
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
overflow: hidden;
padding: 2rpx 8rpx 0;
}
.property-tabs {
flex-shrink: 0;
padding: 8rpx 0 2rpx;
}
.room-type-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding: 8rpx 0 2rpx;
}
.room-type-btn {
background-color: #ffffff;
border: 1rpx solid #d8d8d8;
border-radius: 8rpx;
box-sizing: border-box;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.12);
color: #111111;
font-family: "Inter-Bold";
font-size: 28rpx;
height: 58rpx;
line-height: 58rpx;
margin: 5rpx 6rpx;
text-align: center;
width: calc(16.6666% - 12rpx);
}
.room-type-btn.active {
background-color: #c39a4b;
border-color: #c39a4b;
box-shadow: none;
color: #ffffff;
}
.calendar-refresh-btn {
align-items: center;
display: flex;
height: 88rpx;
justify-content: center;
margin-right: 5rpx;
width: 64rpx;
}
.calendar-empty-state {
flex: 1;
}
.calendar-empty-image-row {
justify-content: center;
margin-top: 80px;
}
.calendar-empty-image {
height: 160px;
width: 160px;
}
.calendar-empty-text {
justify-content: center;
margin: 20px;
}
</style>