From 9f2becfa679cc2a02468aff88b543e0a98d2feb4 Mon Sep 17 00:00:00 2001 From: Mauve Date: Wed, 16 Sep 2026 10:43:59 +0800 Subject: [PATCH] fix bugs --- App.vue | 121 ++- api/commonApi.js | 3 +- api/configApi.js | 3 +- api/roomCalendarApi.js | 6 + booking/bookingRecord/bookingRecord.vue | 14 +- booking/newBooking/newBooking.vue | 9 +- .../playerRoomCalendar/playerRoomCalendar.vue | 867 ++++++++++++++++++ booking/submitBooking/submitBooking.vue | 276 +++--- common/normalizeApiKeys.js | 31 + common/pushDeviceToken.js | 83 ++ common/pushNotification.js | 151 +++ common/request.js | 13 +- common/roomCalendarUi.js | 244 +++++ .../room-calendar-grid/room-calendar-grid.vue | 389 ++++++++ locale/zh-Hant.json | 67 +- pages.json | 29 +- pages/dashboard/dashboard.vue | 235 +++-- pages/login/login.vue | 47 +- pages/roomCalendar/roomCalendar.vue | 781 ++++++++++++++++ pages/setting/setting.vue | 5 +- salessupport/todoList/todoList.vue | 148 ++- static/css/todo.scss | 55 ++ static/images/calendar-fill.png | Bin 0 -> 4324 bytes static/images/calendar-line.png | Bin 0 -> 3742 bytes static/images/limo_warn.png | Bin 0 -> 421322 bytes tests/bookingRecordStatus.test.mjs | 28 + tests/bookingSharerMigration.test.mjs | 35 + tests/bookingSubmissionMigration.test.mjs | 73 ++ tests/calendarFoundation.test.mjs | 100 ++ tests/calendarPage.test.mjs | 134 +++ tests/dashboardMigration.test.mjs | 60 ++ tests/playerCalendar.test.mjs | 76 ++ tests/publicEndpointsMigration.test.mjs | 48 + tests/refreshTokenMigration.test.mjs | 32 + tests/todoMigration.test.mjs | 98 ++ .../dist/build/.automator/h5/.automator.json | 0 36 files changed, 3997 insertions(+), 264 deletions(-) create mode 100644 api/roomCalendarApi.js create mode 100644 booking/playerRoomCalendar/playerRoomCalendar.vue create mode 100644 common/normalizeApiKeys.js create mode 100644 common/pushDeviceToken.js create mode 100644 common/pushNotification.js create mode 100644 common/roomCalendarUi.js create mode 100644 components/room-calendar-grid/room-calendar-grid.vue create mode 100644 pages/roomCalendar/roomCalendar.vue create mode 100644 static/images/calendar-fill.png create mode 100644 static/images/calendar-line.png create mode 100644 static/images/limo_warn.png create mode 100644 tests/bookingRecordStatus.test.mjs create mode 100644 tests/bookingSharerMigration.test.mjs create mode 100644 tests/bookingSubmissionMigration.test.mjs create mode 100644 tests/calendarFoundation.test.mjs create mode 100644 tests/calendarPage.test.mjs create mode 100644 tests/dashboardMigration.test.mjs create mode 100644 tests/playerCalendar.test.mjs create mode 100644 tests/publicEndpointsMigration.test.mjs create mode 100644 tests/refreshTokenMigration.test.mjs create mode 100644 tests/todoMigration.test.mjs create mode 100644 unpackage/dist/build/.automator/h5/.automator.json diff --git a/App.vue b/App.vue index 6c85e4e..b00a7cf 100644 --- a/App.vue +++ b/App.vue @@ -1,12 +1,15 @@ + + diff --git a/booking/submitBooking/submitBooking.vue b/booking/submitBooking/submitBooking.vue index d325089..4797510 100644 --- a/booking/submitBooking/submitBooking.vue +++ b/booking/submitBooking/submitBooking.vue @@ -139,10 +139,10 @@ - + {{ $t("common.misCharge") }} - {{ bookingData.mischarge || "-" }} + {{ bookingData.miscCharge || "-" }} @@ -254,19 +254,18 @@ {{$t("bookingConfirm.approvalRemark")}} - + customStyle="height:30px;width:100%;" label="Extend" name="Extend"> + customStyle="height:30px;width:100%;" label="Remark" name="Remark"> @@ -329,41 +328,32 @@ {{$t("bookingConfirm.approvalRemark")}} - + - - - - - - - - - - - + customStyle="height:30px;width:100%;" label="Extend" name="Extend"> + customStyle="height:30px;width:100%;" label="Remark" name="Remark"> @@ -437,7 +427,8 @@ selectedApprover:'', showRemark: false, showWaitlistModal:false, - showRadio:false, + // Waitlist 弹窗 Extend/Remark(必须与 u-checkbox-group 的 :value 绑定,勿用子项 :value) + waitlistOtherRemarkCheckList: [], unqualifiedDateList: [], currentUnqualifiedIndex: 0, unqualifiedDateRemarks: [], @@ -463,6 +454,17 @@ // 數據方法 getBooking(){ 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!==''){ this.oldDateList = this.bookingData.dateList; this.checkList = []; @@ -498,11 +500,7 @@ this.submitBooking(); } }, - submitApproval() { - // 显示审批模态框 - this.getApprovalReason(); - this.showApprovalModal = true; - this.showModalBtn = true; + buildUnqualifiedDateLists() { this.currentUnqualifiedIndex = 0; this.unqualifiedDateList = []; this.unqualifiedDateRemarks = []; @@ -510,33 +508,27 @@ this.unqualifiedDateApprovers = []; this.unqualifiedDateOtherRemarks = []; this.unqualifiedDateCheckboxStates = []; - if(this.checkList!=null && this.checkList.length>0){ - if(this.bookingData.bookType==1){ - // 把extendDate中的状态都设置成不够分 - for(let i=0;i 0) { + if (this.bookingData.bookType == 1) { + for (let i = 0; i < this.bookingData.extendDate.length; i++) { + for (let j = 0; j < this.checkList.length; j++) { + if (i == this.checkList[j] && this.bookingData.extendDate[i].status == 1) { this.unqualifiedDateList.push(this.bookingData.extendDate[i]); this.unqualifiedDateRemarks.push(this.bookingData.extendDate[i].remark || null); this.unqualifiedDateReasons.push(''); - // 多选 Approver:为每个 index 初始化为数组 this.unqualifiedDateApprovers.push([]); this.unqualifiedDateOtherRemarks.push(''); this.unqualifiedDateCheckboxStates.push([]); } } } - }else{ - // 把dateList中的状态都设置成不够分 - for(let i=0;i uItem === item); + if (unqualifiedIndex === -1 && item.date && item.date.length) { + unqualifiedIndex = this.unqualifiedDateList.findIndex((uItem) => { + const ud = uItem.date; + 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 unqualifiedIndex = this.unqualifiedDateList.findIndex(uItem => uItem === item); - let extendStr = ''; - if (unqualifiedIndex !== -1) { - const checkboxState = this.unqualifiedDateCheckboxStates[unqualifiedIndex] || []; - if (checkboxState.includes('Extend')) { - extendStr = 'Extend'; - } + const roomRemark = this.bookingData.roomRemark; + const idxRemark = unqualifiedIndex !== -1 ? this.unqualifiedDateRemarks[unqualifiedIndex] : ''; + const manualRemark = (idxRemark != null && idxRemark !== '') ? idxRemark : this.freeRemark; + const remarkChecked = unqualifiedIndex !== -1 && + (this.unqualifiedDateCheckboxStates[unqualifiedIndex] || []).includes('Remark'); + + 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 && - item.remarkText != '' && - item.remarkText != undefined) { - if (extendStr) { - remarkArr.push(extendStr + '; ' + item.remarkText); - } else { - remarkArr.push(item.remarkText); - } - } else if (item.status == 1 && extendStr) { - remarkArr.push(extendStr); + item.remarkText != null && item.remarkText !== '' && item.remarkText !== undefined) { + dateParts.push(item.remarkText); + } + if ((remarkChecked || this.showRemark) && manualRemark !== '' && manualRemark != null && item.status == 1) { + if (manualRemark !== item.remarkText) dateParts.push(manualRemark); } - - if(this.showRemark&&this.freeRemark!=''&&item.status==1) remarkArr.push(this.freeRemark); - if(remarkArr.length>0) { - this.bookingData.remark = remarkArr.join('; '); - item.remark = remarkArr.join('; '); - }else{ + const dateRemarkJoin = dateParts.join('; '); + if (dateRemarkJoin) { + this.bookingData.remark = dateRemarkJoin; + item.remark = dateRemarkJoin; + } else { this.bookingData.remark = ""; item.remark = ""; } @@ -1563,6 +1585,10 @@ }, showWaitlistPop() { + // 关键:直接点「放入 Waitlist」不会经过 submitApproval,必须在此处填充 unqualifiedDateList, + // 否则 getWaitlistRemark 里 findIndex 恒为 -1,Extend 永远不会写入 remark + this.buildUnqualifiedDateLists(); + this.waitlistOtherRemarkCheckList = (this.unqualifiedDateCheckboxStates[0] || []).slice(); this.showWaitlistModal = true; this.showMsgBox = false; }, @@ -1606,17 +1632,27 @@ } }); }, - changeRoomRemark(e,item) { - if (e) { - if(e==item.roomRemark){ - item.roomRemark="" - item.showRadio = true; - setTimeout(()=>{item.showRadio = false;this.$forceUpdate()},1) - }else{ - item.roomRemark=e - } + onWaitlistRoomRemarkGroupChange(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); + if (!this.bookingData) { + return; } - 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) { // if (e) { @@ -1635,27 +1671,41 @@ // 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) { - if (e) { - let str = ''; - let show = false; - 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 { + const raw = Array.isArray(e) ? e : (e && Array.isArray(e.detail) ? e.detail : []); + const idx = this.currentUnqualifiedIndex; + if (!raw || raw.length === 0) { this.showRemark = false; - this.unqualifiedDateOtherRemarks[this.currentUnqualifiedIndex] = ''; - this.unqualifiedDateCheckboxStates[this.currentUnqualifiedIndex] = []; + this.$set(this.unqualifiedDateOtherRemarks, idx, ''); + 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:{ + waitlistRoomGroupValue() { + if (!this.bookingData || !this.bookingData.roomRemark) { + return []; + } + const r = this.bookingData.roomRemark; + if (r === '2nd Room' || r === '3rd Room') { + return [r]; + } + return []; + }, getStatusColor(){ return function (booking) { let operaStatus = null; @@ -1991,4 +2051,4 @@ color: #C39A4B; margin-bottom: 15px; } - \ No newline at end of file + diff --git a/common/normalizeApiKeys.js b/common/normalizeApiKeys.js new file mode 100644 index 0000000..ca2cdbe --- /dev/null +++ b/common/normalizeApiKeys.js @@ -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; diff --git a/common/pushDeviceToken.js b/common/pushDeviceToken.js new file mode 100644 index 0000000..b3153ab --- /dev/null +++ b/common/pushDeviceToken.js @@ -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} 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 +} diff --git a/common/pushNotification.js b/common/pushNotification.js new file mode 100644 index 0000000..700a60d --- /dev/null +++ b/common/pushNotification.js @@ -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} 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>} + */ +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 +} diff --git a/common/request.js b/common/request.js index 4439fa9..e930f0f 100644 --- a/common/request.js +++ b/common/request.js @@ -121,8 +121,10 @@ module.exports = (vm) => { break; case 400: kickout() + break; 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(_ => { error.config.header.Authorization = uni.getStorageSync("token"); error.config.header.RefreshToken = uni.getStorageSync("refreshToken") @@ -141,19 +143,22 @@ module.exports = (vm) => { } function tokenRefresh(){ - let oldToken = uni.getStorageSync("token"); - if (oldToken !== null && oldToken !== '') { + const storedRefreshToken = uni.getStorageSync("refreshToken"); + if (storedRefreshToken) { return refreshToken({}) .then((data) => { + if (!data?.success || !data.accessToken) throw new Error('Token refresh failed'); uni.setStorageSync("token",data.accessToken); uni.setStorageSync("tokenTime",dayjs().unix()); return Promise.resolve(true) }) .catch(error => { kickout() + return Promise.reject(error); }) } else { kickout() + return Promise.reject(new Error('Missing RefreshToken')); } } function checkToken(){ @@ -198,4 +203,4 @@ function checkIP(header){ } }*/ } -} \ No newline at end of file +} diff --git a/common/roomCalendarUi.js b/common/roomCalendarUi.js new file mode 100644 index 0000000..1141c15 --- /dev/null +++ b/common/roomCalendarUi.js @@ -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; + }, []); +} diff --git a/components/room-calendar-grid/room-calendar-grid.vue b/components/room-calendar-grid/room-calendar-grid.vue new file mode 100644 index 0000000..1c1bd02 --- /dev/null +++ b/components/room-calendar-grid/room-calendar-grid.vue @@ -0,0 +1,389 @@ + + + + + diff --git a/locale/zh-Hant.json b/locale/zh-Hant.json index 14551b8..e1491ef 100644 --- a/locale/zh-Hant.json +++ b/locale/zh-Hant.json @@ -82,6 +82,10 @@ "calendar.fri": "五", "calendar.sat": "六", "calendar.sun": "日", + "roomCalendar.title": "日曆", + "roomCalendar.playerTitle": "玩家日曆", + "roomCalendar.updated": "日曆更新", + "roomCalendar.maxSelectedRoomTypes": "最多可選{count}個房型", "findPlayer.title": "查找客人", "findPlayer.alert": "請輸入客人ID", "findPlayer.noPermission": "您沒有權限查看此客人.", @@ -242,6 +246,7 @@ "todo.etaRequired": "ETA Time不能為空", "todo.lookupPlayer":"您沒有權限查看該玩家", "hotel.title":"酒店", + "approval.title": "審批", "approval.approve": "批准", "hotel.roombooking":"酒店預定", "approval.pendingApproval":"等待審批", @@ -255,5 +260,63 @@ "notification.pendingApproval":"審批列表", "notification.pendingApprovalRequest": "待批准請求", "playerDetail.confirm":"確認", - "newBooking.validEta":"ETA 時間格式有誤,請重新輸入 eg: HH:mm" -} \ No newline at end of file + "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": "客人聯絡人" +} diff --git a/pages.json b/pages.json index 91a828c..afcc563 100644 --- a/pages.json +++ b/pages.json @@ -22,6 +22,16 @@ "disableSwipeBack": true } }, + { + "path": "pages/roomCalendar/roomCalendar", + "style": { + "navigationBarTitleText": "", + "enablePullDownRefresh": false, + "backgroundColor": "#ffffff", + "navigationStyle": "custom", + "disableSwipeBack": true + } + }, { "path": "pages/dashboard/dashboard", "style": { @@ -69,6 +79,16 @@ "disableSwipeBack": true } }, + { + "path": "booking/playerRoomCalendar/playerRoomCalendar", + "style": { + "navigationBarTitleText": "", + "enablePullDownRefresh": false, + "backgroundColor": "#ffffff", + "navigationStyle": "custom", + "disableSwipeBack": true + } + }, { "path" : "booking/newBooking/newBooking", "style" : @@ -279,6 +299,13 @@ "text": "%hotel.title%", "visible":false }, + { + "pagePath": "pages/roomCalendar/roomCalendar", + "iconPath": "static/images/calendar-line.png", + "selectedIconPath": "static/images/calendar-fill.png", + "text": "%roomCalendar.title%", + "visible": false + }, // approvalRouting // { // "pagePath": "pages/approval/approval", @@ -297,4 +324,4 @@ ] }, "uniIdRouter": {} -} \ No newline at end of file +} diff --git a/pages/dashboard/dashboard.vue b/pages/dashboard/dashboard.vue index 998a6da..e7a2b98 100644 --- a/pages/dashboard/dashboard.vue +++ b/pages/dashboard/dashboard.vue @@ -7,79 +7,84 @@ style="margin-top:36rpx;margin-right:36rpx;"> {{$t('dashboard.dataAsOf')}} {{dataAsOf}} - - {{$t('dashboard.achievement')}} - + + + {{$t('dashboard.achievement')}} + + + + + + + {{record.itemName}} + + + {{getTargetValue(record)}} + LV{{record.level}} + + + + + + + {{getCurrentValue(record)}} + + + + + + {{formatNumber(MFValue)}} + MF Turnover + + + {{formatNumber(IMValue)}} + IM Turnover + + + + + {{formatNumber(NFROValue)}} + NFRO Turnover + + + {{formatNumber(MRIValue)}} + MRI Turnover + + + + - - - - - {{record.itemName}} - - - {{getTargetValue(record)}} - LV{{record.level}} - - - - - - - {{getCurrentValue(record)}} - + + + {{$t('dashboard.kpi')}} - - - - {{formatNumber(MFValue)}} - MF Turnover + + + + + {{record.itemName}} + + + {{getTargetValue(record)}} + - - {{formatNumber(IMValue)}} - IM Turnover + + {{percentFormat(record.currentValue)}} - - - {{formatNumber(NFROValue)}} - NFRO Turnover - - - {{formatNumber(MRIValue)}} - MRI Turnover - - - - - - {{$t('dashboard.kpi')}} + - - - - - {{record.itemName}} - - - {{getTargetValue(record)}} - - - - {{percentFormat(record.currentValue)}} - - - + @@ -127,6 +132,8 @@ NFROValue: '0', IMValue: '0', MRIValue: '0', + showAchievement: false, + showKPI: false, showDashboardView: false, userSegment: null } @@ -149,6 +156,19 @@ }, 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) { return num?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") || "0"; }, @@ -161,39 +181,84 @@ let params = {}; requestBody = JSON.stringify(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(Array.isArray(res.data.records) && res.data.records.length > 0){ - this.dashboardData = res.data.records; + const records = Array.isArray(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.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++) { - if (this.dashboardData[i].itemName == "MF Turnover") this.MFValue = this.dashboardData[i] - .currentValue; - if (this.dashboardData[i].itemName == "IM Turnover") this.IMValue = this.dashboardData[i] - .currentValue; - if (this.dashboardData[i].itemName == "NFRO Turnover") this.NFROValue = this.dashboardData[i] - .currentValue; - if (this.dashboardData[i].itemName == "MRI Turnover") this.MRIValue = this.dashboardData[i] - .currentValue; - + if (this.dashboardData[i].itemName == "MF Turnover") { + this.MFValue = this.dashboardData[i].currentValue; + hasIMValues = true; + } + if (this.dashboardData[i].itemName == "IM Turnover") { + this.IMValue = this.dashboardData[i].currentValue; + hasIMValues = true; + } + 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++) { if (this.dashboardData[i].type == "DataAsOf"){ 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 { this.showDashboardView = false; + this.showAchievement = false; + this.showKPI = false; if(res.errorCode == 'NO_PERMISSION'){ this.dataNull = this.$t("todo.noPermission"); }else if(res.errorCode == 'INVOKE_TODODASHBOARD_API_ERROR'){ this.dataNull = this.$t("todo.invokeError"); }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"); }else{ this.dataNull = this.$t("todo.returnError"); @@ -201,7 +266,9 @@ } } } catch (e) { - + this.showDashboardView = false; + this.showAchievement = false; + this.showKPI = false; } finally { uni.hideLoading(); } @@ -308,4 +375,4 @@ .im-text-gold { color: #cfa34f; } - \ No newline at end of file + diff --git a/pages/login/login.vue b/pages/login/login.vue index 20d83ff..116f103 100644 --- a/pages/login/login.vue +++ b/pages/login/login.vue @@ -44,13 +44,13 @@ - -

{{commitTitle}}

- + +

{{commitTitle}}

+ {{commitmentText1}} {{commitmentText2}} - + {{confirmText}} @@ -391,6 +391,45 @@ diff --git a/pages/setting/setting.vue b/pages/setting/setting.vue index 525d8d4..9d67798 100644 --- a/pages/setting/setting.vue +++ b/pages/setting/setting.vue @@ -368,11 +368,12 @@ this.$nextTick(() => { try { // 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: 1, text: this.$t("todo.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) { // ignore } diff --git a/salessupport/todoList/todoList.vue b/salessupport/todoList/todoList.vue index 4ef42e3..14a6ba2 100644 --- a/salessupport/todoList/todoList.vue +++ b/salessupport/todoList/todoList.vue @@ -98,8 +98,15 @@ {{getItemDescription(itemRecord)}} + class="hz-date-des todo-summary-description" + style="display:block;"> + @@ -107,11 +114,11 @@ - - - (In Queue) - (Room Ready) + + (In Queue) + (Room Ready) @@ -134,7 +141,7 @@ Trip TPD: {{formatNumber(rowRecord.detailData.tripTPD)}} Trip Win: {{formatNumber(rowRecord.detailData.tripWin)}} - Trip RI%: {{getPercentFormat(rowRecord.detailData.tripRIPercentage)}} + Trip RI: {{getPercentFormat(rowRecord.detailData.tripRIPercentage)}} Trip Theo : {{formatNumber(rowRecord.detailData.tripTheo)}} @@ -182,6 +189,7 @@ Today Win : {{formatNumber(rowRecord.detailData.todayWin)}} + Yesterday T/O: {{formatNumber(rowRecord.detailData.yesterdayTO)}} @@ -198,6 +206,8 @@ Prg Avg PlayHour : {{formarHour(rowRecord.detailData.averagePlayHour)}} + IRC Prg Bal : {{formatNumber(rowRecord.detailData.iRCProgramFundBalance)}} + Exp comm : {{formatNumber(rowRecord.detailData.expComm)}} Unsettled Expenses : {{formatNumber(rowRecord.detailData.unsettledExpenses)}} @@ -206,6 +216,8 @@ Front Money: {{formatNumber(rowRecord.detailData.frontMoney)}} TTO: {{formatNumber(rowRecord.detailData.tto)}} + 0.1Comp Balance: {{formatNumber(rowRecord.detailData.comBalance)}} Net Credit outstanding: {{formatNumber(rowRecord.detailData.netCreditOutstanding)}} @@ -218,7 +230,7 @@ R3M TPD: {{formatNumber(rowRecord.detailData.r3MTPD)}} R3M Win: {{formatNumber(rowRecord.detailData.r3MWin)}} - R3M RI%: + R3M RI: {{getPercentFormat(rowRecord.detailData.r3MRIPercentage)}} R3M Theo : {{formatNumber(rowRecord.detailData.r3MTheo)}} @@ -226,7 +238,7 @@ R6M TPD: {{formatNumber(rowRecord.detailData.r6MTPD)}} R6M Win: {{formatNumber(rowRecord.detailData.r6MWin)}} - R6M RI%: + R6M RI: {{getPercentFormat(rowRecord.detailData.r6MRIPercentage)}} R6M Theo : {{formatNumber(rowRecord.detailData.r6MTheo)}} @@ -250,7 +262,7 @@ Available & Unredeem Offer {{getOfferDescription(record)}} + :key="'offer-'+index" class="hz-font-12px todo-description-text">{{getOfferDescription(record)}} @@ -263,11 +275,11 @@ class="icon-img-flight" style="margin-left:0px;margin-right:4px;"> - + - {{getFlightDescription(record)}} + {{getFlightDescription(record)}} @@ -277,11 +289,11 @@ - + - {{getLimoDescription(record)}} + {{getLimoDescription(record)}} @@ -291,21 +303,26 @@ - + - {{getRoomDescription(record)}} - - - - + :key="'room'+index" class="hz-flex-row todo-description-row" + style="align-items: center;"> + + +
+ + [Room Ready] +