diff --git a/client/package-lock.json b/client/package-lock.json index 63b5ea0..2c28366 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -23,7 +23,10 @@ "react-router-dom": "^6.28.0", "react-scripts": "5.0.1", "react-toastify": "^10.0.5", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "xlsx": "^0.18.5", + "xlsx-js-style": "^1.2.0", + "xlsx-style": "^0.8.13" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -5096,6 +5099,14 @@ "node": ">=8.9" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -6156,6 +6167,18 @@ "node": ">=4" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -6315,6 +6338,14 @@ "node": ">= 4.0" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -6343,6 +6374,14 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" }, + "node_modules/colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha512-OsSVtHK8Ir8r3+Fxw/b4jS1ZLPXkV6ZxDRJQzeD7qo0SqMXWrHDM71DgYzPMHY8SFJ0Ao+nNU2p1MmwdzKqPrw==", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -6431,6 +6470,20 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -6533,6 +6586,17 @@ "node": ">=10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -8419,6 +8483,14 @@ "node": ">= 0.8.0" } }, + "node_modules/exit-on-epipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", + "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/expect": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", @@ -8561,6 +8633,11 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz", + "integrity": "sha512-Rr5QlUeGN1mbOHlaqcSYMKVpPbgLy0AWT/W0EHxA6NGI12yO1jpoui2zBBvU2G824ltM6Ut8BFgfHSBGfkmS0A==" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -8977,6 +9054,14 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -12521,6 +12606,14 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-2.4.0.tgz", + "integrity": "sha512-m+yvNmYfRCaf1gr5YFT5e3fnSqLnE9McbNyRd0fNycsT0HltS19NKc18fh3Lvl/AIW/ovL6/MQ1JnfFg4G3o4A==", + "dependencies": { + "pako": "~0.2.5" + } + }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -13534,6 +13627,11 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -15080,6 +15178,17 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/printj": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", + "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==", + "bin": { + "printj": "bin/printj.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -16630,6 +16739,17 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -17669,6 +17789,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -17919,6 +18044,17 @@ "node": ">= 0.8" } }, + "node_modules/voc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/voc/-/voc-1.2.0.tgz", + "integrity": "sha512-BOuDjFFYvJdZO6e/N65AlaDItXo2TgyLjeyRYcqgAPkXpp5yTJcvkL2n+syO1r9Qc5g96tfBD2tuiMhYDmaGcA==", + "bin": { + "voc": "voc.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -18407,6 +18543,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -18804,6 +18956,149 @@ } } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx-js-style": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/xlsx-js-style/-/xlsx-js-style-1.2.0.tgz", + "integrity": "sha512-DDT4FXFSWfT4DXMSok/m3TvmP1gvO3dn0Eu/c+eXHW5Kzmp7IczNkxg/iEPnImbG9X0Vb8QhROda5eatSR/97Q==", + "dependencies": { + "adler-32": "~1.2.0", + "cfb": "^1.1.4", + "codepage": "~1.14.0", + "commander": "~2.17.1", + "crc-32": "~1.2.0", + "exit-on-epipe": "~1.0.1", + "fflate": "^0.3.8", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx-js-style/node_modules/adler-32": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz", + "integrity": "sha512-/vUqU/UY4MVeFsg+SsK6c+/05RZXIHZMGJA+PX5JyWI0ZRcBpupnRuPLU/NXXoFwMYCPCoxIfElM2eS+DUXCqQ==", + "dependencies": { + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" + }, + "bin": { + "adler32": "bin/adler32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx-js-style/node_modules/codepage": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz", + "integrity": "sha512-iz3zJLhlrg37/gYRWgEPkaFTtzmnEv1h+r7NgZum2lFElYQPi0/5bnmuDfODHxfp0INEfnRqyfyeIJDbb7ahRw==", + "dependencies": { + "commander": "~2.14.1", + "exit-on-epipe": "~1.0.1" + }, + "bin": { + "codepage": "bin/codepage.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx-js-style/node_modules/codepage/node_modules/commander": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", + "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==" + }, + "node_modules/xlsx-js-style/node_modules/commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" + }, + "node_modules/xlsx-style": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/xlsx-style/-/xlsx-style-0.8.13.tgz", + "integrity": "sha512-Cj3pGUvzrP2q9oowpLP8GyujovTaBGjBRRUlCKPitNvHWj9JDD5+FDPZIM5QQggGb995ZhkuBSsMZOSd5TzIWg==", + "dependencies": { + "adler-32": "", + "cfb": ">=0.10.0", + "codepage": "~1.3.6", + "commander": "", + "crc-32": "", + "jszip": "2.4.0", + "ssf": "~0.8.1" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx-style/node_modules/codepage": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.3.8.tgz", + "integrity": "sha512-cjAoQW5L/TCKWRbzt/xGBvhwJKQFhcIVO0jWQtpKQx4gr9qvXNkpRfq6gSmjjA8dB2Is/DPOb7gNwqQXP7UgTQ==", + "dependencies": { + "commander": "", + "concat-stream": "", + "voc": "" + }, + "bin": { + "codepage": "bin/codepage.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx-style/node_modules/frac": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/frac/-/frac-0.3.1.tgz", + "integrity": "sha512-1Lzf2jOjhIkRaa013KlxNOn2D9FemmQNeYUDpEIyPeFXmpLvbZXJOlaayMBT6JKXx+afQFgQ1QJ4kaF7Z07QFQ==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx-style/node_modules/ssf": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.8.2.tgz", + "integrity": "sha512-+ZkFDAG+ImJ48DcZvabx6YTrZ67DKkM0kbyOOtH73mbUEvNhQWWgRZrHC8+k7GuGKWQnACYLi7bj0eCt1jmosQ==", + "dependencies": { + "colors": "0.6.2", + "frac": "0.3.1", + "voc": "" + }, + "bin": { + "ssf": "bin/ssf.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xml-name-validator": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", diff --git a/client/package.json b/client/package.json index f466e56..b67e3ac 100644 --- a/client/package.json +++ b/client/package.json @@ -18,7 +18,10 @@ "react-router-dom": "^6.28.0", "react-scripts": "5.0.1", "react-toastify": "^10.0.5", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "xlsx": "^0.18.5", + "xlsx-js-style": "^1.2.0", + "xlsx-style": "^0.8.13" }, "scripts": { "start": "react-scripts start", diff --git a/client/src/Pages/ConsolidatedTable.jsx b/client/src/Pages/ConsolidatedTable.jsx index d38dea5..7f70b7f 100644 --- a/client/src/Pages/ConsolidatedTable.jsx +++ b/client/src/Pages/ConsolidatedTable.jsx @@ -1,207 +1,22 @@ -// import React, { useState, useEffect } from "react"; -// import axios from "axios"; - -// const ConsolidatedTable = () => { -// const [data, setData] = useState([]); -// const [loading, setLoading] = useState(true); - -// useEffect(() => { -// const fetchData = async () => { -// try { -// const response = await axios.get("http://localhost:8080/api/table/consolidated-table"); -// setData(response.data); -// setLoading(false); -// } catch (error) { -// console.error("Error fetching table data:", error); -// setLoading(false); -// } -// }; - -// fetchData(); -// }, []); - -// if (loading) { -// return
Loading...
; -// } - -// return ( -//
-//

Consolidated Table

-// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// {data.map((row, index) => ( -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// ))} -// -//
SemesterCourse CodeCourse NameExam TypeYearMarksNameAffiliation/CollegeHighest QualificationCareer ExperienceOral/PracticalAssessmentReassessmentPaper SettingModerationPwD Paper Setting
{row.semester}{row.courseCode}{row.courseName}{row.examType}{row.year}{row.marks}{row.Name}{row.affiliation}{row.qualification}{row.experience}{row.oralPractical}{row.assessment}{row.reassessment}{row.paperSetting}{row.moderation}{row.pwdPaperSetting}
-//
-// ); -// }; - -// export default ConsolidatedTable; - - - -// import React, { useState, useEffect } from "react"; -// import axios from "axios"; -// import { CSVLink } from "react-csv"; - -// const ConsolidatedTable = () => { -// const [data, setData] = useState([]); -// const [loading, setLoading] = useState(true); - -// useEffect(() => { -// const fetchData = async () => { -// try { -// const response = await axios.get("http://localhost:8080/api/table/consolidated-table"); -// setData(response.data); -// setLoading(false); -// } catch (error) { -// console.error("Error fetching table data:", error); -// setLoading(false); -// } -// }; - -// fetchData(); -// }, []); - -// if (loading) { -// return
Loading...
; -// } - -// // Extract unique faculty names -// const uniqueTeachers = [...new Set(data.map((row) => row.Name))]; - -// return ( -//
-//

Faculty Tables with Download Option

-// {uniqueTeachers.map((teacher, index) => { -// // Filter rows for the current teacher -// const teacherData = data.filter((row) => row.Name === teacher); - -// return ( -//
-//

{teacher}'s Table

-// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// {teacherData.map((row, idx) => ( -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// ))} -// -//
SemesterCourse CodeCourse NameExam TypeYearMarksNameAffiliation/CollegeHighest QualificationCareer ExperienceOral/PracticalAssessmentReassessmentPaper SettingModerationPwD Paper Setting
{row.semester}{row.courseCode}{row.courseName}{row.examType}{row.year}{row.marks}{row.Name}{row.affiliation}{row.qualification}{row.experience}{row.oralPractical}{row.assessment}{row.reassessment}{row.paperSetting}{row.moderation}{row.pwdPaperSetting}
-// {/* CSV Download Button */} -// -// Download CSV -// -//
-// ); -// })} -//
-// ); -// }; - -// export default ConsolidatedTable; - - - -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect } from "react"; import axios from "axios"; -import { CSVLink } from "react-csv"; +import * as XLSX from "xlsx-js-style"; +import { sendEmail } from "../api"; +import { createExcelBook } from "../api"; const ConsolidatedTable = () => { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); - const csvLinksRef = useRef([]); // Ref to store CSV links for bulk download + const [currentPage, setCurrentPage] = useState(1); + const tablesPerPage = 5; + const [expandedTeacher, setExpandedTeacher] = useState(null); useEffect(() => { const fetchData = async () => { try { - const response = await axios.get("http://localhost:8080/api/table/consolidated-table"); + const response = await axios.get( + "http://localhost:8080/api/table/consolidated-table" + ); setData(response.data); setLoading(false); } catch (error) { @@ -220,24 +35,103 @@ const ConsolidatedTable = () => { // Extract unique faculty names const uniqueTeachers = [...new Set(data.map((row) => row.Name))]; - const handleBulkDownload = () => { - // Trigger all individual downloads programmatically - csvLinksRef.current.forEach((csvLink) => { - if (csvLink) { - csvLink.link.click(); - } + // Pagination + const indexOfLastTable = currentPage * tablesPerPage; + const indexOfFirstTable = indexOfLastTable - tablesPerPage; + const currentTeachers = uniqueTeachers.slice(indexOfFirstTable, indexOfLastTable); + + const totalPages = Math.ceil(uniqueTeachers.length / tablesPerPage); + + const handleNextPage = () => { + if (currentPage < totalPages) setCurrentPage((prevPage) => prevPage + 1); + }; + + const handlePrevPage = () => { + if (currentPage > 1) setCurrentPage((prevPage) => prevPage - 1); + }; + + const createExcelFile = (teacherData, teacherName) => { + const workbook = createExcelBook(teacherData, teacherName); + XLSX.writeFile(workbook, `${teacherName.replace(/\s+/g, "_")}_Table.xlsx`); + }; + + const bulkDownload = () => { + uniqueTeachers.forEach((teacher) => { + const teacherData = data.filter((row) => row.Name === teacher); + createExcelFile(teacherData, teacher); }); }; + const handleSendEmail = async (teacher, teacherData) => { + const facultyId = teacherData[0].facultyId; // This assumes all rows for a teacher have the same facultyId + try { + const response = await axios.get( + `http://localhost:8080/api/faculty/${facultyId}` + ); + const facultyEmail = response.data.email; + const workbook = createExcelBook(teacherData, teacher); + const fileName = `${teacher.replace(/\s+/g, "_")}_table.xlsx`; + const excelBlob = XLSX.write(workbook, { + bookType: "xlsx", + type: "array", + }); + + const file = new File([excelBlob], fileName, { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + + const formData = new FormData(); + formData.append("teacher", teacher); + formData.append("fileName", fileName); + formData.append("recipientEmail", facultyEmail); + formData.append("file", file); + + try { + const response = await sendEmail(formData); + alert(`Email sent successfully to ${facultyEmail}`); + console.log("Response from server:", response); + } catch (error) { + console.error("Error sending email:", error); + alert("Failed to send email."); + } + } catch (error) { + console.error("Error fetching Faculty data:", error); + alert("Failed to fetch faculty data."); + } + }; + + // Send emails to all teachers + const sendEmailsToAllTeachers = async () => { + for (let teacher of uniqueTeachers) { + const teacherData = data.filter((row) => row.Name === teacher); + await handleSendEmail(teacher, teacherData); // Wait for each email to be sent before proceeding to the next + } + alert("Emails sent to all teachers."); + }; + return (
-

Faculty Tables with Download Options

+

+ Faculty Tables with Download Options +

- {/* Bulk Download Button for Consolidated Data */} -
- + + - {/* Bulk Download Button for Individual CSV Files */}
- {/* Scrollable Content */}
- {uniqueTeachers.map((teacher, index) => { - // Filter rows for the current teacher + {currentTeachers.map((teacher, index) => { const teacherData = data.filter((row) => row.Name === teacher); - return (
-

{teacher}'s Table

- - - - - - - - - - - - - - - - - - - - - - - {teacherData.map((row, idx) => ( - - - - - - - - - - - - - - - - - - - ))} - -
SemesterCourse CodeCourse NameExam TypeYearMarksNameAffiliation/CollegeHighest QualificationCareer ExperienceOral/PracticalAssessmentReassessmentPaper SettingModerationPwD Paper Setting
{row.semester}{row.courseCode}{row.courseName}{row.examType}{row.year}{row.marks}{row.Name}{row.affiliation}{row.qualification}{row.experience}{row.oralPractical}{row.assessment}{row.reassessment}{row.paperSetting}{row.moderation}{row.pwdPaperSetting}
- {/* Individual CSV Download Button */} - (csvLinksRef.current[index] = el)} // Store ref for bulk download + onClick={() => setExpandedTeacher(expandedTeacher === teacher ? null : teacher)} > - Download {teacher}'s CSV - +

{teacher}'s Table

+
+ + +
+
+ + {expandedTeacher === teacher && ( + + + + + + + + + + + + + + + + + + + + + + + {teacherData.map((row, idx) => ( + + + + + + + + + + + + + + + + + + + ))} + +
SemesterCourse CodeCourse NameExam TypeYearMarksNameAffiliation/CollegeHighest QualificationCareer ExperienceOral/PracticalAssessmentReassessmentPaper SettingModerationPwD Paper Setting
{row.semester}{row.courseCode}{row.courseName}{row.examType}{row.year}{row.marks}{row.Name}{row.affiliation}{row.qualification}{row.experience}{row.oralPractical}{row.assessment}{row.reassessment}{row.paperSetting}{row.moderation}{row.pwdPaperSetting}
+ )}
); })}
+ + {/* Pagination controls */} +
+ + + Page {currentPage} of {totalPages} + + +
); }; diff --git a/client/src/Pages/CourseForm.jsx b/client/src/Pages/CourseForm.jsx index bba7082..1b3490f 100644 --- a/client/src/Pages/CourseForm.jsx +++ b/client/src/Pages/CourseForm.jsx @@ -9,11 +9,8 @@ const CourseForm = () => { const navigate = useNavigate(); const { course } = location.state || {}; - const [options, setOptions] = useState({ - faculties: [], // List of all faculties - }); - - const [suggestions, setSuggestions] = useState({}); // To hold suggestions for each field + const [options, setOptions] = useState({ faculties: [] }); + const [suggestions, setSuggestions] = useState({}); const [formData, setFormData] = useState({ oralsPracticals: "", assessment: "", @@ -32,46 +29,38 @@ const CourseForm = () => { pwdPaperSetter: [], }); + const [examPeriod, setExamPeriod] = useState({ + year: "", + startMonth: "", + endMonth: "", + }); + const [errors, setErrors] = useState({}); - // Fetch faculty list on mount useEffect(() => { const fetchOptionsAndFaculties = async () => { try { - const facultiesData = await fetchFaculties(); // Fetch faculty names from backend + const facultiesData = await fetchFaculties(); setOptions((prev) => ({ ...prev, faculties: facultiesData })); } catch (error) { console.error("Failed to fetch faculties:", error); } }; - fetchOptionsAndFaculties(); }, []); - // Handle input changes for form fields const handleInputChange = (e) => { const { name, value } = e.target; - setFormData({ ...formData, [name]: value }); - if (value.trim() === "") { - // Clear suggestions if input is empty - setSuggestions((prev) => ({ - ...prev, - [name]: [], - })); + setSuggestions((prev) => ({ ...prev, [name]: [] })); return; } - - // Filter suggestions for the current field if (options.faculties.length > 0) { const filteredSuggestions = options.faculties.filter((faculty) => faculty.name.toLowerCase().includes(value.toLowerCase()) ); - setSuggestions((prev) => ({ - ...prev, - [name]: filteredSuggestions, - })); + setSuggestions((prev) => ({ ...prev, [name]: filteredSuggestions })); } }; @@ -79,96 +68,75 @@ const CourseForm = () => { const selectedFaculty = options.faculties.find( (faculty) => faculty.name === formData[field] ); - if (selectedFaculty) { setTempAssignments((prev) => ({ ...prev, [field]: [...prev[field], selectedFaculty], })); - setFormData({ ...formData, [field]: "" }); // Clear input field - setSuggestions((prev) => ({ ...prev, [field]: [] })); // Clear suggestions + setFormData({ ...formData, [field]: "" }); + setSuggestions((prev) => ({ ...prev, [field]: [] })); } }; const handleRemoveFaculty = (field, index) => { setTempAssignments((prev) => { - const updatedAssignments = [...prev[field]]; // Create a shallow copy of the current list for this field - updatedAssignments.splice(index, 1); // Remove the faculty at the specified index - return { ...prev, [field]: updatedAssignments }; // Update the tempAssignments state + const updatedAssignments = [...prev[field]]; + updatedAssignments.splice(index, 1); + return { ...prev, [field]: updatedAssignments }; }); }; const validateForm = () => { const newErrors = {}; - - // Validate that each field in tempAssignments has at least one assigned faculty Object.keys(tempAssignments).forEach((field) => { if (!tempAssignments[field] || tempAssignments[field].length === 0) { newErrors[field] = "At least one faculty must be assigned."; } }); + if (!examPeriod.year || !examPeriod.startMonth || !examPeriod.endMonth) { + newErrors.examPeriod = "Exam period is required."; + } + setErrors(newErrors); - return Object.keys(newErrors).length === 0; // Form is valid if no errors + return Object.keys(newErrors).length === 0; }; - // Handle form submission const handleSubmit = async (e) => { - e.preventDefault(); // Prevent default form submission behavior + e.preventDefault(); - // Validate the form based on tempAssignments if (validateForm()) { try { const groupedTasks = {}; - - // Transform tempAssignments into grouped tasks by facultyId Object.entries(tempAssignments).forEach(([field, facultyList]) => { facultyList.forEach((faculty) => { - // Assuming faculty is an object, not just the faculty name const assignedFaculty = options.faculties.find( (optionFaculty) => optionFaculty.facultyId === faculty.facultyId ); - if (assignedFaculty) { - // Check if the facultyId already exists in groupedTasks if (!groupedTasks[assignedFaculty.facultyId]) { groupedTasks[assignedFaculty.facultyId] = { facultyId: assignedFaculty.facultyId, courseId: course?.courseId || id, tasks: [], + examPeriod: `${examPeriod.year} (${examPeriod.startMonth} - ${examPeriod.endMonth})`, }; } - // Push the task (field) into the tasks array for that faculty groupedTasks[assignedFaculty.facultyId].tasks.push(field); } }); }); - console.log(groupedTasks); - const payload = Object.values(groupedTasks); // Convert grouped tasks into an array - console.log("Saving appointment with payload:", payload); - - if (payload.length === 0) { - throw new Error("No assignments to submit."); - } - - // Call API to save appointments + const payload = Object.values(groupedTasks); await saveAppointment(payload); await updateCourseStatus(course?.courseId || id); - console.log("Form submitted successfully:", payload); - const filteredCourses = JSON.parse(localStorage.getItem("filteredCourses")) || []; - - // Redirect to courses page after successful submission navigate("/courses", { state: { courses: filteredCourses, - updatedCourse: { - ...course, - status: "Submitted", - }, + updatedCourse: { ...course, status: "Submitted" }, }, }); } catch (error) { @@ -227,7 +195,7 @@ const CourseForm = () => { key={faculty.facultyId} onClick={() => { setFormData({ ...formData, [name]: faculty.name }); - setSuggestions((prev) => ({ ...prev, [name]: [] })); // Clear suggestions for this field + setSuggestions((prev) => ({ ...prev, [name]: [] })); }} > {faculty.name} @@ -245,7 +213,7 @@ const CourseForm = () => { onClick={() => handleRemoveFaculty(name, index)} className="remove-faculty-btn" > - ✕ {/* This is the "X" symbol */} + ✕ ))} @@ -256,6 +224,75 @@ const CourseForm = () => { )} ))} +
+ + + + + {errors.examPeriod && ( + {errors.examPeriod} + )} +
diff --git a/client/src/Pages/CourseTable.jsx b/client/src/Pages/CourseTable.jsx index 5edf073..9933b3d 100644 --- a/client/src/Pages/CourseTable.jsx +++ b/client/src/Pages/CourseTable.jsx @@ -26,23 +26,6 @@ const CourseTable = () => { fetchAllCourses(); }, [state?.courses]); - // useEffect(() => { - // if (state?.updatedCourse) { - // setCourses((prevCourses) => { - // // Filter only the updated course - // const filteredCourses = prevCourses.filter((course) => - // course.courseId === state.updatedCourse.courseId - // ); - - // return filteredCourses.map((course) => - // course.courseId === state.updatedCourse.courseId - // ? { ...course, status: "Submitted" } // Update status - // : course - // ); - // }); - // } - // }, [state?.updatedCourse]); - useEffect(() => { if (state?.updatedCourse) { setCourses((prevCourses) => diff --git a/client/src/Pages/FilterPage.jsx b/client/src/Pages/FilterPage.jsx index 2b2572b..85de54f 100644 --- a/client/src/Pages/FilterPage.jsx +++ b/client/src/Pages/FilterPage.jsx @@ -23,26 +23,6 @@ const FilterPage = () => { } }; - // const handleApplyFilter = async () => { - // if (!formData.scheme || !formData.semester || !formData.department || !formData.program) { - // alert("Please fill all the fields before applying the filter."); - // return; - // } - - // try { - // const filteredCourses = await fetchCourses(formData); - // console.log(formData); - // if (filteredCourses.length > 0) { - // navigate("/courses", { state: { courses: filteredCourses } }); - // } else { - // alert("No courses found for the selected filters."); - // } - // } catch (error) { - // console.error("Error fetching courses:", error); - // alert("Failed to fetch courses. Please try again later."); - // } - // }; - const handleApplyFilter = async () => { if (!formData.scheme || !formData.semester || !formData.department || !formData.program) { alert("Please fill all the fields before applying the filter."); diff --git a/client/src/api.js b/client/src/api.js index 3e58baf..5349583 100644 --- a/client/src/api.js +++ b/client/src/api.js @@ -1,4 +1,5 @@ const BASE_URL = "http://localhost:8080/api"; +const XLSX = require("xlsx-js-style"); // Helper function for handling fetch requests const fetchData = async (url, options) => { @@ -77,6 +78,7 @@ export const fetchOptions = async () => { } }; + // Save multiple appointments to MongoDB export const saveAppointment = async (appointmentsData) => { console.log("Saving appointments with payload:", appointmentsData); @@ -137,3 +139,172 @@ export const updateCourseStatus = async (courseId) => { }); }; +export const sendEmail = async (formData) => { + try { + const url = `${BASE_URL}/send-email`; + const response = await fetch(url, { + method: "POST", + body: formData, // Directly pass FormData + }); + + if (!response.ok) { + let errorDetails = {}; + try { + errorDetails = await response.json(); + } catch (err) { + console.warn("Failed to parse error details:", err); + } + throw new Error( + `Error: ${response.statusText} (${response.status}) - ${ + errorDetails.message || "No details available" + }` + ); + } + + return await response.json(); + } catch (error) { + console.error( error.message); + throw error; + } +}; + +export const createExcelBook = (teacherData, teacher) => { + const workbook = XLSX.utils.book_new(); + + // Define header information + const headerInfo = [ + ["Somaiya Vidyavihar University"], + ["K. J. SOMAIYA COLLEGE OF ENGINEERING"], + [ + "Appointment of Internal Examiners for Paper Setting / OR/PR/Assessment/Reassessment", + ], + ["Class: B Tech/M Tech/Honour/Minor"], + ["Department - Computer Engineering"], + [], + ]; + + const tableHeaders = [ + [ + "Sr No", + "Semester", + "Course Code", + "Course Name", + "Exam Type", + "Year", + "Marks", + "Name", + "Affiliation/College", + "Highest Qualification", + "Career Experience", + "Oral/Practical", + "Assessment", + "Reassessment", + "Paper Setting", + "Moderation", + "PwD Paper Setting", + ], + ]; + + const dataRows = teacherData.map((row, index) => [ + index + 1, + row.semester, + row.courseCode, + row.courseName, + row.examType, + row.year, + row.marks, + row.Name, + row.affiliation, + row.qualification, + row.experience, + row.oralPractical, + row.assessment, + row.reassessment, + row.paperSetting, + row.moderation, + row.pwdPaperSetting, + ]); + + const sheetData = [...headerInfo, ...tableHeaders, ...dataRows]; + const worksheet = XLSX.utils.aoa_to_sheet(sheetData); + + // Add merged cells + worksheet["!merges"] = [ + { s: { r: 0, c: 0 }, e: { r: 0, c: 18 } }, + { s: { r: 1, c: 0 }, e: { r: 1, c: 18 } }, + { s: { r: 2, c: 0 }, e: { r: 2, c: 18 } }, + { s: { r: 3, c: 0 }, e: { r: 3, c: 18 } }, + { s: { r: 4, c: 0 }, e: { r: 4, c: 18 } }, + ]; + + // Define styles + const boldStyle = { + font: { bold: true, name: "Times New Roman", sz: 14 }, + alignment: { horizontal: "center", vertical: "center" }, + }; + + const redStyle = { + font: { + bold: true, + color: { rgb: "FF0000" }, + name: "Times New Roman", + sz: 14, + }, + alignment: { horizontal: "center", vertical: "center" }, + }; + + const normalStyle = { + font: { name: "Times New Roman", sz: 12 }, + }; + + // Apply styles to headers + const headerRanges = [ + { row: 0, style: boldStyle }, + { row: 1, style: boldStyle }, + { row: 2, style: boldStyle }, + { row: 3, style: boldStyle }, + { row: 4, style: redStyle }, + ]; + + headerRanges.forEach(({ row, style }) => { + for (let col = 0; col <= 18; col++) { + const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }); + if (!worksheet[cellAddress]) continue; // Skip empty cells + worksheet[cellAddress].s = style; + } + }); + + // Set column widths for better readability + worksheet["!cols"] = [ + { wch: 10 }, // Sr No + { wch: 12 }, // Semester + { wch: 15 }, // Course Code + { wch: 25 }, // Course Name + { wch: 15 }, // Exam Type + { wch: 10 }, // Year + { wch: 10 }, // Marks + { wch: 15 }, // Surname + { wch: 15 }, // First Name + { wch: 15 }, // Middle Name + { wch: 20 }, // Affiliation + { wch: 20 }, // Qualification + { wch: 15 }, // Career Experience + { wch: 15 }, // Oral/Practical + { wch: 15 }, // Assessment + { wch: 15 }, // Reassessment + { wch: 15 }, // Paper Setting + { wch: 15 }, // Moderation + { wch: 15 }, // PwD Paper Setting + ]; + + // Apply normal font style to all cells + Object.keys(worksheet).forEach((key) => { + if (worksheet[key] && key[0] !== "!") { + worksheet[key].s = worksheet[key].s || normalStyle; + } + }); + + // Add worksheet to workbook and save file + XLSX.utils.book_append_sheet(workbook, worksheet, teacher); + return workbook; +} \ No newline at end of file diff --git a/server/models/Appointment.js b/server/models/Appointment.js index 4b19fe9..e3b9efc 100644 --- a/server/models/Appointment.js +++ b/server/models/Appointment.js @@ -2,19 +2,16 @@ const mongoose = require("mongoose"); const { v4: uuidv4 } = require("uuid"); const AppointmentSchema = new mongoose.Schema({ - appointmentId: { - type: String, - required: true, - unique: true, - default: uuidv4 - }, + appointmentId: { type: String, required: true, unique: true, default: uuidv4 }, facultyId: { type: String, required: true }, facultyName: { type: String, required: true }, courseId: { type: String, required: true }, courseName: { type: String, required: true }, task: { type: String, required: true }, + examPeriod: { type: String, required: true }, // New field for exam period }); module.exports = mongoose.model("Appointment", AppointmentSchema); + diff --git a/server/package-lock.json b/server/package-lock.json index d2be342..8fb5c17 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -22,6 +22,7 @@ "jsonwebtoken": "^9.0.2", "mongoose": "^8.9.5", "mongoose-findorcreate": "^4.0.0", + "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.13", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", @@ -119,6 +120,11 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -270,6 +276,22 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -330,6 +352,20 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "node_modules/connect-mongo": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-5.1.0.tgz", @@ -406,6 +442,11 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -1163,6 +1204,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -1390,6 +1436,25 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mongodb": { "version": "6.12.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz", @@ -1530,6 +1595,23 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -1818,6 +1900,11 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1895,6 +1982,25 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2084,6 +2190,27 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -2154,6 +2281,11 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", @@ -2194,6 +2326,11 @@ "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", "license": "BSD" }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -2245,6 +2382,14 @@ "engines": { "node": ">=16" } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } } } } diff --git a/server/package.json b/server/package.json index c0df411..9cc037b 100644 --- a/server/package.json +++ b/server/package.json @@ -31,6 +31,7 @@ "jsonwebtoken": "^9.0.2", "mongoose": "^8.9.5", "mongoose-findorcreate": "^4.0.0", + "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.13", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", diff --git a/server/routes/appointmentRoutes.js b/server/routes/appointmentRoutes.js index 8b50f46..683a656 100644 --- a/server/routes/appointmentRoutes.js +++ b/server/routes/appointmentRoutes.js @@ -7,17 +7,22 @@ const Course = require("../models/Course"); // Save multiple appointments router.post("/", async (req, res) => { try { - const { appointments } = req.body; // Expecting an array of appointments + const { appointments } = req.body; if (!appointments || !Array.isArray(appointments)) { return res.status(400).json({ error: "Invalid or missing data" }); } const savedAppointments = []; for (const appointment of appointments) { - const { facultyId, courseId, tasks } = appointment; + const { facultyId, courseId, tasks, examPeriod } = appointment; - // Validate input data - if (!facultyId || !courseId || !Array.isArray(tasks) || tasks.length === 0) { + if ( + !facultyId || + !courseId || + !Array.isArray(tasks) || + tasks.length === 0 || + !examPeriod + ) { return res.status(400).json({ error: "Invalid appointment data" }); } @@ -30,7 +35,6 @@ router.post("/", async (req, res) => { }); } - // Save each task as a separate appointment for (const task of tasks) { const newAppointment = new Appointment({ facultyId, @@ -38,6 +42,7 @@ router.post("/", async (req, res) => { courseId, courseName: course.name, task, + examPeriod, }); const savedAppointment = await newAppointment.save(); savedAppointments.push(savedAppointment); diff --git a/server/routes/consolidatedRoutes.js b/server/routes/consolidatedRoutes.js index e136ae2..83cd4dd 100644 --- a/server/routes/consolidatedRoutes.js +++ b/server/routes/consolidatedRoutes.js @@ -63,6 +63,7 @@ router.get("/consolidated-table", async (req, res) => { paperSetting: data.tasks.has("paperSetting") ? "✔" : "", moderation: data.tasks.has("moderation") ? "✔" : "", pwdPaperSetting: data.tasks.has("pwdPaperSetter") ? "✔" : "", + facultyId: `${data.facultyId}` })); res.status(200).json(tableData); diff --git a/server/routes/emailRoutes.js b/server/routes/emailRoutes.js new file mode 100644 index 0000000..fae8e7c --- /dev/null +++ b/server/routes/emailRoutes.js @@ -0,0 +1,67 @@ +const express = require("express"); +const nodemailer = require("nodemailer"); +const fs = require("fs"); +const multer = require("multer"); +const router = express.Router(); + +// Setup multer for handling file uploads +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, "./"); // Save to the current directory (you can customize this) + }, + filename: function (req, file, cb) { + cb(null, file.originalname); // Use the original file name + }, +}); + +const upload = multer({ storage }); + +// Route to handle email sending with file attachment +router.post("/", upload.single("file"), async (req, res) => { + const { teacher, fileName, recipientEmail } = req.body; + + if (!teacher || !fileName || !recipientEmail || !req.file) { + return res.status(400).json({ error: "Missing required fields or file" }); + } + + // Configure Nodemailer transporter + const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: "swdc.ate@gmail.com", // Replace with your email + pass: "umlc hbkr dpga iywd", // Replace with your app-specific password or token + }, + connectionTimeout: 15000, + greetingTimeout: 15000, + socketTimeout: 15000, + }); + + // Email options + const mailOptions = { + from: "swdc.ate@gmail.com", // Replace with your email + to: recipientEmail, + subject: `Excel File for ${teacher}`, + text: `Attached is the Excel file for ${teacher}.`, + attachments: [ + { + filename: fileName, + path: req.file.path, // Use the uploaded file's path + }, + ], + }; + + try { + // Send email + await transporter.sendMail(mailOptions); + + // Delete the temporary file after sending the email + fs.unlinkSync(req.file.path); + + res.status(200).json({ message: "Email sent successfully" }); + } catch (error) { + console.error("Error sending email:", error); + res.status(500).json({ error: "Failed to send email" }); + } +}); + +module.exports = router; diff --git a/server/routes/optionsRoutes.js b/server/routes/optionsRoutes.js index 57c606d..93ce3a1 100644 --- a/server/routes/optionsRoutes.js +++ b/server/routes/optionsRoutes.js @@ -1,24 +1,3 @@ -// const express = require('express'); -// const router = express.Router(); - -// // Sample data (replace this with actual data fetching logic) -// const optionsData = { -// assessment: ["Option 1", "Option 2", "Option 3"], -// reassessment: ["Option A", "Option B"], -// paperSetting: ["Option X", "Option Y"], -// moderation: ["Option M", "Option N"], -// pwdPaperSetter: ["Option P", "Option Q"], -// oralsPracticals: ["Option O", "Option P"] -// }; - -// // GET route for fetching options -// router.get('/', (req, res) => { -// res.status(200).json(optionsData); -// }); - -// module.exports = router; - - const express = require('express'); const router = express.Router(); const { getFaculties } = require('../controller/facultyController'); // Import your controller that interacts with the database diff --git a/server/server.js b/server/server.js index 5558ae2..2b1baa9 100644 --- a/server/server.js +++ b/server/server.js @@ -15,6 +15,7 @@ const facultyRoutes = require("./routes/facultyRoutes"); const appointmentRoutes = require("./routes/appointmentRoutes"); const optionsRoutes = require("./routes/optionsRoutes"); const consolidatedRoutes = require("./routes/consolidatedRoutes"); +const emailRoutes = require("./routes/emailRoutes"); const Course = require("./models/Course"); // MongoDB Connection @@ -56,6 +57,7 @@ app.use("/api/faculty", facultyRoutes); app.use("/api/appointments", appointmentRoutes); app.use("/api/options", optionsRoutes); app.use("/api/data", consolidatedRoutes); // Moved after `app` initialization +app.use("/api/send-email", emailRoutes); // Google OAuth Routes app.get(