排班工具

This commit is contained in:
2023-02-16 03:01:04 +08:00
parent 3ec781cc76
commit 3b23b09909
8 changed files with 675 additions and 21 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
/html.html
/json
/hours.json
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies

View File

@@ -1,5 +1,6 @@
import React from "react";
import { get, post } from "@/common";
import { collection } from "@/store";
interface Conflicts {
[index: string]: HTMLInputElement[];
@@ -15,7 +16,24 @@ const indexToElement: IndexToElement = {};
const conflicts: Conflicts = {};
const marks: (HTMLInputElement | null)[][] = [];
const Timetable = ({ user }) => {
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,
disableNetwork = false,
disableConflictCheck = false,
replaceInputType = "checkbox",
}) => {
const [editable, setEditable] = React.useState(true);
const ref = React.useRef();
@@ -23,6 +41,8 @@ const Timetable = ({ user }) => {
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]) {
@@ -47,6 +67,9 @@ const Timetable = ({ user }) => {
}
}
}
if (disableNetwork) return;
// post request
const json = await post("/api/record", {
name: target.name,
@@ -74,11 +97,12 @@ const Timetable = ({ user }) => {
return false;
}
console.log(target.innerHTML);
// turn off editable
setEditable(false);
// empty marks
marks.length = 0;
const table = target.children[0];
table.setAttribute("border", "1");
@@ -105,7 +129,7 @@ const Timetable = ({ user }) => {
// mount click event
const input = document.createElement("input");
input.setAttribute("type", "checkbox");
input.setAttribute("type", replaceInputType);
input.onchange = handleSelect;
input.name = index;
td.innerHTML = "";
@@ -115,6 +139,7 @@ const Timetable = ({ user }) => {
row.push(input);
}
marks.push(row);
// console.log("marks", marks);
}
// resolve conflicts
@@ -156,13 +181,15 @@ const Timetable = ({ user }) => {
}
};
React.useEffect(() => {
if (disableNetwork) return;
const interval = setInterval(() => {
refresh();
}, 1000);
return () => {
clearInterval(interval);
};
});
}, []);
React.useEffect(() => {
const main = async () => {
@@ -176,7 +203,27 @@ const Timetable = ({ user }) => {
return (
<>
<button onClick={refresh}>Test</button>
<button
onClick={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.value !== "") {
data.selections[input.name] = parseFloat(input.value);
}
}
}
console.log(data);
downloadObjectAsJson(data, user);
}}
>
DownloadSelection
</button>
<span>Login as {user}</span>
<div
ref={ref}
@@ -186,6 +233,7 @@ const Timetable = ({ user }) => {
}}
onInput={handleInput}
></div>
<div style={{ display: "none" }} id="download-dom"></div>
</>
);
};

406
package-lock.json generated
View File

@@ -9,12 +9,15 @@
"version": "0.1.0",
"dependencies": {
"@next/font": "13.1.6",
"@types/glob": "^8.0.1",
"@types/node": "18.11.18",
"@types/react": "18.0.27",
"@types/react-dom": "18.0.10",
"esbuild": "^0.17.5",
"eslint": "8.33.0",
"eslint-config-next": "13.1.6",
"glob": "^8.1.0",
"mongodb": "5.0",
"next": "13.1.6",
"react": "18.2.0",
"react-dom": "18.2.0",
@@ -420,6 +423,22 @@
"glob": "7.1.7"
}
},
"node_modules/@next/eslint-plugin-next/node_modules/glob": {
"version": "7.1.7",
"resolved": "https://registry.npmmirror.com/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
}
},
"node_modules/@next/font": {
"version": "13.1.6",
"resolved": "https://registry.npmmirror.com/@next/font/-/font-13.1.6.tgz",
@@ -681,11 +700,25 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/glob": {
"version": "8.0.1",
"resolved": "https://registry.npmmirror.com/@types/glob/-/glob-8.0.1.tgz",
"integrity": "sha512-8bVUjXZvJacUFkJXHdyZ9iH1Eaj5V7I8c4NdH5sQJsdXkqT4CA5Dhb4yb4VE/3asyx4L9ayZr1NIhTsWHczmMw==",
"dependencies": {
"@types/minimatch": "^5.1.2",
"@types/node": "*"
}
},
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
},
"node_modules/@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/@types/minimatch/-/minimatch-5.1.2.tgz",
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA=="
},
"node_modules/@types/node": {
"version": "18.11.18",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-18.11.18.tgz",
@@ -719,6 +752,20 @@
"resolved": "https://registry.npmmirror.com/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
},
"node_modules/@types/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog=="
},
"node_modules/@types/whatwg-url": {
"version": "8.2.2",
"resolved": "https://registry.npmmirror.com/@types/whatwg-url/-/whatwg-url-8.2.2.tgz",
"integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
"dependencies": {
"@types/node": "*",
"@types/webidl-conversions": "*"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "5.50.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-5.50.0.tgz",
@@ -974,6 +1021,14 @@
"node": ">=8"
}
},
"node_modules/bson": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/bson/-/bson-5.0.0.tgz",
"integrity": "sha512-EL2KpZdyhshyyptj6pnQfnFKPoncD9KwZYvgmj/FXQiOUU1HWTHWmBOP4TZXU3YzStcI5qgpIl68YnMo16s26A==",
"engines": {
"node": ">=14.20.1"
}
},
"node_modules/call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.2.tgz",
@@ -1882,19 +1937,18 @@
"integrity": "sha512-YCcF28IqSay3fqpIu5y3Krg/utCBHBeoflkZyHj/QcqI2nrLPC3ZegS9CmIo+hJb8K7aiGsuUl7PwWVjNG2HQQ=="
},
"node_modules/glob": {
"version": "7.1.7",
"resolved": "https://registry.npmmirror.com/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
"version": "8.1.0",
"resolved": "https://registry.npmmirror.com/glob/-/glob-8.1.0.tgz",
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
"minimatch": "^5.0.1",
"once": "^1.3.0"
},
"engines": {
"node": "*"
"node": ">=12"
}
},
"node_modules/glob-parent": {
@@ -1908,6 +1962,25 @@
"node": ">=10.13.0"
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/globals": {
"version": "13.20.0",
"resolved": "https://registry.npmmirror.com/globals/-/globals-13.20.0.tgz",
@@ -2088,6 +2161,11 @@
"node": ">= 0.4"
}
},
"node_modules/ip": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/ip/-/ip-2.0.0.tgz",
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
},
"node_modules/is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/is-arguments/-/is-arguments-1.1.1.tgz",
@@ -2449,6 +2527,12 @@
"node": ">=10"
}
},
"node_modules/memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmmirror.com/memory-pager/-/memory-pager-1.5.0.tgz",
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
"optional": true
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
@@ -2485,6 +2569,47 @@
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.7.tgz",
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g=="
},
"node_modules/mongodb": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/mongodb/-/mongodb-5.0.1.tgz",
"integrity": "sha512-KpjtY+NWFmcic6UDYEdfn768ZTuKyv7CRaui7ZSd6q/0/o1AURMC7KygTUwB1Cl8V10Pe5NiP+Y2eBMCDs/ygQ==",
"dependencies": {
"bson": "^5.0.0",
"mongodb-connection-string-url": "^2.6.0",
"socks": "^2.7.1"
},
"engines": {
"node": ">=14.20.1"
},
"optionalDependencies": {
"saslprep": "^1.0.3"
},
"peerDependencies": {
"@aws-sdk/credential-providers": "^3.201.0",
"mongodb-client-encryption": "^2.3.0",
"snappy": "^7.2.2"
},
"peerDependenciesMeta": {
"@aws-sdk/credential-providers": {
"optional": true
},
"mongodb-client-encryption": {
"optional": true
},
"snappy": {
"optional": true
}
}
},
"node_modules/mongodb-connection-string-url": {
"version": "2.6.0",
"resolved": "https://registry.npmmirror.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz",
"integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==",
"dependencies": {
"@types/whatwg-url": "^8.2.1",
"whatwg-url": "^11.0.0"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz",
@@ -2911,6 +3036,22 @@
"rimraf": "bin.js"
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -2929,6 +3070,18 @@
"is-regex": "^1.1.4"
}
},
"node_modules/saslprep": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/saslprep/-/saslprep-1.0.3.tgz",
"integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
"optional": true,
"dependencies": {
"sparse-bitfield": "^3.0.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/scheduler": {
"version": "0.23.0",
"resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.0.tgz",
@@ -2988,6 +3141,28 @@
"node": ">=8"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.7.1",
"resolved": "https://registry.npmmirror.com/socks/-/socks-2.7.1.tgz",
"integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
"dependencies": {
"ip": "^2.0.0",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.13.0",
"npm": ">= 3.0.0"
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz",
@@ -2996,6 +3171,15 @@
"node": ">=0.10.0"
}
},
"node_modules/sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
"optional": true,
"dependencies": {
"memory-pager": "^1.0.2"
}
},
"node_modules/stop-iteration-iterator": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
@@ -3155,6 +3339,17 @@
"node": ">=8.0"
}
},
"node_modules/tr46": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/tr46/-/tr46-3.0.0.tgz",
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
"dependencies": {
"punycode": "^2.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/tsconfig-paths": {
"version": "3.14.1",
"resolved": "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
@@ -3250,6 +3445,26 @@
"punycode": "^2.1.0"
}
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-url": {
"version": "11.0.0",
"resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-11.0.0.tgz",
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
"dependencies": {
"tr46": "^3.0.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
@@ -3518,6 +3733,21 @@
"integrity": "sha512-o7cauUYsXjzSJkay8wKjpKJf2uLzlggCsGUkPu3lP09Pv97jYlekTC20KJrjQKmSv5DXV0R/uks2ZXhqjNkqAw==",
"requires": {
"glob": "7.1.7"
},
"dependencies": {
"glob": {
"version": "7.1.7",
"resolved": "https://registry.npmmirror.com/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
},
"@next/font": {
@@ -3652,11 +3882,25 @@
"tslib": "^2.4.0"
}
},
"@types/glob": {
"version": "8.0.1",
"resolved": "https://registry.npmmirror.com/@types/glob/-/glob-8.0.1.tgz",
"integrity": "sha512-8bVUjXZvJacUFkJXHdyZ9iH1Eaj5V7I8c4NdH5sQJsdXkqT4CA5Dhb4yb4VE/3asyx4L9ayZr1NIhTsWHczmMw==",
"requires": {
"@types/minimatch": "^5.1.2",
"@types/node": "*"
}
},
"@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
},
"@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/@types/minimatch/-/minimatch-5.1.2.tgz",
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA=="
},
"@types/node": {
"version": "18.11.18",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-18.11.18.tgz",
@@ -3690,6 +3934,20 @@
"resolved": "https://registry.npmmirror.com/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
},
"@types/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog=="
},
"@types/whatwg-url": {
"version": "8.2.2",
"resolved": "https://registry.npmmirror.com/@types/whatwg-url/-/whatwg-url-8.2.2.tgz",
"integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
"requires": {
"@types/node": "*",
"@types/webidl-conversions": "*"
}
},
"@typescript-eslint/parser": {
"version": "5.50.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-5.50.0.tgz",
@@ -3882,6 +4140,11 @@
"fill-range": "^7.0.1"
}
},
"bson": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/bson/-/bson-5.0.0.tgz",
"integrity": "sha512-EL2KpZdyhshyyptj6pnQfnFKPoncD9KwZYvgmj/FXQiOUU1HWTHWmBOP4TZXU3YzStcI5qgpIl68YnMo16s26A=="
},
"call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.2.tgz",
@@ -4621,16 +4884,33 @@
"integrity": "sha512-YCcF28IqSay3fqpIu5y3Krg/utCBHBeoflkZyHj/QcqI2nrLPC3ZegS9CmIo+hJb8K7aiGsuUl7PwWVjNG2HQQ=="
},
"glob": {
"version": "7.1.7",
"resolved": "https://registry.npmmirror.com/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
"version": "8.1.0",
"resolved": "https://registry.npmmirror.com/glob/-/glob-8.1.0.tgz",
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
"minimatch": "^5.0.1",
"once": "^1.3.0"
},
"dependencies": {
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"requires": {
"balanced-match": "^1.0.0"
}
},
"minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"requires": {
"brace-expansion": "^2.0.1"
}
}
}
},
"glob-parent": {
@@ -4785,6 +5065,11 @@
"side-channel": "^1.0.4"
}
},
"ip": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/ip/-/ip-2.0.0.tgz",
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
},
"is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/is-arguments/-/is-arguments-1.1.1.tgz",
@@ -5074,6 +5359,12 @@
"yallist": "^4.0.0"
}
},
"memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmmirror.com/memory-pager/-/memory-pager-1.5.0.tgz",
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
"optional": true
},
"merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
@@ -5101,6 +5392,26 @@
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.7.tgz",
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g=="
},
"mongodb": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/mongodb/-/mongodb-5.0.1.tgz",
"integrity": "sha512-KpjtY+NWFmcic6UDYEdfn768ZTuKyv7CRaui7ZSd6q/0/o1AURMC7KygTUwB1Cl8V10Pe5NiP+Y2eBMCDs/ygQ==",
"requires": {
"bson": "^5.0.0",
"mongodb-connection-string-url": "^2.6.0",
"saslprep": "^1.0.3",
"socks": "^2.7.1"
}
},
"mongodb-connection-string-url": {
"version": "2.6.0",
"resolved": "https://registry.npmmirror.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz",
"integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==",
"requires": {
"@types/whatwg-url": "^8.2.1",
"whatwg-url": "^11.0.0"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz",
@@ -5408,6 +5719,21 @@
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"requires": {
"glob": "^7.1.3"
},
"dependencies": {
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
},
"run-parallel": {
@@ -5428,6 +5754,15 @@
"is-regex": "^1.1.4"
}
},
"saslprep": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/saslprep/-/saslprep-1.0.3.tgz",
"integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
"optional": true,
"requires": {
"sparse-bitfield": "^3.0.3"
}
},
"scheduler": {
"version": "0.23.0",
"resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.0.tgz",
@@ -5472,11 +5807,34 @@
"resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="
},
"smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="
},
"socks": {
"version": "2.7.1",
"resolved": "https://registry.npmmirror.com/socks/-/socks-2.7.1.tgz",
"integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
"requires": {
"ip": "^2.0.0",
"smart-buffer": "^4.2.0"
}
},
"source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
},
"sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
"optional": true,
"requires": {
"memory-pager": "^1.0.2"
}
},
"stop-iteration-iterator": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
@@ -5595,6 +5953,14 @@
"is-number": "^7.0.0"
}
},
"tr46": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/tr46/-/tr46-3.0.0.tgz",
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
"requires": {
"punycode": "^2.1.1"
}
},
"tsconfig-paths": {
"version": "3.14.1",
"resolved": "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
@@ -5673,6 +6039,20 @@
"punycode": "^2.1.0"
}
},
"webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
},
"whatwg-url": {
"version": "11.0.0",
"resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-11.0.0.tgz",
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
"requires": {
"tr46": "^3.0.0",
"webidl-conversions": "^7.0.0"
}
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",

View File

@@ -11,12 +11,15 @@
},
"dependencies": {
"@next/font": "13.1.6",
"@types/glob": "^8.0.1",
"@types/node": "18.11.18",
"@types/react": "18.0.27",
"@types/react-dom": "18.0.10",
"esbuild": "^0.17.5",
"eslint": "8.33.0",
"eslint-config-next": "13.1.6",
"glob": "^8.1.0",
"mongodb": "5.0",
"next": "13.1.6",
"react": "18.2.0",
"react-dom": "18.2.0",

101
pages/api/tool.ts Normal file
View File

@@ -0,0 +1,101 @@
import type { NextApiRequest, NextApiResponse } from "next";
import fs from "fs";
import { store, html } from "@/store";
import glob from "glob";
import { promisify } from "util";
const g = promisify(glob);
const read = promisify(fs.readFile);
// 索引与工时
const indexToHour: Record<string, number> = JSON.parse(
fs.readFileSync("./hours.json", "utf8")
).selections;
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Record<string, string>>
) {
// 读入全部json文件
// users: {姓名: {坐标: 权重}}
const users: Record<string, Record<string, number>> = {};
const files = await g("./json/*.json");
for (const file of files) {
const jsonStr = await read(file, "utf8");
const json: {
user: string;
selections: Record<string, number>;
} = JSON.parse(jsonStr);
// Normalization
// 使用 标准分数 算法 z = (x - mean) / std
const { selections } = json;
const nums = Object.values(selections);
const mean = nums.reduce((a, b) => a + b) / nums.length;
const std = Math.sqrt(
nums.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) /
nums.length
);
const normalizedNums = nums.map((x) => (std === 0 ? 0 : (x - mean) / std));
for (const index in selections) {
selections[index] = std === 0 ? 0 : (selections[index] - mean) / std;
}
users[json.user] = selections;
}
// 计算每一个格子有多少人选择
// counts: {坐标: {姓名: 权重}}
const counts: Record<string, Record<string, Record<string, number>>> = {};
for (const user in users) {
for (const select in users[user]) {
if (counts[select] === undefined) counts[select] = {};
counts[select][user] = users[user];
}
}
// 构造返回数据, key 为 坐标value 为姓名
const resp: Record<string, string> = {};
// 找到选择数量最少的格子
// sortable: [[坐标, 选择人数]]
// 如果遇到选择数量相同的格子,随机打乱顺序进行迭代
const sortable: [string, number][] = [];
for (const index in counts) {
sortable.push([index, Object.keys(counts[index]).length]);
}
sortable.sort((a, b) => a[1] - b[1] || Math.random() - 0.5);
// 记录这个人的出现次数,还有这个人的工时总数
const hoursCount: Record<string, number> = {};
for (const [smallestIndex, _] of sortable) {
// console.log("smallestIndex", smallestIndex);
// console.log("cell", counts[smallestIndex]);
// 找到格子里权重最低的人选 [姓名, 小时数, 出现次数, 权重]
const weightList: [string, number, number][] = [];
for (const user in counts[smallestIndex]) {
weightList.push([
user,
hoursCount[user] || 0,
counts[smallestIndex][user][smallestIndex],
]);
}
weightList.sort((a, b) => a[1] - b[1] || a[2] - b[2]);
// console.log("weightList", weightList);
const theChoosenUser = weightList[0][0];
// console.log("theChoosenUser", theChoosenUser);
// 记录结果
resp[smallestIndex] = theChoosenUser;
hoursCount[theChoosenUser] =
(hoursCount[theChoosenUser] || 0) + indexToHour[smallestIndex];
}
// console.log("hoursCount", hoursCount);
const sortedHoursCount: [string, number][] = [];
for (const user in hoursCount) {
sortedHoursCount.push([user, hoursCount[user]]);
}
sortedHoursCount.sort((a, b) => a[1] - b[1]);
console.log("sortedHoursCount", sortedHoursCount);
console.log("sortedHoursCount.length", sortedHoursCount.length);
res.status(200).json(resp);
}

50
pages/report-tool.tsx Normal file
View File

@@ -0,0 +1,50 @@
import React from "react";
import Head from "next/head";
const ReportPage = () => {
const ref = React.useRef();
const getReport = async () => {
const resp = await fetch("/api/html").then((resp) => resp.json());
ref.current.innerHTML = resp.html;
const json: Record<string, string> = await fetch("/api/tool").then(
(resp) => resp.json()
);
const table = ref.current.children[0];
const tbody = table.children[table.children.length - 1];
for (const tr_index in tbody.children) {
const tr = tbody.children[tr_index];
for (const td_index in tr.children) {
const td = tr.children[td_index];
if (td.tagName !== "TD") continue;
const index = `${tr_index},${td_index}`;
if (json[index] === undefined) continue;
td.innerHTML = json[index];
}
}
};
React.useEffect(() => {
getReport();
}, []);
return (
<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<button onClick={async () => getReport()}>Refresh</button>
<div
ref={ref}
onInput={(event) => {
console.log(event.currentTarget.innerHTML);
}}
contentEditable="true"
></div>
</main>
</>
);
};
export default ReportPage;

55
pages/tool.tsx Normal file
View File

@@ -0,0 +1,55 @@
import React from "react";
import Head from "next/head";
import Timetable from "@/components/Timetable";
export default function Home() {
const [user, setUser] = React.useState("");
const [begin, setBegin] = React.useState(false);
React.useEffect(() => {
setUser(localStorage.getItem("user") || "");
}, []);
return (
<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
{!begin && (
<div>
<input
placeholder="在这输你的名字"
value={user}
onChange={(event) => setUser(event.target.value)}
/>
<button
onClick={() => {
if (user.trim() === "") {
alert("姓名不能为空");
return;
}
setUser(user.trim());
setBegin(true);
localStorage.setItem("user", user);
}}
>
Login
</button>
</div>
)}
{begin && (
<Timetable
user={user}
disableConflictCheck={true}
disableNetwork={true}
replaceInputType={"number"}
/>
)}
</main>
</>
);
}

View File

@@ -1,3 +1,10 @@
import fs from "fs";
import { MongoClient } from "mongodb";
import util from "util";
const write = util.promisify(fs.writeFile);
const read = util.promisify(fs.readFile);
class Store {
record: Record<string, string>;
constructor() {
@@ -27,13 +34,20 @@ class Store {
class HTML {
html: string;
constructor() {
this.html = "";
// load from file
try {
this.html = fs.readFileSync("./html.html", "utf8");
} catch {
this.html = "";
}
}
public get() {
return this.html;
}
public set(html) {
public async set(html: string) {
this.html = html;
// store into file
await write("./html.html", html, "utf8");
}
}