303 lines
8.7 KiB
TypeScript
303 lines
8.7 KiB
TypeScript
import React from "react";
|
||
import { get, post } from "@/common";
|
||
import { collection } from "@/store";
|
||
|
||
interface Conflicts {
|
||
[index: string]: HTMLInputElement[];
|
||
}
|
||
interface ConflictsTmp {
|
||
[index: string]: Set<string>;
|
||
}
|
||
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<HTMLInputElement>): 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 (
|
||
<>
|
||
<h2 style={{ textAlign: "center" }}>ITSC 学 生 助 理 抢 班 系 统</h2>
|
||
<h3 style={{ textAlign: "center" }}>Login as {user}</h3>
|
||
<h3 style={{ textAlign: "center" }}>请勿在短时间内多次操作同一时段的选择,选择后请耐心等待状态更新以确认是否选择成功</h3>
|
||
<div
|
||
align="center"
|
||
ref={ref}
|
||
contentEditable={editable}
|
||
style={{
|
||
overflow: "scroll",
|
||
}}
|
||
onInput={handleInput}
|
||
></div>
|
||
{!hideDownloadButton && (
|
||
<p style={{ display: "flex", justifyContent: "center" }}>
|
||
<button onClick={DownloadMarks}>Download Selection</button>
|
||
</p>
|
||
)}
|
||
<div style={{ display: "none" }} id="download-dom"></div>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default Timetable;
|