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 @@
+
+
+
+ {{ day }}
+
+
+
+
+ {{ month.monthLabel }}
+
+
+
+
+ {{ day.dayNo }}
+
+
+
+
+
+
+ {{ getDisplayRoomName(roomType) }}
+
+ {{ getCellData(day, roomType).criteria }}
+
+
+ {{ getAvailabilityText(getCellData(day, roomType)) }}
+
+
+ {{ getCellData(day, roomType).leader }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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}}
-
+
@@ -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;">
+
+
+
+ {{part.text}}
+
@@ -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;">
+
+
+
+ {{part.text}}
+
+
+
+ [Room Ready]
+
{{record.date}}
-
-
+
+
-
- • {{getEmojiDescription(rowDescription)}}
+
+ • {{getEmojiDescription(rowDescription)}}
@@ -430,6 +447,7 @@
getSetting,
getExtendBooking
} from '../../api/configApi.js';
+ import { normalizeApiKeys } from '../../common/normalizeApiKeys.js';
export default {
components: {
mgmHeader, // 注册组件
@@ -540,7 +558,8 @@
calendarSave: [null, null],
rangeTip: this.$t("common.rangeTip"),
calendarIconColor: "#000000",
- currentTask: null
+ currentTask: null,
+ todoListRequestSeq: 0
}
},
@@ -1182,6 +1201,7 @@
}
},
async getTodoListByData() {
+ const requestSeq = ++this.todoListRequestSeq;
if (this.currentTask) {
try {
@@ -1232,11 +1252,12 @@
}
});
+ if (requestSeq !== this.todoListRequestSeq) return;
this.currentTask = null;
-
- if (Array.isArray(res.data.records) && res.data.records.length > 0) {
+ const records = res && res.data && Array.isArray(res.data.records) ? res.data.records : [];
+ if (res && res.success == true && records.length > 0) {
this.showTodoListView=true;
- this.listData = res.data.records.map(record => ({
+ this.listData = records.map(record => ({
...record,
dateRecords: record.dateRecords.map(dateRecord => ({
...dateRecord,
@@ -1251,9 +1272,10 @@
}))
}));
}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");
- }else if(res.errorCode.toUpperCase().indexOf('NO RECORD') != -1){
+ }else{
this.dataNull = this.$t("todo.returnError");
}
@@ -1261,7 +1283,8 @@
}
} 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){
this.dataNull = e.data.message;
@@ -1273,7 +1296,7 @@
console.error(e);
}
} finally {
- if(!this.currentTask){
+ if(requestSeq === this.todoListRequestSeq && !this.currentTask){
uni.hideLoading();
}
}
@@ -1306,7 +1329,7 @@
else params.Date = this.formatDate(record.actionDate);
const res = await getTodoDetail(params);
if (res != null && res.success == true) {
- record.detailData = res.data;
+ record.detailData = normalizeApiKeys(res.data);
if(record.detailData.descriptions!==null){
for(let i=0;i {
+ 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() {
return function(record) {
let result = false;
@@ -1502,7 +1559,7 @@
getItemDescription() {
return function(record) {
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 {
return record;
}
@@ -1534,11 +1591,8 @@
},
getItemShowSharerIcon() {
return function(record) {
- let sharerRecord = record.split("Sharer:");
- let hasSharer = "N";
- if (sharerRecord.length > 1) {
- hasSharer = sharerRecord[1].trim();
- }
+ let sharerRecord = record.match(/\|\s*Sharer:\s*([YN])(?:\s*\||\s*$)/);
+ let hasSharer = sharerRecord !== null ? sharerRecord[1] : "N";
return hasSharer == "N" ? false : true;
}
},
@@ -1643,4 +1697,4 @@
@import '../../static/css/todo.scss';
@import '../../static/css/custom.scss';
-
\ No newline at end of file
+
diff --git a/static/css/todo.scss b/static/css/todo.scss
index e8f786f..c1e32d9 100644
--- a/static/css/todo.scss
+++ b/static/css/todo.scss
@@ -154,6 +154,61 @@
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 {
position: absolute;
height: 100%;
diff --git a/static/images/calendar-fill.png b/static/images/calendar-fill.png
new file mode 100644
index 0000000..963090d
Binary files /dev/null and b/static/images/calendar-fill.png differ
diff --git a/static/images/calendar-line.png b/static/images/calendar-line.png
new file mode 100644
index 0000000..b439265
Binary files /dev/null and b/static/images/calendar-line.png differ
diff --git a/static/images/limo_warn.png b/static/images/limo_warn.png
new file mode 100644
index 0000000..5afbbd2
Binary files /dev/null and b/static/images/limo_warn.png differ
diff --git a/tests/bookingRecordStatus.test.mjs b/tests/bookingRecordStatus.test.mjs
new file mode 100644
index 0000000..bda81e5
--- /dev/null
+++ b/tests/bookingRecordStatus.test.mjs
@@ -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');
+});
diff --git a/tests/bookingSharerMigration.test.mjs b/tests/bookingSharerMigration.test.mjs
new file mode 100644
index 0000000..83fe50b
--- /dev/null
+++ b/tests/bookingSharerMigration.test.mjs
@@ -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(/