// ==UserScript==
// @name NEMT for IOA
// @namespace http://conanluo.com/
// @version v.1.3.4
// @description Fixed New System
// @author Conan
// @match https://provider.nemtplatform.com/*
// @require https://code.jquery.com/jquery-2.2.4.min.js
// @grant GM_xmlhttpRequest
// @icon https://www.google.com/s2/favicons?sz=64&domain=nemtplatform.com
// ==/UserScript==
(function($){
//const _url='http://127.0.0.1:5500' //测试服
const _url='https://work.conanluo.com' //正式服
const popUrl = _url+"/popRoute.html?arr=";
const routeColors=["000000","b51548","188225","f3f600","AAAA00","443ea1","b1f9d1","aa77aa","01b4fa","f685e6","e08536","dddddd"];
const wander = [
"Auyeung, Shum K",
"Chan, Suk Ching",
"Chen, Buoy Lan",
"Chiang, Kuo Hsiun",
"Chin, Ang Lin",
"Chu, Pak",
"Chung, Isaac I",
"Hui, Wai Chun Ngai",
"Hwang, Soon",
"Chow, James",
"Lee, Lap Chow",
"Lee, Ngan Chu",
"Lee, Sooja",
"Mosier, Jessica",
"Penry, Paul E",
"Wong, Gam Fee",
"Yuan, Fu Mei"
];
const pmSpecialTime = [1400, 1430, 1500];
const driverInfo = {
"Han Yang Zhou": 0,
"Jie Qian": 11,
"Rong Tang":1,
"Mauricio Reina": 2,
"Jerry Higgins": 3,
"Mingzhan Li": 4,
"Fook Fung": 5,
"Walter Mejia": 6,
"Zhao Zhong Zheng": 7,
"Jabari Tyler": 8,
"Yingyang Chen": 9,
"Bert Reid": 10,
"Jerald Alejandro": 5,
"Wilson Ochoa": 12
}
let addrs;
let driversTrip=[
{id: "Han Yang Zhou",passengers: []},
{id: "Rong Tang",passengers: []},
{id: "Mauricio Reina",passengers: []},
{id: "Jerry Higgins",passengers: []},
{id: "Mingzhan Li",passengers: []},
{id: "Fook Fung",passengers: []},
{id: "Walter Mejia",passengers: []},
{id: "Zhao Zhong Zheng",passengers: []},
{id: "Jabari Tyler",passengers: []},
{id: "Yingyang Chen",passengers: []},
{id: "Bert Reid",passengers: []},
{id: "Jie Qian",passengers: []},
{id: "Jerald Alejandro",passengers: []},
{id: "Wilson Ochoa",passengers: []},
{id: "unknow Driver",passengers: []}
]
function delay(time){
return new Promise((res,rej)=>{
setTimeout(_=>res(),time)
})
}
//**********隐藏alert 提示*******开始*******
let toastLeftBottom=setInterval(function(){
if($(".toast-top-right").length!=0){
$(".toast-top-right").removeClass("toast-top-right")//.addClass("toast-bottom-left")
clearInterval(toastLeftBottom)
}
},1000)
//**********隐藏alert 提示******结束********
//**********添加nav导航按钮******开始********
let topNav=`
<nav id='topNav' class="conan-added-on form-inline row" style="display: none;">
<button id='routePrt' class='btn btn-primary'>routePrt</button>
<button id='popAmRoute' class='btn btn-danger'>popAmRoute</button>
<button id='popPmRoute' class='btn btn-danger'>popPmRoute</button>
<button id='popTripSheet' class='btn btn-danger'>popTripSheet</button>
</nav>
`;
let addNavListener=setInterval(function(){
if($("app-side-nav").length!=0){
$("body").prepend(topNav);
//添加监听按钮,显示与隐藏nav,默认隐藏
$("app-side-nav div div div").eq(0).prepend($("app-side-nav div div div").eq(0).html()).find("a").eq(1).hide();//模仿side nav的头的标签建一个隐藏显示conan-added-on的功能
$(".space-y-1").append(`<textarea id="conan-added-on-info" class="ng-pristine ng-valid ng-touched conan-added-on" style="display:none" rows="10"></textarea>`)//添加一个textarea 框负责输入一些资料(例如地址对象,等)
$("app-side-nav a").eq(0).attr("href","javascript:").off("click").click(function(){
$(".conan-added-on").toggle("fast")
return false;
})
//添加am,pm 2个监听
$("#popAmRoute").click(function(){
const $table = $('table');
const tableData = parseTableToData($table);
const result = groupPatientsByDriver(tableData, driverInfo);
let url=popUrl+JSON.stringify(result)
window.open(url+`&rt= AM Route Sheet`)
console.log(JSON.stringify(result));
})
$("#popPmRoute").click(function(){
const $table = $('table');
const tableData = parseTableToData($table,"pm");
const result = groupPatientsByDriver(tableData, driverInfo);
let url=popUrl+JSON.stringify(result)
window.open(url+`&rt= PM Route Sheet`)
console.log(JSON.stringify(result));
})
//添加routePrt 监听器
$("#routePrt").click(function(){
let str=$("#conan-added-on-info").val();
if(str=="") return;
eval(addressFormat(str))
if($("#show_route_color").length==0){
let spanColor=""
for(let i=0;i<routeColors.length;i++){
let x=i;
if(x==11 || x==0) x="Check Which Route"
else x="Route "+x
spanColor+=`<span style="padding:5px 30px; background-color:#${routeColors[i]};${i==5||i==0?"color:white;":""}">${x}</span> `
}
$("main").prepend(`<div id="show_route_color"><div class='panel-primary'><h3 class='panel-heading' style="background:#0d6efd;color:white;margin-bottom:10px">The Color For The Route</h3>${spanColor}</div><br><hr><br></div></div>`)
}
//把route上色
$(".relative.h-full.ng-star-inserted").each(function(){
let $this=$(this);
let add1=addressFormat($this.find(".text-main-secondary").eq(1).text().split(" San Francisco")[0].split(" #")[0].trim())
let add2=addressFormat($this.find(".text-main-secondary").eq(2).text().split(" San Francisco")[0].split(" #")[0].trim())
//console.log($this.find(".text-main-secondary").length)
let tempColor=addrs[add1]?addrs[add1]:(addrs[add2]?addrs[add2]:0)
//console.log(tempColor)
$this.css("background","#"+routeColors[tempColor])
})
function addressFormat(addr){
return addr.replaceAll(" Street"," St").replaceAll(" Avenue"," Ave").replaceAll(" Boulevard"," Blvd")
}
})
//添加popTripSheet 监听器
$("#popTripSheet").click(async function(){
if($("tbody tr").length>0){
let tripStr=``
let drivers=JSON.parse(JSON.stringify(driversTrip));
$("tbody tr").each(function(){
let driver= $(this).find("td").eq(3).text().replace("Reassign","").trim()
let prt= $(this).find("td").eq(11).text().trim()
let pickup=$(this).find("td").eq(13).text().trim().slice(-4)
let dropoff=$(this).find("td").eq(14).text().trim().slice(-4)
let status=$(this).find("td").eq(2).text().trim()
let puaddress=$(this).find("td").eq(13).text().split("San Francisco")[0].trim()
let doaddress=$(this).find("td").eq(14).text().split("San Francisco")[0].trim()
prt=(status.includes("VIP")?"🔥":"")+prt
status=status.includes("Cancelled")?3:
status.includes("Finished")?2:
status.includes("Onboard")?1:0
let temp={id:prt,pickup,dropoff,status,puaddress,doaddress}
console.log(JSON.stringify(temp))
if(driver=="Han Yang Zhou"){
drivers[0].passengers.push(temp)
}else if(driver=="Rong Tang"){
drivers[1].passengers.push(temp)
}else if(driver=="Mauricio Reina"){
drivers[2].passengers.push(temp)
}else if(driver=="Jerry Higgins"){
drivers[3].passengers.push(temp)
}else if(driver=="Mingzhan Li"){
drivers[4].passengers.push(temp)
}else if(driver=="Fook Fung"){
drivers[5].passengers.push(temp)
}else if(driver=="Walter Mejia"){
drivers[6].passengers.push(temp)
}else if(driver=="Zhao Zhong Zheng"){
drivers[7].passengers.push(temp)
}else if(driver=="Jabari Tyler"){
drivers[8].passengers.push(temp)
}else if(driver=="Yingyang Chen"){
drivers[9].passengers.push(temp)
}else if(driver=="Bert Reid"){
drivers[10].passengers.push(temp)
}else if(driver=="Jie Qian"){
drivers[11].passengers.push(temp)
}else if(driver=="Jerald Alejandro"){
drivers[12].passengers.push(temp)
}else if(driver=="Wilson Ochoa"){
drivers[13].passengers.push(temp)
}else{
drivers[14].passengers.push(temp)
}
})
try {
console.log(111)
await fetchLunchTimes(drivers);
} catch (err) {
console.error('按钮点击失败:', err);
}
tripStr=JSON.stringify(drivers)
sendTrips(tripStr)
}
})
//清除添加监听
clearInterval(addNavListener);
}
})
//**********添加nav导航按钮*******结束*******
//*****************处理am, pm Route 表 ********开始******
/**
* 解析表格数据,根据类型(上午或下午)筛选并格式化患者信息
* @param {jQuery} $table - jQuery 选择的表格元素
* @param {string} [type="am"] - 处理类型,"am" 为上午,"pm" 为下午
* @returns {Array} - 包含司机和患者信息的对象数组
*/
function parseTableToData($table, type = "am") {
const data = [];
// 遍历表格 tbody 中的每一行
$table.find('tbody tr').each(function() {
const $cells = $(this).find('td');
// Driver 在第 4 列(索引 3),取第 3 个 div 的文本
const driver = $cells.eq(3).find('div').eq(2).text().trim();
// Patient 在第 12 列(索引 11)
const rawPatient = $cells.eq(11).text().trim();
// 格式化 Patient 名字:首字母大写,其他小写
const patient = rawPatient.toLowerCase().replace(/(^|\s)\w/g, char => char.toUpperCase());
// Pickup Address 在第 14 列(索引 13),用于下午筛选和时间提取
const pickupAddress = $cells.eq(13).text().trim();
const pickupAddressTime = $cells.eq(13).find('div').last().text().trim().slice(-4);
// Dropoff Address 在第 15 列(索引 14),用于上午筛选
const dropoffAddress = $cells.eq(14).text().trim();
// Space Type 在第 8 列(索引 7)
const spaceType = $cells.eq(7).text().trim();
// 时间处理
const timeValue = parseInt(pickupAddressTime, 10);
if (isNaN(timeValue)) return; // 无效时间,跳过
// 检查 Patient 是否在 wander 数组中(忽略大小写)
const isWander = wander.some(w => w.toLowerCase() === rawPatient.toLowerCase());
let isValid = false;
let patientWithTags = patient;
if (type === "am") {
// 上午筛选条件:时间 < 10:30,Dropoff Address 包含 "3575 Geary",Space Type 不含 "DME"
const isBefore1030 = timeValue < 1030;
const is2ndRound = timeValue < 1000;
const hasGeary = dropoffAddress.toLowerCase().includes('3575 geary');
const noDME = !spaceType.toLowerCase().includes('dme');
if (driver && patient && isBefore1030 && hasGeary && noDME) {
isValid = true;
patientWithTags = patient +
(is2ndRound ? "" : " - @@" + pickupAddressTime) +
(!isWander ? "" : "__<b>Warning</b>");
}
} else if (type === "pm") {
// 下午筛选条件:时间 >= 15:30 或在 pmSpecialTime 中,Pickup Address 包含 "3575 Geary",Space Type 不含 "DME"
const isAfter1530OrSpecial = timeValue >= 1530 || pmSpecialTime.includes(timeValue);
const hasGeary = pickupAddress.toLowerCase().includes('3575 geary');
const noDME = !spaceType.toLowerCase().includes('dme');
if (driver && patient && isAfter1530OrSpecial && hasGeary && noDME) {
isValid = true;
const isSpecialTime = pmSpecialTime.includes(timeValue);
patientWithTags = patient +
(isSpecialTime ? " - @@" + pickupAddressTime : "") +
(!isWander ? "" : "__<b>Warning</b>");
}
}
if (isValid) {
data.push({ Driver: driver, Patient: patientWithTags });
}
});
return data;
}
/**
* 按司机分组患者,并按患者名字正序排序
* @param {Array} tableData - 解析后的表格数据,包含司机和患者信息
* @param {Object} driverInfo - 司机信息对象,键为司机名字,值为二维数组索引
* @returns {Array} - 按 driverInfo 索引分配的二维数组,每个子数组包含排序后的患者名字
*/
function groupPatientsByDriver(tableData, driverInfo) {
// Step 1: 按 Driver 分组 Patient
const patientGroups = {};
tableData.forEach(row => {
const driver = row.Driver;
const patient = row.Patient;
if (!patientGroups[driver]) {
patientGroups[driver] = [];
}
patientGroups[driver].push(patient);
});
// Step 2: 对每个司机的 Patient 数组按名字正序排序(忽略大小写)
for (const driver in patientGroups) {
patientGroups[driver].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
}
// Step 3: 创建二维数组
const maxIndex = Math.max(...Object.values(driverInfo), -1) + 1;
const result = Array(maxIndex).fill().map(() => []);
for (const [driver, index] of Object.entries(driverInfo)) {
result[index] = patientGroups[driver] || [];
}
return result;
}
//*****************处理am, pm Route 表 ********结束******
//*****************处理trip sheet ********开始******
//传入trips(String 类型) 格式 `[{"id":"driverName","passengers":[{...},{...}]},....]
// passengers里面的格式{"id":"prtName","pickup":"","dropoff":"",.....}这3个是一定要有
function sendTrips(trips) {
//const tripsInput = document.getElementById('trips').value;
//const status = document.getElementById('status');
try {
const drivers = JSON.parse(trips);
// 验证数据格式
if (!Array.isArray(drivers) || !drivers.every(d => d.id && Array.isArray(d.passengers))) {
throw new Error('Invalid drivers format');
}
// 目标地址( 或本地测试)
const targetUrl = _url+'/nemt/index.html';
const targetOrigin = _url;
const targetWindow = window.open(targetUrl, '_blank');
if (!targetWindow) {
//status.textContent = '无法打开目标页面,请检查浏览器弹出窗口设置!';
console.error('Failed to open target window');
return;
}
// 重试发送消息
let attempts = 0;
const maxAttempts = 10;
const sendMessage = () => {
if (attempts >= maxAttempts) {
//status.textContent = '发送失败:目标页面未响应,请检查 index.html 是否加载';
console.error('Failed to send trips data after max attempts');
return;
}
attempts++;
try {
targetWindow.postMessage({
type: 'tripsData',
data: drivers
}, targetOrigin);
console.log(`Attempt ${attempts}: Sent trips data:`, drivers);
//status.textContent = '数据发送成功!';
} catch (e) {
console.warn(`Retry ${attempts}/${maxAttempts}:`, e);
setTimeout(sendMessage, 1000);
}
};
setTimeout(sendMessage, 2000); // 初始延迟 2 秒
} catch (error) {
//status.textContent = '无效的 JSON 格式,请检查输入!';
console.error('Error parsing trips input:', error);
}
}
//用异步包装获取司机吃饭时间-----------------------开始
const apiUrl = 'https://script.google.com/macros/s/AKfycbzBdbSd3xujI7CAwxPfD5b8KOktG6Z4VjqE_8q512q5Bc3MJjFRurs-aODOI-sIFzJR/exec';
// 包装 GM_xmlhttpRequest 为 Promise
function gmFetch(options) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
...options,
onload: (response) => {
resolve(response);
},
onerror: (err) => {
reject(err);
}
});
});
}
// 异步获取数据
async function fetchLunchTimes(drivers) {
try {
const response = await gmFetch({
method: 'GET',
url: apiUrl
});
console.log('请求完成,状态码:', response.status);
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
console.log('数据获取成功:', data);
for(let i=1;i<data.length;i++){
let temp={"id":"Lunch","pickup":data[i][1]+"","dropoff":data[i][2]+""}
let driver=data[i][0]
if(driver=="Han Yang Zhou"){
drivers[0].passengers.push(temp)
}else if(driver=="Rong Tang"){
drivers[1].passengers.push(temp)
}else if(driver=="Mauricio Reina"){
drivers[2].passengers.push(temp)
}else if(driver=="Jerry Higgins"){
drivers[3].passengers.push(temp)
}else if(driver=="Mingzhan Li"){
drivers[4].passengers.push(temp)
}else if(driver=="Fook Fung"){
drivers[5].passengers.push(temp)
}else if(driver=="Walter Mejia"){
drivers[6].passengers.push(temp)
}else if(driver=="Zhao Zhong Zheng"){
drivers[7].passengers.push(temp)
}else if(driver=="Jabari Tyler"){
drivers[8].passengers.push(temp)
}else if(driver=="Yingyang Chen"){
drivers[9].passengers.push(temp)
}else if(driver=="Bert Reid"){
drivers[10].passengers.push(temp)
}else if(driver=="Jie Qian"){
drivers[11].passengers.push(temp)
}else if(driver=="Jerald Alejandro"){
drivers[12].passengers.push(temp)
}else if(driver=="Wilson Ochoa"){
drivers[13].passengers.push(temp)
}else{
drivers[14].passengers.push(temp)
}
}
//return data;
} catch (e) {
console.error('解析 JSON 失败:', e, response.responseText);
throw new Error(`JSON 解析失败: ${e.message}`);
}
} else {
console.error('请求失败,状态码:', response.status, '响应:', response.responseText);
throw new Error(`请求失败,状态码: ${response.status}`);
}
} catch (err) {
console.error('请求错误:', err);
throw err;
}
}
//用异步包装获取司机吃饭时间--------------------------结束
//*****************处理trip sheet ********结束******
//
//*****************添加数据表功能,排序,隐藏等. ********开始******
// 使用setInterval定期检查新生成的表格
setInterval(function() {
// 只选择不含sortable类且不含owl-dt-calendar-table类的table
$("table").not(".sortable, .owl-dt-calendar-table").each(function() {
let $table = $(this);
// 为当前表格添加sortable类
$table.addClass("sortable");
// 在每个有内容的表头前添加箭头,后添加交叉(排除第一列)
$table.find("thead th").filter(function(index) {
return $(this).text().trim().length > 0 && index !== 0; // 排除第一列
}).each(function(index) {
let originalText = $(this).text().trim();
$(this).attr("style", "cursor: pointer;"); // 添加鼠标pointer样式
$(this).data("original-text", originalText); // 存储原始文本
$(this).data("col-index", index); // 存储原始列索引
$(this).html('<span></span>' + originalText + '<span style="margin-left: 5px; font-size: 12px; cursor: pointer;" class="hide-column">❌</span>');
}).click(function(e) {
// 防止点击交叉符号时触发排序
if ($(e.target).hasClass("hide-column")) {
let colIndex = $(this).data("col-index"); // 使用存储的原始列索引
let table = $(this).closest("table");
// 隐藏当前列(仅视觉隐藏,不删除)
table.find("tr").each(function() {
$(this).children().eq(colIndex+1).hide();
});
// 隐藏表头(仅视觉隐藏)
$(this).hide();
return; // 阻止排序
}
let table = $(this).closest("table");
let headerText = $(this).data("original-text"); // 使用存储的原始文本
let rows = table.find("tbody tr").get();
let isAsc = $(this).data("sort-dir") === "asc";
let colIndex = $(this).data("col-index"); // 使用存储的原始列索引
// 切换排序方向
$(this).data("sort-dir", isAsc ? "desc" : "asc");
// 重置所有未隐藏的表头(排除第一列和已隐藏的th)
$(this).closest("tr").find("th").filter(function(index) {
return index !== 0 && $(this).is(":visible"); // 只处理未隐藏的th
}).each(function() {
let text = $(this).data("original-text");
$(this).attr("style", "cursor: pointer;"); // 保持鼠标pointer样式
$(this).html('<span></span>' + text + '<span style="margin-left: 5px; font-size: 12px; cursor: pointer;" class="hide-column">❌</span>');
});
// 当前表头显示升序/降序箭头
$(this).html('<span>' + (isAsc ? "⬇️" : "⬆️") + '</span>' + headerText + '<span style="margin-left: 5px; font-size: 12px; cursor: pointer;" class="hide-column">❌</span>');
// 动态判断列类型
let isNumber = headerText.toLowerCase().includes("miles") || headerText.match(/\b(number|count|distance|size)\b/i);
let isDate = headerText.toLowerCase().includes("time") || headerText.toLowerCase().includes("date");
// 排序逻辑
rows.sort((b, a) => {
let valA = $(a).children("td").eq(colIndex).text().trim();
let valB = $(b).children("td").eq(colIndex).text().trim();
if (isNumber) {
valA = parseFloat(valA.replace(/[^0-9.-]+/g, "")) || 0;
valB = parseFloat(valB.replace(/[^0-9.-]+/g, "")) || 0;
} else if (isDate) {
valA = new Date(valA).getTime() || 0;
valB = new Date(valB).getTime() || 0;
} else {
valA = valA.toLowerCase();
valB = valB.toLowerCase();
}
return (valA < valB ? 1 : -1) * (isAsc ? -1 : 1);
});
// 移动行
let tbody = table.find("tbody");
rows.forEach(row => tbody.append(row));
});
// 添加恢复隐藏列的功能(在表格后添加一个恢复按钮)
if (!$table.next().hasClass("restore-columns")) {
$table.after('<button class="restore-columns" style="margin: 10px; padding: 5px 10px; cursor: pointer;">恢复隐藏列</button>');
$table.next(".restore-columns").click(function() {
let table = $(this).prev("table");
table.find("tr").each(function() {
$(this).children().show(); // 显示所有单元格
});
table.find("thead th").each(function(index) {
if (index === 0) return; // 跳过第一列
let text = $(this).data("original-text");
$(this).attr("style", "cursor: pointer;");
$(this).html('<span></span>' + text + '<span style="margin-left: 5px; font-size: 12px; cursor: pointer;" class="hide-column">❌</span>');
$(this).show(); // 显示所有表头(除第一列外)
});
table.find("th").data("sort-dir", null); // 重置排序状态
});
}
});
}, 1000); // 每1秒检查一次
//*****************添加数据表功能,排序,隐藏等. ********结束******
/* 修改药用的....
function updateMed(){
(function() {
'use strict';
const waitForElement = (selector, callback, maxAttempts = 20, interval = 500) => {
let attempts = 0;
const tryFind = () => {
const element = document.querySelector(selector);
if (element) {
callback(element);
} else if (attempts < maxAttempts) {
attempts++;
setTimeout(tryFind, interval);
}
};
tryFind();
};
waitForElement('#preScheduleTime', (input) => {
input.value = '05/19/2025 1700';
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
});
waitForElement('#preAppointmentTime', (input) => {
input.value = '05/19/2025 1730';
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
});
waitForElement('#displayName', (input) => {
const newValue = input.value + '💊';
input.value = newValue;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
});
})();
// 获取指定 div 内的 button 并触发点击事件
$('div.border-t.border-main-base.flex.flex-wrap.justify-end.py-2.px-4.bg-container-base.gap-2 button').click();
}
/////监听每1秒执行一次
updating=setInterval(function(){
if($(".flex-1.flex.flex-col.bg-main-base-100.overflow-hidden").length>0&&$(".flex-1.flex.flex-col.bg-main-base-100.overflow-hidden.edited").length==0){
$(".flex-1.flex.flex-col.bg-main-base-100.overflow-hidden").addClass("edited")
updateMed();
console.log("Update:"+$("#displayName").val())
}
},1000)
//////////清除监听
clearInterval(updating)
*/
})($);