// ==UserScript==
// @name 雲科一鍵填寫工作日誌
// @namespace Anong0u0
// @version 2.0.8
// @description 😎輕鬆一鍵自動填寫
// @author Anong0u0
// @match *://*/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=www.yuntech.edu.tw
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_addValueChangeListener
// @grant GM_openInTab
// @grant window.focus
// @grant window.close
// @grant GM_xmlhttpRequest
// @connect webapp.yuntech.edu.tw
// @require https://cdnjs.cloudflare.com/ajax/libs/izitoast/1.4.0/js/iziToast.min.js
// @noframes
// @license Beerware
// ==/UserScript==
const Enum = (descriptions) =>
{
const result = {};
Object.keys(descriptions).forEach((description) =>{
result[description] = descriptions[description];
});
return Object.freeze(result);
}
const controlState = Enum({
self:"SCRIPT_CONTROL_STATE",
checkLogin: "checking_login",
fillWork: "fill_work",
none: null,
value: {
now: GM_getValue("SCRIPT_CONTROL_STATE", null),
is(state) {return this.now===state},
not(state) {return this.now!==state}
},
change(state) {GM_setValue(this.self, state)},
})
const workState = Enum({
waiting: "🕐待填寫",
done: "✅填寫完成",
error: "❌填寫錯誤"
})
const FILL_WORK_URL = `https://webapp.yuntech.edu.tw/workstudy/StudWorkRecord/ContractList?date=${new Date().getMonth()+1}%2F1`;
const iziToastCSS = document.createElement("link")
iziToastCSS.rel="stylesheet"
iziToastCSS.href = "https://cdnjs.cloudflare.com/ajax/libs/izitoast/1.4.0/css/iziToast.min.css"
const iziToastProxy = new Proxy(iziToast, {
get(target, propKey, receiver) {
const originalProperty = Reflect.get(target, propKey, receiver);
if (typeof originalProperty !== 'function') return originalProperty
return (...args) => {
document.head.append(iziToastCSS)
return Reflect.apply(originalProperty, target, args);
};
}
});
const GM_XHR = (method, url, data = null, headers = {}) =>
new Promise((resolve) => {
GM_xmlhttpRequest({
method: method,
url: url,
headers: headers,
data: data,
onload: resolve
});
});
const checkIsLogin = () =>
new Promise(async (r) => {
const result = await GM_XHR("HEAD", FILL_WORK_URL)
if(result.finalUrl.includes(FILL_WORK_URL)===false)
{
controlState.change(controlState.checkLogin)
GM_addValueChangeListener(controlState.self, ()=>{window.focus(); r()})
GM_openInTab(FILL_WORK_URL, false)
}
else r()
})
const getWorkContent = async (planName) =>
{
const t = GM_getValue(planName)
if(t)
{
if(!("timeout" in t) || t.timeout < Date.now()) GM_deleteValue(planName)
else return t
}
await checkIsLogin()
const workListHTML = (await GM_XHR("GET", `/workstudy/StudWorkRecord/ContractList?date=${new Date(new Date().getFullYear(), new Date().getMonth(), 1).toLocaleDateString("ja").replace(/\//g,"%2F")}`)).responseXML,
workInfoHTML = (await GM_XHR("GET", "/workstudy/Stud/ContractList")).responseXML,
workInfo = {}
for(const e of [...workInfoHTML.querySelectorAll("tbody > tr")].map((tr)=>[...tr.querySelectorAll("td")].map((e)=>e?.innerText.trim() || e.querySelector("a")?.href.match(/(?<=ApplyId=)\d+/))).reverse())
{
const text = (await GM_XHR("GET", `/workstudy/Stud/JobContractInfo?ApplyId=${e[10]}`)).responseText,
desc = String(text.match(/(?<=工作內容:\r\n).+/)).trim()
workInfo[e[2]] = desc
}
for(const tr of workListHTML.querySelectorAll("tbody > tr"))
{
const workList = [...tr.querySelectorAll("td")].map((e)=>e.innerText.trim() || e.querySelector("a").href.match(/(?<=ContractId=)\d+/)[0])
const pn = workList[2],
workID = workList[8]
GM_setValue(pn, {id:workID, desc:workInfo[pn], timeout: new Date(new Date().getFullYear(), new Date().getMonth()+1, 1).getTime()})
}
return GM_getValue(planName)
}
(() =>
{
const workQueue = GM_getValue("workQueue", []);
if(GM_getValue("workQueueTimeout") < Date.now())
{
workQueue.length = 0
GM_setValue("workQueue", [])
GM_deleteValue("workQueueTimeout")
}
(async () =>
{
if (controlState.value.not(controlState.none)) return;
let needUpdate = false;
for (const work of workQueue)
{
const {state, planName, date, start, end, hour, desc} = work
if(work.state !== workState.waiting || Date.now() < Number(new Date(date))+300000) continue;
await checkIsLogin()
const body = `DateContract=${date.replace(/\//g,"%2F")}%2C${(await getWorkContent(planName))?.id}&`+
`StartHour=${Number(start.split(":")[0])}&StartMin=${Number(start.split(":")[1])}&`+
`EndHour=${Number(end.split(":")[0])}&EndMin=${Number(end.split(":")[1])}&`+
`IsAnnualLeave=false&WorkContent=${encodeURIComponent(desc)}&Hours=${hour}`;
const result = (await GM_XHR("POST", "https://webapp.yuntech.edu.tw/workstudy/StudWorkRecord/ApplyAction", body, {"content-type": "application/x-www-form-urlencoded"})).responseText;
work.fillTime = new Date().toLocaleString("pa").replace(/-/g, "/")
const content = {
drag: false,
timeout: 30000,
}
if(result.includes("填寫完成"))
{
work.state = workState.done
content.title = work.state
content.message = `${planName} ${date} ${desc} ${start}-${end} 共${hour}小時`
iziToastProxy.success(content)
}
else
{
work.state = workState.error
work.errorMessage = String(result.match(/(?<=×<\/button>)[^<]+/)??"未知錯誤(可能已超時)").trim()
content.title = work.state
content.message = `<b>原因:${work.errorMessage}</b> ${planName} ${date} ${desc} ${start}-${end} 共${hour}小時`
iziToastProxy.error(content)
}
needUpdate = true;
}
if(needUpdate)
{
iziToastProxy.info({
title: "[雲科工作日誌]",
message: "已觸發自動填寫",
drag: false,
timeout: 45000,
buttons: [
["<button>前往查看填寫佇列</button>",
() => {
GM_openInTab(FILL_WORK_URL, false);
iziToastProxy.destroy()
}
]
]
})
GM_setValue("workQueue", workQueue)
}
})()
if (workQueue.length < 1 && controlState.value.is(controlState.none) && Date.now() > GM_getValue("dont_notify",0)) {
const warning = {
title: "[雲科工作日誌]",
message: "本月未設置自動填寫佇列,是否前往設置?",
close: false,
drag: false,
timeout: 10000,
buttons: [
[
"<button style='background-color:lightgreen'>前往設置</button>",
(instance, toast) => {
controlState.change(controlState.fillWork);
GM_openInTab(FILL_WORK_URL, false);
instance.hide({}, `#${toast.id}`)
},
true
],
[
"<button style='background-color:#BBB'>關閉</button>",
(instance, toast) => instance.hide({}, `#${toast.id}`)
],
[
"<button style='background-color:lightblue'>延後一天提醒</button>",
(instance, toast) => {
instance.hide({}, `#${toast.id}`)
GM_setValue("dont_notify", new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate()+1).getTime())
iziToastProxy.info({
title: "[雲科工作日誌]",
message: "延後至明天提醒✅",
position: "center",
drag: false,
});
}
],
[
"<button style='background-color:Salmon'>本月不再提醒</button>",
(instance, toast) => {
instance.hide({}, `#${toast.id}`)
iziToastProxy.question({
timeout: 20000,
close: false,
overlay: true,
title: '[雲科工作日誌]',
message: '是否本月之內不再提醒"工作日誌自動填寫"?',
position: 'center',
drag: false,
buttons: [
['<button>確定</button>', (instance, toast) => {
GM_setValue("dont_notify", new Date(new Date().getFullYear(), new Date().getMonth()+1, 1).getTime())
instance.hide({}, `#${toast.id}`);
iziToastProxy.info({
title: "[雲科工作日誌]",
message: "本月之內將不再提醒",
position: "center",
drag: false,
});
}],
['<button><b>取消</b></button>', (instance, toast) => {
instance.hide({}, `#${toast.id}`);
iziToastProxy.warning(warning);
}, true],
],
});
}
]
]
}
iziToastProxy.warning(warning);
}
if (location.pathname.includes("/workstudy/StudWorkRecord")===false)
{
if (controlState.value.not(controlState.none))
{
if (location.pathname.includes("/YuntechSSO/Account/Login"))
{
iziToastProxy.info({
title: "[雲科工作日誌]",
message: "檢測到單一未登入,請登入單一",
position: "topCenter",
drag: false,
timeout:10000
});
}
else location.replace(FILL_WORK_URL);
}
return;
}
controlState.change(controlState.none);
if (controlState.value.is(controlState.checkLogin)) window.close()
const btn = document.createElement("input"),
YM = new Date().toLocaleDateString("en-za").slice(0,-3),
autoFillForm = document.createElement("div")
btn.type = "button"
btn.value = `😎一鍵填寫 ${YM} 工作日誌`
btn.className = "btn btn-success"
const footer = document.querySelector(".panel-footer")
footer.append(btn)
const initAutoFillForm = () =>
{
const tableText = workQueue.map(work =>
{
const { state, planName, date, start, end, hour, desc, errorMessage, fillTime } = work;
return `
<tr>
<td>${state}${state === workState.error ? ",原因:" + errorMessage : ""}</td>
<td>${state !== workState.waiting ? fillTime : "-"}</td>
<td>${planName}</td>
<td>${date}</td>
<td>${start}-${end}</td>
<td>${hour}</td>
<td>${desc}</td>
</tr>`;
}).join("");
autoFillForm.innerHTML = `
<style>td>input {width:100% !important}</style>
<div class="dataTables_wrapper form-inline dt-bootstrap no-footer" style="padding-bottom:16px">
<h4><b>自動填寫佇列📬</b></h4>
<table class="table table-striped table-bordered table-hover dataTable no-footer dtr-inline">
<thead>
<tr>
<th>狀態</th>
<th>填寫時間</th>
<th>計畫</th>
<th>工作日期</th>
<th>工作期間</th>
<th>時數</th>
<th>工作內容</th>
</tr>
</thead>
<tbody>
${tableText}
</tbody>
</table>
</div>`
}
initAutoFillForm()
document.querySelector(":is(form[action='/workstudy/StudWorkRecord/ApplyAction'],#page-wrapper > :last-child)").insertAdjacentElement("afterend", autoFillForm)
btn.onclick = async () =>
{
btn.value = "🔄載入資料中..."
btn.disabled = true
autoFillForm.hidden = true
const monthStart = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
const monthEnd = new Date(new Date().getFullYear(), new Date().getMonth()+1, 1);
let allWork = (await fetch("/workstudy/Stud/CalendarData").then((e)=>e.json())).filter((e)=>(!e.backgroundColor || e.title.indexOf("月保:")===0) && new Date(e.start)>monthStart && new Date(e.start)<monthEnd)
if (allWork?.[0]?.title.indexOf("月保:")===0)
{
const title = allWork[0].title.replace("月保:", ""),
year = new Date().getFullYear(),
month = new Date().getMonth()
allWork = Array(new Date(year, month + 1, 0).getDate()-1)
.fill().map((_, i) => new Date(year, month, i + 2, 8))
.filter(day => day.getDay() % 6 !== 0).map((day)=>{return {title: title, start: day.toISOString(), end:new Date(year, month, day.getDate(), 17).toISOString()}});
}
let tableText = ""
for(const work of allWork)
{
const start = Number(new Date(work.start)),
end = Number(new Date(work.end)),
planName = work.title.split("by")[0].trim()
let remain = (end-start)/3600000,
offset = 0
while(remain>0)
{
const t = Math.min(remain, 4)
const s = new Date(start+offset*3600000-Math.ceil((Math.random()*5))*60000).toLocaleTimeString("sv").slice(0,-3),
e = new Date(start+(t+offset)*3600000+Math.ceil((Math.random()*5))*60000).toLocaleTimeString("sv").slice(0,-3)
remain -= t
offset += t
if(t==4)
{
offset++
remain--
}
tableText += `
<tr>
<td>${planName}</td>
<td>${new Date(start).toLocaleDateString("en-za")}</td>
<td><input class="form-control time start" value="${s}"></td>
<td><input class="form-control time end" value="${e}"></td>
<td class="hour">${t}</td>
<td><input class="form-control workDesc" value="${(await getWorkContent(planName))?.desc}"></td>
</tr>`
}
}
const form = document.createElement("div")
form.innerHTML = `
<style>td>input {width:100% !important}</style>
<div class="dataTables_wrapper form-inline dt-bootstrap no-footer" style="padding-bottom:16px">
<h4><b>${YM}</b> 預計填寫內容💌 (${new Date(Math.max(Date.now()-604800000, monthStart)).toLocaleDateString("en-za").slice(5)}-${new Date().toLocaleDateString("en-za").slice(5)}以外時間可能無法正常填寫)</h4>
<table class="table table-striped table-bordered table-hover dataTable no-footer dtr-inline">
<thead>
<tr>
<th>計畫</th>
<th>工作日期</th>
<th>開始時間</th>
<th>結束時間</th>
<th>時數</th>
<th>工作內容</th>
</tr>
</thead>
<tbody>
${tableText}
</tbody>
</table>
</div>`
form.querySelectorAll("tbody > tr").forEach((tr)=>
{
const start = tr.querySelector(".start"),
end = tr.querySelector(".end"),
hour = tr.querySelector(".hour")
start.oninput = () =>
{
hour.innerText = Math.floor((new Date(`2000/1/1 ${end.value}`)-new Date(`2000/1/1 ${start.value}`))/3600000) || "錯誤"
}
end.oninput = start.oninput
})
document.querySelector(":is(form[action='/workstudy/StudWorkRecord/ApplyAction'],#page-wrapper > :last-child").insertAdjacentElement("afterend", form)
window.scrollTo(0, document.body.scrollHeight);
btn.value = "🔽等待確認中..."
const btnArray = Array(5).fill().map(()=>document.createElement("input"))
const [cancel,enqueue,submit,change,del] = btnArray
cancel.className = "btn btn-danger"
cancel.value = "🤔取消"
enqueue.className = "btn btn-success"
enqueue.value = "📆放入自動填寫佇列"
submit.className = "btn btn-primary"
submit.value = "📝直接填寫"
change.className = "btn btn-info"
change.value = "🧐修改所有工作內容描述"
del.className = "btn btn-danger"
del.value = "😮刪除特定工作"
btnArray.forEach((e)=>
{
e.type = "button"
e.style.marginLeft = "4px"
footer.append(e)
})
del.onclick = () =>
{
const desc = prompt(`輸入需刪除的工作,可匹配所有欄位\n✅支援正規表達式\n刪除14-27號範例:${YM}/(1[4-9]|2[0-7])`, `${YM}/(1[4-9]|2[0-7])`)
if(!desc) return
let checkText = "", delArray = []
form.querySelectorAll("tbody > tr").forEach((tr)=>
{
const work = [...tr.querySelectorAll("td")].map((e)=>e.innerText || e.querySelector("input")?.value)
if(work.join("\n").match(desc))
{
checkText += `${work[0]}-${work[1]}-${work[2]}\n`
delArray.push(tr)
}
})
if(delArray.length==0) return
checkText = `檢測到${delArray.length}筆刪除資料,是否繼續刪除?\n` + checkText
if(confirm(checkText)) delArray.forEach((e)=>e.remove())
}
change.onclick = () =>
{
const desc = prompt("輸入需修改的描述", document.querySelector(".workDesc").value)
if(desc) document.querySelectorAll(".workDesc").forEach((e)=>{e.value=desc})
}
cancel.onclick = () =>
{
btnArray.forEach((e)=>e.remove())
form.remove()
btn.value = `😎一鍵填寫 ${YM} 工作日誌`
btn.disabled = false
}
enqueue.onclick = ()=>
{
btnArray.forEach((e)=>e.remove())
btn.value = "😎已放入自動填寫佇列"
iziToastProxy.success({title:"[雲科工作日誌]", message:"已放入自動填寫佇列", position: "topCenter"})
workQueue.length = 0;
form.querySelectorAll("tbody > tr").forEach((tr)=>
{
const [planName, date, start, end, hour, desc] = [...tr.querySelectorAll("td")].map((e)=>e.innerText || e.querySelector("input")?.value)
workQueue.push({state: workState.waiting, planName, date, start, end, hour, desc})
})
initAutoFillForm()
autoFillForm.hidden = false
GM_setValue("workQueueTimeout", monthEnd.getTime())
GM_setValue("workQueue", workQueue)
}
submit.onclick = async () =>
{
if([...form.querySelectorAll(".time")].some((e)=>!e.value.match(/^\d{2}:\d{2}$/)) || [...form.querySelectorAll(".hour")].some((e)=>!Number(e.innerText))) if(!confirm("偵測到填寫錯誤,是否繼續送出?")) return
btnArray.forEach((e)=>e.remove())
btn.value = "😋處理送出中..."
for(const tr of form.querySelectorAll("tbody > tr"))
{
const [planName, date, start, end, hour, desc] = [...tr.querySelectorAll("td")].map((e)=>e.innerText || e.querySelector("input")?.value)
const body = `DateContract=${date.replace(/\//g,"%2F")}%2C${(await getWorkContent(planName))?.id}&`+
`StartHour=${Number(start.split(":")[0])}&StartMin=${Number(start.split(":")[1])}&`+
`EndHour=${Number(end.split(":")[0])}&EndMin=${Number(end.split(":")[1])}&`+
`IsAnnualLeave=false&WorkContent=${encodeURIComponent(desc)}&Hours=${hour}`;
tr.querySelector("td").innerText += await fetch("https://webapp.yuntech.edu.tw/workstudy/StudWorkRecord/ApplyAction", {
"headers": {"content-type": "application/x-www-form-urlencoded",},
"body": body,
"method": "POST",
}).then((e)=>e.text()).then((e)=>e.includes("填寫完成") ? "✅":` ❌${String(e.match(/(?<=×<\/button>)[^<]+/)??"未知錯誤(可能已超時)").trim()}`);
}
btn.value = "😎填寫完成"
}
}
console.log("it's works!")
})()