import React from "react"; import { get, post } from "@/common"; import { collection } from "@/store"; interface Conflicts { [index: string]: HTMLInputElement[]; } interface ConflictsTmp { [index: string]: Set; } interface IndexToElement { [index: string]: HTMLInputElement; } interface IndexToCell { [index: string]: HTMLTableCellElement; } const indexToElement: IndexToElement = {}; const indexToCell: IndexToCell = {}; const conflicts: Conflicts = {}; const marks: (HTMLInputElement | null)[][] = []; const tds: (HTMLTableCellElement | null)[][] = []; const downloadObjectAsJson = (exportObj: any, exportName: string) => { const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportObj)); const downloadAnchorNode = document.createElement("a"); downloadAnchorNode.setAttribute("href", dataStr); downloadAnchorNode.setAttribute("download", `${exportName}.json`); document.body.appendChild(downloadAnchorNode); // required for firefox downloadAnchorNode.click(); downloadAnchorNode.remove(); }; const Timetable = ({ user, isRegular = false, disableNetwork = false, disableConflictCheck = false, replaceInputType = "checkbox", apiRecordEndPoint = "/api/record", openRecordMode = false, hideDownloadButton = true, }) => { const [editable, setEditable] = React.useState(true); const ref = React.useRef(); const handleSelect = async (event: Event) => { const target: HTMLInputElement = event.target; console.log("select", target.name, target.checked); if (disableConflictCheck) return; const changedInputs: any = []; // find whether there are checked input in conflict for (const input of conflicts[target.name]) { if (input.name === target.name) continue; if (input.checked) { alert("Error: Conflict select"); location.reload(); return; } } for (const input of conflicts[target.name]) { if (input.name === target.name) continue; if (target.checked) { if (input.getAttribute("disabled") === null) { input.setAttribute("disabled", "true"); changedInputs.push({ input, disable: false }); } } else { if (input.getAttribute("disabled") === "true") { input.removeAttribute("disabled"); changedInputs.push({ input, disable: true }); } } } if (disableNetwork) return; // Immediately revert the checkbox state target.checked = !target.checked; // post request const json = await post("/api/record", { name: target.name, checked: !target.checked, user, }); if (json.error !== undefined) { alert(json.error); // revert conflict changed input for (const { input, disable } of changedInputs) { if (disable) { input.setAttribute("disabled", "true"); } else { input.removeAttribute("disabled"); } } } }; const handleInput = (event: React.ChangeEvent): boolean => { const { target } = event; // validate if (target?.children[0]?.tagName !== "TABLE") { console.log("not a table"); return false; } // turn off editable setEditable(false); // empty marks marks.length = 0; // empty tds tds.length = 0; const table = target.children[0]; table.setAttribute("border", "0"); // mark cell const conflictsTmp: ConflictsTmp = {}; const tbody = table.children[table.children.length - 1]; for (const tr_index in tbody.children) { const tr = tbody.children[tr_index]; const row: (HTMLInputElement | null)[] = []; const rowTD: (HTMLTableCellElement | null)[] = []; for (const td_index in tr.children) { const td: HTMLTableCellElement = tr.children[td_index]; if (td.tagName !== "TD") continue; if (td.getAttribute("bgcolor")?.toUpperCase() !== "#39CEFF") { // const position (assigned by supervisor) if (td?.textContent?.trim() === user) { const constSelected = document.createElement("input"); constSelected.setAttribute("type", "checkbox"); constSelected.setAttribute("checked", "1"); constSelected.setAttribute("disabled", "1"); td.innerHTML = ""; td.appendChild(constSelected); } row.push(null); continue; } const index = `${tr_index},${td_index}`; const placeholders = td.textContent?.trim().split(","); if (placeholders === undefined) continue; if (conflictsTmp[index] === undefined) conflictsTmp[index] = new Set(); for (const ph of placeholders) conflictsTmp[index].add(ph); // mount click event const input = document.createElement("input"); input.setAttribute("type", replaceInputType); input.onchange = handleSelect; input.name = index; td.innerHTML = ""; td.appendChild(input); indexToElement[index] = input; indexToCell[index] = td; row.push(input); rowTD.push(td); } marks.push(row); tds.push(rowTD); // console.log("marks", marks); } // resolve conflicts for (const index in conflictsTmp) { if (conflicts[index] === undefined) conflicts[index] = []; for (const ph of Array.from(conflictsTmp[index])) { for (const conflictIndex in conflictsTmp) { if (conflictsTmp[conflictIndex].has(ph)) conflicts[index].push(indexToElement[conflictIndex]); } } } console.log(conflicts); return true; }; const refresh = async () => { const json = await get(`${apiRecordEndPoint}?name=${user}`); if (openRecordMode) { for (const index in json) { const input = indexToElement[index]; const td = indexToCell[index]; if (json[index] !== user) { td.innerHTML = json[index]; td.removeAttribute("bgcolor"); } else { input.checked = true; input.disabled = true; } } return; } const occupied: string[] = json.occupied; const myselect: string[] = json.myselect; // console.log(json); for (const index in indexToElement) { if (occupied.includes(index)) { indexToElement[index].style.display = "none"; } else { indexToElement[index].style.display = ""; } const includes = myselect.includes(index); indexToElement[index].checked = includes; // after checked, find conflicts input if (disableConflictCheck) continue; if (includes) { for (const input of conflicts[index]) { if (input.name === index) continue; input.setAttribute("disabled", "true"); } } } }; React.useEffect(() => { if (disableNetwork || isRegular) return; const interval = setInterval(() => { refresh(); }, 1500); return () => { clearInterval(interval); }; }, []); React.useEffect(() => { const main = async () => { const json = await get(isRegular ? "/api/html-regular" : "/api/html"); ref.current.innerHTML = json.html; handleInput({ target: ref.current }); refresh(); }; main(); // Auto refresh the entire timetable every 3 minutes const fullRefreshInterval = setInterval(() => { main(); }, 3 * 60 * 1000); // 3 minutes in milliseconds return () => { clearInterval(fullRefreshInterval); }; }, []); const DownloadMarks = async () => { console.log("download marks", marks); const data = { user, selections: {}, }; for (const row of marks) { for (const input of row) { if (input === null) continue; if (input.checked) { data.selections[input.name] = 1; } else if (parseFloat(input.value)) { data.selections[input.name] = parseFloat(input.value); } } } console.log(data); downloadObjectAsJson(data, user); }; return ( <>

ITSC 学 生 助 理 抢 班 系 统

Login as {user}

请勿在短时间内多次操作同一时段的选择,选择后请耐心等待状态更新以确认是否选择成功

{!hideDownloadButton && (

)}
); }; export default Timetable;