雲科一鍵填寫工作日誌

😡垃圾學校不讓人輕鬆填寫

// ==UserScript==
// @name          雲科一鍵填寫工作日誌
// @namespace     Anong0u0
// @version       1.3.1
// @description   😡垃圾學校不讓人輕鬆填寫
// @author        Anong0u0
// @match         https://webapp.yuntech.edu.tw/workstudy/StudWorkRecord/*
// @icon          https://www.google.com/s2/favicons?sz=64&domain=www.yuntech.edu.tw
// @grant         GM_getValue
// @grant         GM_setValue
// @license       Beerware
// ==/UserScript==

const getWorkContent = async (planName) =>
{
    const t = GM_getValue(planName)
    if(t) return t

    const workListHTML = document.createElement("div"),
          workInfoHTML = document.createElement("div"),
          workInfo = {}

    workListHTML.innerHTML = await fetch(`/workstudy/StudWorkRecord/ContractList?date=${new Date(new Date().getFullYear(), new Date().getMonth(), 1).toLocaleDateString("ja").replace(/\//g,"%2F")}`).then((e)=>e.text())
    workInfoHTML.innerHTML = await fetch("/workstudy/Stud/ContractList").then((e)=>e.text());
    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+/))))
    {
        const text = await fetch(`/workstudy/Stud/JobContractInfo?ApplyId=${e[10]}`).then((e)=>e.text()),
              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+/))
        const pn = workList[2],
        workID = workList[8]

        GM_setValue(pn, {id:workID, desc:workInfo[pn]})
    }
    return GM_getValue(planName)
}


(() =>
{
    const btn = document.createElement("input"), YM = new Date().toLocaleDateString("en-za").slice(0,-3)
    btn.type = "button"
    btn.value = `😎一鍵填寫 ${YM} 工作日誌`
    btn.className = "btn btn-success"
    document.querySelector(".panel-footer").append(btn)
    btn.onclick = async () =>
    {
        btn.value = "🔄載入資料中..."
        btn.disabled = 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(new Date().getFullYear(), 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]
            let remain = (end-start)/3600000,
                offset = 0
            while(remain>0)
            {
                const t = Math.min(remain, 4)
                const s = new Date(start+offset*3600000-Math.floor((Math.random()*5))*60000).toLocaleTimeString("sv").slice(0,-3),
                      e = new Date(start+(t+offset)*3600000+Math.floor((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 > .row:last-child)").insertAdjacentElement("afterend", form)

        btn.value = "🔽等待確認中..."
        const cancel = document.createElement("input"),
              submit = document.createElement("input"),
              change = document.createElement("input"),
              del = document.createElement("input")
        cancel.className = "btn btn-danger"
        cancel.type = "button"
        cancel.value = "🤔取消"
        cancel.style = "margin: 0 4px;"
        submit.className = "btn btn-success"
        submit.type = "button"
        submit.value = "🤗確認送出"
        change.style = "margin: 0 4px;"
        change.className = "btn btn-info"
        change.type = "button"
        change.value = "🧐修改所有工作內容描述"
        del.className = "btn btn-danger"
        del.type = "button"
        del.value = "😨刪除特定工作"
        document.querySelector(".panel-footer").append(cancel)
        document.querySelector(".panel-footer").append(submit)
        document.querySelector(".panel-footer").append(change)
        document.querySelector(".panel-footer").append(del)
        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 = () =>
        {
            cancel.remove()
            submit.remove()
            change.remove()
            del.remove()
            form.remove()
            btn.value = `😎一鍵填寫 ${YM} 工作日誌`
            btn.disabled = false
        }
        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
            submit.disabled = true
            cancel.remove()
            change.remove()
            del.remove()
            btn.remove()
            submit.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.indexOf("填寫完成")!=-1 ? "✅":` ❌${String(e.match(/(?<=×<\/button>)[^<]+/)??"未知錯誤").trim()}`);
            }
            submit.value = "😎填寫完成"
        }
    }

    console.log("it's works!")
})()