// ==UserScript== // @name 别样红辅助(大道店) // @namespace https://bbs.tampermonkey.net.cn/ // @version 0.5.8 // @description 别样红辅助 // @author EmpyrealTear // @icon https://c1.beyondh.com/maxpms/5.2.1.2/favicon.ico // @match .*://mn.beyondh.com:8101/* // @require https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js // @require https://scriptcat.org/lib/513/2.0.0/ElementGetter.js // @grant unsafeWindow // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_setClipboard // ==/UserScript== (function () { 'use strict' const options = { menus: { isShowCleaner: { toStr: (x) => '显示保洁人员:' + (x ? '✓' : '✕') }, isShowCleanerCount: { toStr: (x) => '显示保洁统计:' + (x ? '✓' : '✕') }, isShowDetailContractName: { toStr: (x) => '显示中介明细:' + (x ? '✓' : '✕') }, isHighlightBalancenePrice: { toStr: (x) => '高亮余额≠房价:' + (x ? '✓' : '✕') }, isShowGuestRecords: { toStr: (x) => '显示客历入住:' + (x ? '✓' : '✕') }, isAllowUploadFaceImg: { toStr: (x) => '显示上传照片:' + (x ? '✓' : '✕') }, isShowFaceImg: { toStr: (x) => '显示住客照片:' + (x ? '✓' : '✕') }, isShowDetail: { toStr: (x) => '显示住客信息:' + (x ? '✓' : '✕') }, isFilterEx: { toStr: (x) => '细分其他过滤:' + (x ? '✓' : '✕') }, isShowUnwashedAC: { toStr: (x) => '显示待洗空调:' + (x ? '✓' : '✕') }, isDisableCreditType: { toStr: (x) => '仅可用AR和快钱:' + (x ? '✓' : '✕') }, }, loads: function () { Object.keys(this.menus).forEach(v => { let val = GM_getValue(v) this.menus[v]['_menu'] = GM_registerMenuCommand(this.menus[v].toStr(val), () => { GM_setValue(v, !val) Object.keys(this.menus).forEach(v => GM_unregisterMenuCommand(this.menus[v]['_menu'])) this.loads() }) }) } } options.loads() const getReactStateNode = (dom) => { let key = Object.keys(dom).find(key => key.startsWith("__reactInternalInstance$")) let internalInstance = dom[key] if (internalInstance == null) return null return internalInstance._debugOwner ?? internalInstance.return ?? internalInstance._currentElement._owner } const sum = (arr) => { if (arr == undefined) return undefined let res = 0 for (let i of arr) res += i return res } const getCleaner = (id) => { if ((id >= 301 && id <= 311) || (id >= 601 && id <= 611)) return '谢' else if ((id >= 401 && id <= 411) || id == 706 || (id >= 708 && id <= 711) || (id >= 808 && id <= 812)) return '钟' // return '易' else if ((id >= 401 && id <= 411)) return '段' // else if (id == 706 || (id >= 708 && id <= 711) || (id >= 807 && id <= 812)) // return '谢' // else if (id >= 807 && id <= 812) // return '谢' // else if (id >= 701 && id <= 711) // return '钟' else return '段' } const getRoomPriceAndBalance = (arr) => { let price = 0, balance = 0, groupNumber = -1, rooms = [] if (Object.prototype.toString.call(arr) == '[object Array]') { for (let i = 0; i < arr.length; i++) { let res = getRoomPriceAndBalance(arr[i]) price += res.price balance += res.balance groupNumber = res.groupNumber if (res.rooms.length > 0) rooms = [...rooms, ...res.rooms] } } else { // 每日5:49后完成夜审 let prices = arr.DailyPrices?.filter(v => new Date(v.Date).AddDays(1).AddMinutes(60 * 6 - 11) > Date.now()) if (prices != undefined) price += sum(prices.map(v => v.ActualPrice ?? 0)) balance += arr.Balance rooms.push(arr) groupNumber = arr.GroupOrRelationNumber } return { price: Math.round(price * 100) / 100, balance: Math.round(balance * 100) / 100, rooms: rooms, groupNumber: groupNumber } } const asyncPool = async (arr, process = (item, arr) => item, begin = (v) => v, end = (v) => v, poolLimit = 10, percentComplete = (pct) => pct) => { let ret = [] let executing = new Set() let arr_res = new Array(arr.length) let completeCount = 0 // begin percentComplete(completeCount / arr.length * 100) arr = begin(arr) // process for (let [index, item] of arr.entries()) { var p = Promise.resolve().then(async () => { try { let res = await process(item, arr) arr_res[index] = res } catch (err) { console.warn(err) arr_res[index] = err } return }).finally(() => { percentComplete((++completeCount) / arr.length * 100) }) ret.push(p) executing.add(p) let clean = () => executing.delete(p) p.then(clean).catch(clean) if (executing.size >= poolLimit) { await Promise.race(executing) } } // end return Promise.all(ret).then(() => { percentComplete(completeCount / arr.length * 100) return end(arr_res) }) } const ajax = { GET: (url, isAsync = false) => { let res = null $.ajax({ url: url, type: 'GET', xhrFields: { withCredentials: true }, dataType: 'json', async: isAsync, success: (e) => { res = e } }) return res }, POST: (url, data, isAsync = false) => { let res = null $.ajax({ url: url, type: 'POST', xhrFields: { withCredentials: true }, data: JSON.stringify(data), contentType: 'application/json', dataType: 'json', async: isAsync, success: (e) => { res = e } }) return res } } const apis = { GetMixAllRoomDetails: () => { // 获取房态信息 let res = ajax.GET('https://mn.beyondh.com:8111/api/Room/GetMixAllRoomDetails') if (res == null) return res for (let i = 0; i < res.Content.length; i++) { let id = parseInt(res.Content[i].RoomNumber) res.Content[i].Cleaner = getCleaner(id) } return res }, GetMixSingleRoomOccupationInfo: (id) => { // 获取房间摘要信息 return ajax.GET(`https://mn.beyondh.com:8111/api/Room/GetMixSingleRoomOccupationInfo?roomNumber=${id}`) }, SearchGuestFolioCheckIn: () => { // 获取住客在住单 return ajax.GET(`https://mn.beyondh.com:8111/API/GuestFolio/SearchGuestFolio?PageSize=100&PageIndex=1&IsDesc=false&Status%5B%5D=I&IsMeiTuanOrder=false&DateRangeType=RecentWithOneYear`) }, SearchGuestFolioCheckOut: (keywords, pageIndex = 1, pageSize = 10) => { // 获取住客退房单(不含挂账单) return ajax.GET(`https://mn.beyondh.com:8111/API/GuestFolio/SearchGuestFolio?PageSize=${pageSize}&PageIndex=${pageIndex}&IsDesc=false&Status%5B%5D=O&Keywords=${keywords}&IsMeiTuanOrder=false&DateRangeType=RecentWithOneYear`) }, GetCustomerForEdit: (id) => { // 获取住客信息 return ajax.GET(`https://mn.beyondh.com:8111/API/Customer/GetCustomerForEdit?CustomerId=${id}`) }, GetBillStatistics: (billIds) => { // 获取订单收付总额 return ajax.POST( `https://mn.beyondh.com:8111/API/Bill/GetBillStatistics`, Object.prototype.toString.call(billIds) === '[object Array]' ? billIds : [billIds] ) }, GetCustomerPhoto: (id) => { // 获取住客照片 return ajax.GET(`https://mn.beyondh.com:8111/API/Customer/GetCustomerPhoto?customerId=${id}`) }, UploadCustomerPhoto: (id, photoBase64) => { // 上传住客照片 return ajax.POST( `https://mn.beyondh.com:8111/API/Customer/UploadCustomerPhoto`, { CustomerId: id, CustomerPhoto: photoBase64 }) }, GetCheckinMenuModelMix: (checkinId) => { return ajax.GET(`https://mn.beyondh.com:8111/API/Checkin/GetCheckinMenuModelMix?checkinId=${checkinId}`) }, GetCheckInList: () => { let date = new Date() let checkDate = new Date(new Date().Format('yyyy-MM-dd') + ' 05:49') let cur = (date < checkDate ? date.AddDays(-1) : date).Format('yyyy-MM-dd') let params = { 'SearchType[]': 'Checkin', 'PageIndex': 1, 'PageSize': 500, 'IsDesc': true, 'BeginCheckinTime': `${cur} 08:00`, 'EndCheckinTime': `${cur} 23:59` } let res = [] let data do { data = ajax.GET('https://mn.beyondh.com:8111/API/GuestFolio/AdvanceSearchGuestFolio?' + Object.entries(params).map(v => `${encodeURIComponent(v[0])}=${encodeURIComponent(v[1])}`).join('&')) res.push(...data.Content.Content) } while (res.length < data.Content.RecordCount) return res } } unsafeWindow.jQuery = $ unsafeWindow.getReactStateNode = getReactStateNode unsafeWindow.ajax = ajax unsafeWindow.apis = apis elmGetter.selector('jquery') const xhrSend = XMLHttpRequest.prototype.send; const CheckinType = { Normal: '正常', Hour2: '2H', Hour4: '4H', Hour8: '8H', Internal: '自用', LongTerm: '长包', Free: '免费' } XMLHttpRequest.prototype.send = function (data) { const xhr = this if (GM_getValue('isShowGuestRecords') && /API\/Customer\/SearchCustomerCheckinsByPost/.test(xhr.url)) { // 修复客历无法显示历史入住记录 let xhrSendBody = JSON.parse(data) // let idCardNumber = apis.GetCustomerForEdit(xhrSendBody.CustomerId).Content.IdCardNumber let props = getReactStateNode(jQuery('div.ant-col-11 > div:nth-child(2)')[0]).return.stateNode.props let idCardNumber = props.customerDetail.IdCardNumber let checkouts = apis.SearchGuestFolioCheckOut(idCardNumber, xhrSendBody.PageIndex, xhrSendBody.PageSize) const getter = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, "responseText").get Object.defineProperty(xhr, "responseText", { get: () => { let result = JSON.parse(getter.call(xhr)) let rows = [] for (let i of checkouts.Content.Content) { let detail = i.GuestFolioDetailModels[0] let bill = undefined // apis.GetBillStatistics([i.BillId]) rows.push({ RoomNumber: detail.RoomNumber[0], // 房号 ArriveTime: i.ArriveTime, // 抵店时间 DepatureTime: i.DepatureTime, // 离店时间 Days: i.CheckinTypeId.startsWith('Hour') ? CheckinType[i.CheckinTypeId] : detail.DailyPrices.length, // 天数 PriceRoomTypeName: `${detail.ControlRoomTypeName}`, // 房型 FirstDayPrice: detail.FirstDayPrice, // 房价 DailyPrices: detail.DailyPrices, // 房价 RoomChargeAmount: bill == undefined ? (i.Contract.Name ?? '散客') : bill.Content?.CreditAmount, // 总房费 OtherChargeAmount: i.CheckinMemo, // 其他消费 }) } result.Content = { ...checkouts.Content, Content: rows } return JSON.stringify(result) }, }) } else if (GM_getValue('isAllowUploadFaceImg') && /API\/Configure\/UploadFaceImage/.test(xhr.url)) { // 上传住客图片 let props = getReactStateNode(jQuery('div.ant-col-11 > div:nth-child(2)')[0]).return.stateNode.props let reader = new FileReader() reader.readAsDataURL(data) reader.onload = (e) => apis.UploadCustomerPhoto(props.customerDetail.CustomerId, e.target.result.replace('data:image/jpeg;base64,', '')) const getter = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, "response").get Object.defineProperty(xhr, "response", { get: () => { let result = JSON.parse(getter.call(xhr)) if (result.Content == false) result = { Code: 0, Message: "上传完毕", Content: true } return JSON.stringify(result) } }) } if (/AddBillItem/.test(xhr.url) && $('.ant-modal-title').text() == '结账退房') { let xhrSendBody = JSON.parse(data) if (parseFloat(`${xhrSendBody.Amount}`) != 0) { // 防止未清账误点结账 return null } } return xhrSend.apply(xhr, arguments) } const xhrOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function () { const xhr = this if (/api\/Room\/GetMixAllRoomDetails/.test(arguments[1])) { const getter = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, "responseText").get Object.defineProperty(xhr, "responseText", { get: () => { let result = getter.call(xhr) let roomInfo = JSON.parse(result) roomInfo.Content = roomInfo.Content.map(v => ({ ...v, ApplyRoundsRoom: false })) let roomSet = {} let groupSet = {} let roomDetails = apis.SearchGuestFolioCheckIn().Content.Content for (let i = 0; i < roomInfo.Content.length; i++) { let id = parseInt(roomInfo.Content[i].RoomNumber) roomInfo.Content[i].Cleaner = getCleaner(id) let guests = roomDetails .filter(v => v.GuestFolioDetailModels[0].RoomNumber.some(num => num == roomInfo.Content[i].RoomNumber)) roomInfo.Content[i].Remark = guests.map(v => v.CheckinMemo).join(';') if (guests.length > 0) { roomInfo.Content[i].Balance = sum(guests.map(v => v.Balance)) roomInfo.Content[i].TodayPrice = guests[0].GuestFolioDetailModels[0].FirstDayPrice.ActualPrice roomInfo.Content[i].DailyPrices = guests[0].GuestFolioDetailModels[0].DailyPrices } roomSet[roomInfo.Content[i].RoomNumber] = roomInfo.Content[i] if (roomInfo.Content[i].GroupOrRelationNumber != -1) { let groupNumber = roomInfo.Content[i].GroupOrRelationNumber if (groupNumber in groupSet) { groupSet[groupNumber].push(roomInfo.Content[i]) } else { groupSet[groupNumber] = [roomInfo.Content[i]] } } } for (let k of Object.keys(groupSet)) { if (groupSet[k].length == 1) { let checkinId = groupSet[k][0].CheckinId let menulist = apis.GetCheckinMenuModelMix(checkinId) if (menulist?.Content?.MenuItemList?.length > 1) { // console.log(menulist?.Content?.MenuItemList.filter(v => v.CheckinId != checkinId)) groupSet[k].push(...(menulist?.Content?.MenuItemList.filter(v => v.CheckinId != checkinId).map(v => ({ ...v, GroupOrRelationNumber: parseInt(k) })) ?? [])) } } } // console.log(roomSet) console.log(groupSet) let cleans = {} let rooms = roomInfo.Content for (let i = 0; i < rooms.length; i++) { let k = rooms[i].Cleaner if (!(k in cleans)) cleans[k] = 0 // VD=空脏; VC=空净; V_C=空夜; OOO=维修; OK=已检; OD=住脏; OC=住净; ED=预退; if (rooms[i].CustomerStatus == 'IsEstArrival' || /VD|OD|OC/.test(rooms[i].RoomStatus)) // if (rooms[i].CustomerStatus == 'IsEstArrival' || /OD|OC/.test(rooms[i].RoomStatus)) cleans[k] += 1 } // 统计需保洁数量 if (GM_getValue('isShowCleanerCount')) { jQuery('#cleaner').remove() jQuery('div>div.small:contains("出租率")').parent().prepend(`