fix bugs
This commit is contained in:
@@ -1,12 +1,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import config from './common/config.js'
|
import config from './common/config.js'
|
||||||
import {writeDebugLog,getSettingByKey,getSettings,verifyToken,logout} from './api/commonApi.js'
|
import {writeDebugLog,writePublicDebugLog,getSettingByKey,getSettings,verifyToken,logout} from './api/commonApi.js'
|
||||||
import { getCfgSettingByKey } from './api/configApi.js'
|
import { getCfgSettingByKey } from './api/configApi.js'
|
||||||
import store from './store/index.js'
|
import store from './store/index.js'
|
||||||
import FixedCoordinateConverter from './common/fixedCoordinateConverter.js'
|
import FixedCoordinateConverter from './common/fixedCoordinateConverter.js'
|
||||||
|
import { getDeviceToken, refreshDeviceTokenAsync, refreshDeviceTokenAndFetch } from './common/pushDeviceToken.js';
|
||||||
|
import { listenPushNotification, syncPendingPushNotifications } from './common/pushNotification.js';
|
||||||
import {refreshToken} from './api/commonApi.js'
|
import {refreshToken} from './api/commonApi.js'
|
||||||
import dayjs from './uni_modules/uview-ui/libs/util/dayjs.js';
|
import dayjs from './uni_modules/uview-ui/libs/util/dayjs.js';
|
||||||
import { decryptCfgSettingValue } from './common/cfgSettingCrypto.js';
|
import { decryptCfgSettingValue } from './common/cfgSettingCrypto.js';
|
||||||
|
import {saveDeviceToken} from './api/notification.js';
|
||||||
// #ifdef APP-PLUS
|
// #ifdef APP-PLUS
|
||||||
var networkModule = uni.requireNativePlugin("NetworkUtils-NetworkUtilsModule")
|
var networkModule = uni.requireNativePlugin("NetworkUtils-NetworkUtilsModule")
|
||||||
// #endif
|
// #endif
|
||||||
@@ -59,6 +62,16 @@
|
|||||||
});*/
|
});*/
|
||||||
// #ifdef APP-PLUS
|
// #ifdef APP-PLUS
|
||||||
this.getSettingByKey('MACAU_STRING');
|
this.getSettingByKey('MACAU_STRING');
|
||||||
|
|
||||||
|
let userData = uni.getStorageSync("userData");
|
||||||
|
if(userData != null && userData != undefined && userData != '' && userData.userId !=null){
|
||||||
|
this.syncPushDeviceTokenFromNative();
|
||||||
|
// 同步点击通知产生的 pending push,并在有 ConfirmNo 时跳转到 Limo Detail
|
||||||
|
setTimeout(() => {
|
||||||
|
this.syncPendingPushNotificationsFromNative();
|
||||||
|
this.tryConsumePendingPushNavigation();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
// #endif
|
// #endif
|
||||||
},
|
},
|
||||||
onHide: function() {
|
onHide: function() {
|
||||||
@@ -257,8 +270,34 @@
|
|||||||
this.navTo('/pages/login/login',2,false);
|
this.navTo('/pages/login/login',2,false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
tryConsumePendingPushNavigation() {
|
||||||
|
const pendingUrl = uni.getStorageSync('pendingPushNavigation');
|
||||||
|
if (!pendingUrl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!this.isUserLoggedIn()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uni.removeStorageSync('pendingPushNavigation');
|
||||||
|
console.log('[Push] consume pending navigation:', pendingUrl);
|
||||||
|
this.navTo(pendingUrl, 0, false);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
async syncPendingPushNotificationsFromNative() {
|
||||||
|
console.log('[Push] syncPendingPushNotificationsFromNative')
|
||||||
|
await syncPendingPushNotifications((payload, meta) => {
|
||||||
|
this.handlePushNotificationMessage(payload, {
|
||||||
|
...meta,
|
||||||
|
showToast: meta?.fromPending ? false : meta?.showToast
|
||||||
|
})
|
||||||
|
}, { showToast: false })
|
||||||
|
await this.flushPendingPushSaveQueue()
|
||||||
|
},
|
||||||
//判断是否有Permission
|
//判断是否有Permission
|
||||||
redirectByPermission(userData,needRedirect){
|
redirectByPermission(userData,needRedirect){
|
||||||
|
if (this.tryConsumePendingPushNavigation()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(userData!==null && userData!==undefined){
|
if(userData!==null && userData!==undefined){
|
||||||
if(userData.canViewDashboard){
|
if(userData.canViewDashboard){
|
||||||
this.navTo('/pages/dashboard/dashboard',1,false);
|
this.navTo('/pages/dashboard/dashboard',1,false);
|
||||||
@@ -271,8 +310,71 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/** 从原生读取当前缓存的 APNs DeviceToken,并写入本地 Storage(供业务上报) */
|
||||||
|
syncPushDeviceTokenFromNative() {
|
||||||
|
getDeviceToken({})
|
||||||
|
.then(async (ret) => {
|
||||||
|
console.log('[APNs] getDeviceToken', ret.status, ret.updatedAt);
|
||||||
|
const deviceToken = ret.deviceToken ? String(ret.deviceToken).trim() : '';
|
||||||
|
let useLanguage = 0;
|
||||||
|
//获取AppSetting,后端可以判断发中文还是发英文 英文:0 中文:1 默认为0
|
||||||
|
if(uni.getLocale()!==null && uni.getLocale()!==undefined){
|
||||||
|
useLanguage = uni.getLocale()=='en'? 0 : 1;
|
||||||
|
}
|
||||||
|
if (deviceToken) {
|
||||||
|
const userId = this.getCachedUserId();
|
||||||
|
console.log('[APNs] saveDeviceToken params', {
|
||||||
|
userId,
|
||||||
|
deviceToken,
|
||||||
|
rawUserData: uni.getStorageSync('userData'),
|
||||||
|
rawRet: ret,
|
||||||
|
useLanguage
|
||||||
|
});
|
||||||
|
this.writeLog(
|
||||||
|
userId,
|
||||||
|
'syncPushDeviceTokenFromNative',
|
||||||
|
'',
|
||||||
|
'[APP][App.vue][getDevice Token][Success]' + deviceToken,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
1,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
uni.setStorageSync('apnsDeviceToken', deviceToken);
|
||||||
|
uni.setStorageSync('apnsDeviceTokenUpdatedAt', ret.updatedAt || 0);
|
||||||
|
try {
|
||||||
|
const res = await saveDeviceToken({ userId, deviceToken, useLanguage});
|
||||||
|
console.log('[APNs] saveDeviceToken success', res);
|
||||||
|
} catch (saveErr) {
|
||||||
|
console.error('[APNs] saveDeviceToken failed', saveErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.warn('[APNs] getDeviceToken fail', e);
|
||||||
|
uni.showToast({
|
||||||
|
title: 'error = ' + e,
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
const userId = this.getCachedUserId();
|
||||||
|
this.writeLog(
|
||||||
|
userId,
|
||||||
|
'syncPushDeviceTokenFromNative',
|
||||||
|
'',
|
||||||
|
'[APP][App.vue][getDevice Token][Fail] Error Message :' + e,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
1,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
//根据Permission显示Tabbar
|
//根据Permission显示Tabbar
|
||||||
showTabbarByPermission(userData){
|
showTabbarByPermission(userData){
|
||||||
|
uni.setTabBarItem({
|
||||||
|
index: 3,
|
||||||
|
visible: !!userData && userData.canViewCalendar === true
|
||||||
|
});
|
||||||
if(userData!==null && userData!==undefined){
|
if(userData!==null && userData!==undefined){
|
||||||
console.log('Setting Tabbar');
|
console.log('Setting Tabbar');
|
||||||
if(userData.canViewDashboard){
|
if(userData.canViewDashboard){
|
||||||
@@ -483,7 +585,8 @@
|
|||||||
//console.log(obj);
|
//console.log(obj);
|
||||||
|
|
||||||
try{
|
try{
|
||||||
const response = await writeDebugLog(obj);
|
const token = uni.getStorageSync('token');
|
||||||
|
const response = token ? await writeDebugLog(obj) : await writePublicDebugLog(obj);
|
||||||
}
|
}
|
||||||
catch(e){
|
catch(e){
|
||||||
console.error(e.errMsg);
|
console.error(e.errMsg);
|
||||||
@@ -513,6 +616,7 @@
|
|||||||
index:2,
|
index:2,
|
||||||
visible:false
|
visible:false
|
||||||
});
|
});
|
||||||
|
uni.setTabBarItem({ index: 3, visible: false });
|
||||||
if(siteData.mockSSO==false&&!siteData.internalTag){
|
if(siteData.mockSSO==false&&!siteData.internalTag){
|
||||||
//this.logoutWithADFSApp()
|
//this.logoutWithADFSApp()
|
||||||
uni.redirectTo({
|
uni.redirectTo({
|
||||||
@@ -546,6 +650,19 @@
|
|||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getCachedUserId() {
|
||||||
|
const userData = uni.getStorageSync('userData');
|
||||||
|
if (
|
||||||
|
userData != null &&
|
||||||
|
userData !== '' &&
|
||||||
|
userData != undefined &&
|
||||||
|
userData.userId != null &&
|
||||||
|
userData.userId !== ''
|
||||||
|
) {
|
||||||
|
return userData.userId;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
async navTo(url,type,auth){
|
async navTo(url,type,auth){
|
||||||
let token = uni.getStorageSync('token');
|
let token = uni.getStorageSync('token');
|
||||||
if(token){
|
if(token){
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
export const writeDebugLog = (params, config = {}) => uni.$u.http.post('/system/write_log', params, config)
|
export const writeDebugLog = (params, config = {}) => uni.$u.http.post('/system/write_log', params, config)
|
||||||
|
export const writePublicDebugLog = (params, config = {}) => uni.$u.http.post('/system/write_public_log', params, config)
|
||||||
export const getSettingByKey = (params, config = {}) => uni.$u.http.post('/system/sys_setting/get', params, config)
|
export const getSettingByKey = (params, config = {}) => uni.$u.http.post('/system/sys_setting/get', params, config)
|
||||||
export const getSettings = (params, config = {}) => uni.$u.http.post('/system/sys_setting/get_list', params, config)
|
export const getSettings = (params, config = {}) => uni.$u.http.post('/system/sys_setting/get_public_login_settings', params, config)
|
||||||
export const verifyToken = (params, config = {}) => uni.$u.http.post('/app/me', params, config)
|
export const verifyToken = (params, config = {}) => uni.$u.http.post('/app/me', params, config)
|
||||||
export const refreshToken = (params, config = {}) => uni.$u.http.post('/app/refresh_token?isRenewRefreshToken=false', params, config)
|
export const refreshToken = (params, config = {}) => uni.$u.http.post('/app/refresh_token?isRenewRefreshToken=false', params, config)
|
||||||
export const logout = (params, config = {}) => uni.$u.http.post('/app/logout', params, config)
|
export const logout = (params, config = {}) => uni.$u.http.post('/app/logout', params, config)
|
||||||
|
|||||||
+2
-1
@@ -9,4 +9,5 @@ export const getSetting = (params, config = {}) => uni.$u.http.post('/app/config
|
|||||||
export const getAvailableRoom = (params, config = {}) => uni.$u.http.post('/app/config/available_room_list', params, config)
|
export const getAvailableRoom = (params, config = {}) => uni.$u.http.post('/app/config/available_room_list', params, config)
|
||||||
export const getExtendBooking = (params, config = {}) => uni.$u.http.post('/app/config/is_extend_booking', params, config)
|
export const getExtendBooking = (params, config = {}) => uni.$u.http.post('/app/config/is_extend_booking', params, config)
|
||||||
export const getWaitlistBooking = (params, config = {}) => uni.$u.http.post('/app/config/get_waitlist_booking', params, config)
|
export const getWaitlistBooking = (params, config = {}) => uni.$u.http.post('/app/config/get_waitlist_booking', params, config)
|
||||||
export const getCfgSettingByKey = (params, config = {}) => uni.$u.http.post('/web/setting/get_setting_by_key', params, config)
|
export const getCfgSettingByKey = (params, config = {}) => uni.$u.http.post('/web/setting/get_public_setting_by_key', params, config)
|
||||||
|
export const getCalendarSetting = (params, config = {}) => uni.$u.http.post('/app/setting/get_calendar_setting', params, config)
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export const requestRoomCalendar = (params, config = {}) => uni.$u.http.post('/app/room_calendar/request_room_calendar', params, config)
|
||||||
|
|
||||||
|
export const getPropertiesRoomTypes = (params, config = {}) =>
|
||||||
|
uni.$u.http.get('/app/room_calendar/get_properties_room_types', { params, ...config })
|
||||||
|
|
||||||
|
export const getPlayerRoomQualification = requestRoomCalendar
|
||||||
@@ -18,7 +18,8 @@
|
|||||||
<view style="border:1rpx solid #ccc;border-radius: 14px;padding:4px 12px;" class="hz-flex-row hz-flex-vertical-mid">
|
<view style="border:1rpx solid #ccc;border-radius: 14px;padding:4px 12px;" class="hz-flex-row hz-flex-vertical-mid">
|
||||||
<image @click="navTo('/pages/findPlayer/findPlayer',1,false)" src="../../static/images/ico_find_player.png" class="hz-nav-btn"></image>
|
<image @click="navTo('/pages/findPlayer/findPlayer',1,false)" src="../../static/images/ico_find_player.png" class="hz-nav-btn"></image>
|
||||||
<view style="height:20px;border-left:1px solid #ccc;margin-left:8px;margin-right:8px;"></view>
|
<view style="height:20px;border-left:1px solid #ccc;margin-left:8px;margin-right:8px;"></view>
|
||||||
<image @click="getDataByPage" src="../../static/images/ico_list.png" class="hz-nav-btn"></image>
|
<image v-if="userData && userData.canViewCalendar === true" @click="navToPlayerRoomCalendar" src="../../static/images/calendar-line.png" class="hz-nav-btn"></image>
|
||||||
|
<image v-else @click="getDataByPage" src="../../static/images/ico_list.png" class="hz-nav-btn"></image>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</u-navbar>
|
</u-navbar>
|
||||||
@@ -152,6 +153,11 @@
|
|||||||
this.pullDownRefresh();
|
this.pullDownRefresh();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
navToPlayerRoomCalendar(){
|
||||||
|
const userData = uni.getStorageSync("userData");
|
||||||
|
if (!userData || userData.canViewCalendar !== true) return;
|
||||||
|
this.navTo('/booking/playerRoomCalendar/playerRoomCalendar',0,false);
|
||||||
|
},
|
||||||
// 數據方法
|
// 數據方法
|
||||||
async getDataByPage(){
|
async getDataByPage(){
|
||||||
if(this.userData.canViewBooking==false) return;
|
if(this.userData.canViewBooking==false) return;
|
||||||
@@ -549,10 +555,10 @@
|
|||||||
return function (booking) {
|
return function (booking) {
|
||||||
if(booking.approveStatus==='W'|| booking.approveStatus==='WA'){
|
if(booking.approveStatus==='W'|| booking.approveStatus==='WA'){
|
||||||
return 'hz-card-waitlist'
|
return 'hz-card-waitlist'
|
||||||
}else if((booking.lastOperaStatus==='DUE_OUT' || booking.lastOperaStatus==='CHECKED_IN') && booking.confirmationNo!=null && (booking.sharer==null || booking.sharer==='[]' || booking.sharer==='')){
|
|
||||||
return 'hz-card-booking'
|
|
||||||
}else if(booking.approveStatus==='AP'){
|
}else if(booking.approveStatus==='AP'){
|
||||||
return 'hz-card-approval'
|
return 'hz-card-approval'
|
||||||
|
}else if((booking.lastOperaStatus==='DUE_OUT' || booking.lastOperaStatus==='CHECKED_IN') && booking.confirmationNo!=null && (booking.sharer==null || booking.sharer==='[]' || booking.sharer==='')){
|
||||||
|
return 'hz-card-booking'
|
||||||
}else{
|
}else{
|
||||||
return 'hz-card-booking'
|
return 'hz-card-booking'
|
||||||
}
|
}
|
||||||
@@ -576,4 +582,4 @@
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -460,6 +460,7 @@
|
|||||||
this.modelBooking.propertyId = this.userData.propertyId;
|
this.modelBooking.propertyId = this.userData.propertyId;
|
||||||
|
|
||||||
this.modelBooking.teamId = this.userData.teamStructureId;
|
this.modelBooking.teamId = this.userData.teamStructureId;
|
||||||
|
this.modelBooking.sectionName = this.userData.section;
|
||||||
this.modelBooking.sectionId = this.userData.sectionId;
|
this.modelBooking.sectionId = this.userData.sectionId;
|
||||||
this.modelBooking.createdBy = this.userData.userId;
|
this.modelBooking.createdBy = this.userData.userId;
|
||||||
this.modelBooking.createdByName = this.userData.userName;
|
this.modelBooking.createdByName = this.userData.userName;
|
||||||
@@ -806,10 +807,10 @@
|
|||||||
let item = this.modelBooking.sharerList[index];
|
let item = this.modelBooking.sharerList[index];
|
||||||
let book = uni.getStorageSync("bookingData")
|
let book = uni.getStorageSync("bookingData")
|
||||||
let oldItem = book.sharerList[index];
|
let oldItem = book.sharerList[index];
|
||||||
//清空plaerId
|
// 姓名被手动修改后,清除不再匹配的玩家 ID。
|
||||||
// if (item.name != oldItem.name && item.playerId.length == 8) {
|
if (item.name != oldItem.name && item.playerId.length > 4) {
|
||||||
// item.playerId = "";
|
item.playerId = "";
|
||||||
// }
|
}
|
||||||
},
|
},
|
||||||
async buildCalendar() {
|
async buildCalendar() {
|
||||||
uni.showLoading({
|
uni.showLoading({
|
||||||
|
|||||||
@@ -0,0 +1,867 @@
|
|||||||
|
<template>
|
||||||
|
<view class="player-room-calendar-page">
|
||||||
|
<u-navbar
|
||||||
|
:safeAreaInsetTop="true"
|
||||||
|
:placeholder="true"
|
||||||
|
:title="title"
|
||||||
|
:fixed="true"
|
||||||
|
:autoBack="false"
|
||||||
|
leftIconColor="#000000"
|
||||||
|
titleStyle="font-weight:bold;"
|
||||||
|
@leftClick="back"
|
||||||
|
>
|
||||||
|
<view slot="right" class="calendar-refresh-btn" @click.stop="refreshCalendar">
|
||||||
|
<u-icon name="reload" size="24" color="#000000"></u-icon>
|
||||||
|
</view>
|
||||||
|
</u-navbar>
|
||||||
|
|
||||||
|
<!-- <view class="player-summary"> -->
|
||||||
|
<view class="hz-card-profile mb-[40]" style="margin:9px 9px 6px;">
|
||||||
|
<view class="hz-flex-row">
|
||||||
|
<view style="width:60%">
|
||||||
|
<view class="hz-font-color-light hz-font-sm">{{$t("common.playerId")}}</view>
|
||||||
|
<view class="hz-font-bolder hz-font-lg" >{{ safePlayerData.playerId }}</view>
|
||||||
|
</view>
|
||||||
|
<view style="width:40%">
|
||||||
|
<view class="hz-font-color-light hz-font-sm">{{$t("common.tier")}}</view>
|
||||||
|
<view class="hz-font-bolder hz-font-lg" >{{ safePlayerData.tierName }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="hz-flex-row hz-mr-t-30">
|
||||||
|
<view style="width:100%">
|
||||||
|
<view class="hz-font-color-light hz-font-sm">{{$t("common.name")}}</view>
|
||||||
|
<view class="hz-font-bolder hz-font-lg" >{{ playerName }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- </view> -->
|
||||||
|
|
||||||
|
<view class="calendar-panel">
|
||||||
|
<view class="property-tabs">
|
||||||
|
<u-tabs
|
||||||
|
:current="selectedPropertyIndex"
|
||||||
|
:scrollable="true"
|
||||||
|
: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="qualification"
|
||||||
|
:months="calendarMonths"
|
||||||
|
:weekday-labels="weekdayLabels"
|
||||||
|
:room-types="selectedRoomTypes"
|
||||||
|
:data-map="qualificationMap"
|
||||||
|
:initial-week-id="todayWeekId"
|
||||||
|
:center-content="true"
|
||||||
|
:show-leader="false"
|
||||||
|
></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 { getPlayerRoomQualification, getPropertiesRoomTypes } from '../../api/roomCalendarApi.js';
|
||||||
|
import { getCalendarSetting as requestCalendarSetting } from '../../api/configApi.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
title: this.$t("roomCalendar.playerTitle"),
|
||||||
|
userData: null,
|
||||||
|
playerData: null,
|
||||||
|
selectedPropertyCode: '',
|
||||||
|
selectedRoomTypeCodes: [],
|
||||||
|
selectedOtherPropertyCodes: [],
|
||||||
|
qualificationMap: {},
|
||||||
|
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: {
|
||||||
|
safePlayerData() {
|
||||||
|
return this.playerData || {};
|
||||||
|
},
|
||||||
|
playerName() {
|
||||||
|
const salutation = this.safePlayerData.salutation || '';
|
||||||
|
const name = this.safePlayerData.name || '';
|
||||||
|
const text = `${salutation} ${name}`.trim();
|
||||||
|
return text || '-';
|
||||||
|
},
|
||||||
|
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: '70rpx',
|
||||||
|
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() {
|
||||||
|
this.playerData = uni.getStorageSync("playerData");
|
||||||
|
this.userData = uni.getStorageSync("userData");
|
||||||
|
this.loadPropertiesRoomTypes();
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
this.$i18n.locale = uni.getLocale();
|
||||||
|
this.title = this.$t("roomCalendar.playerTitle");
|
||||||
|
},
|
||||||
|
onUnload() {
|
||||||
|
this.calendarRequestId += 1;
|
||||||
|
this.abortCalendarRequest();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
back() {
|
||||||
|
this.navBack();
|
||||||
|
},
|
||||||
|
async loadPropertiesRoomTypes() {
|
||||||
|
if (!this.canLoadPlayerCalendar()) 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.getDefaultPropertyCode();
|
||||||
|
this.selectDefaultOptions();
|
||||||
|
this.buildCalendar();
|
||||||
|
await this.loadQualificationData();
|
||||||
|
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.loadQualificationData();
|
||||||
|
},
|
||||||
|
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.loadQualificationData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.selectedRoomTypeCodes.length === 1) {
|
||||||
|
uni.$u.toast('At least one room type is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.selectedRoomTypeCodes.splice(index, 1);
|
||||||
|
this.loadQualificationData();
|
||||||
|
},
|
||||||
|
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.loadQualificationData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.selectedOtherPropertyCodes.length === 1) {
|
||||||
|
uni.$u.toast('At least one property is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.selectedOtherPropertyCodes.splice(index, 1);
|
||||||
|
this.loadQualificationData();
|
||||||
|
},
|
||||||
|
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.canLoadPlayerCalendar()) return;
|
||||||
|
this.ensureSelectedOptionsForRefresh(true);
|
||||||
|
this.loadQualificationData();
|
||||||
|
},
|
||||||
|
async loadQualificationData() {
|
||||||
|
if (!this.canLoadPlayerCalendar()) return;
|
||||||
|
const records = [];
|
||||||
|
const dateRange = this.getCalendarDateRange();
|
||||||
|
const requestId = this.startCalendarRequest();
|
||||||
|
this.hideCalendarForLoading();
|
||||||
|
try {
|
||||||
|
uni.showLoading({
|
||||||
|
title: this.$t("common.loading"),
|
||||||
|
});
|
||||||
|
const params = this.buildQualificationParams(dateRange);
|
||||||
|
if (!params.RoomType) {
|
||||||
|
this.qualificationMap = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('[PlayerRoomCalendar] request Property:', params.Property, 'RoomType:', params.RoomType);
|
||||||
|
const res = await getPlayerRoomQualification(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');
|
||||||
|
}
|
||||||
|
if (this.isOtherProperty) {
|
||||||
|
records.push(...this.buildOtherPropertyRecords(res.data || []));
|
||||||
|
} else {
|
||||||
|
records.push(...(res.data || []));
|
||||||
|
}
|
||||||
|
this.qualificationMap = mapRoomCalendarQualificationRecords(records);
|
||||||
|
} catch (e) {
|
||||||
|
if (!this.isLatestCalendarRequest(requestId) || this.isRequestAbortError(e)) return;
|
||||||
|
this.qualificationMap = {};
|
||||||
|
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';
|
||||||
|
},
|
||||||
|
canLoadPlayerCalendar() {
|
||||||
|
this.userData = uni.getStorageSync("userData");
|
||||||
|
if (!this.userData || this.userData.canViewCalendar !== true) {
|
||||||
|
this.showCalendarNoPermission();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!this.safePlayerData.playerId) {
|
||||||
|
this.calendarReady = false;
|
||||||
|
uni.$u.toast(this.$t("common.dataNotFound"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
showCalendarNoPermission() {
|
||||||
|
this.qualificationMap = {};
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
buildQualificationParams(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 {
|
||||||
|
PlayerId: this.safePlayerData.playerId,
|
||||||
|
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.DefaultProperty ||
|
||||||
|
this.currentProperty.defaultProperties ||
|
||||||
|
this.currentProperty.DefaultProperties ||
|
||||||
|
this.currentProperty.defaultOtherProperty ||
|
||||||
|
this.currentProperty.DefaultOtherProperty ||
|
||||||
|
this.currentProperty.defaultOtherProperties ||
|
||||||
|
this.currentProperty.DefaultOtherProperties ||
|
||||||
|
this.currentProperty.defaultPropertyList ||
|
||||||
|
this.currentProperty.DefaultPropertyList ||
|
||||||
|
this.currentProperty.defaultPropertyCodes ||
|
||||||
|
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 propertiesWithDefaultRoomTypes = [...new Set(this.currentOtherProperties
|
||||||
|
.filter(property => (property.defaultRoomTypes || property.DefaultRoomTypes || []).length)
|
||||||
|
.map(property => property.code))];
|
||||||
|
if (propertiesWithDefaultRoomTypes.length) {
|
||||||
|
return this.limitRoomTypeCodes(propertiesWithDefaultRoomTypes);
|
||||||
|
}
|
||||||
|
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],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getDefaultPropertyCode() {
|
||||||
|
const userText = JSON.stringify(this.userData || {}).toUpperCase();
|
||||||
|
const property = this.propertyList.find(item => {
|
||||||
|
const name = String(item.name || '').toUpperCase();
|
||||||
|
return userText.indexOf(name) !== -1 || (item.code === 'OTHER' && userText.indexOf('OTHER') !== -1);
|
||||||
|
});
|
||||||
|
return (property || this.propertyList[0]).code;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
page {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-room-calendar-page {
|
||||||
|
background-color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-summary {
|
||||||
|
background-color: #c39a4b;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 16rpx 16rpx 12rpx;
|
||||||
|
padding: 26rpx 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-name-row {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-player {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-tier {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-label {
|
||||||
|
color: #fff8e8;
|
||||||
|
font-family: "Inter-Bold";
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value {
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: "Inter-Bold";
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 42rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-panel {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 2rpx 8rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-tabs {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 4rpx 0 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-type-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 6rpx 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>
|
||||||
@@ -139,10 +139,10 @@
|
|||||||
<!-- <view class="hz-font-bolder hz-font-lg" >{{ handleShowWhenScreenshot(bookingData.eta) }}</view> -->
|
<!-- <view class="hz-font-bolder hz-font-lg" >{{ handleShowWhenScreenshot(bookingData.eta) }}</view> -->
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="bookingData.mischarge!=null" class="hz-flex-row">
|
<view v-if="bookingData.miscCharge!=null" class="hz-flex-row">
|
||||||
<view style="width:100%">
|
<view style="width:100%">
|
||||||
<view class="hz-font-color-black hz-font-sm">{{ $t("common.misCharge") }}</view>
|
<view class="hz-font-color-black hz-font-sm">{{ $t("common.misCharge") }}</view>
|
||||||
<view class="hz-font-bolder hz-font-lg">{{ bookingData.mischarge || "-" }}</view>
|
<view class="hz-font-bolder hz-font-lg">{{ bookingData.miscCharge || "-" }}</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -254,19 +254,18 @@
|
|||||||
<!-- Remark -->
|
<!-- Remark -->
|
||||||
<view style="margin-bottom: 16px;margin-top: 16px;">
|
<view style="margin-bottom: 16px;margin-top: 16px;">
|
||||||
<view style="text-align: left;">{{$t("bookingConfirm.approvalRemark")}}</view>
|
<view style="text-align: left;">{{$t("bookingConfirm.approvalRemark")}}</view>
|
||||||
<u-checkbox-group placement="row" @change="changeOtherRemark"
|
<u-checkbox-group placement="row"
|
||||||
v-model="unqualifiedDateCheckboxStates[currentUnqualifiedIndex]"
|
:value="unqualifiedDateCheckboxStates[currentUnqualifiedIndex] || []"
|
||||||
|
@change="changeOtherRemark"
|
||||||
style="flex-direction: unset;display: block;">
|
style="flex-direction: unset;display: block;">
|
||||||
<view style="width:50%;display: inline-flex;">
|
<view style="width:50%;display: inline-flex;">
|
||||||
<u-checkbox activeColor="#C39A4B" labelColor="#333333" shape="circle"
|
<u-checkbox activeColor="#C39A4B" labelColor="#333333" shape="circle"
|
||||||
customStyle="height:30px;width:100%;" label="Extend" name="Extend"
|
customStyle="height:30px;width:100%;" label="Extend" name="Extend"></u-checkbox>
|
||||||
:value="unqualifiedDateCheckboxStates[currentUnqualifiedIndex] && unqualifiedDateCheckboxStates[currentUnqualifiedIndex].includes('Extend')"></u-checkbox>
|
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
<view style="width:50%;display: inline-flex;position: relative;left:10%;">
|
<view style="width:50%;display: inline-flex;position: relative;left:10%;">
|
||||||
<u-checkbox activeColor="#C39A4B" labelColor="#333333" shape="circle"
|
<u-checkbox activeColor="#C39A4B" labelColor="#333333" shape="circle"
|
||||||
customStyle="height:30px;width:100%;" label="Remark" name="Remark"
|
customStyle="height:30px;width:100%;" label="Remark" name="Remark"></u-checkbox>
|
||||||
:value="unqualifiedDateCheckboxStates[currentUnqualifiedIndex] && unqualifiedDateCheckboxStates[currentUnqualifiedIndex].includes('Remark')"></u-checkbox>
|
|
||||||
</view>
|
</view>
|
||||||
</u-checkbox-group>
|
</u-checkbox-group>
|
||||||
<view style="width:100%;margin-top:16px;" v-show="showRemark">
|
<view style="width:100%;margin-top:16px;" v-show="showRemark">
|
||||||
@@ -329,41 +328,32 @@
|
|||||||
<rich-text v-html="msgStr" style="line-height:26px;"></rich-text>
|
<rich-text v-html="msgStr" style="line-height:26px;"></rich-text>
|
||||||
</view>
|
</view>
|
||||||
<view style="margin-bottom: 10px;text-align: left;"> {{$t("bookingConfirm.approvalRemark")}}</view>
|
<view style="margin-bottom: 10px;text-align: left;"> {{$t("bookingConfirm.approvalRemark")}}</view>
|
||||||
<u-radio-group placement="row" @change="changeRoomRemark" v-if="showRadio"
|
<!-- uView:u-checkbox 必须在 u-checkbox-group 内;房型互斥用 group + 受控 :value -->
|
||||||
|
<u-checkbox-group placement="row"
|
||||||
|
:value="waitlistRoomGroupValue"
|
||||||
|
@change="onWaitlistRoomRemarkGroupChange"
|
||||||
style="flex-direction: unset;display: block;">
|
style="flex-direction: unset;display: block;">
|
||||||
<view style="width:50%;display: inline-flex;">
|
|
||||||
<u-radio activeColor="#C39A4B" labelColor="#333333" shape="circle"
|
|
||||||
customStyle="height:30px;width:100%;" label="2nd Room" name="2nd Room"></u-radio>
|
|
||||||
|
|
||||||
</view>
|
|
||||||
<view style="width:50%;display: inline-flex;position: relative;left:10%;">
|
|
||||||
<u-radio activeColor="#C39A4B" labelColor="#333333" shape="circle"
|
|
||||||
customStyle="height:30px;width:100%;" label="3rd Room" name="3rd Room"></u-radio>
|
|
||||||
</view>
|
|
||||||
</u-radio-group>
|
|
||||||
<u-checkbox-group placement="row" v-else style="flex-direction: unset;display: block;">
|
|
||||||
<view style="width:50%;display: inline-flex;">
|
<view style="width:50%;display: inline-flex;">
|
||||||
<u-checkbox activeColor="#C39A4B" labelColor="#333333" shape="circle"
|
<u-checkbox activeColor="#C39A4B" labelColor="#333333" shape="circle"
|
||||||
customStyle="height:30px;width:100%;" label="2nd Room" name="2nd Room"></u-checkbox>
|
customStyle="height:30px;width:100%;" label="2nd Room" name="2nd Room"></u-checkbox>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
<view style="width:50%;display: inline-flex;position: relative;left:10%;">
|
<view style="width:50%;display: inline-flex;position: relative;left:10%;">
|
||||||
<u-checkbox activeColor="#C39A4B" labelColor="#333333" shape="circle"
|
<u-checkbox activeColor="#C39A4B" labelColor="#333333" shape="circle"
|
||||||
customStyle="height:30px;width:100%;" label="3rd Room" name="3rd Room"></u-checkbox>
|
customStyle="height:30px;width:100%;" label="3rd Room" name="3rd Room"></u-checkbox>
|
||||||
</view>
|
</view>
|
||||||
</u-checkbox-group>
|
</u-checkbox-group>
|
||||||
<u-checkbox-group placement="row" @change="changeOtherRemark"
|
<u-checkbox-group placement="row"
|
||||||
|
:value="waitlistOtherRemarkCheckList"
|
||||||
|
@change="changeWaitlistOtherRemark"
|
||||||
style="flex-direction: unset;display: block;">
|
style="flex-direction: unset;display: block;">
|
||||||
<view style="width:50%;display: inline-flex;">
|
<view style="width:50%;display: inline-flex;">
|
||||||
<u-checkbox activeColor="#C39A4B" labelColor="#333333" shape="circle"
|
<u-checkbox activeColor="#C39A4B" labelColor="#333333" shape="circle"
|
||||||
customStyle="height:30px;width:100%;" label="Extend" name="Extend"
|
customStyle="height:30px;width:100%;" label="Extend" name="Extend"></u-checkbox>
|
||||||
:value="unqualifiedDateCheckboxStates[currentUnqualifiedIndex] && unqualifiedDateCheckboxStates[currentUnqualifiedIndex].includes('Extend')"></u-checkbox>
|
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
<view style="width:50%;display: inline-flex;position: relative;left:10%;">
|
<view style="width:50%;display: inline-flex;position: relative;left:10%;">
|
||||||
<u-checkbox activeColor="#C39A4B" labelColor="#333333" shape="circle"
|
<u-checkbox activeColor="#C39A4B" labelColor="#333333" shape="circle"
|
||||||
customStyle="height:30px;width:100%;" label="Remark" name="Remark"
|
customStyle="height:30px;width:100%;" label="Remark" name="Remark"></u-checkbox>
|
||||||
:value="unqualifiedDateCheckboxStates[currentUnqualifiedIndex] && unqualifiedDateCheckboxStates[currentUnqualifiedIndex].includes('Remark')"></u-checkbox>
|
|
||||||
</view>
|
</view>
|
||||||
</u-checkbox-group>
|
</u-checkbox-group>
|
||||||
<view style="width:100%;margin-top:16px;" v-show="showRemark">
|
<view style="width:100%;margin-top:16px;" v-show="showRemark">
|
||||||
@@ -437,7 +427,8 @@
|
|||||||
selectedApprover:'',
|
selectedApprover:'',
|
||||||
showRemark: false,
|
showRemark: false,
|
||||||
showWaitlistModal:false,
|
showWaitlistModal:false,
|
||||||
showRadio:false,
|
// Waitlist 弹窗 Extend/Remark(必须与 u-checkbox-group 的 :value 绑定,勿用子项 :value)
|
||||||
|
waitlistOtherRemarkCheckList: [],
|
||||||
unqualifiedDateList: [],
|
unqualifiedDateList: [],
|
||||||
currentUnqualifiedIndex: 0,
|
currentUnqualifiedIndex: 0,
|
||||||
unqualifiedDateRemarks: [],
|
unqualifiedDateRemarks: [],
|
||||||
@@ -463,6 +454,17 @@
|
|||||||
// 數據方法
|
// 數據方法
|
||||||
getBooking(){
|
getBooking(){
|
||||||
this.bookingData = uni.getStorageSync("bookingData");
|
this.bookingData = uni.getStorageSync("bookingData");
|
||||||
|
// 确保 roomRemark 是响应式字段,否则 iOS 上可能出现 u-checkbox 状态未被正确回刷导致“可多选”
|
||||||
|
if (this.bookingData !== null && this.bookingData !== undefined && typeof this.bookingData === 'object') {
|
||||||
|
const hasRoomRemark = Object.prototype.hasOwnProperty.call(this.bookingData, 'roomRemark');
|
||||||
|
if (!hasRoomRemark || this.bookingData.roomRemark === null || this.bookingData.roomRemark === undefined || this.bookingData.roomRemark === 'null') {
|
||||||
|
(this.$set ? this.$set(this.bookingData, 'roomRemark', '') : this.bookingData.roomRemark = '');
|
||||||
|
}
|
||||||
|
const hasOtherRemark = Object.prototype.hasOwnProperty.call(this.bookingData, 'otherRemark');
|
||||||
|
if (!hasOtherRemark || this.bookingData.otherRemark === null || this.bookingData.otherRemark === undefined || this.bookingData.otherRemark === 'null') {
|
||||||
|
(this.$set ? this.$set(this.bookingData, 'otherRemark', '') : this.bookingData.otherRemark = '');
|
||||||
|
}
|
||||||
|
}
|
||||||
if(this.bookingData!==null && this.bookingData!==undefined && this.bookingData!==''){
|
if(this.bookingData!==null && this.bookingData!==undefined && this.bookingData!==''){
|
||||||
this.oldDateList = this.bookingData.dateList;
|
this.oldDateList = this.bookingData.dateList;
|
||||||
this.checkList = [];
|
this.checkList = [];
|
||||||
@@ -498,11 +500,7 @@
|
|||||||
this.submitBooking();
|
this.submitBooking();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
submitApproval() {
|
buildUnqualifiedDateLists() {
|
||||||
// 显示审批模态框
|
|
||||||
this.getApprovalReason();
|
|
||||||
this.showApprovalModal = true;
|
|
||||||
this.showModalBtn = true;
|
|
||||||
this.currentUnqualifiedIndex = 0;
|
this.currentUnqualifiedIndex = 0;
|
||||||
this.unqualifiedDateList = [];
|
this.unqualifiedDateList = [];
|
||||||
this.unqualifiedDateRemarks = [];
|
this.unqualifiedDateRemarks = [];
|
||||||
@@ -510,33 +508,27 @@
|
|||||||
this.unqualifiedDateApprovers = [];
|
this.unqualifiedDateApprovers = [];
|
||||||
this.unqualifiedDateOtherRemarks = [];
|
this.unqualifiedDateOtherRemarks = [];
|
||||||
this.unqualifiedDateCheckboxStates = [];
|
this.unqualifiedDateCheckboxStates = [];
|
||||||
if(this.checkList!=null && this.checkList.length>0){
|
if (this.checkList != null && this.checkList.length > 0) {
|
||||||
if(this.bookingData.bookType==1){
|
if (this.bookingData.bookType == 1) {
|
||||||
// 把extendDate中的状态都设置成不够分
|
for (let i = 0; i < this.bookingData.extendDate.length; i++) {
|
||||||
for(let i=0;i<this.bookingData.extendDate.length;i++){
|
for (let j = 0; j < this.checkList.length; j++) {
|
||||||
for(let j=0;j<this.checkList.length;j++){
|
if (i == this.checkList[j] && this.bookingData.extendDate[i].status == 1) {
|
||||||
if(i == this.checkList[j] && this.bookingData.extendDate[i].status==1){
|
|
||||||
console.log(this.bookingData.extendDate[i]);
|
|
||||||
this.unqualifiedDateList.push(this.bookingData.extendDate[i]);
|
this.unqualifiedDateList.push(this.bookingData.extendDate[i]);
|
||||||
this.unqualifiedDateRemarks.push(this.bookingData.extendDate[i].remark || null);
|
this.unqualifiedDateRemarks.push(this.bookingData.extendDate[i].remark || null);
|
||||||
this.unqualifiedDateReasons.push('');
|
this.unqualifiedDateReasons.push('');
|
||||||
// 多选 Approver:为每个 index 初始化为数组
|
|
||||||
this.unqualifiedDateApprovers.push([]);
|
this.unqualifiedDateApprovers.push([]);
|
||||||
this.unqualifiedDateOtherRemarks.push('');
|
this.unqualifiedDateOtherRemarks.push('');
|
||||||
this.unqualifiedDateCheckboxStates.push([]);
|
this.unqualifiedDateCheckboxStates.push([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}else{
|
} else {
|
||||||
// 把dateList中的状态都设置成不够分
|
for (let i = 0; i < this.bookingData.dateList.length; i++) {
|
||||||
for(let i=0;i<this.bookingData.dateList.length;i++){
|
for (let j = 0; j < this.checkList.length; j++) {
|
||||||
for(let j=0;j<this.checkList.length;j++){
|
if (i == this.checkList[j] && this.bookingData.dateList[i].status == 1) {
|
||||||
if(i == this.checkList[j] && this.bookingData.dateList[i].status==1){
|
|
||||||
// console.log(this.bookingData.dateList[i]);
|
|
||||||
this.unqualifiedDateList.push(this.bookingData.dateList[i]);
|
this.unqualifiedDateList.push(this.bookingData.dateList[i]);
|
||||||
this.unqualifiedDateRemarks.push(this.bookingData.dateList[i].remark || '');
|
this.unqualifiedDateRemarks.push(this.bookingData.dateList[i].remark || '');
|
||||||
this.unqualifiedDateReasons.push('');
|
this.unqualifiedDateReasons.push('');
|
||||||
// 多选 Approver:为每个 index 初始化为数组
|
|
||||||
this.unqualifiedDateApprovers.push([]);
|
this.unqualifiedDateApprovers.push([]);
|
||||||
this.unqualifiedDateOtherRemarks.push('');
|
this.unqualifiedDateOtherRemarks.push('');
|
||||||
this.unqualifiedDateCheckboxStates.push([]);
|
this.unqualifiedDateCheckboxStates.push([]);
|
||||||
@@ -544,10 +536,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}else{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
submitApproval() {
|
||||||
|
// 与 Waitlist 共用:必须先填充 unqualifiedDateList,否则 getWaitlistRemark 无法匹配 Extend
|
||||||
|
this.buildUnqualifiedDateLists();
|
||||||
|
// 显示审批模态框
|
||||||
|
this.getApprovalReason();
|
||||||
|
this.showApprovalModal = true;
|
||||||
|
this.showModalBtn = true;
|
||||||
|
},
|
||||||
onApprovalChange(value){
|
onApprovalChange(value){
|
||||||
this.selectedApprover = value;
|
this.selectedApprover = value;
|
||||||
// 多选:value 为数组,需要拼接 approverName
|
// 多选:value 为数组,需要拼接 approverName
|
||||||
@@ -1103,7 +1101,7 @@
|
|||||||
if (this.bookingData.extendDate[i].status == 1) {
|
if (this.bookingData.extendDate[i].status == 1) {
|
||||||
|
|
||||||
// remarkText只保存remark输入的内容,不加Extend前缀
|
// remarkText只保存remark输入的内容,不加Extend前缀
|
||||||
this.bookingData.extendData[i].remarkText = this.unqualifiedDateRemarks[unqualifiedIndex] || '';
|
this.bookingData.extendDate[i].remarkText = this.unqualifiedDateRemarks[unqualifiedIndex] || '';
|
||||||
|
|
||||||
// 添加approvalReason字段
|
// 添加approvalReason字段
|
||||||
this.bookingData.extendDate[i].approvalReason = this.unqualifiedDateReasons[unqualifiedIndex] || null;
|
this.bookingData.extendDate[i].approvalReason = this.unqualifiedDateReasons[unqualifiedIndex] || null;
|
||||||
@@ -1434,40 +1432,64 @@
|
|||||||
},
|
},
|
||||||
getWaitlistRemark(type,index){
|
getWaitlistRemark(type,index){
|
||||||
let item = (type==2?this.bookingData.extendDate[index]:this.bookingData.dateList[index]);
|
let item = (type==2?this.bookingData.extendDate[index]:this.bookingData.dateList[index]);
|
||||||
let remarkArr = [];
|
let unqualifiedIndex = this.unqualifiedDateList.findIndex(uItem => uItem === item);
|
||||||
let roomRemark = this.bookingData.roomRemark;
|
if (unqualifiedIndex === -1 && item.date && item.date.length) {
|
||||||
let otherRemark = this.bookingData.otherRemark;
|
unqualifiedIndex = this.unqualifiedDateList.findIndex((uItem) => {
|
||||||
if(roomRemark!=''&&roomRemark!=undefined) remarkArr.push(roomRemark);
|
const ud = uItem.date;
|
||||||
if(otherRemark!=''&&otherRemark!=undefined) remarkArr.push(otherRemark);
|
const id = item.date;
|
||||||
|
return ud && id && ud.length && id.length &&
|
||||||
|
ud[0] === id[0] && ud[ud.length - 1] === id[id.length - 1];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 仅一条 Waitlist/Unqualified 时,引用不一致也会导致 findIndex 失败,直接对齐下标 0
|
||||||
|
if (unqualifiedIndex === -1 && this.unqualifiedDateList.length === 1) {
|
||||||
|
unqualifiedIndex = 0;
|
||||||
|
}
|
||||||
|
const checkboxState = unqualifiedIndex !== -1
|
||||||
|
? (this.unqualifiedDateCheckboxStates[unqualifiedIndex] || [])
|
||||||
|
: [];
|
||||||
|
const otherSlot = unqualifiedIndex !== -1
|
||||||
|
? (this.unqualifiedDateOtherRemarks[unqualifiedIndex] || '')
|
||||||
|
: '';
|
||||||
|
const wlTags = this.waitlistOtherRemarkCheckList || [];
|
||||||
|
const globalOther = (this.bookingData && this.bookingData.otherRemark) ? String(this.bookingData.otherRemark) : '';
|
||||||
|
const parseTags = (s) => String(s).split(';').map((x) => x.trim()).filter(Boolean);
|
||||||
|
const hasExtend = checkboxState.includes('Extend') ||
|
||||||
|
parseTags(otherSlot).includes('Extend') ||
|
||||||
|
wlTags.includes('Extend') ||
|
||||||
|
parseTags(globalOther).includes('Extend');
|
||||||
|
|
||||||
// 找到对应的unqualifiedDate索引
|
const roomRemark = this.bookingData.roomRemark;
|
||||||
const unqualifiedIndex = this.unqualifiedDateList.findIndex(uItem => uItem === item);
|
const idxRemark = unqualifiedIndex !== -1 ? this.unqualifiedDateRemarks[unqualifiedIndex] : '';
|
||||||
let extendStr = '';
|
const manualRemark = (idxRemark != null && idxRemark !== '') ? idxRemark : this.freeRemark;
|
||||||
if (unqualifiedIndex !== -1) {
|
const remarkChecked = unqualifiedIndex !== -1 &&
|
||||||
const checkboxState = this.unqualifiedDateCheckboxStates[unqualifiedIndex] || [];
|
(this.unqualifiedDateCheckboxStates[unqualifiedIndex] || []).includes('Remark');
|
||||||
if (checkboxState.includes('Extend')) {
|
|
||||||
extendStr = 'Extend';
|
if (unqualifiedIndex !== -1 && this.unqualifiedDateOtherRemarks[unqualifiedIndex] != null) {
|
||||||
}
|
this.bookingData.otherRemark = this.unqualifiedDateOtherRemarks[unqualifiedIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 对应 BookingDate 的 remark:勾选 Extend 时放在字符串首位
|
||||||
|
const dateParts = [];
|
||||||
|
if (hasExtend) {
|
||||||
|
dateParts.push('Extend');
|
||||||
|
}
|
||||||
|
if (roomRemark != null && roomRemark !== '' && roomRemark !== undefined) {
|
||||||
|
dateParts.push(roomRemark);
|
||||||
|
}
|
||||||
if (item.status == 1 &&
|
if (item.status == 1 &&
|
||||||
item.remarkText != '' &&
|
item.remarkText != null && item.remarkText !== '' && item.remarkText !== undefined) {
|
||||||
item.remarkText != undefined) {
|
dateParts.push(item.remarkText);
|
||||||
if (extendStr) {
|
}
|
||||||
remarkArr.push(extendStr + '; ' + item.remarkText);
|
if ((remarkChecked || this.showRemark) && manualRemark !== '' && manualRemark != null && item.status == 1) {
|
||||||
} else {
|
if (manualRemark !== item.remarkText) dateParts.push(manualRemark);
|
||||||
remarkArr.push(item.remarkText);
|
|
||||||
}
|
|
||||||
} else if (item.status == 1 && extendStr) {
|
|
||||||
remarkArr.push(extendStr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.showRemark&&this.freeRemark!=''&&item.status==1) remarkArr.push(this.freeRemark);
|
|
||||||
|
|
||||||
if(remarkArr.length>0) {
|
const dateRemarkJoin = dateParts.join('; ');
|
||||||
this.bookingData.remark = remarkArr.join('; ');
|
if (dateRemarkJoin) {
|
||||||
item.remark = remarkArr.join('; ');
|
this.bookingData.remark = dateRemarkJoin;
|
||||||
}else{
|
item.remark = dateRemarkJoin;
|
||||||
|
} else {
|
||||||
this.bookingData.remark = "";
|
this.bookingData.remark = "";
|
||||||
item.remark = "";
|
item.remark = "";
|
||||||
}
|
}
|
||||||
@@ -1563,6 +1585,10 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
showWaitlistPop() {
|
showWaitlistPop() {
|
||||||
|
// 关键:直接点「放入 Waitlist」不会经过 submitApproval,必须在此处填充 unqualifiedDateList,
|
||||||
|
// 否则 getWaitlistRemark 里 findIndex 恒为 -1,Extend 永远不会写入 remark
|
||||||
|
this.buildUnqualifiedDateLists();
|
||||||
|
this.waitlistOtherRemarkCheckList = (this.unqualifiedDateCheckboxStates[0] || []).slice();
|
||||||
this.showWaitlistModal = true;
|
this.showWaitlistModal = true;
|
||||||
this.showMsgBox = false;
|
this.showMsgBox = false;
|
||||||
},
|
},
|
||||||
@@ -1606,17 +1632,27 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
changeRoomRemark(e,item) {
|
onWaitlistRoomRemarkGroupChange(e) {
|
||||||
if (e) {
|
const raw = Array.isArray(e) ? e : (e && Array.isArray(e.detail) ? e.detail : []);
|
||||||
if(e==item.roomRemark){
|
const names = raw.map((x) => (x != null ? String(x) : '')).filter(Boolean);
|
||||||
item.roomRemark=""
|
if (!this.bookingData) {
|
||||||
item.showRadio = true;
|
return;
|
||||||
setTimeout(()=>{item.showRadio = false;this.$forceUpdate()},1)
|
|
||||||
}else{
|
|
||||||
item.roomRemark=e
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.$forceUpdate()
|
if (!Object.prototype.hasOwnProperty.call(this.bookingData, 'roomRemark')) {
|
||||||
|
this.$set(this.bookingData, 'roomRemark', '');
|
||||||
|
}
|
||||||
|
if (names.length === 0) {
|
||||||
|
this.bookingData.roomRemark = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (names.length === 1) {
|
||||||
|
this.bookingData.roomRemark = names[0];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 双选时视为互斥:保留本次切换目标(与原先不同的那一项)
|
||||||
|
const prev = this.bookingData.roomRemark;
|
||||||
|
const next = names.find((n) => n !== prev) || names[names.length - 1];
|
||||||
|
this.bookingData.roomRemark = next;
|
||||||
},
|
},
|
||||||
// changeOtherRemark(e,item) {
|
// changeOtherRemark(e,item) {
|
||||||
// if (e) {
|
// if (e) {
|
||||||
@@ -1635,27 +1671,41 @@
|
|||||||
// this.$forceUpdate()
|
// this.$forceUpdate()
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
|
changeWaitlistOtherRemark(e) {
|
||||||
|
const raw = Array.isArray(e) ? e : (e && Array.isArray(e.detail) ? e.detail : []);
|
||||||
|
const names = raw.map((x) => (x != null ? String(x) : '')).filter(Boolean);
|
||||||
|
this.waitlistOtherRemarkCheckList = names.slice();
|
||||||
|
const extras = names.filter((n) => n !== 'Remark');
|
||||||
|
const str = extras.join('; ');
|
||||||
|
this.showRemark = names.includes('Remark');
|
||||||
|
const n = this.unqualifiedDateCheckboxStates.length;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
this.$set(this.unqualifiedDateCheckboxStates, i, names.slice());
|
||||||
|
this.$set(this.unqualifiedDateOtherRemarks, i, str);
|
||||||
|
}
|
||||||
|
if (this.bookingData) {
|
||||||
|
this.bookingData.otherRemark = str;
|
||||||
|
}
|
||||||
|
},
|
||||||
changeOtherRemark(e) {
|
changeOtherRemark(e) {
|
||||||
if (e) {
|
const raw = Array.isArray(e) ? e : (e && Array.isArray(e.detail) ? e.detail : []);
|
||||||
let str = '';
|
const idx = this.currentUnqualifiedIndex;
|
||||||
let show = false;
|
if (!raw || raw.length === 0) {
|
||||||
for (let i = 0; i < e.length; i++) {
|
|
||||||
|
|
||||||
if (e[i] == 'Remark') {
|
|
||||||
show = true;
|
|
||||||
} else {
|
|
||||||
str = e[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.showRemark = show;
|
|
||||||
// 存储到当前索引的otherRemark
|
|
||||||
this.unqualifiedDateOtherRemarks[this.currentUnqualifiedIndex] = str;
|
|
||||||
// 存储checkbox状态
|
|
||||||
this.unqualifiedDateCheckboxStates[this.currentUnqualifiedIndex] = e;
|
|
||||||
} else {
|
|
||||||
this.showRemark = false;
|
this.showRemark = false;
|
||||||
this.unqualifiedDateOtherRemarks[this.currentUnqualifiedIndex] = '';
|
this.$set(this.unqualifiedDateOtherRemarks, idx, '');
|
||||||
this.unqualifiedDateCheckboxStates[this.currentUnqualifiedIndex] = [];
|
this.$set(this.unqualifiedDateCheckboxStates, idx, []);
|
||||||
|
if (this.bookingData) {
|
||||||
|
this.bookingData.otherRemark = '';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const names = raw.map((x) => (x != null ? String(x) : '')).filter(Boolean);
|
||||||
|
const extras = names.filter((n) => n !== 'Remark');
|
||||||
|
this.showRemark = names.includes('Remark');
|
||||||
|
this.$set(this.unqualifiedDateCheckboxStates, idx, names.slice());
|
||||||
|
this.$set(this.unqualifiedDateOtherRemarks, idx, extras.join('; '));
|
||||||
|
if (this.bookingData) {
|
||||||
|
this.bookingData.otherRemark = extras.join('; ');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1671,6 +1721,16 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed:{
|
computed:{
|
||||||
|
waitlistRoomGroupValue() {
|
||||||
|
if (!this.bookingData || !this.bookingData.roomRemark) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const r = this.bookingData.roomRemark;
|
||||||
|
if (r === '2nd Room' || r === '3rd Room') {
|
||||||
|
return [r];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
getStatusColor(){
|
getStatusColor(){
|
||||||
return function (booking) {
|
return function (booking) {
|
||||||
let operaStatus = null;
|
let operaStatus = null;
|
||||||
@@ -1991,4 +2051,4 @@
|
|||||||
color: #C39A4B;
|
color: #C39A4B;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* 将 API 返回的 PascalCase 字段名转为前端使用的 camelCase(递归处理嵌套对象/数组)
|
||||||
|
*/
|
||||||
|
function toCamelCaseKey(key) {
|
||||||
|
if (!key || typeof key !== 'string') return key;
|
||||||
|
if (key[0] === key[0].toLowerCase()) return key;
|
||||||
|
return key.charAt(0).toLowerCase() + key.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeApiKeys(value) {
|
||||||
|
if (value === null || value === undefined) return value;
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(item => normalizeApiKeys(item));
|
||||||
|
}
|
||||||
|
if (typeof value !== 'object') return value;
|
||||||
|
|
||||||
|
const result = {};
|
||||||
|
for (const [key, val] of Object.entries(value)) {
|
||||||
|
result[toCamelCaseKey(key)] = normalizeApiKeys(val);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 兼容 API 返回 records / Records */
|
||||||
|
export function getApiRecords(data) {
|
||||||
|
if (!data) return [];
|
||||||
|
const records = data.records ?? data.Records;
|
||||||
|
return Array.isArray(records) ? records : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default normalizeApiKeys;
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* iOS APNs DeviceToken 封装(原生插件:NetworkUtils-NetworkUtilsModule)
|
||||||
|
* 对应原生方法:getDeviceTokenAsync、refreshDeviceTokenAsync
|
||||||
|
*/
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
const pushNetworkModule = uni.requireNativePlugin('NetworkUtils-NetworkUtilsModule')
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取当前缓存的 DeviceToken(最近一次 didRegisterForRemoteNotifications 写入本地)
|
||||||
|
* @param {Record<string, unknown>} options 透传给原生,可为 {}
|
||||||
|
*/
|
||||||
|
export function getDeviceToken(options = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
if (!pushNetworkModule || typeof pushNetworkModule.getDeviceTokenAsync !== 'function') {
|
||||||
|
reject(new Error('NetworkUtils 原生插件不可用或未注册 getDeviceTokenAsync'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pushNetworkModule.getDeviceTokenAsync(Object.assign({ registerIfEmpty: true }, options || {}), (ret) => {
|
||||||
|
if (ret && (ret.status === 'success' || ret.status === 'empty')) {
|
||||||
|
resolve(ret)
|
||||||
|
} else {
|
||||||
|
reject(ret || new Error('getDeviceTokenAsync 返回异常'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
// #ifndef APP-PLUS
|
||||||
|
resolve({
|
||||||
|
status: 'empty',
|
||||||
|
deviceToken: '',
|
||||||
|
hasToken: false,
|
||||||
|
updatedAt: 0,
|
||||||
|
message: '非 App-PLUS 环境'
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发重新向 APNs 注册(token 更新后由原生写入本地,再调用 getDeviceToken 读取)
|
||||||
|
* @param {{ requestAuthorization?: boolean }} options requestAuthorization 默认 true;已授权后可传 false 避免重复弹权限逻辑
|
||||||
|
*/
|
||||||
|
export function refreshDeviceTokenAsync(options = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
if (!pushNetworkModule || typeof pushNetworkModule.refreshDeviceTokenAsync !== 'function') {
|
||||||
|
reject(new Error('NetworkUtils 原生插件不可用或未注册 refreshDeviceTokenAsync'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const opts = {
|
||||||
|
requestAuthorization: options.requestAuthorization !== false
|
||||||
|
}
|
||||||
|
pushNetworkModule.refreshDeviceTokenAsync(opts, (ret) => {
|
||||||
|
if (ret && ret.status === 'requested') {
|
||||||
|
resolve(ret)
|
||||||
|
} else if (ret && (ret.status === 'error' || ret.status === 'denied')) {
|
||||||
|
reject(ret)
|
||||||
|
} else {
|
||||||
|
resolve(ret || {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
// #ifndef APP-PLUS
|
||||||
|
resolve({ status: 'skipped', message: '非 App-PLUS 环境' })
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 先刷新注册再轮询读取(适合首次拿 token 或服务端判定失效后)
|
||||||
|
*/
|
||||||
|
export async function refreshDeviceTokenAndFetch(options = {}) {
|
||||||
|
await refreshDeviceTokenAsync(options)
|
||||||
|
// 授权弹窗关闭 + registerForRemoteNotifications 多为异步,适度拉长首次等待
|
||||||
|
await new Promise((r) => setTimeout(r, 900))
|
||||||
|
let last = await getDeviceToken()
|
||||||
|
for (let i = 0; i < 25 && last.status === 'empty'; i++) {
|
||||||
|
await new Promise((r) => setTimeout(r, 400))
|
||||||
|
last = await getDeviceToken()
|
||||||
|
}
|
||||||
|
return last
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
/**
|
||||||
|
* iOS 远程推送消息封装(原生插件:NetworkUtils-NetworkUtilsModule)
|
||||||
|
* 对应原生方法:onPushNotificationAsync、consumePendingPushNotificationsAsync
|
||||||
|
*/
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
const pushNetworkModule = uni.requireNativePlugin('NetworkUtils-NetworkUtilsModule')
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
let lastPushNotification = null
|
||||||
|
const processedPushKeys = {}
|
||||||
|
|
||||||
|
function getPushDedupeKey(title, content) {
|
||||||
|
return `${title || ''}|${content || ''}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function markPushProcessed(title, content) {
|
||||||
|
const key = getPushDedupeKey(title, content)
|
||||||
|
processedPushKeys[key] = true
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPushProcessed(title, content) {
|
||||||
|
return !!processedPushKeys[getPushDedupeKey(title, content)]
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchPushMessage(onMessage, payload, meta = {}) {
|
||||||
|
if (!meta.userTapped && isPushProcessed(payload.title, payload.content)) {
|
||||||
|
console.log('[Push] skip duplicate push:', getPushDedupeKey(payload.title, payload.content))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
markPushProcessed(payload.title, payload.content)
|
||||||
|
lastPushNotification = payload
|
||||||
|
if (typeof onMessage === 'function') {
|
||||||
|
onMessage(payload, meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUserTappedFlag(value) {
|
||||||
|
return value === true || value === 1 || value === '1' || value === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPushConfirmNo(res = {}) {
|
||||||
|
const confirmNo = res.ConfirmNo || res.confirmNo || res.confNo || res.raw?.ConfirmNo || res.raw?.confirmNo || res.raw?.confNo
|
||||||
|
return confirmNo ? String(confirmNo) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将原生回调数据规范化为 { title, content, body, confirmNo, userTapped, raw }
|
||||||
|
* @param {Record<string, unknown>} res
|
||||||
|
*/
|
||||||
|
export function normalizePushPayload(res = {}) {
|
||||||
|
const title = typeof res.title === 'string' ? res.title : (res.title ? String(res.title) : '')
|
||||||
|
const body = typeof res.body === 'string' ? res.body : (res.body ? String(res.body) : '')
|
||||||
|
const confirmNo = getPushConfirmNo(res)
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
content: body,
|
||||||
|
body,
|
||||||
|
confirmNo,
|
||||||
|
userTapped: parseUserTappedFlag(res.userTapped),
|
||||||
|
raw: res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近一次收到的推送(内存缓存)
|
||||||
|
*/
|
||||||
|
export function getLastPushNotification() {
|
||||||
|
return lastPushNotification
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取并清空 Native 侧缓存的待处理推送(后台/冷启动场景)
|
||||||
|
* @returns {Promise<Array<{ title: string, content: string, body: string, raw: object }>>}
|
||||||
|
*/
|
||||||
|
export function consumePendingPushNotifications() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
if (!pushNetworkModule) {
|
||||||
|
console.error('[Push] NetworkUtils 原生插件不可用')
|
||||||
|
resolve([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof pushNetworkModule.consumePendingPushNotificationsAsync !== 'function') {
|
||||||
|
console.warn('[Push] 原生暂不支持 consumePendingPushNotificationsAsync,跳过 Native pending 同步')
|
||||||
|
resolve([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('[Push] 调用 consumePendingPushNotificationsAsync ...')
|
||||||
|
pushNetworkModule.consumePendingPushNotificationsAsync({}, (ret) => {
|
||||||
|
const items = Array.isArray(ret?.items) ? ret.items : []
|
||||||
|
console.log('[Push] consumePendingPushNotificationsAsync 返回 count=', ret?.count ?? items.length)
|
||||||
|
resolve(items.map((item) => normalizePushPayload(item)))
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
// #ifndef APP-PLUS
|
||||||
|
resolve([])
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步处理 Native 缓存的推送
|
||||||
|
* @param {(payload: { title: string, content: string, body: string, raw: object }, meta?: object) => void} onMessage
|
||||||
|
* @param {{ showToast?: boolean }} [options]
|
||||||
|
*/
|
||||||
|
export async function syncPendingPushNotifications(onMessage, options = {}) {
|
||||||
|
console.log('[Push] syncPendingPushNotifications start')
|
||||||
|
const pending = await consumePendingPushNotifications()
|
||||||
|
if (!pending.length) {
|
||||||
|
console.log('[Push] syncPendingPushNotifications: 队列为空')
|
||||||
|
return pending
|
||||||
|
}
|
||||||
|
console.log('[Push] syncPendingPushNotifications count=', pending.length)
|
||||||
|
for (const payload of pending) {
|
||||||
|
dispatchPushMessage(onMessage, payload, {
|
||||||
|
fromPending: true,
|
||||||
|
userTapped: payload.userTapped === true,
|
||||||
|
showToast: options.showToast !== false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return pending
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册推送消息监听;每次收到远程推送时 callback 会被调用
|
||||||
|
* @param {(payload: { title: string, content: string, body: string, raw: object }, meta?: object) => void} onMessage
|
||||||
|
* @returns {() => void}
|
||||||
|
*/
|
||||||
|
export function listenPushNotification(onMessage) {
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
if (!pushNetworkModule || typeof pushNetworkModule.onPushNotificationAsync !== 'function') {
|
||||||
|
console.warn('[Push] NetworkUtils 原生插件不可用或未注册 onPushNotificationAsync')
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
pushNetworkModule.onPushNotificationAsync({}, (res) => {
|
||||||
|
const payload = normalizePushPayload(res)
|
||||||
|
console.log('[Push] 收到推送通知:', JSON.stringify(payload))
|
||||||
|
dispatchPushMessage(onMessage, payload, {
|
||||||
|
fromPending: !!res?.fromPending,
|
||||||
|
userTapped: payload.userTapped === true,
|
||||||
|
showToast: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return () => {}
|
||||||
|
// #endif
|
||||||
|
// #ifndef APP-PLUS
|
||||||
|
console.warn('[Push] 非 App-PLUS 环境,跳过推送监听')
|
||||||
|
return () => {}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
+9
-4
@@ -121,8 +121,10 @@ module.exports = (vm) => {
|
|||||||
break;
|
break;
|
||||||
case 400:
|
case 400:
|
||||||
kickout()
|
kickout()
|
||||||
|
break;
|
||||||
case 401:
|
case 401:
|
||||||
if(error.config.url.indexOf('refresh_token')==-1 && error.config.url.indexOf('logout')==-1){
|
if(error.config.url.indexOf('refresh_token')==-1 && error.config.url.indexOf('logout')==-1 && !error.config.custom?.tokenRetried){
|
||||||
|
error.config.custom = {...error.config.custom, tokenRetried: true};
|
||||||
return tokenRefresh().then(_ => {
|
return tokenRefresh().then(_ => {
|
||||||
error.config.header.Authorization = uni.getStorageSync("token");
|
error.config.header.Authorization = uni.getStorageSync("token");
|
||||||
error.config.header.RefreshToken = uni.getStorageSync("refreshToken")
|
error.config.header.RefreshToken = uni.getStorageSync("refreshToken")
|
||||||
@@ -141,19 +143,22 @@ module.exports = (vm) => {
|
|||||||
|
|
||||||
}
|
}
|
||||||
function tokenRefresh(){
|
function tokenRefresh(){
|
||||||
let oldToken = uni.getStorageSync("token");
|
const storedRefreshToken = uni.getStorageSync("refreshToken");
|
||||||
if (oldToken !== null && oldToken !== '') {
|
if (storedRefreshToken) {
|
||||||
return refreshToken({})
|
return refreshToken({})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
if (!data?.success || !data.accessToken) throw new Error('Token refresh failed');
|
||||||
uni.setStorageSync("token",data.accessToken);
|
uni.setStorageSync("token",data.accessToken);
|
||||||
uni.setStorageSync("tokenTime",dayjs().unix());
|
uni.setStorageSync("tokenTime",dayjs().unix());
|
||||||
return Promise.resolve(true)
|
return Promise.resolve(true)
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
kickout()
|
kickout()
|
||||||
|
return Promise.reject(error);
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
kickout()
|
kickout()
|
||||||
|
return Promise.reject(new Error('Missing RefreshToken'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function checkToken(){
|
function checkToken(){
|
||||||
@@ -198,4 +203,4 @@ function checkIP(header){
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
function parseDate(value) {
|
||||||
|
const parts = value.split('-').map(Number);
|
||||||
|
return new Date(parts[0], parts[1] - 1, parts[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(date) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = `${date.getMonth() + 1}`.padStart(2, '0');
|
||||||
|
const day = `${date.getDate()}`.padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDays(date, count) {
|
||||||
|
const next = new Date(date);
|
||||||
|
next.setDate(next.getDate() + count);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMonths(date, count) {
|
||||||
|
return new Date(date.getFullYear(), date.getMonth() + count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startOfWeek(date) {
|
||||||
|
return addDays(date, -date.getDay());
|
||||||
|
}
|
||||||
|
|
||||||
|
function endOfWeek(date) {
|
||||||
|
return addDays(date, 6 - date.getDay());
|
||||||
|
}
|
||||||
|
|
||||||
|
function endOfMonth(date) {
|
||||||
|
return new Date(date.getFullYear(), date.getMonth() + 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthLabels = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'];
|
||||||
|
|
||||||
|
export function buildCalendarMonths({ today, monthCount, weekdayLabels }) {
|
||||||
|
const todayDate = parseDate(today);
|
||||||
|
const months = [];
|
||||||
|
|
||||||
|
for (let monthIndex = 0; monthIndex < monthCount; monthIndex++) {
|
||||||
|
const monthDate = addMonths(todayDate, monthIndex);
|
||||||
|
const monthStart = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1);
|
||||||
|
const start = startOfWeek(monthStart);
|
||||||
|
const end = endOfWeek(endOfMonth(monthStart));
|
||||||
|
const weeks = [];
|
||||||
|
let cursor = start;
|
||||||
|
let weekIndex = 0;
|
||||||
|
|
||||||
|
while (cursor <= end) {
|
||||||
|
const days = weekdayLabels.map((item, dayIndex) => {
|
||||||
|
const date = addDays(cursor, dayIndex);
|
||||||
|
const dateText = formatDate(date);
|
||||||
|
const isLeadingPastDate = monthIndex === 0 && date < todayDate;
|
||||||
|
const isOutsideMonth = date.getMonth() !== monthStart.getMonth() && !isLeadingPastDate;
|
||||||
|
return {
|
||||||
|
date: dateText,
|
||||||
|
dayNo: isOutsideMonth ? '' : `${date.getDate()}`.padStart(2, '0'),
|
||||||
|
isToday: !isOutsideMonth && dateText === today,
|
||||||
|
isPast: date < todayDate,
|
||||||
|
isOutsideMonth,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
weeks.push({
|
||||||
|
weekKey: formatDate(cursor),
|
||||||
|
days,
|
||||||
|
});
|
||||||
|
cursor = addDays(cursor, 7);
|
||||||
|
weekIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
months.push({
|
||||||
|
monthKey: formatDate(monthStart).slice(0, 7),
|
||||||
|
monthLabel: monthLabels[monthStart.getMonth()],
|
||||||
|
weeks,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return months;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCalendarDateRange({ today, startDate, endDate, weekdayLabels }) {
|
||||||
|
const todayDate = parseDate(today);
|
||||||
|
const rangeStart = parseDate(startDate);
|
||||||
|
const rangeEnd = parseDate(endDate);
|
||||||
|
const months = [];
|
||||||
|
let monthDate = new Date(rangeStart.getFullYear(), rangeStart.getMonth(), 1);
|
||||||
|
const lastMonthDate = new Date(rangeEnd.getFullYear(), rangeEnd.getMonth(), 1);
|
||||||
|
|
||||||
|
while (monthDate <= lastMonthDate) {
|
||||||
|
const monthStart = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1);
|
||||||
|
const visibleStart = rangeStart > monthStart ? rangeStart : monthStart;
|
||||||
|
const monthEnd = endOfMonth(monthStart);
|
||||||
|
const visibleEnd = rangeEnd < monthEnd ? rangeEnd : monthEnd;
|
||||||
|
const start = startOfWeek(visibleStart);
|
||||||
|
const end = endOfWeek(visibleEnd);
|
||||||
|
const weeks = [];
|
||||||
|
let cursor = start;
|
||||||
|
|
||||||
|
while (cursor <= end) {
|
||||||
|
const days = weekdayLabels.map((item, dayIndex) => {
|
||||||
|
const date = addDays(cursor, dayIndex);
|
||||||
|
const dateText = formatDate(date);
|
||||||
|
const isOutsideMonth = date < rangeStart || date > rangeEnd || date.getMonth() !== monthStart.getMonth();
|
||||||
|
return {
|
||||||
|
date: dateText,
|
||||||
|
dayNo: isOutsideMonth ? '' : `${date.getDate()}`.padStart(2, '0'),
|
||||||
|
isToday: !isOutsideMonth && dateText === today,
|
||||||
|
isPast: date < todayDate,
|
||||||
|
isOutsideMonth,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
weeks.push({
|
||||||
|
weekKey: formatDate(cursor),
|
||||||
|
days,
|
||||||
|
});
|
||||||
|
cursor = addDays(cursor, 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
months.push({
|
||||||
|
monthKey: formatDate(monthStart).slice(0, 7),
|
||||||
|
monthLabel: monthLabels[monthStart.getMonth()],
|
||||||
|
weeks,
|
||||||
|
});
|
||||||
|
monthDate = addMonths(monthDate, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return months;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildContinuousCalendarWeeks(months) {
|
||||||
|
const weeks = [];
|
||||||
|
|
||||||
|
months.forEach(month => {
|
||||||
|
month.weeks.forEach((week, weekIndex) => {
|
||||||
|
const days = week.days.map(day => ({ ...day }));
|
||||||
|
const startsMonth = days.some(day => day.dayNo === '01');
|
||||||
|
weeks.push({
|
||||||
|
...week,
|
||||||
|
weekId: `${month.monthKey}-${week.weekKey}`,
|
||||||
|
days,
|
||||||
|
monthLabel: weekIndex === 0 || startsMonth ? month.monthLabel : '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return weeks;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTodayWeekId(months) {
|
||||||
|
for (const month of months) {
|
||||||
|
for (const week of month.weeks) {
|
||||||
|
if (week.days.some(day => day.isToday && !day.isOutsideMonth)) {
|
||||||
|
return `week-${month.monthKey}-${week.weekKey}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAvailabilityStatusClass(item) {
|
||||||
|
if (item.count > 0) return 'available';
|
||||||
|
return 'zero';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCriteriaStatusClass(item) {
|
||||||
|
if (!item.statusText) return '';
|
||||||
|
return item.qualified ? 'qualified' : 'unqualified';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRecordValue(record, lowerKey, upperKey) {
|
||||||
|
return record[lowerKey] !== undefined ? record[lowerKey] : record[upperKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRecordDate(value) {
|
||||||
|
if (!value) return '';
|
||||||
|
return String(value).slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCriteria(value) {
|
||||||
|
if (value === undefined || value === null || value === '') return '-';
|
||||||
|
const numberValue = Number(value);
|
||||||
|
if (Number.isNaN(numberValue)) return String(value);
|
||||||
|
return numberValue.toLocaleString('en-US');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapRoomCalendarQualificationRecords(records = []) {
|
||||||
|
return records.reduce((map, record) => {
|
||||||
|
const date = normalizeRecordDate(getRecordValue(record, 'date', 'Date'));
|
||||||
|
const roomType = getRecordValue(record, 'roomType', 'RoomType');
|
||||||
|
if (!date || !roomType) return map;
|
||||||
|
|
||||||
|
const flag = Number(getRecordValue(record, 'bookingDateQualificationFlag', 'BookingDateQualificationFlag'));
|
||||||
|
map[`${date}_${roomType}`] = {
|
||||||
|
criteria: formatCriteria(getRecordValue(record, 'roomCriteria', 'RoomCriteria')),
|
||||||
|
count: Number(getRecordValue(record, 'roomCount', 'RoomCount') || 0),
|
||||||
|
leader: getRecordValue(record, 'propertyLeader', 'PropertyLeader') || getRecordValue(record, 'leader', 'Leader') || '',
|
||||||
|
qualified: flag === 1,
|
||||||
|
statusText: flag === 1 ? 'Qualified' : 'Unqualified',
|
||||||
|
};
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLeaderForRoomType(roomType) {
|
||||||
|
const leaderMap = {
|
||||||
|
GR: 'Samuel H.',
|
||||||
|
X1: 'Hope C.',
|
||||||
|
X2: 'Clara N.',
|
||||||
|
R1: 'Hugo L.',
|
||||||
|
R2: 'Selina T.',
|
||||||
|
MO_MOGR: 'Hugo L.',
|
||||||
|
MO_SUIT: 'Hugo L.',
|
||||||
|
MO: 'Hugo L.',
|
||||||
|
ASC: 'Clara N.',
|
||||||
|
LMG: 'Selina T.',
|
||||||
|
LAR: 'Kevin L.',
|
||||||
|
YOBT: 'Momo C.',
|
||||||
|
HBV: 'Hope C.',
|
||||||
|
YOHO: 'Samuel H.',
|
||||||
|
};
|
||||||
|
return leaderMap[roomType.code] || 'Kevin L.';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOtherPropertyRoomTypes(roomTypes) {
|
||||||
|
const displayNameMap = {
|
||||||
|
ASC: 'Ascott',
|
||||||
|
};
|
||||||
|
return roomTypes.reduce((list, roomType) => {
|
||||||
|
if (roomType.code === 'MO') {
|
||||||
|
list.push(
|
||||||
|
{ ...roomType, code: 'MO_MOGR', label: 'MO', displayName: 'MOGR', parentCode: 'MO' },
|
||||||
|
{ ...roomType, code: 'MO_SUIT', label: 'MO', displayName: 'MO Suit', parentCode: 'MO' },
|
||||||
|
);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
list.push({
|
||||||
|
...roomType,
|
||||||
|
displayName: roomType.displayName || displayNameMap[roomType.code],
|
||||||
|
parentCode: roomType.parentCode || roomType.code,
|
||||||
|
});
|
||||||
|
return list;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
<template>
|
||||||
|
<view class="room-calendar-grid">
|
||||||
|
<view class="weekdays">
|
||||||
|
<view v-for="day in weekdayLabels" :key="day" class="weekday-cell">{{ day }}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="calendar-scroll" :scroll-top="calendarScrollTop" scroll-with-animation>
|
||||||
|
<view v-for="month in months" :key="month.monthKey" class="month-section">
|
||||||
|
<view class="month-marker">{{ month.monthLabel }}</view>
|
||||||
|
<view v-for="week in month.weeks" :key="getWeekId(month, week)">
|
||||||
|
<view class="week-block">
|
||||||
|
<view class="date-row">
|
||||||
|
<view
|
||||||
|
v-for="day in week.days"
|
||||||
|
:key="day.date"
|
||||||
|
class="date-cell"
|
||||||
|
:class="{ today: day.isToday, past: day.isPast, 'outside-month': day.isOutsideMonth }"
|
||||||
|
>
|
||||||
|
{{ day.dayNo }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-for="roomType in roomTypes"
|
||||||
|
:key="week.weekKey + roomType.code"
|
||||||
|
class="data-row"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
v-for="day in week.days"
|
||||||
|
:key="day.date + roomType.code"
|
||||||
|
class="data-cell"
|
||||||
|
:class="{
|
||||||
|
today: day.isToday,
|
||||||
|
past: day.isPast,
|
||||||
|
'outside-month': day.isOutsideMonth,
|
||||||
|
'center-content': centerContent,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template v-if="!day.isOutsideMonth">
|
||||||
|
<view class="room-code" :class="getRoomCodeSizeClass(roomType)">{{ getDisplayRoomName(roomType) }}</view>
|
||||||
|
<view
|
||||||
|
class="room-criteria"
|
||||||
|
:class="mode === 'qualification' ? getCriteriaStatusClass(getCellData(day, roomType)) : ''"
|
||||||
|
>
|
||||||
|
{{ getCellData(day, roomType).criteria }}
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="mode === 'availability'"
|
||||||
|
class="room-count"
|
||||||
|
:class="getAvailabilityStatusClass(getCellData(day, roomType))"
|
||||||
|
>
|
||||||
|
{{ getAvailabilityText(getCellData(day, roomType)) }}
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="showLeader && hasLeader(day, roomType)"
|
||||||
|
class="room-leader"
|
||||||
|
>
|
||||||
|
{{ getCellData(day, roomType).leader }}
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
getAvailabilityStatusClass,
|
||||||
|
getCriteriaStatusClass,
|
||||||
|
} from '../../common/roomCalendarUi.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'room-calendar-grid',
|
||||||
|
props: {
|
||||||
|
months: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
weekdayLabels: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
roomTypes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
dataMap: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: 'availability',
|
||||||
|
},
|
||||||
|
initialWeekId: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
centerContent: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showLeader: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
calendarScrollTop: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
initialWeekId() {
|
||||||
|
this.scrollToInitialWeek();
|
||||||
|
},
|
||||||
|
months() {
|
||||||
|
this.scrollToInitialWeek();
|
||||||
|
},
|
||||||
|
roomTypes() {
|
||||||
|
this.scrollToInitialWeek();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.scrollToInitialWeek();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getCellKey(date, roomTypeCode) {
|
||||||
|
return `${date}_${roomTypeCode}`;
|
||||||
|
},
|
||||||
|
getWeekId(month, week) {
|
||||||
|
return `week-${month.monthKey}-${week.weekKey}`;
|
||||||
|
},
|
||||||
|
scrollToInitialWeek() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const scrollTop = this.getInitialWeekScrollTop();
|
||||||
|
this.calendarScrollTop = scrollTop === 0 ? 0.1 : scrollTop;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (scrollTop === 0) {
|
||||||
|
this.calendarScrollTop = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getInitialWeekScrollTop() {
|
||||||
|
if (!this.initialWeekId) return 0;
|
||||||
|
const weekHeight = this.getWeekHeightPx();
|
||||||
|
const monthMarkerHeight = uni.upx2px(38);
|
||||||
|
let scrollTop = 0;
|
||||||
|
for (const month of this.months) {
|
||||||
|
const weekIndex = (month.weeks || []).findIndex(
|
||||||
|
week => this.getWeekId(month, week) === this.initialWeekId
|
||||||
|
);
|
||||||
|
if (weekIndex >= 0) {
|
||||||
|
return scrollTop + weekIndex * weekHeight;
|
||||||
|
}
|
||||||
|
scrollTop += monthMarkerHeight + (month.weeks || []).length * weekHeight;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
getWeekHeightPx() {
|
||||||
|
return uni.upx2px(44 + this.roomTypes.length * 128);
|
||||||
|
},
|
||||||
|
getDisplayRoomName(roomType) {
|
||||||
|
return roomType.displayName || roomType.label;
|
||||||
|
},
|
||||||
|
getRoomCodeSizeClass(roomType) {
|
||||||
|
return this.getDisplayRoomName(roomType).length > 5 ? 'compact' : '';
|
||||||
|
},
|
||||||
|
getCellData(day, roomType) {
|
||||||
|
return this.dataMap[this.getCellKey(day.date, roomType.code)] || {
|
||||||
|
criteria: '-',
|
||||||
|
count: 0,
|
||||||
|
waitlist: false,
|
||||||
|
waitlistCount: 0,
|
||||||
|
qualified: false,
|
||||||
|
statusText: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getAvailabilityStatusClass(item) {
|
||||||
|
return getAvailabilityStatusClass(item);
|
||||||
|
},
|
||||||
|
getAvailabilityText(item) {
|
||||||
|
if (item.waitlist) return item.waitlistCount;
|
||||||
|
return item.count;
|
||||||
|
},
|
||||||
|
getCriteriaStatusClass(item) {
|
||||||
|
return getCriteriaStatusClass(item);
|
||||||
|
},
|
||||||
|
hasLeader(day, roomType) {
|
||||||
|
const leader = this.getCellData(day, roomType).leader;
|
||||||
|
return leader !== undefined && leader !== null && String(leader).trim() !== '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.room-calendar-grid {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
height: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-scroll {
|
||||||
|
flex: 1;
|
||||||
|
height: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekdays,
|
||||||
|
.date-row,
|
||||||
|
.data-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekdays {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-cell {
|
||||||
|
background-color: #c39a4b;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: "Inter-Bold";
|
||||||
|
font-size: 24rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
line-height: 44rpx;
|
||||||
|
text-align: center;
|
||||||
|
width: 14.2857%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-section {
|
||||||
|
padding-top: 38rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-block {
|
||||||
|
border-left: 1rpx solid #e1e1e1;
|
||||||
|
border-right: 1rpx solid #e1e1e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-marker {
|
||||||
|
background-color: #c39a4b;
|
||||||
|
border-top: 1rpx solid rgba(255, 255, 255, 0.5);
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: "Inter-Bold";
|
||||||
|
font-size: 24rpx;
|
||||||
|
height: 38rpx;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 38rpx;
|
||||||
|
margin-top: -38rpx;
|
||||||
|
padding-left: 16rpx;
|
||||||
|
position: -webkit-sticky;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-cell {
|
||||||
|
background-color: #dfc895;
|
||||||
|
border-bottom: 1rpx solid #d4bd88;
|
||||||
|
border-right: 1rpx solid #d4bd88;
|
||||||
|
color: #111111;
|
||||||
|
font-family: "Inter-Bold";
|
||||||
|
font-size: 28rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
line-height: 44rpx;
|
||||||
|
text-align: center;
|
||||||
|
width: 14.2857%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-cell.today {
|
||||||
|
background-color: #c39a4b;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-cell.today {
|
||||||
|
background-color: #fff7e6;
|
||||||
|
// box-shadow: inset 0 0 0 2rpx #c39a4b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-cell.past,
|
||||||
|
.data-cell.past {
|
||||||
|
background-color: #eeeeee;
|
||||||
|
color: #9a9a9a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-cell.past .room-code,
|
||||||
|
.data-cell.past .room-criteria,
|
||||||
|
.data-cell.past .room-count,
|
||||||
|
.data-cell.past .room-leader {
|
||||||
|
color: #9a9a9a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-cell.outside-month,
|
||||||
|
.data-cell.outside-month {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-cell {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-bottom: 1rpx solid #e1e1e1;
|
||||||
|
border-right: 1rpx solid #e1e1e1;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 128rpx;
|
||||||
|
padding: 12rpx 2rpx 8rpx;
|
||||||
|
text-align: center;
|
||||||
|
width: 14.2857%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-cell.center-content {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-cell.center-content .room-code,
|
||||||
|
.data-cell.center-content .room-criteria {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-code {
|
||||||
|
color: #111111;
|
||||||
|
font-family: "Inter-Bold";
|
||||||
|
font-size: 23rpx;
|
||||||
|
line-height: 32rpx;
|
||||||
|
min-height: 32rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-code.compact {
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-criteria {
|
||||||
|
color: #111111;
|
||||||
|
font-family: "Inter-Bold";
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 28rpx;
|
||||||
|
min-height: 28rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-count {
|
||||||
|
// font-family: "Inter-Bold";
|
||||||
|
// font-size: 28rpx;
|
||||||
|
// line-height: 34rpx;
|
||||||
|
// margin-top: 6rpx;
|
||||||
|
font-size: 21rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-count.available {
|
||||||
|
color: #1d8b48;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-count.zero {
|
||||||
|
color: #d0021b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-leader {
|
||||||
|
color: #111111;
|
||||||
|
font-family: "Inter-Bold";
|
||||||
|
font-size: 18rpx;
|
||||||
|
line-height: 24rpx;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-criteria.qualified {
|
||||||
|
color: #1d8b48;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-criteria.unqualified {
|
||||||
|
color: #d0021b;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+65
-2
@@ -82,6 +82,10 @@
|
|||||||
"calendar.fri": "五",
|
"calendar.fri": "五",
|
||||||
"calendar.sat": "六",
|
"calendar.sat": "六",
|
||||||
"calendar.sun": "日",
|
"calendar.sun": "日",
|
||||||
|
"roomCalendar.title": "日曆",
|
||||||
|
"roomCalendar.playerTitle": "玩家日曆",
|
||||||
|
"roomCalendar.updated": "日曆更新",
|
||||||
|
"roomCalendar.maxSelectedRoomTypes": "最多可選{count}個房型",
|
||||||
"findPlayer.title": "查找客人",
|
"findPlayer.title": "查找客人",
|
||||||
"findPlayer.alert": "請輸入客人ID",
|
"findPlayer.alert": "請輸入客人ID",
|
||||||
"findPlayer.noPermission": "您沒有權限查看此客人.",
|
"findPlayer.noPermission": "您沒有權限查看此客人.",
|
||||||
@@ -242,6 +246,7 @@
|
|||||||
"todo.etaRequired": "ETA Time不能為空",
|
"todo.etaRequired": "ETA Time不能為空",
|
||||||
"todo.lookupPlayer":"您沒有權限查看該玩家",
|
"todo.lookupPlayer":"您沒有權限查看該玩家",
|
||||||
"hotel.title":"酒店",
|
"hotel.title":"酒店",
|
||||||
|
"approval.title": "審批",
|
||||||
"approval.approve": "批准",
|
"approval.approve": "批准",
|
||||||
"hotel.roombooking":"酒店預定",
|
"hotel.roombooking":"酒店預定",
|
||||||
"approval.pendingApproval":"等待審批",
|
"approval.pendingApproval":"等待審批",
|
||||||
@@ -255,5 +260,63 @@
|
|||||||
"notification.pendingApproval":"審批列表",
|
"notification.pendingApproval":"審批列表",
|
||||||
"notification.pendingApprovalRequest": "待批准請求",
|
"notification.pendingApprovalRequest": "待批准請求",
|
||||||
"playerDetail.confirm":"確認",
|
"playerDetail.confirm":"確認",
|
||||||
"newBooking.validEta":"ETA 時間格式有誤,請重新輸入 eg: HH:mm"
|
"newBooking.validEta":"ETA 時間格式有誤,請重新輸入 eg: HH:mm",
|
||||||
}
|
"bookingRecord.newLimo": "预定车辆",
|
||||||
|
"bookingRecord.calendar": "CALENDAR",
|
||||||
|
"newLimo.createTitle": "建立預訂",
|
||||||
|
"newLimo.editTitle": "編輯預訂",
|
||||||
|
"newLimo.sectionBookingInformation": "預訂資訊",
|
||||||
|
"newLimo.sectionBookingItinerary": "行程",
|
||||||
|
"newLimo.sectionApprovalDetails": "審批詳情",
|
||||||
|
"newLimo.serviceType": "服務類型",
|
||||||
|
"newLimo.pickUpDate": "接載日期",
|
||||||
|
"newLimo.pickUpTime": "接載時間",
|
||||||
|
"newLimo.carType": "車型",
|
||||||
|
"newLimo.noOfPax": "人數",
|
||||||
|
"newLimo.pickUpLocation": "上車地點",
|
||||||
|
"newLimo.flightDetail": "航班詳情",
|
||||||
|
"newLimo.eta": "預計抵達時間",
|
||||||
|
"newLimo.flightStatus": "航班狀態",
|
||||||
|
"newLimo.greetType": "迎賓方式",
|
||||||
|
"newLimo.signage": "接機牌 / 顯示文字",
|
||||||
|
"newLimo.dropOffLocation": "下車地點",
|
||||||
|
"newLimo.addStop": "+新增",
|
||||||
|
"newLimo.extraDropOffLocation": "額外下車地點",
|
||||||
|
"newLimo.hour": "小時",
|
||||||
|
"newLimo.approvedBy": "審批人",
|
||||||
|
"newLimo.paymentMethod": "付款方式",
|
||||||
|
"newLimo.tripPrice": "行程價格",
|
||||||
|
"newLimo.hourlyRate": "每小時費率",
|
||||||
|
"newLimo.tripContact": "行程聯絡人",
|
||||||
|
"newLimo.remarks": "備註",
|
||||||
|
"newLimo.selectDate": "選擇日期",
|
||||||
|
"newLimo.selectTime": "選擇時間",
|
||||||
|
"newLimo.notApplicable": "不適用",
|
||||||
|
"newLimo.remarksPlaceholder": "",
|
||||||
|
"newLimo.removeStop": "刪除",
|
||||||
|
"approvalRouting.title": "審批路由",
|
||||||
|
"reservations.title": "預訂",
|
||||||
|
"notification.title": "通知",
|
||||||
|
"limoBookingRecord.bookingType": "預定類型",
|
||||||
|
"limoBookingRecord.pickupDatetime": "接載日期",
|
||||||
|
"limoBookingRecord.pickup": "接載地點",
|
||||||
|
"limoBookingRecord.extraDropoff": "額外下車點",
|
||||||
|
"limoBookingRecord.dropoff": "下車地點",
|
||||||
|
"limoBookingDetail.carTypeAndPlate": "車型和車牌",
|
||||||
|
"limoBookingDetail.overrideBy": "覆蓋人",
|
||||||
|
"limoBookingDetail.tripPrice": "行程價格",
|
||||||
|
"limoBookingDetail.driverContact": "司機聯係方式",
|
||||||
|
"limoBookingDetail.extraDropoff": "額外下車點",
|
||||||
|
"limoBookingDetail.pickupPoint": "上車地點",
|
||||||
|
"limoBookingDetail.dropoffPoint": "下車地點",
|
||||||
|
"limoBookingCopy.carType": "車型",
|
||||||
|
"limoBookingCopy.carNo": "車牌號",
|
||||||
|
"limoBookingCopy.driverContact": "司機聯係方式",
|
||||||
|
"newLimo.bookingContact": "預訂聯絡人",
|
||||||
|
"newLimo.phoneNumber": "電話號碼",
|
||||||
|
"newLimo.highImportance": "重要",
|
||||||
|
"newLimo.tenOut": "10 Out",
|
||||||
|
"newLimo.tenOutContact": "10 Out 聯絡人",
|
||||||
|
"newLimo.contactGuestDirectly": "直接聯絡客人",
|
||||||
|
"newLimo.guestContact": "客人聯絡人"
|
||||||
|
}
|
||||||
|
|||||||
+28
-1
@@ -22,6 +22,16 @@
|
|||||||
"disableSwipeBack": true
|
"disableSwipeBack": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/roomCalendar/roomCalendar",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "",
|
||||||
|
"enablePullDownRefresh": false,
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableSwipeBack": true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/dashboard/dashboard",
|
"path": "pages/dashboard/dashboard",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -69,6 +79,16 @@
|
|||||||
"disableSwipeBack": true
|
"disableSwipeBack": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "booking/playerRoomCalendar/playerRoomCalendar",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "",
|
||||||
|
"enablePullDownRefresh": false,
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableSwipeBack": true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path" : "booking/newBooking/newBooking",
|
"path" : "booking/newBooking/newBooking",
|
||||||
"style" :
|
"style" :
|
||||||
@@ -279,6 +299,13 @@
|
|||||||
"text": "%hotel.title%",
|
"text": "%hotel.title%",
|
||||||
"visible":false
|
"visible":false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/roomCalendar/roomCalendar",
|
||||||
|
"iconPath": "static/images/calendar-line.png",
|
||||||
|
"selectedIconPath": "static/images/calendar-fill.png",
|
||||||
|
"text": "%roomCalendar.title%",
|
||||||
|
"visible": false
|
||||||
|
},
|
||||||
// approvalRouting
|
// approvalRouting
|
||||||
// {
|
// {
|
||||||
// "pagePath": "pages/approval/approval",
|
// "pagePath": "pages/approval/approval",
|
||||||
@@ -297,4 +324,4 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"uniIdRouter": {}
|
"uniIdRouter": {}
|
||||||
}
|
}
|
||||||
|
|||||||
+151
-84
@@ -7,79 +7,84 @@
|
|||||||
style="margin-top:36rpx;margin-right:36rpx;">
|
style="margin-top:36rpx;margin-right:36rpx;">
|
||||||
{{$t('dashboard.dataAsOf')}} {{dataAsOf}}
|
{{$t('dashboard.dataAsOf')}} {{dataAsOf}}
|
||||||
</view>
|
</view>
|
||||||
<view class="hz-font-bolder hz-font-xl hz-font-color-black hz-text-left"
|
<view v-if="showAchievement">
|
||||||
style="margin-top:36rpx;margin-left:36rpx;">
|
<view class="hz-font-bolder hz-font-xl hz-font-color-black hz-text-left"
|
||||||
{{$t('dashboard.achievement')}}
|
style="margin-top:36rpx;margin-left:36rpx;">
|
||||||
|
{{$t('dashboard.achievement')}}
|
||||||
|
|
||||||
|
</view>
|
||||||
|
<uni-card>
|
||||||
|
<view v-for="(record,index) in dashboardData" :key="index" v-show="getShowAchievement(record)"
|
||||||
|
style="margin-top:12px;">
|
||||||
|
<view class="hz-flex-row">
|
||||||
|
<view style="width:37%">
|
||||||
|
<view class="hz-font-color-dark hz-font-base">{{record.itemName}}</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="record.targetValue!==0&&record.targetValue!==null&&record.targetValue!==undefined" class="hz-flex-row"
|
||||||
|
style="width:63%;justify-content: flex-end;align-items: center;align-items: center;">
|
||||||
|
<view class="hz-font-color-dark hz-font-12px" style="white-space: nowrap;">{{getTargetValue(record)}}</view>
|
||||||
|
<view class="hz-card-level" style="margin-left:4px;">LV{{record.level}}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view style="margin-top:3px;" v-if="record.targetValue!==0&&record.targetValue!==null&&record.targetValue!==undefined">
|
||||||
|
<u-line-progress :percentage="getPercentValue(record)" activeColor="#C39A4B"
|
||||||
|
height="16"></u-line-progress>
|
||||||
|
</view>
|
||||||
|
<view style="margin-top:3px;" v-else>
|
||||||
|
<u-line-progress :percentage="0" activeColor="#C39A4B" height="16"><span
|
||||||
|
style="font-size:10px;padding-left: 10px;">{{getCurrentValue(record)}}</span></u-line-progress>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-show="userSegment =='IM'">
|
||||||
|
<view class="im-view-outside">
|
||||||
|
<view class="im-view-inside" style="margin-right:5px;">
|
||||||
|
<view class="im-text-gold">{{formatNumber(MFValue)}}</view>
|
||||||
|
<view class="hz-font-color-dark hz-font-base">MF Turnover</view>
|
||||||
|
</view>
|
||||||
|
<view class="im-view-inside" style="margin-left:5px;">
|
||||||
|
<view class="im-text-gold">{{formatNumber(IMValue)}}</view>
|
||||||
|
<view class="hz-font-color-dark hz-font-base">IM Turnover</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="im-view-outside">
|
||||||
|
<view class="im-view-inside" style="margin-right:5px;">
|
||||||
|
<view class="im-text-gold">{{formatNumber(NFROValue)}}</view>
|
||||||
|
<view class="hz-font-color-dark hz-font-base">NFRO Turnover</view>
|
||||||
|
</view>
|
||||||
|
<view class="im-view-inside" style="margin-left:5px;">
|
||||||
|
<view class="im-text-gold">{{formatNumber(MRIValue)}}</view>
|
||||||
|
<view class="hz-font-color-dark hz-font -base">MRI Turnover</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</uni-card>
|
||||||
</view>
|
</view>
|
||||||
<uni-card>
|
<view v-if="showKPI">
|
||||||
<view v-for="(record,index) in dashboardData" :key="index" v-show="getShowAchievement(record)"
|
<view class="hz-font-bolder hz-font-xl hz-font-color-black hz-text-left"
|
||||||
style="margin-top:12px;">
|
style="margin-top:36rpx;margin-left:36rpx;">
|
||||||
<view class="hz-flex-row">
|
{{$t('dashboard.kpi')}}
|
||||||
<view style="width:37%">
|
|
||||||
<view class="hz-font-color-dark hz-font-base">{{record.itemName}}</view>
|
|
||||||
</view>
|
|
||||||
<view v-if="record.targetValue!==0&&record.targetValue!==null&&record.targetValue!==undefined" class="hz-flex-row"
|
|
||||||
style="width:63%;justify-content: flex-end;align-items: center;align-items: center;">
|
|
||||||
<view class="hz-font-color-dark hz-font-12px" style="white-space: nowrap;">{{getTargetValue(record)}}</view>
|
|
||||||
<view class="hz-card-level" style="margin-left:4px;">LV{{record.level}}</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view style="margin-top:3px;" v-if="record.targetValue!==0&&record.targetValue!==null&&record.targetValue!==undefined">
|
|
||||||
<u-line-progress :percentage="getPercentValue(record)" activeColor="#C39A4B"
|
|
||||||
height="16"></u-line-progress>
|
|
||||||
</view>
|
|
||||||
<view style="margin-top:3px;" v-else>
|
|
||||||
<u-line-progress :percentage="0" activeColor="#C39A4B" height="16"><span
|
|
||||||
style="font-size:10px;padding-left: 10px;">{{getCurrentValue(record)}}</span></u-line-progress>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
<view v-show="userSegment =='IM'">
|
<uni-card>
|
||||||
<view class="im-view-outside">
|
<view v-for="(record,index) in dashboardData" :key="index" v-show="getShowKPI(record)"
|
||||||
<view class="im-view-inside" style="margin-right:5px;">
|
style="margin-top:12px;">
|
||||||
<view class="im-text-gold">{{formatNumber(MFValue)}}</view>
|
<view class="hz-flex-row">
|
||||||
<view class="hz-font-color-dark hz-font-base">MF Turnover</view>
|
<view style="width:85%">
|
||||||
|
<view class="hz-font-color-dark hz-font-base">{{record.itemName}}</view>
|
||||||
|
</view>
|
||||||
|
<view class="hz-flex-row"
|
||||||
|
style="width:15%;justify-content: flex-end;align-items: center;align-items: center;">
|
||||||
|
<view class="hz-font-color-dark hz-font-12px">{{getTargetValue(record)}}</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="im-view-inside" style="margin-left:5px;">
|
<view style="margin-top:3px;">
|
||||||
<view class="im-text-gold">{{formatNumber(IMValue)}}</view>
|
<u-line-progress :percentage="getPercentValue(record)" activeColor="#C39A4B"
|
||||||
<view class="hz-font-color-dark hz-font-base">IM Turnover</view>
|
height="16"><span
|
||||||
|
style="font-size:10px;margin-right:6px;padding-left: 10px;">{{percentFormat(record.currentValue)}}</span></u-line-progress>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="im-view-outside">
|
</uni-card>
|
||||||
<view class="im-view-inside" style="margin-right:5px;">
|
|
||||||
<view class="im-text-gold">{{formatNumber(NFROValue)}}</view>
|
|
||||||
<view class="hz-font-color-dark hz-font-base">NFRO Turnover</view>
|
|
||||||
</view>
|
|
||||||
<view class="im-view-inside" style="margin-left:5px;">
|
|
||||||
<view class="im-text-gold">{{formatNumber(MRIValue)}}</view>
|
|
||||||
<view class="hz-font-color-dark hz-font -base">MRI Turnover</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</uni-card>
|
|
||||||
<view class="hz-font-bolder hz-font-xl hz-font-color-black hz-text-left"
|
|
||||||
style="margin-top:36rpx;margin-left:36rpx;">
|
|
||||||
{{$t('dashboard.kpi')}}
|
|
||||||
</view>
|
</view>
|
||||||
<uni-card>
|
|
||||||
<view v-for="(record,index) in dashboardData" :key="index" v-show="getShowKPI(record)"
|
|
||||||
style="margin-top:12px;">
|
|
||||||
<view class="hz-flex-row">
|
|
||||||
<view style="width:85%">
|
|
||||||
<view class="hz-font-color-dark hz-font-base">{{record.itemName}}</view>
|
|
||||||
</view>
|
|
||||||
<view class="hz-flex-row"
|
|
||||||
style="width:15%;justify-content: flex-end;align-items: center;align-items: center;">
|
|
||||||
<view class="hz-font-color-dark hz-font-12px">{{getTargetValue(record)}}</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view style="margin-top:3px;">
|
|
||||||
<u-line-progress :percentage="getPercentValue(record)" activeColor="#C39A4B"
|
|
||||||
height="16"><span
|
|
||||||
style="font-size:10px;margin-right:6px;padding-left: 10px;">{{percentFormat(record.currentValue)}}</span></u-line-progress>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</uni-card>
|
|
||||||
<u-modal :content="msgStr" :show="showMsg" :confirmText="okText" :showCancelButton="false"
|
<u-modal :content="msgStr" :show="showMsg" :confirmText="okText" :showCancelButton="false"
|
||||||
closeOnClickOverlay @confirm="showMsg=false" confirmColor="#C39A4B"
|
closeOnClickOverlay @confirm="showMsg=false" confirmColor="#C39A4B"
|
||||||
@close="showMsg=false"></u-modal>
|
@close="showMsg=false"></u-modal>
|
||||||
@@ -127,6 +132,8 @@
|
|||||||
NFROValue: '0',
|
NFROValue: '0',
|
||||||
IMValue: '0',
|
IMValue: '0',
|
||||||
MRIValue: '0',
|
MRIValue: '0',
|
||||||
|
showAchievement: false,
|
||||||
|
showKPI: false,
|
||||||
showDashboardView: false,
|
showDashboardView: false,
|
||||||
userSegment: null
|
userSegment: null
|
||||||
}
|
}
|
||||||
@@ -149,6 +156,19 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
normalizeDashboardRecord(record) {
|
||||||
|
record = record || {};
|
||||||
|
return {
|
||||||
|
currentValue: record.currentValue ?? record.CurrentValue,
|
||||||
|
identifier: record.identifier ?? record.Identifier,
|
||||||
|
itemName: record.itemName ?? record.ItemName,
|
||||||
|
level: record.level ?? record.Level,
|
||||||
|
targetValue: record.targetValue ?? record.TargetValue,
|
||||||
|
type: record.type ?? record.Type,
|
||||||
|
userId: record.userId ?? record.UserId,
|
||||||
|
userSegment: record.userSegment ?? record.UserSegment,
|
||||||
|
};
|
||||||
|
},
|
||||||
formatNumber(num) {
|
formatNumber(num) {
|
||||||
return num?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") || "0";
|
return num?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") || "0";
|
||||||
},
|
},
|
||||||
@@ -161,39 +181,84 @@
|
|||||||
let params = {};
|
let params = {};
|
||||||
requestBody = JSON.stringify(params);
|
requestBody = JSON.stringify(params);
|
||||||
const res = await getTodoDashboard(params);
|
const res = await getTodoDashboard(params);
|
||||||
|
this.dashboardData = [];
|
||||||
|
this.dataAsOf = "";
|
||||||
|
this.userSegment = null;
|
||||||
|
this.MFValue = this.IMValue = this.NFROValue = this.MRIValue = "0";
|
||||||
if (res.data != null && res.success == true) {
|
if (res.data != null && res.success == true) {
|
||||||
if(Array.isArray(res.data.records) && res.data.records.length > 0){
|
const records = Array.isArray(res.data.records)
|
||||||
this.dashboardData = res.data.records;
|
? res.data.records
|
||||||
|
: Array.isArray(res.data.Records)
|
||||||
|
? res.data.Records
|
||||||
|
: [];
|
||||||
|
if (records.length > 0) {
|
||||||
|
this.dashboardData = records.map((record) => this.normalizeDashboardRecord(record));
|
||||||
this.showDashboardView = true;
|
this.showDashboardView = true;
|
||||||
|
// 设置用户类型
|
||||||
this.userSegment = this.dashboardData[0].userSegment;
|
this.userSegment = this.dashboardData[0].userSegment;
|
||||||
this.hasTargetValue = this.dashboardData[0].targetValue !== null ||this.dashboardData[0].targetValue !== undefined ? true :false;
|
this.hasTargetValue = this.dashboardData[0].targetValue !== null && this.dashboardData[0].targetValue !== undefined;
|
||||||
|
} else {
|
||||||
|
this.dashboardData = [];
|
||||||
|
this.showDashboardView = false;
|
||||||
|
this.dataNull = this.$t("common.noRecord");
|
||||||
}
|
}
|
||||||
if(this.userSegment == 'IM' && this.userSegment !== null){
|
// 重置显示标志
|
||||||
|
this.showAchievement = false;
|
||||||
|
this.showKPI = false;
|
||||||
|
let hasIMValues = false;
|
||||||
|
|
||||||
|
if(this.dashboardData.length > 0){
|
||||||
|
// 检查是否有IM类型的特殊数据
|
||||||
|
if(this.userSegment == 'IM'){
|
||||||
for (let i = 0; i < this.dashboardData.length; i++) {
|
for (let i = 0; i < this.dashboardData.length; i++) {
|
||||||
if (this.dashboardData[i].itemName == "MF Turnover") this.MFValue = this.dashboardData[i]
|
if (this.dashboardData[i].itemName == "MF Turnover") {
|
||||||
.currentValue;
|
this.MFValue = this.dashboardData[i].currentValue;
|
||||||
if (this.dashboardData[i].itemName == "IM Turnover") this.IMValue = this.dashboardData[i]
|
hasIMValues = true;
|
||||||
.currentValue;
|
}
|
||||||
if (this.dashboardData[i].itemName == "NFRO Turnover") this.NFROValue = this.dashboardData[i]
|
if (this.dashboardData[i].itemName == "IM Turnover") {
|
||||||
.currentValue;
|
this.IMValue = this.dashboardData[i].currentValue;
|
||||||
if (this.dashboardData[i].itemName == "MRI Turnover") this.MRIValue = this.dashboardData[i]
|
hasIMValues = true;
|
||||||
.currentValue;
|
}
|
||||||
|
if (this.dashboardData[i].itemName == "NFRO Turnover") {
|
||||||
|
this.NFROValue = this.dashboardData[i].currentValue;
|
||||||
|
hasIMValues = true;
|
||||||
|
}
|
||||||
|
if (this.dashboardData[i].itemName == "MRI Turnover") {
|
||||||
|
this.MRIValue = this.dashboardData[i].currentValue;
|
||||||
|
hasIMValues = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 检查是否有DataAsOf类型的数据
|
||||||
for (let i = 0; i < this.dashboardData.length; i++) {
|
for (let i = 0; i < this.dashboardData.length; i++) {
|
||||||
if (this.dashboardData[i].type == "DataAsOf"){
|
if (this.dashboardData[i].type == "DataAsOf"){
|
||||||
this.dataAsOf = this.dashboardData[i].itemName;
|
this.dataAsOf = this.dashboardData[i].itemName;
|
||||||
}
|
}
|
||||||
|
// 检查是否有Achievement类型的数据
|
||||||
|
if (this.dashboardData[i].type == "Achievement" && this.dashboardData[i].itemName !=="MF Turnover" && this.dashboardData[i].itemName !=="IM Turnover"
|
||||||
|
&& this.dashboardData[i].itemName !=="NFRO Turnover" && this.dashboardData[i].itemName !=="MRI Turnover") {
|
||||||
|
this.showAchievement = true;
|
||||||
|
}
|
||||||
|
// 检查是否有KPI类型的数据
|
||||||
|
if (this.dashboardData[i].type == "KPI") {
|
||||||
|
this.showKPI = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// 如果没有Achievement数据,但有IM用户的四个Value,也要显示卡片
|
||||||
|
if(!this.showAchievement && hasIMValues){
|
||||||
|
this.showAchievement = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.showDashboardView = false;
|
this.showDashboardView = false;
|
||||||
|
this.showAchievement = false;
|
||||||
|
this.showKPI = false;
|
||||||
if(res.errorCode == 'NO_PERMISSION'){
|
if(res.errorCode == 'NO_PERMISSION'){
|
||||||
this.dataNull = this.$t("todo.noPermission");
|
this.dataNull = this.$t("todo.noPermission");
|
||||||
}else if(res.errorCode == 'INVOKE_TODODASHBOARD_API_ERROR'){
|
}else if(res.errorCode == 'INVOKE_TODODASHBOARD_API_ERROR'){
|
||||||
this.dataNull = this.$t("todo.invokeError");
|
this.dataNull = this.$t("todo.invokeError");
|
||||||
}else {
|
}else {
|
||||||
if(res.errorCode.toUpperCase().indexOf('NO RECORD') != -1){
|
if(String(res.errorCode || res.code || '').toUpperCase().indexOf('NO RECORD') != -1){
|
||||||
this.dataNull = this.$t("common.noRecord");
|
this.dataNull = this.$t("common.noRecord");
|
||||||
}else{
|
}else{
|
||||||
this.dataNull = this.$t("todo.returnError");
|
this.dataNull = this.$t("todo.returnError");
|
||||||
@@ -201,7 +266,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
this.showDashboardView = false;
|
||||||
|
this.showAchievement = false;
|
||||||
|
this.showKPI = false;
|
||||||
} finally {
|
} finally {
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
}
|
}
|
||||||
@@ -308,4 +375,4 @@
|
|||||||
.im-text-gold {
|
.im-text-gold {
|
||||||
color: #cfa34f;
|
color: #cfa34f;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+43
-4
@@ -44,13 +44,13 @@
|
|||||||
|
|
||||||
<view style="width: 100%;">
|
<view style="width: 100%;">
|
||||||
<u-popup :show="showCommitment" :round="10" mode="center" customStyle="margin:0rpx 40rpx;">
|
<u-popup :show="showCommitment" :round="10" mode="center" customStyle="margin:0rpx 40rpx;">
|
||||||
<view style="padding: 20px 22px 32px 22px;">
|
<view class="commitment-dialog" style="padding: 20px 22px 32px 22px;">
|
||||||
<h4 style="font-size: 18px; display: flex; justify-content: center; padding: 7px;">{{commitTitle}}</h4>
|
<h4 class="commitment-title" style="font-size: 18px; display: flex; justify-content: center; padding: 7px;">{{commitTitle}}</h4>
|
||||||
<view style="height:490px;font-size:13px;color:#333;">
|
<view class="commitment-body" style="height:490px;font-size:13px;color:#333;">
|
||||||
<view style="margin-bottom: 24rpx;line-height: 1.7;">{{commitmentText1}}</view>
|
<view style="margin-bottom: 24rpx;line-height: 1.7;">{{commitmentText1}}</view>
|
||||||
<view >{{commitmentText2}}</view>
|
<view >{{commitmentText2}}</view>
|
||||||
</view>
|
</view>
|
||||||
<view >
|
<view class="commitment-footer">
|
||||||
<view @click="handleConfirm()" class="hz-btn hz-font-color-white hz-font-lg hz-font-bold hz-text-center" >{{confirmText}}</view>
|
<view @click="handleConfirm()" class="hz-btn hz-font-color-white hz-font-lg hz-font-bold hz-text-center" >{{confirmText}}</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -391,6 +391,45 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* #ifdef H5 */
|
||||||
|
.commitment-dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-height: calc(100vh - 48px);
|
||||||
|
max-height: calc(100dvh - 48px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.commitment-title,
|
||||||
|
.commitment-footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.commitment-body {
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
padding-right: 10px;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
.commitment-body::-webkit-scrollbar {
|
||||||
|
/* uView globally sets scrollbar width to 0 !important. */
|
||||||
|
width: 2px !important;
|
||||||
|
height: 2px !important;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.commitment-body::-webkit-scrollbar-track {
|
||||||
|
background: #eee;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.commitment-body::-webkit-scrollbar-thumb {
|
||||||
|
background: #a67c30;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.commitment-footer {
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
/* #endif */
|
||||||
.commit-content{
|
.commit-content{
|
||||||
line-height: 1.5; /* 增加行间距提升可读性 */
|
line-height: 1.5; /* 增加行间距提升可读性 */
|
||||||
color: #333; /* 深灰提升阅读舒适度 */
|
color: #333; /* 深灰提升阅读舒适度 */
|
||||||
|
|||||||
@@ -0,0 +1,781 @@
|
|||||||
|
<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="true"
|
||||||
|
: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 {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
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>
|
||||||
@@ -368,11 +368,12 @@
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
try {
|
try {
|
||||||
// pages.json tabBar.list 顺序:
|
// pages.json tabBar.list 顺序:
|
||||||
// 0 dashboard, 1 todo, 2 hotel(findPlayer), 3 setting
|
// 0 dashboard, 1 todo, 2 hotel(findPlayer), 3 calendar, 4 setting
|
||||||
uni.setTabBarItem({ index: 0, text: this.$t("dashboard.title") });
|
uni.setTabBarItem({ index: 0, text: this.$t("dashboard.title") });
|
||||||
uni.setTabBarItem({ index: 1, text: this.$t("todo.title") });
|
uni.setTabBarItem({ index: 1, text: this.$t("todo.title") });
|
||||||
uni.setTabBarItem({ index: 2, text: this.$t("hotel.title") });
|
uni.setTabBarItem({ index: 2, text: this.$t("hotel.title") });
|
||||||
uni.setTabBarItem({ index: 3, text: this.$t("setting.title") });
|
uni.setTabBarItem({ index: 3, text: this.$t("roomCalendar.title") });
|
||||||
|
uni.setTabBarItem({ index: 4, text: this.$t("setting.title") });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,8 +98,15 @@
|
|||||||
<view class="hz-flex-row" style="align-items: flex-start;">
|
<view class="hz-flex-row" style="align-items: flex-start;">
|
||||||
<view class="text-container" v-if="!rowRecord.showDetail">
|
<view class="text-container" v-if="!rowRecord.showDetail">
|
||||||
<span v-for="(itemRecord,index) in rowRecord.descriptions" :key="'item'+index"
|
<span v-for="(itemRecord,index) in rowRecord.descriptions" :key="'item'+index"
|
||||||
class="hz-date-des"
|
class="hz-date-des todo-summary-description"
|
||||||
style="display:flex;flex-direction:row;align-items:center;">{{getItemDescription(itemRecord)}}
|
style="display:block;">
|
||||||
|
<template v-for="(part, partIndex) in getDescriptionWithSharerIconParts(itemRecord)">
|
||||||
|
<image v-if="part.type === 'sharerIcon'" :key="'item-sharer-'+index+'-'+partIndex"
|
||||||
|
src="../../static/images/sharer_line.svg"
|
||||||
|
class="icon-img-sharer todo-description-sharer-icon">
|
||||||
|
</image>
|
||||||
|
<span v-else :key="'item-text-'+index+'-'+partIndex">{{part.text}}</span>
|
||||||
|
</template>
|
||||||
<image
|
<image
|
||||||
v-show="index == 0 && currentIndex !== 2 && rowRecord.hasFlightRecord ===1"
|
v-show="index == 0 && currentIndex !== 2 && rowRecord.hasFlightRecord ===1"
|
||||||
src="../../static/images/flight_line.svg" class="icon-img-flight">
|
src="../../static/images/flight_line.svg" class="icon-img-flight">
|
||||||
@@ -107,11 +114,11 @@
|
|||||||
<image
|
<image
|
||||||
v-show="index == 0 && currentIndex !== 2 && rowRecord.hasLimoRecord ===1"
|
v-show="index == 0 && currentIndex !== 2 && rowRecord.hasLimoRecord ===1"
|
||||||
src="../../static/images/limo_line.svg" class="icon-img-limo"></image>
|
src="../../static/images/limo_line.svg" class="icon-img-limo"></image>
|
||||||
<image v-show="getItemShowSharerIcon(itemRecord)"
|
<image
|
||||||
src="../../static/images/sharer_line.svg" class="icon-img-sharer">
|
v-show="index == 0 && currentIndex !== 2 && rowRecord.hasLimoRecord =='-1'"
|
||||||
</image>
|
src="../../static/images/limo_warn.png" class="icon-img-limo"></image>
|
||||||
<span v-show="itemRecord.isQueue"> (In Queue) </span>
|
<span v-show="index === 0 && rowRecord.isQueue"> (In Queue) </span>
|
||||||
<span v-show="itemRecord.roomReady"> (Room Ready) </span>
|
<span v-show="index === 0 && rowRecord.roomReady"> (Room Ready) </span>
|
||||||
</span>
|
</span>
|
||||||
</view>
|
</view>
|
||||||
<!-- 直接使用v-if配合CSS动画类 -->
|
<!-- 直接使用v-if配合CSS动画类 -->
|
||||||
@@ -134,7 +141,7 @@
|
|||||||
<u-col v-show="currentIndex === 2" span="6">
|
<u-col v-show="currentIndex === 2" span="6">
|
||||||
<view class="hz-font-12px">Trip TPD: {{formatNumber(rowRecord.detailData.tripTPD)}}</view>
|
<view class="hz-font-12px">Trip TPD: {{formatNumber(rowRecord.detailData.tripTPD)}}</view>
|
||||||
<view class="hz-font-12px">Trip Win: {{formatNumber(rowRecord.detailData.tripWin)}}</view>
|
<view class="hz-font-12px">Trip Win: {{formatNumber(rowRecord.detailData.tripWin)}}</view>
|
||||||
<view class="hz-font-12px">Trip RI%: {{getPercentFormat(rowRecord.detailData.tripRIPercentage)}}
|
<view class="hz-font-12px">Trip RI: {{getPercentFormat(rowRecord.detailData.tripRIPercentage)}}
|
||||||
</view>
|
</view>
|
||||||
<view v-show="currentIndex === 2 && filterType ==='MASS'" class="hz-font-12px">Trip Theo : {{formatNumber(rowRecord.detailData.tripTheo)}}
|
<view v-show="currentIndex === 2 && filterType ==='MASS'" class="hz-font-12px">Trip Theo : {{formatNumber(rowRecord.detailData.tripTheo)}}
|
||||||
</view>
|
</view>
|
||||||
@@ -182,6 +189,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="hz-font-12px">Today Win : {{formatNumber(rowRecord.detailData.todayWin)}}
|
<view class="hz-font-12px">Today Win : {{formatNumber(rowRecord.detailData.todayWin)}}
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
</u-col>
|
</u-col>
|
||||||
<u-col span="6">
|
<u-col span="6">
|
||||||
<view class="hz-font-12px">Yesterday T/O: {{formatNumber(rowRecord.detailData.yesterdayTO)}}</view>
|
<view class="hz-font-12px">Yesterday T/O: {{formatNumber(rowRecord.detailData.yesterdayTO)}}</view>
|
||||||
@@ -198,6 +206,8 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="hz-font-12px">Prg Avg PlayHour : {{formarHour(rowRecord.detailData.averagePlayHour)}}
|
<view class="hz-font-12px">Prg Avg PlayHour : {{formarHour(rowRecord.detailData.averagePlayHour)}}
|
||||||
</view>
|
</view>
|
||||||
|
<view class="hz-font-12px">IRC Prg Bal : {{formatNumber(rowRecord.detailData.iRCProgramFundBalance)}}
|
||||||
|
</view>
|
||||||
<view class="hz-font-12px">Exp comm : {{formatNumber(rowRecord.detailData.expComm)}}
|
<view class="hz-font-12px">Exp comm : {{formatNumber(rowRecord.detailData.expComm)}}
|
||||||
</view>
|
</view>
|
||||||
<view class="hz-font-12px">Unsettled Expenses : {{formatNumber(rowRecord.detailData.unsettledExpenses)}}
|
<view class="hz-font-12px">Unsettled Expenses : {{formatNumber(rowRecord.detailData.unsettledExpenses)}}
|
||||||
@@ -206,6 +216,8 @@
|
|||||||
<u-col span="6">
|
<u-col span="6">
|
||||||
<view class="hz-font-12px">Front Money: {{formatNumber(rowRecord.detailData.frontMoney)}}</view>
|
<view class="hz-font-12px">Front Money: {{formatNumber(rowRecord.detailData.frontMoney)}}</view>
|
||||||
<view class="hz-font-12px">TTO: {{formatNumber(rowRecord.detailData.tto)}}</view>
|
<view class="hz-font-12px">TTO: {{formatNumber(rowRecord.detailData.tto)}}</view>
|
||||||
|
<!-- <view class="hz-font-12px">HA : {{getPercentFormat(rowRecord.detailData.houseAdvantage)}}
|
||||||
|
</view> -->
|
||||||
<view class="hz-font-12px">0.1Comp Balance: {{formatNumber(rowRecord.detailData.comBalance)}}
|
<view class="hz-font-12px">0.1Comp Balance: {{formatNumber(rowRecord.detailData.comBalance)}}
|
||||||
</view>
|
</view>
|
||||||
<view class="hz-font-12px">Net Credit outstanding: {{formatNumber(rowRecord.detailData.netCreditOutstanding)}}
|
<view class="hz-font-12px">Net Credit outstanding: {{formatNumber(rowRecord.detailData.netCreditOutstanding)}}
|
||||||
@@ -218,7 +230,7 @@
|
|||||||
<u-col span="6">
|
<u-col span="6">
|
||||||
<view class="hz-font-12px">R3M TPD: {{formatNumber(rowRecord.detailData.r3MTPD)}}</view>
|
<view class="hz-font-12px">R3M TPD: {{formatNumber(rowRecord.detailData.r3MTPD)}}</view>
|
||||||
<view class="hz-font-12px">R3M Win: {{formatNumber(rowRecord.detailData.r3MWin)}}</view>
|
<view class="hz-font-12px">R3M Win: {{formatNumber(rowRecord.detailData.r3MWin)}}</view>
|
||||||
<view class="hz-font-12px">R3M RI%:
|
<view class="hz-font-12px">R3M RI:
|
||||||
{{getPercentFormat(rowRecord.detailData.r3MRIPercentage)}}</view>
|
{{getPercentFormat(rowRecord.detailData.r3MRIPercentage)}}</view>
|
||||||
<view v-show="currentIndex === 2 && filterType ==='MASS'" class="hz-font-12px">R3M Theo : {{formatNumber(rowRecord.detailData.r3MTheo)}}
|
<view v-show="currentIndex === 2 && filterType ==='MASS'" class="hz-font-12px">R3M Theo : {{formatNumber(rowRecord.detailData.r3MTheo)}}
|
||||||
</view>
|
</view>
|
||||||
@@ -226,7 +238,7 @@
|
|||||||
<u-col span="6">
|
<u-col span="6">
|
||||||
<view class="hz-font-12px">R6M TPD: {{formatNumber(rowRecord.detailData.r6MTPD)}}</view>
|
<view class="hz-font-12px">R6M TPD: {{formatNumber(rowRecord.detailData.r6MTPD)}}</view>
|
||||||
<view class="hz-font-12px">R6M Win: {{formatNumber(rowRecord.detailData.r6MWin)}}</view>
|
<view class="hz-font-12px">R6M Win: {{formatNumber(rowRecord.detailData.r6MWin)}}</view>
|
||||||
<view class="hz-font-12px">R6M RI%:
|
<view class="hz-font-12px">R6M RI:
|
||||||
{{getPercentFormat(rowRecord.detailData.r6MRIPercentage)}}</view>
|
{{getPercentFormat(rowRecord.detailData.r6MRIPercentage)}}</view>
|
||||||
<view v-show="currentIndex === 2 && filterType ==='MASS'" class="hz-font-12px">R6M Theo : {{formatNumber(rowRecord.detailData.r6MTheo)}}
|
<view v-show="currentIndex === 2 && filterType ==='MASS'" class="hz-font-12px">R6M Theo : {{formatNumber(rowRecord.detailData.r6MTheo)}}
|
||||||
</view>
|
</view>
|
||||||
@@ -250,7 +262,7 @@
|
|||||||
<view v-show="rowRecord.showOffer">
|
<view v-show="rowRecord.showOffer">
|
||||||
<view class="hz-font-color-dark hz-font-md">Available & Unredeem Offer</view>
|
<view class="hz-font-color-dark hz-font-md">Available & Unredeem Offer</view>
|
||||||
<view v-for="(record,index) in rowRecord.detailData.descriptions"
|
<view v-for="(record,index) in rowRecord.detailData.descriptions"
|
||||||
:key="'offer-'+index" class="hz-font-12px">{{getOfferDescription(record)}}
|
:key="'offer-'+index" class="hz-font-12px todo-description-text">{{getOfferDescription(record)}}
|
||||||
</view>
|
</view>
|
||||||
<view class="hz-split"></view>
|
<view class="hz-split"></view>
|
||||||
</view>
|
</view>
|
||||||
@@ -263,11 +275,11 @@
|
|||||||
class="icon-img-flight" style="margin-left:0px;margin-right:4px;">
|
class="icon-img-flight" style="margin-left:0px;margin-right:4px;">
|
||||||
</image>
|
</image>
|
||||||
</view>
|
</view>
|
||||||
<view class="hz-flex-col">
|
<view class="hz-flex-col todo-description-list">
|
||||||
<view v-for="(record,index) in rowRecord.detailData.descriptions"
|
<view v-for="(record,index) in rowRecord.detailData.descriptions"
|
||||||
:key="'flight'+index" class="hz-flex-row"
|
:key="'flight'+index" class="hz-flex-row todo-description-row"
|
||||||
style="align-items: flex-start;">
|
style="align-items: flex-start;">
|
||||||
<span class="hz-font-12px">{{getFlightDescription(record)}}</span>
|
<span class="hz-font-12px todo-description-text">{{getFlightDescription(record)}}</span>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -277,11 +289,11 @@
|
|||||||
<image src="../../static/images/limo_line_2.svg" class="icon-img-limo"
|
<image src="../../static/images/limo_line_2.svg" class="icon-img-limo"
|
||||||
style="margin-left:0px;margin-right:4px;"></image>
|
style="margin-left:0px;margin-right:4px;"></image>
|
||||||
</view>
|
</view>
|
||||||
<view class="hz-flex-col">
|
<view class="hz-flex-col todo-description-list">
|
||||||
<view v-for="(record,index) in rowRecord.detailData.descriptions"
|
<view v-for="(record,index) in rowRecord.detailData.descriptions"
|
||||||
:key="'limo'+index" class="hz-flex-row"
|
:key="'limo'+index" class="hz-flex-row todo-description-row"
|
||||||
style="align-items: flex-start;">
|
style="align-items: flex-start;">
|
||||||
<span class="hz-font-12px">{{getLimoDescription(record)}}</span>
|
<span class="hz-font-12px todo-description-text">{{getLimoDescription(record)}}</span>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -291,21 +303,26 @@
|
|||||||
<image src="../../static/images/room_line_2.svg" class="icon-img-room"
|
<image src="../../static/images/room_line_2.svg" class="icon-img-room"
|
||||||
style="margin-top:2px;margin-right:3px;"></image>
|
style="margin-top:2px;margin-right:3px;"></image>
|
||||||
</view>
|
</view>
|
||||||
<view class="hz-flex-col">
|
<view class="hz-flex-col todo-description-list">
|
||||||
<view v-for="(record,index) in rowRecord.detailData.descriptions"
|
<view v-for="(record,index) in rowRecord.detailData.descriptions"
|
||||||
:key="'room'+index" class="hz-flex-row"
|
:key="'room'+index" class="hz-flex-row todo-description-row"
|
||||||
style="align-items: flex-start;">
|
style="align-items: center;">
|
||||||
<span class="hz-font-12px">{{getRoomDescription(record)}}</span>
|
<span class="hz-font-12px todo-description-text">
|
||||||
<!-- <view v-show="showSharerIcon(record)" class="icon-group"> -->
|
<template v-for="(part, partIndex) in getRoomDescriptionParts(record)">
|
||||||
<view v-show="getItemShowSharerIcon(record.itemRecordDescription)" class="icon-group">
|
<image v-if="part.type === 'sharerIcon'" :key="'room-sharer-'+index+'-'+partIndex"
|
||||||
<image src="../../static/images/sharer_line_2.svg"
|
src="../../static/images/sharer_line_2.svg"
|
||||||
class="icon-img-limo"></image>
|
class="icon-img-sharer todo-description-sharer-icon"></image>
|
||||||
</view>
|
<span v-else :key="'room-text-'+index+'-'+partIndex">{{part.text}}</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view v-if="currentIndex === 2 && rowRecord.roomReady" class="hz-font-12px todo-description-text" >
|
||||||
|
[Room Ready]
|
||||||
|
</view>
|
||||||
</uni-card>
|
</uni-card>
|
||||||
<template v-slot:right>
|
<template v-slot:right>
|
||||||
<view v-if="currentIndex === 0" class="hz-flex-row" style="height:100%;align-items: center;margin-left:15px;"
|
<view v-if="currentIndex === 0" class="hz-flex-row" style="height:100%;align-items: center;margin-left:15px;"
|
||||||
@@ -332,11 +349,11 @@
|
|||||||
|
|
||||||
<view v-if="currentIndex === 3" v-for="(record,index) in listData" :key="'record'+index" style="margin-bottom:8px;">
|
<view v-if="currentIndex === 3" v-for="(record,index) in listData" :key="'record'+index" style="margin-bottom:8px;">
|
||||||
<view class="hz-date-title">{{record.date}}</view>
|
<view class="hz-date-title">{{record.date}}</view>
|
||||||
<uni-card v-for="(rowRecord,index) in record.dateRecords" :key="'rowRecord'+index" style="margin-left:4rpx;margin-right:4rpx;padding:12px;">
|
<uni-card v-for="(rowRecord,rowIndex) in record.dateRecords" :key="'other-card-' + index + '-' + rowIndex" style="margin-left:4rpx;margin-right:4rpx;padding:12px;">
|
||||||
<u-row v-for="(rowDescription,index) in getOtherDescription(rowRecord.descriptions)" :key="'rowDescription'+index">
|
<u-row v-for="(rowDescription,descIndex) in getOtherDescription(rowRecord.descriptions)" :key="'other-desc-' + index + '-' + rowIndex + '-' + descIndex">
|
||||||
<u-col span="12">
|
<u-col span="12">
|
||||||
<view class="hz-font-color-dark hz-font-md" style="margin-bottom:6px;">
|
<view class="hz-font-color-dark hz-font-md todo-description-text" style="margin-bottom:6px;">
|
||||||
<span v-if="index !== 0">•</span> {{getEmojiDescription(rowDescription)}}
|
<span v-if="descIndex !== 0">•</span> {{getEmojiDescription(rowDescription)}}
|
||||||
</view>
|
</view>
|
||||||
</u-col>
|
</u-col>
|
||||||
</u-row>
|
</u-row>
|
||||||
@@ -430,6 +447,7 @@
|
|||||||
getSetting,
|
getSetting,
|
||||||
getExtendBooking
|
getExtendBooking
|
||||||
} from '../../api/configApi.js';
|
} from '../../api/configApi.js';
|
||||||
|
import { normalizeApiKeys } from '../../common/normalizeApiKeys.js';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
mgmHeader, // 注册组件
|
mgmHeader, // 注册组件
|
||||||
@@ -540,7 +558,8 @@
|
|||||||
calendarSave: [null, null],
|
calendarSave: [null, null],
|
||||||
rangeTip: this.$t("common.rangeTip"),
|
rangeTip: this.$t("common.rangeTip"),
|
||||||
calendarIconColor: "#000000",
|
calendarIconColor: "#000000",
|
||||||
currentTask: null
|
currentTask: null,
|
||||||
|
todoListRequestSeq: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1182,6 +1201,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getTodoListByData() {
|
async getTodoListByData() {
|
||||||
|
const requestSeq = ++this.todoListRequestSeq;
|
||||||
|
|
||||||
if (this.currentTask) {
|
if (this.currentTask) {
|
||||||
try {
|
try {
|
||||||
@@ -1232,11 +1252,12 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (requestSeq !== this.todoListRequestSeq) return;
|
||||||
this.currentTask = null;
|
this.currentTask = null;
|
||||||
|
const records = res && res.data && Array.isArray(res.data.records) ? res.data.records : [];
|
||||||
if (Array.isArray(res.data.records) && res.data.records.length > 0) {
|
if (res && res.success == true && records.length > 0) {
|
||||||
this.showTodoListView=true;
|
this.showTodoListView=true;
|
||||||
this.listData = res.data.records.map(record => ({
|
this.listData = records.map(record => ({
|
||||||
...record,
|
...record,
|
||||||
dateRecords: record.dateRecords.map(dateRecord => ({
|
dateRecords: record.dateRecords.map(dateRecord => ({
|
||||||
...dateRecord,
|
...dateRecord,
|
||||||
@@ -1251,9 +1272,10 @@
|
|||||||
}))
|
}))
|
||||||
}));
|
}));
|
||||||
}else{
|
}else{
|
||||||
if(res.data.records.length === 0){
|
const errorCode = res && (res.errorCode || res.code) ? String(res.errorCode || res.code).toUpperCase() : '';
|
||||||
|
if(errorCode.indexOf('NO RECORD') !== -1 || (res && res.success == true && records.length === 0)){
|
||||||
this.dataNull = this.$t("common.noRecord");
|
this.dataNull = this.$t("common.noRecord");
|
||||||
}else if(res.errorCode.toUpperCase().indexOf('NO RECORD') != -1){
|
}else{
|
||||||
this.dataNull = this.$t("todo.returnError");
|
this.dataNull = this.$t("todo.returnError");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1261,7 +1283,8 @@
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
||||||
if (e.errMsg.includes("abort")) {
|
if (requestSeq !== this.todoListRequestSeq) return;
|
||||||
|
if (e.errMsg && e.errMsg.includes("abort")) {
|
||||||
|
|
||||||
} else if (e.data!==null&&e.data!==undefined&&e.data.message!==undefined){
|
} else if (e.data!==null&&e.data!==undefined&&e.data.message!==undefined){
|
||||||
this.dataNull = e.data.message;
|
this.dataNull = e.data.message;
|
||||||
@@ -1273,7 +1296,7 @@
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if(!this.currentTask){
|
if(requestSeq === this.todoListRequestSeq && !this.currentTask){
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1306,7 +1329,7 @@
|
|||||||
else params.Date = this.formatDate(record.actionDate);
|
else params.Date = this.formatDate(record.actionDate);
|
||||||
const res = await getTodoDetail(params);
|
const res = await getTodoDetail(params);
|
||||||
if (res != null && res.success == true) {
|
if (res != null && res.success == true) {
|
||||||
record.detailData = res.data;
|
record.detailData = normalizeApiKeys(res.data);
|
||||||
if(record.detailData.descriptions!==null){
|
if(record.detailData.descriptions!==null){
|
||||||
for(let i=0;i<record.detailData.descriptions.length;i++){
|
for(let i=0;i<record.detailData.descriptions.length;i++){
|
||||||
if(record.detailData.descriptions[i].itemRecordType == 'Offer'){
|
if(record.detailData.descriptions[i].itemRecordType == 'Offer'){
|
||||||
@@ -1481,10 +1504,44 @@
|
|||||||
getRoomDescription() {
|
getRoomDescription() {
|
||||||
return function(record) {
|
return function(record) {
|
||||||
if (record.itemRecordType == 'Room'){
|
if (record.itemRecordType == 'Room'){
|
||||||
return record.itemRecordDescription.split('|').slice(0, 2).join("|");
|
return record.itemRecordDescription.replace(/\s*\|\s*Sharer:\s*[YN]\s*/," ");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
getRoomDescriptionParts() {
|
||||||
|
return (record) => {
|
||||||
|
if (!record || record.itemRecordType !== 'Room') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.getDescriptionWithSharerIconParts(record.itemRecordDescription);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getDescriptionWithSharerIconParts() {
|
||||||
|
return (description) => {
|
||||||
|
if (description === null || description === undefined || description === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const segments = String(description).split('|');
|
||||||
|
const parts = [];
|
||||||
|
for (let i = 0; i < segments.length; i++) {
|
||||||
|
const segment = segments[i].trim();
|
||||||
|
if (segment === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const sharerMatch = segment.match(/^Sharer\s*:\s*([YN])$/i);
|
||||||
|
if (sharerMatch) {
|
||||||
|
if (String(sharerMatch[1]).toUpperCase() === 'Y') {
|
||||||
|
parts.push({ type: 'sharerIcon' });
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const lastPart = parts.length ? parts[parts.length - 1] : null;
|
||||||
|
const text = lastPart && lastPart.type === 'text' ? ` | ${segment}` : segment;
|
||||||
|
parts.push({ type: 'text', text });
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
};
|
||||||
|
},
|
||||||
hasRoomRecord() {
|
hasRoomRecord() {
|
||||||
return function(record) {
|
return function(record) {
|
||||||
let result = false;
|
let result = false;
|
||||||
@@ -1502,7 +1559,7 @@
|
|||||||
getItemDescription() {
|
getItemDescription() {
|
||||||
return function(record) {
|
return function(record) {
|
||||||
if (this.currentIndex !== 3 && record.indexOf("Sharer") !== -1) {
|
if (this.currentIndex !== 3 && record.indexOf("Sharer") !== -1) {
|
||||||
return record.split('|').slice(0, 2).join("|");
|
return record.replace(/\s*\|\s*Sharer:\s*[YN]\s*/," ");
|
||||||
} else {
|
} else {
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
@@ -1534,11 +1591,8 @@
|
|||||||
},
|
},
|
||||||
getItemShowSharerIcon() {
|
getItemShowSharerIcon() {
|
||||||
return function(record) {
|
return function(record) {
|
||||||
let sharerRecord = record.split("Sharer:");
|
let sharerRecord = record.match(/\|\s*Sharer:\s*([YN])(?:\s*\||\s*$)/);
|
||||||
let hasSharer = "N";
|
let hasSharer = sharerRecord !== null ? sharerRecord[1] : "N";
|
||||||
if (sharerRecord.length > 1) {
|
|
||||||
hasSharer = sharerRecord[1].trim();
|
|
||||||
}
|
|
||||||
return hasSharer == "N" ? false : true;
|
return hasSharer == "N" ? false : true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1643,4 +1697,4 @@
|
|||||||
@import '../../static/css/todo.scss';
|
@import '../../static/css/todo.scss';
|
||||||
@import '../../static/css/custom.scss';
|
@import '../../static/css/custom.scss';
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -154,6 +154,61 @@
|
|||||||
flex-shrink: 0; /* 双重保险防止压缩 */
|
flex-shrink: 0; /* 双重保险防止压缩 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.todo-summary-description,
|
||||||
|
.todo-description-list,
|
||||||
|
.todo-description-row,
|
||||||
|
.todo-description-text {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-summary-description,
|
||||||
|
.todo-description-text {
|
||||||
|
white-space: normal;
|
||||||
|
overflow: visible;
|
||||||
|
text-overflow: clip;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-date-des.todo-summary-description {
|
||||||
|
white-space: normal;
|
||||||
|
overflow: visible;
|
||||||
|
text-overflow: clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-description-list {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-description-row {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-description-row .icon-group {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 1;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-description-sharer-icon {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-summary-description .icon-img-flight,
|
||||||
|
.todo-summary-description .icon-img-limo,
|
||||||
|
.todo-summary-description .icon-img-sharer {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 411 KiB |
@@ -0,0 +1,28 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {readFile} from 'node:fs/promises';
|
||||||
|
const source=await readFile(new URL('../booking/bookingRecord/bookingRecord.vue',import.meta.url),'utf8');
|
||||||
|
function computed(name,next) {
|
||||||
|
const start=source.indexOf(name+'(){');
|
||||||
|
const end=source.indexOf(next+'()',start);
|
||||||
|
assert.ok(start>=0 && end>start);
|
||||||
|
return new Function(`return ({${source.slice(start,end).trim().replace(/,$/,'')}}).${name}();`)();
|
||||||
|
}
|
||||||
|
const color=computed('getRecordColor','getETA');
|
||||||
|
const confirmation=computed('getConfirmationNo','dateFormat');
|
||||||
|
test('AP retains approval color when lodging status also matches an ordinary booking',()=>{
|
||||||
|
for(const lastOperaStatus of ['CHECKED_IN','DUE_OUT','RESERVED',null]) {
|
||||||
|
for(const sharer of [null,'[]','']) {
|
||||||
|
assert.equal(color({approveStatus:'AP',lastOperaStatus,confirmationNo:'12345',sharer}),'hz-card-approval');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('waitlist and ordinary colors are preserved',()=>{
|
||||||
|
for(const approveStatus of ['W','WA']) assert.equal(color({approveStatus,lastOperaStatus:'CHECKED_IN',confirmationNo:'12345'}),'hz-card-waitlist');
|
||||||
|
assert.equal(color({approveStatus:'N',lastOperaStatus:'CHECKED_IN',confirmationNo:'12345'}),'hz-card-booking');
|
||||||
|
});
|
||||||
|
test('Daylight still displays an existing AP confirmation number',()=>{
|
||||||
|
assert.equal(confirmation({approveStatus:'AP',confirmationNo:'12345'}),'12345');
|
||||||
|
assert.equal(confirmation({approveStatus:'AP',confirmationNo:null}),'N/A');
|
||||||
|
assert.equal(confirmation({approveStatus:'W',confirmationNo:'12345'}),'N/A');
|
||||||
|
});
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {readFile} from 'node:fs/promises';
|
||||||
|
const source=await readFile(new URL('../booking/newBooking/newBooking.vue',import.meta.url),'utf8');
|
||||||
|
const script=source.match(/<script>([\s\S]*?)<\/script>/)[1]
|
||||||
|
.replace(/import[\s\S]*?from\s+['"][^'"]+['"];?[^\n]*/g,'').replace('export default','return');
|
||||||
|
function setup() {
|
||||||
|
let stored,scheduled,delay;
|
||||||
|
const calls=[];
|
||||||
|
const page=new Function('uni','playerLookup','setTimeout','clearTimeout',script)(
|
||||||
|
{getStorageSync:()=>structuredClone(stored),setStorageSync:(k,v)=>stored=structuredClone(v),
|
||||||
|
showLoading(){},hideLoading(){},$u:{toast(){}}},
|
||||||
|
async params=>{calls.push(params);return {success:true,data:{englishName:'Original Name',tierName:'Gold'}};},
|
||||||
|
(fn,ms)=>{scheduled=fn;delay=ms;return 1;},()=>{});
|
||||||
|
const vm={modelBooking:{sharerList:[{playerId:'80001',name:'',tierName:''}]},
|
||||||
|
playerData:{playerId:'90001'},userData:{userId:'1'},$t:k=>k,currentTask:null,getPlayerTimer:null};
|
||||||
|
for(const [k,fn] of Object.entries(page.methods))vm[k]=fn.bind(vm);
|
||||||
|
return {vm,calls,run:()=>scheduled(),delay:()=>delay};
|
||||||
|
}
|
||||||
|
test('Daylight delayed lookup and tier fill survive; editing resolved name clears only player ID',async()=>{
|
||||||
|
const {vm,calls,run,delay}=setup();
|
||||||
|
await vm.getPlayer(0);assert.equal(delay(),800);assert.equal(calls.length,0);
|
||||||
|
await run();assert.equal(calls[0].playerId,'80001');
|
||||||
|
const item=vm.modelBooking.sharerList[0];
|
||||||
|
assert.equal(item.name,'Original Name');assert.equal(item.tierName,'Gold');
|
||||||
|
await vm.getPlayerName(0);assert.equal(item.playerId,'80001');
|
||||||
|
item.name='Edited Name';await vm.getPlayerName(0);
|
||||||
|
assert.equal(item.playerId,'');assert.equal(item.name,'Edited Name');
|
||||||
|
});
|
||||||
|
test('source length threshold is retained for partial IDs',async()=>{
|
||||||
|
const {vm,run}=setup();await vm.getPlayer(0);await run();
|
||||||
|
const item=vm.modelBooking.sharerList[0];item.name='Edited Name';item.playerId='1234';
|
||||||
|
await vm.getPlayerName(0);assert.equal(item.playerId,'1234');
|
||||||
|
item.playerId='12345';await vm.getPlayerName(0);assert.equal(item.playerId,'');
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {readFile} from 'node:fs/promises';
|
||||||
|
const source=await readFile(new URL('../booking/submitBooking/submitBooking.vue',import.meta.url),'utf8');
|
||||||
|
const script=source.match(/<script>([\s\S]*?)<\/script>/)[1]
|
||||||
|
.replace(/import[\s\S]*?from\s+['"][^'"]+['"];?[^\n]*/g,'').replace('export default','return');
|
||||||
|
function setup(bookType=0) {
|
||||||
|
const saved=[],logs=[];
|
||||||
|
const page=new Function('uni','saveBooking','getApprover',script)(
|
||||||
|
{showLoading(){},hideLoading(){},showToast(){},$u:{throttle(){}}},
|
||||||
|
async data=>{saved.push(structuredClone(data));return {success:true};},
|
||||||
|
async()=>({success:true,data:{records:[{approverList:[{employeeId:'7',employeeName:'Approver'}],approvalRoutingMessage:'Route'}]}}));
|
||||||
|
const vm={$t:k=>k,$set:(o,k,v)=>o[k]=v,writeLog:async(...args)=>logs.push(args)};
|
||||||
|
Object.assign(vm,page.data.call(vm));
|
||||||
|
for(const [k,fn] of Object.entries(page.methods))vm[k]=fn.bind(vm);
|
||||||
|
for(const [k,fn] of Object.entries(page.computed))Object.defineProperty(vm,k,{get:fn.bind(vm)});
|
||||||
|
vm.bookingData={bookType,dateList:[{date:['2026-09-09','2026-09-10'],status:1}],
|
||||||
|
extendDate:[{date:['2026-09-10','2026-09-11'],status:1}],roomRemark:'',otherRemark:''};
|
||||||
|
vm.checkList=[0]; vm.playerData={playerId:'80001'};vm.userData={userId:'1'};
|
||||||
|
return {vm,saved,logs};
|
||||||
|
}
|
||||||
|
test('direct Waitlist initializes selected dates for normal and extension booking',()=>{
|
||||||
|
for(const type of [0,1]) {
|
||||||
|
const {vm}=setup(type); vm.showWaitlistPop();
|
||||||
|
assert.equal(vm.unqualifiedDateList[0],type?vm.bookingData.extendDate[0]:vm.bookingData.dateList[0]);
|
||||||
|
assert.deepEqual(vm.unqualifiedDateCheckboxStates,[[]]);assert.equal(vm.showWaitlistModal,true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('Waitlist room choice is mutually exclusive and can be deselected',()=>{
|
||||||
|
const {vm}=setup();
|
||||||
|
vm.onWaitlistRoomRemarkGroupChange(['2nd Room']);
|
||||||
|
assert.deepEqual(vm.waitlistRoomGroupValue,['2nd Room']);
|
||||||
|
vm.onWaitlistRoomRemarkGroupChange(['2nd Room','3rd Room']);
|
||||||
|
assert.deepEqual(vm.waitlistRoomGroupValue,['3rd Room']);
|
||||||
|
vm.onWaitlistRoomRemarkGroupChange([]);assert.deepEqual(vm.waitlistRoomGroupValue,[]);
|
||||||
|
});
|
||||||
|
test('Waitlist remark includes Extend first and preserves date-specific manual text',()=>{
|
||||||
|
const {vm}=setup();vm.showWaitlistPop();vm.onWaitlistRoomRemarkGroupChange(['2nd Room']);
|
||||||
|
vm.changeWaitlistOtherRemark(['Extend','Remark']);vm.unqualifiedDateRemarks[0]='Late arrival';
|
||||||
|
vm.getWaitlistRemark(1,0);
|
||||||
|
assert.equal(vm.bookingData.dateList[0].remark,'Extend; 2nd Room; Late arrival');
|
||||||
|
vm.changeWaitlistOtherRemark([]);vm.onWaitlistRoomRemarkGroupChange([]);vm.unqualifiedDateRemarks[0]='';
|
||||||
|
vm.getWaitlistRemark(1,0);assert.equal(vm.bookingData.dateList[0].remark,'');
|
||||||
|
});
|
||||||
|
test('Waitlist date matching survives reconstructed objects',()=>{
|
||||||
|
const {vm}=setup();vm.showWaitlistPop();vm.changeWaitlistOtherRemark(['Extend','Remark']);
|
||||||
|
vm.unqualifiedDateRemarks[0]='Date note';
|
||||||
|
vm.bookingData.dateList[0]=structuredClone(vm.bookingData.dateList[0]);
|
||||||
|
vm.getWaitlistRemark(1,0);assert.equal(vm.bookingData.dateList[0].remark,'Extend; Date note');
|
||||||
|
});
|
||||||
|
test('different existing and manual remarks are both preserved',()=>{
|
||||||
|
const {vm}=setup();vm.showWaitlistPop();vm.changeWaitlistOtherRemark(['Remark']);
|
||||||
|
vm.bookingData.dateList[0].remarkText='Existing note';vm.unqualifiedDateRemarks[0]='Additional note';
|
||||||
|
vm.getWaitlistRemark(1,0);
|
||||||
|
assert.equal(vm.bookingData.dateList[0].remark,'Existing note; Additional note');
|
||||||
|
});
|
||||||
|
test('extension approval submits extendDate remark and approver fields',async()=>{
|
||||||
|
const {vm,saved,logs}=setup(1);vm.showWaitlistPop();
|
||||||
|
vm.unqualifiedDateReasons[0]='reason';vm.unqualifiedDateApprovers[0]=['7'];
|
||||||
|
vm.unqualifiedDateRemarks[0]='Extension note';vm.changeWaitlistOtherRemark(['Extend','Remark']);
|
||||||
|
vm.selectApproverData=[{value:'7',label:'Approver'}];
|
||||||
|
await vm.submitApprovalRequest();
|
||||||
|
assert.equal(logs.length,0);assert.equal(saved.length,1);
|
||||||
|
assert.equal(saved[0].extendDate[0].remarkText,'Extension note');
|
||||||
|
assert.equal(saved[0].extendDate[0].approver,'7');assert.equal(saved[0].isApproval,1);
|
||||||
|
assert.equal(saved[0].extendDate[0].remark,'Extend; Extension note');
|
||||||
|
assert.equal(saved[0].extendData,undefined);
|
||||||
|
});
|
||||||
|
test('approver response retains Daylight records envelope',async()=>{
|
||||||
|
const {vm}=setup();await vm.getApprover();
|
||||||
|
assert.equal(vm.selectApproverData[0].value,'7');assert.equal(vm.selectApproverData[0].label,'Approver');
|
||||||
|
assert.equal(vm.routingResaon,'Route');
|
||||||
|
});
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
|
||||||
|
async function load(relative) {
|
||||||
|
const source = await readFile(new URL(relative, import.meta.url), 'utf8');
|
||||||
|
return import('data:text/javascript;base64,' + Buffer.from(source).toString('base64'));
|
||||||
|
}
|
||||||
|
const ui = await load('../common/roomCalendarUi.js');
|
||||||
|
const normalizer = await load('../common/normalizeApiKeys.js');
|
||||||
|
const api = await load('../api/roomCalendarApi.js');
|
||||||
|
const configApi = await load('../api/configApi.js');
|
||||||
|
const weekdayLabels = ['SUN','MON','TUE','WED','THU','FRI','SAT'];
|
||||||
|
|
||||||
|
test('API routes preserve auth and request-task options through Daylight transport', async () => {
|
||||||
|
const calls = [];
|
||||||
|
globalThis.uni = { $u: { http: {
|
||||||
|
post: (...args) => { calls.push(['POST',...args]); return Promise.resolve('ok'); },
|
||||||
|
get: (...args) => { calls.push(['GET',...args]); return Promise.resolve('ok'); }
|
||||||
|
}}};
|
||||||
|
try {
|
||||||
|
const params = { Property:'MO', RoomType:'SUITE', PlayerId:'123' };
|
||||||
|
const options = { custom:{auth:true}, getTask:()=>{} };
|
||||||
|
await api.requestRoomCalendar(params,options);
|
||||||
|
await api.getPlayerRoomQualification(params,options);
|
||||||
|
await api.getPropertiesRoomTypes({},options);
|
||||||
|
await configApi.getCalendarSetting({},options);
|
||||||
|
await configApi.getCfgSettingByKey({keyName:'MACAU_STRING'},options);
|
||||||
|
assert.equal(calls[0][1],'/app/room_calendar/request_room_calendar');
|
||||||
|
assert.equal(calls[1][1],calls[0][1]);
|
||||||
|
assert.equal(calls[0][2],params);
|
||||||
|
assert.equal(calls[0][3],options);
|
||||||
|
assert.equal(calls[2][1],'/app/room_calendar/get_properties_room_types');
|
||||||
|
assert.equal(calls[2][2].custom,options.custom);
|
||||||
|
assert.equal(calls[2][2].getTask,options.getTask);
|
||||||
|
assert.equal(calls[3][1],'/app/setting/get_calendar_setting');
|
||||||
|
assert.equal(calls[3][3],options);
|
||||||
|
assert.equal(calls[4][1],'/web/setting/get_public_setting_by_key');
|
||||||
|
} finally { delete globalThis.uni; }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PascalCase normalization preserves nested values and input objects', () => {
|
||||||
|
const original = { Records:[{RoomType:'SUITE',RoomCount:0,Nested:{Enabled:false}}],Data:null };
|
||||||
|
const snapshot = structuredClone(original);
|
||||||
|
assert.deepEqual(normalizer.normalizeApiKeys(original),
|
||||||
|
{records:[{roomType:'SUITE',roomCount:0,nested:{enabled:false}}],data:null});
|
||||||
|
assert.deepEqual(original,snapshot);
|
||||||
|
assert.deepEqual(normalizer.getApiRecords(original),original.Records);
|
||||||
|
assert.deepEqual(normalizer.getApiRecords({records:[1]}),[1]);
|
||||||
|
assert.deepEqual(normalizer.getApiRecords({Records:{}}),[]);
|
||||||
|
assert.equal(normalizer.normalizeApiKeys(null),null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cross-year range has unique month-owned weeks and no visible out-of-range dates', () => {
|
||||||
|
const months = ui.buildCalendarDateRange({
|
||||||
|
today:'2026-12-31',startDate:'2026-12-30',endDate:'2027-01-02',weekdayLabels
|
||||||
|
});
|
||||||
|
assert.deepEqual(months.map(m=>m.monthKey),['2026-12','2027-01']);
|
||||||
|
const weeks = ui.buildContinuousCalendarWeeks(months);
|
||||||
|
assert.equal(new Set(weeks.map(w=>w.weekId)).size,weeks.length);
|
||||||
|
assert.deepEqual(weeks.flatMap(w=>w.days).filter(d=>!d.isOutsideMonth).map(d=>d.date),
|
||||||
|
['2026-12-30','2026-12-31','2027-01-01','2027-01-02']);
|
||||||
|
assert.equal(ui.getTodayWeekId(months),'week-2026-12-2026-12-27');
|
||||||
|
const firstDate = months[0].weeks[0].days[0].date;
|
||||||
|
weeks[0].days[0].date = 'changed';
|
||||||
|
assert.equal(months[0].weeks[0].days[0].date,firstDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('leap day is retained and adjacent month cells remain blank', () => {
|
||||||
|
const months = ui.buildCalendarDateRange({
|
||||||
|
today:'2028-02-29',startDate:'2028-02-28',endDate:'2028-03-01',weekdayLabels
|
||||||
|
});
|
||||||
|
const days = ui.buildContinuousCalendarWeeks(months).flatMap(w=>w.days);
|
||||||
|
assert.deepEqual(days.filter(d=>!d.isOutsideMonth).map(d=>d.date),
|
||||||
|
['2028-02-28','2028-02-29','2028-03-01']);
|
||||||
|
assert.equal(days.filter(d=>d.isToday).length,1);
|
||||||
|
assert.ok(days.filter(d=>d.isOutsideMonth).every(d=>d.dayNo===''));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('both response casings map dates counts qualification and leader consistently', () => {
|
||||||
|
const upper = {Date:'2026-09-10T00:00:00',RoomType:'SUITE',RoomCriteria:1200,
|
||||||
|
RoomCount:'2',PropertyLeader:'Leader',BookingDateQualificationFlag:'1'};
|
||||||
|
const lower = normalizer.normalizeApiKeys(upper);
|
||||||
|
const expected = {'2026-09-10_SUITE':{criteria:'1,200',count:2,leader:'Leader',
|
||||||
|
qualified:true,statusText:'Qualified'}};
|
||||||
|
assert.deepEqual(ui.mapRoomCalendarQualificationRecords([upper]),expected);
|
||||||
|
assert.deepEqual(ui.mapRoomCalendarQualificationRecords([lower]),expected);
|
||||||
|
assert.deepEqual(ui.mapRoomCalendarQualificationRecords([{Date:'',RoomType:'X'}]),{});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zero availability and denied qualification are preserved', () => {
|
||||||
|
const mapped = ui.mapRoomCalendarQualificationRecords([{
|
||||||
|
date:'2026-09-10',roomType:'X',roomCount:0,roomCriteria:null,
|
||||||
|
bookingDateQualificationFlag:0,leader:'Fallback'
|
||||||
|
}])['2026-09-10_X'];
|
||||||
|
assert.equal(mapped.criteria,'-');
|
||||||
|
assert.equal(mapped.leader,'Fallback');
|
||||||
|
assert.equal(ui.getAvailabilityStatusClass(mapped),'zero');
|
||||||
|
assert.equal(ui.getCriteriaStatusClass(mapped),'unqualified');
|
||||||
|
});
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const dayjs = require('../uni_modules/uview-ui/libs/util/dayjs.js');
|
||||||
|
const utilitySource = await readFile(new URL('../common/roomCalendarUi.js',import.meta.url),'utf8');
|
||||||
|
const ui = await import('data:text/javascript;base64,'+Buffer.from(utilitySource).toString('base64'));
|
||||||
|
const pageSource = await readFile(new URL('../pages/roomCalendar/roomCalendar.vue',import.meta.url),'utf8');
|
||||||
|
const appSource = await readFile(new URL('../App.vue',import.meta.url),'utf8');
|
||||||
|
const tabMethod = appSource.slice(appSource.indexOf('showTabbarByPermission(userData){'),
|
||||||
|
appSource.indexOf('// 返回',appSource.indexOf('showTabbarByPermission(userData){'))).trim().replace(/,$/,'');
|
||||||
|
const applyTabPermissions = new Function('uni',`return ({${tabMethod}}).showTabbarByPermission;`);
|
||||||
|
const script = pageSource.match(/<script>([\s\S]*?)<\/script>/)[1]
|
||||||
|
.replace(/import[\s\S]*?from\s+['"][^'"]+['"];?/g,'').replace('export default','return');
|
||||||
|
function setup({permission=true,metadata,availability}={}) {
|
||||||
|
const calls = [];
|
||||||
|
const toasts = [];
|
||||||
|
const user = {canViewCalendar:permission};
|
||||||
|
globalThis.uni = {
|
||||||
|
getLocale:()=> 'en', getStorageSync:()=>user,
|
||||||
|
showLoading(){},hideLoading(){},$u:{toast:m=>toasts.push(m)}
|
||||||
|
};
|
||||||
|
const page = new Function('dayjs','buildCalendarDateRange','getTodayWeekId',
|
||||||
|
'mapRoomCalendarQualificationRecords','getPropertiesRoomTypes','requestRoomCalendar',
|
||||||
|
'requestCalendarSetting',script)(dayjs,ui.buildCalendarDateRange,ui.getTodayWeekId,
|
||||||
|
ui.mapRoomCalendarQualificationRecords,
|
||||||
|
async()=>{calls.push('metadata');return metadata ?? {success:true,propertyList:[
|
||||||
|
{code:'Macau',roomTypes:[{code:'SUITE',label:'SUITE'}],defaultRoomTypes:['SUITE']}
|
||||||
|
]};},
|
||||||
|
async(params,config)=>{calls.push(params);return availability ? availability(params,config) : {success:true,data:[]};},
|
||||||
|
async()=>{calls.push('settings');return {success:true,data:[]};});
|
||||||
|
const vm = {$t:k=>k,$i18n:{},$nextTick:fn=>fn(),showTabbarByPermission(){}};
|
||||||
|
Object.assign(vm,page.data.call(vm));
|
||||||
|
vm.userData = user;
|
||||||
|
for(const [key,fn] of Object.entries(page.methods)) vm[key]=fn.bind(vm);
|
||||||
|
for(const [key,fn] of Object.entries(page.computed)) Object.defineProperty(vm,key,{get:fn.bind(vm)});
|
||||||
|
return {vm,page,calls,toasts};
|
||||||
|
}
|
||||||
|
test('direct navigation and refresh without permission issue no queries',async()=>{
|
||||||
|
const {vm,page,calls}=setup({permission:false});
|
||||||
|
page.onShow.call(vm);
|
||||||
|
vm.refreshCalendar();
|
||||||
|
await vm.loadPropertiesRoomTypes();
|
||||||
|
assert.equal(vm.calendarNoPermission,true);
|
||||||
|
assert.equal(vm.calendarReady,false);
|
||||||
|
assert.deepEqual(calls,[]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('real tab permission method hides Calendar after an allowed user switches to a denied user',()=>{
|
||||||
|
const visible = new Map();
|
||||||
|
const apply = applyTabPermissions({setTabBarItem:({index,visible:value})=>visible.set(index,value)});
|
||||||
|
apply({canViewCalendar:true});
|
||||||
|
assert.equal(visible.get(3),true);
|
||||||
|
for (const user of [{canViewCalendar:false},{},{canViewCalendar:'true'},null,undefined]) {
|
||||||
|
apply({canViewCalendar:true});
|
||||||
|
apply(user);
|
||||||
|
assert.equal(visible.get(3),false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('page onShow applies real tab visibility and blocks denied users before any query',()=>{
|
||||||
|
const {vm,page,calls}=setup({permission:false});
|
||||||
|
const visible = new Map();
|
||||||
|
vm.showTabbarByPermission = applyTabPermissions({setTabBarItem:({index,visible:value})=>visible.set(index,value)});
|
||||||
|
page.onShow.call(vm);
|
||||||
|
assert.equal(visible.get(3),false);
|
||||||
|
assert.equal(vm.calendarNoPermission,true);
|
||||||
|
assert.deepEqual(calls,[]);
|
||||||
|
});
|
||||||
|
test('metadata denial handles Daylight code and stops availability request',async()=>{
|
||||||
|
const {vm,calls}=setup({metadata:{success:false,code:'NO_PERMISSION'}});
|
||||||
|
await vm.loadPropertiesRoomTypes();
|
||||||
|
assert.equal(vm.calendarNoPermission,true);
|
||||||
|
assert.equal(vm.calendarReady,false);
|
||||||
|
assert.deepEqual(calls,['settings','metadata']);
|
||||||
|
});
|
||||||
|
test('empty property list is reported without accessing first element',async()=>{
|
||||||
|
const {vm,calls,toasts}=setup({metadata:{success:true,propertyList:[]}});
|
||||||
|
await vm.loadPropertiesRoomTypes();
|
||||||
|
assert.equal(vm.calendarReady,false);
|
||||||
|
assert.deepEqual(calls,['settings','metadata']);
|
||||||
|
assert.equal(toasts[0],'No calendar properties configured');
|
||||||
|
});
|
||||||
|
test('initial availability denial does not re-enable the grid',async()=>{
|
||||||
|
const {vm}=setup({availability:async()=>({success:false,errorCode:'NO_PERMISSION'})});
|
||||||
|
await vm.loadPropertiesRoomTypes();
|
||||||
|
assert.equal(vm.calendarNoPermission,true);
|
||||||
|
assert.equal(vm.calendarReady,false);
|
||||||
|
});
|
||||||
|
test('older availability response cannot overwrite a newer selection',async()=>{
|
||||||
|
const pending=[];
|
||||||
|
const {vm}=setup({availability:()=>new Promise(resolve=>pending.push(resolve))});
|
||||||
|
vm.propertyList=[{code:'Macau',roomTypes:[{code:'SUITE'}]}];
|
||||||
|
vm.selectedPropertyCode='Macau';vm.selectedRoomTypeCodes=['SUITE'];
|
||||||
|
vm.buildCalendar();
|
||||||
|
const first=vm.loadAvailabilityData();
|
||||||
|
const second=vm.loadAvailabilityData();
|
||||||
|
const date=vm.getCalendarDateRange().startDate;
|
||||||
|
pending[1]({success:true,data:[{date,roomType:'SUITE',roomCount:2}]});
|
||||||
|
await second;
|
||||||
|
pending[0]({success:true,data:[{date,roomType:'SUITE',roomCount:9}]});
|
||||||
|
await first;
|
||||||
|
assert.equal(vm.availabilityMap[date+'_SUITE'].count,2);
|
||||||
|
});
|
||||||
|
test('OTHER request sends real hotel codes and deduplicated room types',()=>{
|
||||||
|
const {vm}=setup();
|
||||||
|
vm.propertyList=[{code:'OTHER',otherProperties:[
|
||||||
|
{code:'A',roomTypes:[{code:'SUITE'}]},{code:'B',roomTypes:[{code:'SUITE'}]}
|
||||||
|
]}];
|
||||||
|
vm.selectedPropertyCode='OTHER';vm.selectedOtherPropertyCodes=['A','B'];
|
||||||
|
assert.deepEqual(vm.buildAvailabilityParams({startDate:'2026-09-01',endDate:'2026-09-02'}),
|
||||||
|
{Property:'A,B',RoomType:'SUITE',StartDate:'2026-09-01',EndDate:'2026-09-02'});
|
||||||
|
});
|
||||||
|
test('room selection cap retains existing selection',()=>{
|
||||||
|
const {vm}=setup();
|
||||||
|
vm.maxSelectedRoomTypes=1;vm.selectedRoomTypeCodes=['A'];
|
||||||
|
vm.toggleRoomType('B');
|
||||||
|
assert.deepEqual(vm.selectedRoomTypeCodes,['A']);
|
||||||
|
});
|
||||||
|
test('login remains launch page and calendar/settings labels have translations',async()=>{
|
||||||
|
const raw=await readFile(new URL('../pages.json',import.meta.url),'utf8');
|
||||||
|
const routes=JSON.parse(raw.replace(/^\s*\/\/.*$/gm,''));
|
||||||
|
assert.equal(routes.pages[0].path,'pages/login/login');
|
||||||
|
assert.deepEqual(routes.tabBar.list.map(t=>t.pagePath),[
|
||||||
|
'pages/dashboard/dashboard','salessupport/todoList/todoList','pages/findPlayer/findPlayer',
|
||||||
|
'pages/roomCalendar/roomCalendar','pages/setting/setting']);
|
||||||
|
assert.equal(routes.tabBar.list[3].visible,false);
|
||||||
|
for(const locale of ['en','zh-Hant']){
|
||||||
|
const messages=JSON.parse(await readFile(new URL('../locale/'+locale+'.json',import.meta.url),'utf8'));
|
||||||
|
assert.ok(messages['roomCalendar.title']);
|
||||||
|
assert.ok(messages['roomCalendar.maxSelectedRoomTypes']);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {readFile} from 'node:fs/promises';
|
||||||
|
const source = await readFile(new URL('../pages/dashboard/dashboard.vue',import.meta.url),'utf8');
|
||||||
|
const script = source.match(/<script>([\s\S]*?)<\/script>/)[1]
|
||||||
|
.replace(/import[\s\S]*?from\s+['"][^'"]+['"];?[^\n]*/g,'').replace('export default','return');
|
||||||
|
function setup(data) {
|
||||||
|
let requests=0;
|
||||||
|
const page = new Function('mgmHeader','uni','getTodoDashboard',script)({},
|
||||||
|
{showLoading(){},hideLoading(){}},async()=>{requests++;return typeof data === 'function' ? data() : {success:true,data};});
|
||||||
|
const vm={$t:k=>k};
|
||||||
|
Object.assign(vm,page.data.call(vm),{userData:{userId:'80001'}});
|
||||||
|
for(const [key,fn] of Object.entries(page.methods)) vm[key]=fn.bind(vm);
|
||||||
|
return {vm,requests:()=>requests};
|
||||||
|
}
|
||||||
|
test('Dashboard request accepts both response casings and sets IM totals and data date',async()=>{
|
||||||
|
for(const pascal of [false,true]) {
|
||||||
|
const records=[{currentValue:0,itemName:'MF Turnover',userSegment:'IM',type:'Achievement',targetValue:100},
|
||||||
|
{itemName:'2026-09-09',type:'DataAsOf'}];
|
||||||
|
const data=pascal?{Records:records.map(r=>Object.fromEntries(Object.entries(r).map(([k,v])=>[k[0].toUpperCase()+k.slice(1),v])))}:{records};
|
||||||
|
const {vm,requests}=setup(data);
|
||||||
|
await vm.getTodoDashboardByuserId();
|
||||||
|
assert.equal(requests(),1);
|
||||||
|
assert.equal(vm.showDashboardView,true);
|
||||||
|
assert.equal(vm.userSegment,'IM');
|
||||||
|
assert.equal(vm.MFValue,0);
|
||||||
|
assert.equal(vm.dataAsOf,'2026-09-09');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('Dashboard shows only populated sections and preserves IM turnover-only Achievement',async()=>{
|
||||||
|
for(const [records,achievement,kpi] of [
|
||||||
|
[[{type:'KPI',itemName:'Rate'}],false,true],
|
||||||
|
[[{type:'Achievement',itemName:'Target'}],true,false],
|
||||||
|
[[{type:'Achievement',itemName:'MF Turnover',userSegment:'IM',currentValue:0}],true,false]
|
||||||
|
]) {
|
||||||
|
const {vm}=setup({records}); await vm.getTodoDashboardByuserId();
|
||||||
|
assert.equal(vm.showAchievement,achievement); assert.equal(vm.showKPI,kpi);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('Dashboard empty refresh clears old data and hides both sections',async()=>{
|
||||||
|
const data={records:[{type:'Achievement',itemName:'MF Turnover',userSegment:'IM',currentValue:99}]};
|
||||||
|
const {vm}=setup(data); await vm.getTodoDashboardByuserId();
|
||||||
|
data.records=[]; await vm.getTodoDashboardByuserId();
|
||||||
|
assert.deepEqual(vm.dashboardData,[]); assert.equal(vm.MFValue,'0');
|
||||||
|
assert.equal(vm.showDashboardView,false); assert.equal(vm.showAchievement,false); assert.equal(vm.showKPI,false);
|
||||||
|
});
|
||||||
|
test('Dashboard failed refresh hides previously displayed data',async()=>{
|
||||||
|
let fail=false;
|
||||||
|
const {vm}=setup(()=>{if(fail)throw Error('network');return {success:true,data:{records:[{type:'KPI',itemName:'Rate'}]}};});
|
||||||
|
await vm.getTodoDashboardByuserId(); fail=true; await vm.getTodoDashboardByuserId();
|
||||||
|
assert.equal(vm.showDashboardView,false); assert.equal(vm.showKPI,false); assert.equal(vm.showAchievement,false);
|
||||||
|
});
|
||||||
|
test('Dashboard normalization preserves camelCase zero over PascalCase fallback',()=>{
|
||||||
|
const {vm}=setup({});
|
||||||
|
const record={currentValue:0,CurrentValue:99,targetValue:0,TargetValue:100};
|
||||||
|
const normalized=vm.normalizeDashboardRecord(record);
|
||||||
|
assert.equal(normalized.currentValue,0);
|
||||||
|
assert.equal(normalized.targetValue,0);
|
||||||
|
assert.equal(record.CurrentValue,99);
|
||||||
|
});
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {readFile} from 'node:fs/promises';
|
||||||
|
import {createRequire} from 'node:module';
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const dayjs = require('../uni_modules/uview-ui/libs/util/dayjs.js');
|
||||||
|
const source = await readFile(new URL('../booking/playerRoomCalendar/playerRoomCalendar.vue', import.meta.url), 'utf8');
|
||||||
|
const script = source.match(/<script>([\s\S]*?)<\/script>/)[1]
|
||||||
|
.replace(/import[\s\S]*?from\s+['"][^'"]+['"];?/g, '').replace('export default', 'return');
|
||||||
|
const utility = await readFile(new URL('../common/roomCalendarUi.js', import.meta.url), 'utf8');
|
||||||
|
const ui = await import('data:text/javascript;base64,' + Buffer.from(utility).toString('base64'));
|
||||||
|
function setup({permission=true, player={playerId:'80001'}, metadata, response}={}) {
|
||||||
|
const calls = [], toasts = [];
|
||||||
|
const user = {canViewCalendar:permission};
|
||||||
|
const uni = {getStorageSync:key=>key==='userData'?user:player,
|
||||||
|
showLoading(){},hideLoading(){},$u:{toast:m=>toasts.push(m)}};
|
||||||
|
const page = new Function('uni','dayjs','buildCalendarDateRange','getTodayWeekId',
|
||||||
|
'mapRoomCalendarQualificationRecords','getPropertiesRoomTypes','getPlayerRoomQualification',
|
||||||
|
'requestCalendarSetting',script)(uni,dayjs,ui.buildCalendarDateRange,ui.getTodayWeekId,
|
||||||
|
ui.mapRoomCalendarQualificationRecords,
|
||||||
|
async()=>{calls.push('metadata'); return metadata ?? {propertyList:[
|
||||||
|
{code:'Macau',name:'Macau',roomTypes:[{code:'SUITE',label:'SUITE'}],defaultRoomTypes:['SUITE']}
|
||||||
|
]};},
|
||||||
|
async p=>{calls.push(p);return response ?? {success:true,data:[]};},
|
||||||
|
async()=>{calls.push('settings');return {data:[]};});
|
||||||
|
const vm = {$t:k=>k,$nextTick:fn=>fn()};
|
||||||
|
Object.assign(vm,page.data.call(vm),{playerData:player,userData:user});
|
||||||
|
for (const [k,fn] of Object.entries(page.methods)) vm[k]=fn.bind(vm);
|
||||||
|
for (const [k,fn] of Object.entries(page.computed)) Object.defineProperty(vm,k,{get:fn.bind(vm)});
|
||||||
|
return {vm,page,calls,toasts};
|
||||||
|
}
|
||||||
|
test('denied direct entry and refresh make no requests',async()=>{
|
||||||
|
for (const permission of [false,undefined,'true']) {
|
||||||
|
const {vm,calls}=setup({permission:permission ?? null});
|
||||||
|
await vm.loadPropertiesRoomTypes(); vm.refreshCalendar(); await vm.loadQualificationData();
|
||||||
|
assert.deepEqual(calls,[]); assert.equal(vm.calendarNoPermission,true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('missing player cannot accidentally request the general Calendar',async()=>{
|
||||||
|
const {vm,calls}=setup({player:null});
|
||||||
|
await vm.loadPropertiesRoomTypes(); await vm.loadQualificationData();
|
||||||
|
assert.deepEqual(calls,[]); assert.equal(vm.calendarReady,false);
|
||||||
|
});
|
||||||
|
test('player query carries current player, property, room type and date range',async()=>{
|
||||||
|
const {vm,calls}=setup(); await vm.loadPropertiesRoomTypes();
|
||||||
|
const query=calls.find(c=>typeof c==='object');
|
||||||
|
assert.equal(query.PlayerId,'80001'); assert.equal(query.Property,'Macau');
|
||||||
|
assert.equal(query.RoomType,'SUITE');
|
||||||
|
assert.match(query.StartDate,/^\d{4}-\d{2}-\d{2}$/);
|
||||||
|
assert.ok(query.EndDate>=query.StartDate); assert.equal(vm.calendarReady,true);
|
||||||
|
});
|
||||||
|
test('metadata denial and empty configuration do not call player query',async()=>{
|
||||||
|
for (const metadata of [{success:false,code:'NO_PERMISSION'},{propertyList:[]}]) {
|
||||||
|
const {vm,calls}=setup({metadata}); await vm.loadPropertiesRoomTypes();
|
||||||
|
assert.deepEqual(calls,['settings','metadata']); assert.equal(vm.calendarReady,false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('both denial response formats remain denied after initial load completes',async()=>{
|
||||||
|
for (const key of ['code','errorCode']) {
|
||||||
|
const {vm}=setup({response:{success:false,[key]:'NO_PERMISSION'}});
|
||||||
|
await vm.loadPropertiesRoomTypes();
|
||||||
|
assert.equal(vm.calendarReady,false); assert.equal(vm.calendarNoPermission,true);
|
||||||
|
assert.deepEqual(vm.qualificationMap,{});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('booking entry rechecks permission before navigation',async()=>{
|
||||||
|
const booking=await readFile(new URL('../booking/bookingRecord/bookingRecord.vue',import.meta.url),'utf8');
|
||||||
|
const method=booking.match(/navToPlayerRoomCalendar\(\)\{([\s\S]*?)\n\s*\},/)[1];
|
||||||
|
const visits=[];
|
||||||
|
for (const permission of [false,true]) {
|
||||||
|
new Function('uni',method).call({navTo:(...args)=>visits.push(args)},
|
||||||
|
{getStorageSync:()=>({canViewCalendar:permission})});
|
||||||
|
}
|
||||||
|
assert.deepEqual(visits,[['/booking/playerRoomCalendar/playerRoomCalendar',0,false]]);
|
||||||
|
assert.match(booking, /<image\s+v-if="userData && userData\.canViewCalendar === true"\s+@click="navToPlayerRoomCalendar"[^>]*><\/image>\s*<image\s+v-else\s+@click="getDataByPage"[^>]*><\/image>/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import vm from 'node:vm';
|
||||||
|
|
||||||
|
const root = new URL('../', import.meta.url);
|
||||||
|
const app = fs.readFileSync(new URL('App.vue', root), 'utf8');
|
||||||
|
const method = app.slice(app.indexOf('async writeLog('), app.indexOf('async logout(){'));
|
||||||
|
|
||||||
|
test('actual APP log method routes by token and preserves debugMode gate', async () => {
|
||||||
|
for (const token of ['', null, undefined, 'valid-token']) {
|
||||||
|
const calls = [];
|
||||||
|
const scope = {
|
||||||
|
uni: {getStorageSync: key => key === 'token' ? token : {debugMode: true}},
|
||||||
|
writeDebugLog: async value => calls.push(['private', value]),
|
||||||
|
writePublicDebugLog: async value => calls.push(['public', value]), console,
|
||||||
|
};
|
||||||
|
const {writeLog} = vm.runInNewContext(`({${method}})`, scope);
|
||||||
|
await writeLog('80001', 'LOGIN', 'txn', 'description', null, null, 1, false);
|
||||||
|
assert.equal(calls.length, 1);
|
||||||
|
assert.equal(calls[0][0], token ? 'private' : 'public');
|
||||||
|
assert.equal(calls[0][1].eventCode, 'LOGIN');
|
||||||
|
scope.uni.getStorageSync = key => key === 'token' ? token : {debugMode: false};
|
||||||
|
await writeLog('80001', 'LOGIN', 'txn', 'description', null, null, 1, false);
|
||||||
|
assert.equal(calls.length, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('API wrappers use public bootstrap routes and preserve refresh protocol', async () => {
|
||||||
|
const calls = [];
|
||||||
|
const scope = {uni: {$u: {http: {post: (...args) => calls.push(args)}}}};
|
||||||
|
function load(file, names) {
|
||||||
|
const source = fs.readFileSync(new URL(file, root), 'utf8').replaceAll('export const ', 'const ');
|
||||||
|
return vm.runInNewContext(`${source}\n;({${names.join(',')}})`, scope);
|
||||||
|
}
|
||||||
|
const common = load('api/commonApi.js', ['getSettings', 'writeDebugLog', 'writePublicDebugLog', 'refreshToken']);
|
||||||
|
const config = load('api/configApi.js', ['getCfgSettingByKey']);
|
||||||
|
const params = {keyName: 'MACAU_STRING'};
|
||||||
|
const options = {custom: {test: true}};
|
||||||
|
common.getSettings(params, options); common.writeDebugLog(params, options);
|
||||||
|
common.writePublicDebugLog(params, options); config.getCfgSettingByKey(params, options);
|
||||||
|
common.refreshToken(params, options);
|
||||||
|
assert.deepEqual(calls.map(call => call[0]), [
|
||||||
|
'/system/sys_setting/get_public_login_settings', '/system/write_log', '/system/write_public_log',
|
||||||
|
'/web/setting/get_public_setting_by_key', '/app/refresh_token?isRenewRefreshToken=false',
|
||||||
|
]);
|
||||||
|
for (const call of calls) { assert.equal(call[1], params); assert.equal(call[2], options); }
|
||||||
|
});
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import vm from 'node:vm';
|
||||||
|
const source=fs.readFileSync(new URL('../common/request.js',import.meta.url),'utf8').replace(/^import .*$/mg,'');
|
||||||
|
function harness(refresh) {
|
||||||
|
const storage=new Map([['token','old'],['refreshToken','refresh'],['clientId','client']]);
|
||||||
|
let failure, retried=0, redirects=0;
|
||||||
|
const context={module:{exports:{}},console:{log(){}},dayjs:()=>({unix:()=>123}),refreshToken:refresh,
|
||||||
|
uni:{getStorageSync:k=>storage.get(k),setStorageSync:(k,v)=>storage.set(k,v),removeStorageSync:k=>storage.delete(k),
|
||||||
|
redirectTo:()=>redirects++, $u:{toast(){},http:{request:async()=>{retried++;return 'retried';},
|
||||||
|
interceptors:{request:{use(){}},response:{use:(ok,bad)=>failure=bad}}}}}};
|
||||||
|
vm.runInNewContext(source,context); context.module.exports({});
|
||||||
|
return {storage, failure:e=>failure(e), counts:()=>({retried,redirects})};
|
||||||
|
}
|
||||||
|
const unauthorized=()=>({statusCode:401,config:{url:'/app/me',header:{},custom:{auth:true}}});
|
||||||
|
test('successful refresh retries once with new access credential', async()=>{
|
||||||
|
const h=harness(async()=>({success:true,accessToken:'new'})); const error=unauthorized();
|
||||||
|
assert.equal(await h.failure(error),'retried'); assert.equal(error.config.header.Authorization,'new');
|
||||||
|
assert.equal(h.storage.get('refreshToken'),'refresh');
|
||||||
|
await assert.rejects(h.failure(error)); assert.equal(h.counts().retried,1);
|
||||||
|
});
|
||||||
|
test('failed, malformed or missing refresh stops original request',async()=>{
|
||||||
|
for(const mode of ['failure','malformed','missing']) {
|
||||||
|
let calls=0;
|
||||||
|
const h=harness(async()=>{calls++;if(mode==='failure')throw new Error('denied');return {success:false};});
|
||||||
|
if(mode==='missing')h.storage.delete('refreshToken');
|
||||||
|
await assert.rejects(h.failure(unauthorized()));
|
||||||
|
assert.equal(h.counts().retried,0);assert.equal(h.storage.has('token'),false);
|
||||||
|
assert.equal(calls,mode==='missing'?0:1);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {readFile} from 'node:fs/promises';
|
||||||
|
const source=await readFile(new URL('../salessupport/todoList/todoList.vue',import.meta.url),'utf8');
|
||||||
|
const script=source.match(/<script>([\s\S]*?)<\/script>/)[1]
|
||||||
|
.replace(/import[\s\S]*?from\s+['"][^'"]+['"];?[^\n]*/g,'').replace('export default','return');
|
||||||
|
const utility=await readFile(new URL('../common/normalizeApiKeys.js',import.meta.url),'utf8');
|
||||||
|
const {normalizeApiKeys}=await import('data:text/javascript;base64,'+Buffer.from(utility).toString('base64'));
|
||||||
|
function setup({list=async()=>({success:true,data:{records:[]}}),detail=async()=>({success:true,data:{}})}={}) {
|
||||||
|
const notices=[];
|
||||||
|
const page=new Function('mgmHeader','SimpleWatermark','uni','normalizeApiKeys','getTodoList','getTodoDetail','setTimeout',script)(
|
||||||
|
{},{},{showLoading(){},hideLoading(){},showToast:v=>notices.push(v)},normalizeApiKeys,list,detail,fn=>fn());
|
||||||
|
const vm={$t:k=>k,currentTask:null,todoListRequestSeq:0,filterType:'IM',currentIndex:0,
|
||||||
|
startDate:'2026-09-09',endDate:'2026-09-10',calendarSave:[],listData:[]};
|
||||||
|
for(const [k,fn] of Object.entries(page.methods)) vm[k]=fn.bind(vm);
|
||||||
|
for(const [k,fn] of Object.entries(page.computed)) Object.defineProperty(vm,k,{get:fn.bind(vm)});
|
||||||
|
vm.formatDate=v=>v; vm.checkCalendarSave=()=>{};
|
||||||
|
return {vm,page,notices};
|
||||||
|
}
|
||||||
|
const rows=id=>({success:true,data:{records:[{date:'2026-09-09',dateRecords:[{playerId:id,eta:'13:45'}]}]}});
|
||||||
|
test('older success and error cannot overwrite latest Todo response',async()=>{
|
||||||
|
for(const rejectOld of [false,true]) {
|
||||||
|
const pending=[];
|
||||||
|
const {vm}=setup({list:()=>new Promise((resolve,reject)=>pending.push({resolve,reject}))});
|
||||||
|
const old=vm.getTodoListByData(); const latest=vm.getTodoListByData();
|
||||||
|
pending[1].resolve(rows('new')); await latest;
|
||||||
|
if(rejectOld) pending[0].reject(new Error('old failure')); else pending[0].resolve(rows('old'));
|
||||||
|
await old;
|
||||||
|
assert.equal(vm.listData[0].dateRecords[0].playerId,'new');
|
||||||
|
assert.equal(vm.listData[0].dateRecords[0].eta,'13:45');
|
||||||
|
assert.equal(vm.showTodoListView,true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('empty success and incomplete error response have distinct messages',async()=>{
|
||||||
|
for(const [response,message] of [[{success:true,data:{records:[]}},'common.noRecord'],
|
||||||
|
[{success:false},'todo.returnError'],[{success:false,code:'NO RECORD'},'common.noRecord']]) {
|
||||||
|
const {vm}=setup({list:async()=>response}); await vm.getTodoListByData();
|
||||||
|
assert.equal(vm.dataNull,message); assert.equal(vm.showTodoListView,false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('native Error without errMsg is handled',async()=>{
|
||||||
|
const {vm}=setup({list:async()=>{throw new Error('network');}});
|
||||||
|
await vm.getTodoListByData(); assert.equal(vm.dataNull,'todo.returnError');
|
||||||
|
});
|
||||||
|
test('detail normalizes IRC balance and nested Offer fields in both casings',async()=>{
|
||||||
|
for(const data of [{IRCProgramFundBalance:'0',Descriptions:[{ItemRecordType:'Offer'}]},
|
||||||
|
{iRCProgramFundBalance:'0',descriptions:[{itemRecordType:'Offer'}]}]) {
|
||||||
|
const {vm}=setup({detail:async()=>({success:true,data})});
|
||||||
|
const record={playerId:'80001',actionDate:'2026-09-09',arrowName:'arrow-down'};
|
||||||
|
await vm.fetchTodoDetail(record);
|
||||||
|
assert.equal(record.detailData.iRCProgramFundBalance,'0');
|
||||||
|
assert.equal(record.showOffer,true); assert.equal(record.showDetail,true);
|
||||||
|
assert.equal(record.isProcessing,false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('ETA label still uses Daylight eta field',()=>{
|
||||||
|
const {vm,page}=setup();
|
||||||
|
assert.equal(page.computed.getETALable.call(vm)({eta:'13:45',eTA:'09:00'}),'13:45');
|
||||||
|
});
|
||||||
|
test('Todo formatters accept API string amounts, hours and ratios',()=>{
|
||||||
|
const {vm}=setup();
|
||||||
|
assert.equal(vm.formatNumber('3000000000'),'3,000,000,000');
|
||||||
|
assert.equal(vm.formatNumber('12.75'),'12.75');
|
||||||
|
assert.equal(vm.formatNumber('0'),'0');assert.equal(vm.formatNumber(null),'0');
|
||||||
|
assert.equal(vm.formarHour('1.5'),'1.5 hours');
|
||||||
|
assert.equal(vm.formarHour('0'),'0 hour');
|
||||||
|
assert.equal(vm.getPercentFormat('0.25'),'25%');
|
||||||
|
assert.equal(vm.getPercentFormat('0'),'0%');
|
||||||
|
});
|
||||||
|
test('Sharer renderer preserves trailing room status and notes and only renders Y icon',()=>{
|
||||||
|
const {vm}=setup();
|
||||||
|
for(const flag of ['Y','N']) {
|
||||||
|
const parts=vm.getRoomDescriptionParts({itemRecordType:'Room',itemRecordDescription:`Conf#123 [Rm Ready] | Suite | Sharer:${flag} | Note`});
|
||||||
|
assert.equal(parts.filter(p=>p.type==='sharerIcon').length,flag==='Y'?1:0);
|
||||||
|
const text=parts.filter(p=>p.type==='text').map(p=>p.text).join('');
|
||||||
|
assert.ok(text.includes('Conf#123 [Rm Ready]'));assert.ok(text.includes('Note'));
|
||||||
|
assert.ok(!text.includes('Sharer:'));
|
||||||
|
}
|
||||||
|
assert.equal(vm.getItemShowSharerIcon('Conf#123 | Sharer:N | Note'),false);
|
||||||
|
assert.deepEqual(vm.getDescriptionWithSharerIconParts(null),[]);
|
||||||
|
});
|
||||||
|
test('summary room markers use row flags only on first line and Limo warning excludes Inhouse',()=>{
|
||||||
|
const conditions=[...source.matchAll(/v-show="([^"]+)"/g)].map(m=>m[1]);
|
||||||
|
for(const field of ['isQueue','roomReady']) {
|
||||||
|
const expression=conditions.find(c=>c.includes(`rowRecord.${field}`));
|
||||||
|
assert.ok(expression);
|
||||||
|
const evaluate=new Function('index','rowRecord',`return ${expression}`);
|
||||||
|
assert.equal(evaluate(0,{[field]:true}),true);
|
||||||
|
assert.equal(evaluate(1,{[field]:true}),false);
|
||||||
|
assert.equal(evaluate(0,{[field]:false}),false);
|
||||||
|
}
|
||||||
|
const warning=conditions.find(c=>c.includes("hasLimoRecord =='-1'"));
|
||||||
|
const evaluate=new Function('index','currentIndex','rowRecord',`return ${warning}`);
|
||||||
|
assert.equal(evaluate(0,0,{hasLimoRecord:-1}),true);
|
||||||
|
assert.equal(evaluate(0,2,{hasLimoRecord:-1}),false);
|
||||||
|
assert.equal(evaluate(1,0,{hasLimoRecord:-1}),false);
|
||||||
|
assert.equal(evaluate(0,0,{hasLimoRecord:1}),false);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user