From 712c2aea9035ad0a391edc00e9a99382e55dd0f5 Mon Sep 17 00:00:00 2001 From: vikashbalajik <143927351+vikashbalajik@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:52:29 -0500 Subject: [PATCH 01/26] Refactor: Integrated supervisor reminder mail using cron jobs --- .gitignore | 1 + server/.DS_Store | Bin 0 -> 6148 bytes 2 files changed, 1 insertion(+) create mode 100644 server/.DS_Store diff --git a/.gitignore b/.gitignore index 7f5b38cc..3f1d97f8 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,4 @@ dist /client/package-lock.json /server/package-lock.json +.DS_Store diff --git a/server/.DS_Store b/server/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0e5b7fa2a7a0bcfd99df50059615f68c21eb50e8 GIT binary patch literal 6148 zcmeHK%Z}496us_bI_Y#k1%Wghq)2SbP^JSCVpGbnU`r7!0F|UEB}^u+(ic)yscZNL zet|7t!oRSBb8T13qh*N@vLpLA&OP?M$F*G(k?79CdqfQ)a*-LGLlj#Qf9G6E71MGZ zC{!OGY4R+VgCLec$=Wtp1*`)9ngYCbtK`!d&k5z%FK-IpkiY&?Nx5F70ZB^eJw-Ii ztaW26u4ZiO#&$|6I5D0fYS|rvS&xxVGAviI=E~Nm5e@UBh+EifvZJ{0B%u)YpLRx& z?cYI^l7gZi+;1*Y2mVToU+`CaDcBv-B|Hf6aC{4BnU;M1S>RN`k;><|)Ia-?_4f$T zY1_VG&D2QS{L1pKVMcvud4Uot(j65KzpHshpGiSkM3z(bsL;bM9!HlQxvJ)SMpKMh z@Uk-clWnk=jLq5cJs8K)WZG)|5S2Y=zk1*vxOMk!?}J?Qrv7v`>H6twetRKh9Bk@- z@G=@M`i&b;WHR-mWSA;K6b@nX=2eu0a?zEuB%CO2Pk(T0Zmr)qUM}0Mv!>U+f4XXV z%ZI0}rgzqUuv*pJo44;idfvZ?FBAD0{v!#TcPd>mxB>ok+A(j=A%$ITpt`+C5{}F}T*K=_JhMLzt0; zxuFO(I{NpNIf;%&TUrII0(k}Y^lO99|EIg}|9O&qvkF)R{woE9bKX1eA|!LRmV)E6 v) Date: Sat, 19 Apr 2025 14:30:06 -0500 Subject: [PATCH 02/26] Revert "Group d/vikash sprint2" # Conflicts: # client/src/router.js # server/.DS_Store # server/index.js # server/utils/cronUtils.test.js --- client/src/router.js | 4 ++++ server/.DS_Store | Bin 6148 -> 0 bytes server/index.js | 22 ++++++++++++++++++++++ 3 files changed, 26 insertions(+) delete mode 100644 server/.DS_Store diff --git a/client/src/router.js b/client/src/router.js index c1bfe4ef..9bc5c664 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -2,6 +2,10 @@ import React from "react"; import { createBrowserRouter } from "react-router-dom"; import A1InternshipRequestForm from "./pages/A1InternshipRequestForm"; +<<<<<<< HEAD +======= + +>>>>>>> 6b783982 (Revert "Group d/vikash sprint2") // Layout import Layout from "./components/Layout"; diff --git a/server/.DS_Store b/server/.DS_Store deleted file mode 100644 index 0e5b7fa2a7a0bcfd99df50059615f68c21eb50e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%Z}496us_bI_Y#k1%Wghq)2SbP^JSCVpGbnU`r7!0F|UEB}^u+(ic)yscZNL zet|7t!oRSBb8T13qh*N@vLpLA&OP?M$F*G(k?79CdqfQ)a*-LGLlj#Qf9G6E71MGZ zC{!OGY4R+VgCLec$=Wtp1*`)9ngYCbtK`!d&k5z%FK-IpkiY&?Nx5F70ZB^eJw-Ii ztaW26u4ZiO#&$|6I5D0fYS|rvS&xxVGAviI=E~Nm5e@UBh+EifvZJ{0B%u)YpLRx& z?cYI^l7gZi+;1*Y2mVToU+`CaDcBv-B|Hf6aC{4BnU;M1S>RN`k;><|)Ia-?_4f$T zY1_VG&D2QS{L1pKVMcvud4Uot(j65KzpHshpGiSkM3z(bsL;bM9!HlQxvJ)SMpKMh z@Uk-clWnk=jLq5cJs8K)WZG)|5S2Y=zk1*vxOMk!?}J?Qrv7v`>H6twetRKh9Bk@- z@G=@M`i&b;WHR-mWSA;K6b@nX=2eu0a?zEuB%CO2Pk(T0Zmr)qUM}0Mv!>U+f4XXV z%ZI0}rgzqUuv*pJo44;idfvZ?FBAD0{v!#TcPd>mxB>ok+A(j=A%$ITpt`+C5{}F}T*K=_JhMLzt0; zxuFO(I{NpNIf;%&TUrII0(k}Y^lO99|EIg}|9O&qvkF)R{woE9bKX1eA|!LRmV)E6 v)>>>>>> 6b783982 (Revert "Group d/vikash sprint2") const express = require("express"); const mongoose = require("mongoose"); @@ -11,20 +16,30 @@ const emailRoutes = require("./routes/emailRoutes"); const tokenRoutes = require("./routes/token"); const approvalRoutes = require("./routes/approvalRoutes"); +<<<<<<< HEAD const outcomeRoutes = require("./routes/outcomeRoutes"); +======= +>>>>>>> 6b783982 (Revert "Group d/vikash sprint2") // Import cron job manager and register jobs const cronJobManager = require("./utils/cronUtils"); const { registerAllJobs } = require("./jobs/registerCronJobs"); const Evaluation = require("./models/Evaluation"); +<<<<<<< HEAD +======= + +>>>>>>> 6b783982 (Revert "Group d/vikash sprint2") const app = express(); app.use(express.json()); app.use(cors()); app.use("/api/form", formRoutes); // register route as /api/form/submit +<<<<<<< HEAD app.use("/api/email", emailRoutes); app.use("/api/token", tokenRoutes); app.use("/api", outcomeRoutes); +======= +>>>>>>> 6b783982 (Revert "Group d/vikash sprint2") const mongoConfig = { serverSelectionTimeoutMS: 5000, @@ -76,10 +91,14 @@ app.get("/api/message", (req, res) => { app.use("/api/email", emailRoutes); app.use("/api/token", tokenRoutes); app.use("/api", approvalRoutes); +<<<<<<< HEAD +======= +>>>>>>> 6b783982 (Revert "Group d/vikash sprint2") app.use("/api/reports", weeklyReportRoutes); app.post("/api/createUser", async (req, res) => { try { + const { userName, email, password, role } = req.body; const user = new User({ userName, email, password, role }); @@ -123,12 +142,15 @@ app.post("/api/evaluation", async (req, res) => { }); +<<<<<<< HEAD //Form A.4 const presentationRoutes = require("./routes/presentationRoutes"); app.use("/api/presentation", presentationRoutes); +======= +>>>>>>> 6b783982 (Revert "Group d/vikash sprint2") // Graceful shutdown (async Mongoose support) process.on("SIGINT", async () => { try { From 5486dedf5e21f7168def37660145ea38d9128051 Mon Sep 17 00:00:00 2001 From: vikashbalajik <143927351+vikashbalajik@users.noreply.github.com> Date: Mon, 14 Apr 2025 01:28:15 -0500 Subject: [PATCH 03/26] Revert "Group d/vikash sprint2" --- client/src/router.js | 3 --- server/index.js | 18 ------------------ 2 files changed, 21 deletions(-) diff --git a/client/src/router.js b/client/src/router.js index 9bc5c664..d9c2f404 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -2,10 +2,7 @@ import React from "react"; import { createBrowserRouter } from "react-router-dom"; import A1InternshipRequestForm from "./pages/A1InternshipRequestForm"; -<<<<<<< HEAD -======= ->>>>>>> 6b783982 (Revert "Group d/vikash sprint2") // Layout import Layout from "./components/Layout"; diff --git a/server/index.js b/server/index.js index 8523370f..cef5d93b 100644 --- a/server/index.js +++ b/server/index.js @@ -1,10 +1,7 @@ require("dotenv").config(); const weeklyReportRoutes = require("./routes/weeklyReportRoutes"); -<<<<<<< HEAD -======= ->>>>>>> 6b783982 (Revert "Group d/vikash sprint2") const express = require("express"); const mongoose = require("mongoose"); @@ -16,30 +13,21 @@ const emailRoutes = require("./routes/emailRoutes"); const tokenRoutes = require("./routes/token"); const approvalRoutes = require("./routes/approvalRoutes"); -<<<<<<< HEAD const outcomeRoutes = require("./routes/outcomeRoutes"); -======= ->>>>>>> 6b783982 (Revert "Group d/vikash sprint2") // Import cron job manager and register jobs const cronJobManager = require("./utils/cronUtils"); const { registerAllJobs } = require("./jobs/registerCronJobs"); const Evaluation = require("./models/Evaluation"); -<<<<<<< HEAD -======= ->>>>>>> 6b783982 (Revert "Group d/vikash sprint2") const app = express(); app.use(express.json()); app.use(cors()); app.use("/api/form", formRoutes); // register route as /api/form/submit -<<<<<<< HEAD app.use("/api/email", emailRoutes); app.use("/api/token", tokenRoutes); app.use("/api", outcomeRoutes); -======= ->>>>>>> 6b783982 (Revert "Group d/vikash sprint2") const mongoConfig = { serverSelectionTimeoutMS: 5000, @@ -91,10 +79,7 @@ app.get("/api/message", (req, res) => { app.use("/api/email", emailRoutes); app.use("/api/token", tokenRoutes); app.use("/api", approvalRoutes); -<<<<<<< HEAD -======= ->>>>>>> 6b783982 (Revert "Group d/vikash sprint2") app.use("/api/reports", weeklyReportRoutes); app.post("/api/createUser", async (req, res) => { try { @@ -142,15 +127,12 @@ app.post("/api/evaluation", async (req, res) => { }); -<<<<<<< HEAD //Form A.4 const presentationRoutes = require("./routes/presentationRoutes"); app.use("/api/presentation", presentationRoutes); -======= ->>>>>>> 6b783982 (Revert "Group d/vikash sprint2") // Graceful shutdown (async Mongoose support) process.on("SIGINT", async () => { try { From 52ceb7649b5123b3586edb23a40d94d48bdda09a Mon Sep 17 00:00:00 2001 From: vikashbalajik <143927351+vikashbalajik@users.noreply.github.com> Date: Mon, 14 Apr 2025 01:33:55 -0500 Subject: [PATCH 04/26] Update cronUtils.test.js --- server/utils/cronUtils.test.js | 141 +++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 server/utils/cronUtils.test.js diff --git a/server/utils/cronUtils.test.js b/server/utils/cronUtils.test.js new file mode 100644 index 00000000..16dc205e --- /dev/null +++ b/server/utils/cronUtils.test.js @@ -0,0 +1,141 @@ +// cronUtils.test.js +const cron = require("node-cron"); +const logger = require("./logger"); +const cronJobManager = require("./cronUtils"); + +jest.mock("node-cron", () => ({ + validate: jest.fn(), + schedule: jest.fn(), +})); + +jest.mock("./logger", () => ({ + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), +})); + +describe("cronUtils", () => { + let mockJobFunction; + + beforeEach(() => { + mockJobFunction = jest.fn().mockResolvedValue(); + cron.validate.mockClear(); + cron.schedule.mockClear(); + logger.info.mockClear(); + logger.warn.mockClear(); + logger.error.mockClear(); + cronJobManager.stopAllJobs(); // <-- CORRECT WAY + }); + + afterEach(() => { + cronJobManager.stopAllJobs(); // <-- CORRECT WAY + jest.clearAllMocks(); + }); + + it("create instance of CronJobManager", () => { + expect(cronJobManager).toBeDefined(); + expect(cronJobManager.jobs).toEqual(new Map()); + expect(cronJobManager.logger).toEqual(logger); + }); + + describe("registerJob", () => { + it("registerJob succeeds with runOnInit", () => { + cron.validate.mockReturnValue(true); + cron.schedule.mockReturnValue({ stop: jest.fn() }); + const result = cronJobManager.registerJob( + "testJob", + "*/5 * * * *", + mockJobFunction, + { runOnInit: true } + ); + // Check scedule and logger.info + expect(result).toBe(true); + expect(cron.schedule).toHaveBeenCalledTimes(1); + expect(cron.schedule).toHaveBeenCalledWith( + "*/5 * * * *", + expect.any(Function), + expect.objectContaining({ scheduled: true }) + ); + expect(logger.info).toHaveBeenCalledWith( + `Running job testJob immediately on init` + ); + }); + + it("registerJob succeeds without runOnInit", () => { + cron.validate.mockReturnValue(true); + cron.schedule.mockReturnValue({ stop: jest.fn() }); + const result = cronJobManager.registerJob( + "testJob", + "*/5 * * * *", + mockJobFunction, + { timezone: "UTC" } + ); + expect(result).toBe(true); + expect(cron.schedule).toHaveBeenCalledTimes(1); + expect(cron.schedule).toHaveBeenCalledWith( + "*/5 * * * *", + expect.any(Function), + expect.objectContaining({ scheduled: true, timezone: "UTC" }) + ); + expect(logger.info).not.toHaveBeenCalledWith( + `Running job testJob immediately on init` + ); + }); + + it("registerJob errors with invalid cron", () => { + cron.validate.mockReturnValue(false); + const result = cronJobManager.registerJob( + "invalidJob", + "invalid-cron-expression", + mockJobFunction + ); + // Check the correct logs were sent + expect(result).toBe(false); + expect(logger.error).toHaveBeenCalledWith( + "Invalid cron expression: invalid-cron-expression" + ); + expect(logger.info).not.toHaveBeenCalled(); + }); + + it("registerJob warns & replaces duplicate jobs", () => { + cron.validate.mockReturnValue(true); + cron.schedule.mockReturnValue({ stop: jest.fn() }); + // Create Job to check against + cronJobManager.registerJob("testJob", "*/5 * * * *", mockJobFunction); + // attempt to register with same job name + const result = cronJobManager.registerJob( + "testJob", + "*/10 * * * *", + mockJobFunction + ); + // Check correct logs were sent + expect(result).toBe(true); + expect(logger.warn).toHaveBeenCalledWith( + "Job 'testJob' already exists. Replacing it..." + ); + expect(logger.info).toHaveBeenCalledWith("Stopped job: testJob"); + }); + }); + + it("stopJob stops a given job", () => { + cron.validate.mockReturnValue(true); + cron.schedule.mockReturnValue({ stop: jest.fn() }); + // Create Job to stop + cronJobManager.registerJob("testJob", "*/5 * * * *", mockJobFunction); + // Check that it exists + expect(cronJobManager.jobs.has("testJob")).toBe(true); + // remove it + cronJobManager.stopJob("testJob"); + // check that its gone + expect(logger.info).toHaveBeenCalledWith("Stopped job: testJob"); + }); + + it("listJobs prints out jobs", () => { + cron.validate.mockReturnValue(true); + // Create Job to list + cronJobManager.registerJob("testJob", "*/5 * * * *", mockJobFunction); + const result = cronJobManager.listJobs(); + expect(result[0].name).toBe("testJob"); + }); + +}); From 899380ff5b52d7acc04af301c76a743db4b91d23 Mon Sep 17 00:00:00 2001 From: vikashbalajik <143927351+vikashbalajik@users.noreply.github.com> Date: Mon, 14 Apr 2025 01:36:57 -0500 Subject: [PATCH 05/26] Update cronUtils.test.js --- server/utils/cronUtils.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/utils/cronUtils.test.js b/server/utils/cronUtils.test.js index 16dc205e..9df2a8df 100644 --- a/server/utils/cronUtils.test.js +++ b/server/utils/cronUtils.test.js @@ -94,7 +94,9 @@ describe("cronUtils", () => { expect(logger.error).toHaveBeenCalledWith( "Invalid cron expression: invalid-cron-expression" ); - expect(logger.info).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + "Invalid cron expression: invalid-cron-expression" +); }); it("registerJob warns & replaces duplicate jobs", () => { From 3abc6b2c30ff782b604738fcb9c463ae4c205b6c Mon Sep 17 00:00:00 2001 From: vikashbalajik <143927351+vikashbalajik@users.noreply.github.com> Date: Mon, 14 Apr 2025 15:17:42 -0500 Subject: [PATCH 06/26] Fix: Integrated cron jobs & API routes properly for 4-week report --- client/src/pages/WeeklyFourWeekReportForm.css | 147 +++++++++++++++++ client/src/pages/WeeklyFourWeekReportForm.js | 151 ++++++++++++++++++ client/src/router.js | 9 +- .../controllers/fourWeekReportController.js | 39 +++++ server/index.js | 51 +++--- server/jobs/reminderSupervisor.js | 40 +++++ server/models/fourWeekReport.js | 33 ++++ server/routes/fourWeekReportRoutes.js | 8 + server/utils/cronUtils.js | 5 + server/utils/cronUtils.test.js | 12 +- 10 files changed, 460 insertions(+), 35 deletions(-) create mode 100644 client/src/pages/WeeklyFourWeekReportForm.css create mode 100644 client/src/pages/WeeklyFourWeekReportForm.js create mode 100644 server/controllers/fourWeekReportController.js create mode 100644 server/jobs/reminderSupervisor.js create mode 100644 server/models/fourWeekReport.js create mode 100644 server/routes/fourWeekReportRoutes.js diff --git a/client/src/pages/WeeklyFourWeekReportForm.css b/client/src/pages/WeeklyFourWeekReportForm.css new file mode 100644 index 00000000..2d4e8cd1 --- /dev/null +++ b/client/src/pages/WeeklyFourWeekReportForm.css @@ -0,0 +1,147 @@ + +.four-week-container { + max-width: 650px; + margin: 50px auto; + padding: 40px; + background-color: #f9f9f9; + border-radius: 15px; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1); + transition: 0.3s; + } + + .four-week-container:hover { + transform: scale(1.01); + } + + .four-week-title { + text-align: center; + margin-bottom: 30px; + color: #1c1c1c; + font-size: 32px; + font-weight: bold; + letter-spacing: 1px; + } + + .four-week-label { + font-weight: 600; + margin-top: 20px; + display: block; + color: #333; + font-size: 14px; + transition: 0.3s; + } + + .four-week-input, + .four-week-textarea, + .four-week-select { + width: 100%; + padding: 12px; + margin-top: 8px; + margin-bottom: 20px; + border-radius: 8px; + border: 1px solid #ccc; + background-color: #fff; + transition: 0.3s; + font-size: 15px; + } + + .four-week-input:hover, + .four-week-textarea:hover, + .four-week-select:hover { + background-color: #f3f3f3; + border-color: #a00027; + } + + .four-week-input:focus, + .four-week-textarea:focus, + .four-week-select:focus { + border-color: #a00027; + box-shadow: 0 0 8px rgba(160, 0, 39, 0.3); + outline: none; + } + + .four-week-textarea { + min-height: 100px; + } + + .four-week-button { + width: 100%; + padding: 14px; + background-color: #a00027; + color: #fff; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 17px; + font-weight: 600; + transition: 0.3s; + } + + .four-week-button:hover { + background-color: #800020; + transform: translateY(-2px); + } + + .form-message { + text-align: center; + font-weight: 600; + color: green; + margin-top: 15px; + animation: fade 1s ease-in-out; + } + + @keyframes fade { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + .input-group { + transition: transform 0.3s ease, box-shadow 0.3s ease; + padding: 10px; + border-radius: 10px; + } + + .input-group:hover, + .input-group:focus-within { + transform: scale(1.02); + box-shadow: 0 0 12px rgba(160, 0, 39, 0.15); + } + .input-group { + margin-bottom: 25px; + padding: 15px; + border-radius: 12px; + background-color: #f9f9f9; + transition: 0.4s ease-in-out; + box-shadow: 0 0 0px rgba(0, 0, 0, 0.1); + } + + .input-group:has(textarea:focus), + .input-group:has(select:focus) { + box-shadow: 0 0 12px rgba(160, 0, 39, 0.3), 0 0 25px rgba(160, 0, 39, 0.1); + transform: scale(1.02); + background-color: #ffffff; + border: 1px solid rgba(160, 0, 39, 0.2); + } + .four-week-textarea:disabled { + background-color: #f1f1f1; + cursor: not-allowed; + opacity: 0.8; + } + /* Pop effect on hover for form fields */ +.four-week-textarea:hover, +.four-week-select:hover { + transform: scale(1.03); + transition: all 0.3s ease; + box-shadow: 0 0 10px rgba(160, 0, 39, 0.2); + border-color: #a00027; +} + +.four-week-textarea:focus, +.four-week-select:focus { + transform: scale(1.03); + box-shadow: 0 0 15px rgba(160, 0, 39, 0.3); + border-color: #a00027; +} \ No newline at end of file diff --git a/client/src/pages/WeeklyFourWeekReportForm.js b/client/src/pages/WeeklyFourWeekReportForm.js new file mode 100644 index 00000000..f1a1a391 --- /dev/null +++ b/client/src/pages/WeeklyFourWeekReportForm.js @@ -0,0 +1,151 @@ + +import React, { useState } from "react"; +import axios from "axios"; +import "./WeeklyFourWeekReportForm.css"; + +const WeeklyFourWeekReportForm = () => { + const userRole = localStorage.getItem("role") || "student"; // Default role is student + + const isStudent = userRole === "student"; + const isSupervisor = userRole === "supervisor"; + const isCoordinator = userRole === "coordinator"; + + const [formData, setFormData] = useState({ + week: "Week 1", + tasks: "", + lessons: "", + challenges: "", + supervisorComments: "", + coordinatorComments: "", + }); + + const [message, setMessage] = useState(""); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + const payload = { + studentId: "123456", + ...formData, + }; + + console.log("Payload Sending:", payload); + + try { + await axios.post(`${process.env.REACT_APP_API_URL}/api/fourWeekReports`, payload); + setMessage("✅ Report Submitted Successfully!"); + + setFormData({ + week: "Week 1", + tasks: "", + lessons: "", + challenges: "", + supervisorComments: "", + coordinatorComments: "", + }); + } catch (error) { + console.error(error); + setMessage("❌ Submission Failed! Please try again."); + } + }; + + return ( +
+

4-Week Progress Report

+ +
+ + {/* Select Week */} + + + + {/* Tasks */} + + + + + + {message &&

{message}

} +
+ ); +}; + +export default CoordinatorReviewForm; diff --git a/client/src/pages/SubmittedReports.js b/client/src/pages/SubmittedReports.js index 04980974..fb69be6e 100644 --- a/client/src/pages/SubmittedReports.js +++ b/client/src/pages/SubmittedReports.js @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import axios from "axios"; import { useNavigate } from "react-router-dom"; -import "./SubmittedReports.css"; +import "../styles/SubmittedReports.css"; const SubmittedReports = () => { const [reports, setReports] = useState([]); diff --git a/client/src/pages/WeeklyFourWeekReportForm.js b/client/src/pages/WeeklyFourWeekReportForm.js index f1a1a391..440cfbab 100644 --- a/client/src/pages/WeeklyFourWeekReportForm.js +++ b/client/src/pages/WeeklyFourWeekReportForm.js @@ -1,7 +1,7 @@ import React, { useState } from "react"; import axios from "axios"; -import "./WeeklyFourWeekReportForm.css"; +import "../styles/WeeklyFourWeekReportForm.css"; const WeeklyFourWeekReportForm = () => { const userRole = localStorage.getItem("role") || "student"; // Default role is student diff --git a/client/src/pages/WeeklyProgressReportForm.js b/client/src/pages/WeeklyProgressReportForm.js index 2024a3c7..cbbe0c59 100644 --- a/client/src/pages/WeeklyProgressReportForm.js +++ b/client/src/pages/WeeklyProgressReportForm.js @@ -1,43 +1,43 @@ import React, { useEffect, useState } from "react"; import axios from "axios"; import { useNavigate, useParams } from "react-router-dom"; -import "./WeeklyProgressReportForm.css"; +import "../styles/WeeklyProgressReportForm.css"; const WeeklyProgressReportForm = ({ role = "student", readOnly = false }) => { const navigate = useNavigate(); const { reportId } = useParams(); const [formData, setFormData] = useState({ + name: "", + email: "", + supervisorName: "", + supervisorEmail: "", + coordinatorName: "Naveena", + coordinatorEmail: "naveena.suddapalli-1@ou.edu", + creditHours: "", + completedHours: 0, + requiredHours: 0, week: "", hours: "", tasks: "", lessons: "", supervisorComments: "", + coordinatorComments: "", }); const [message, setMessage] = useState(""); + // Load report data in read-only mode useEffect(() => { if (readOnly && reportId) { axios .get(`${process.env.REACT_APP_API_URL}/api/reports/${reportId}`) .then((res) => { if (res.data.success) { - const { - week, - hours, - tasks, - lessons, - supervisorComments, - } = res.data.report; - - setFormData({ - week: week || "", - hours: hours || "", - tasks: tasks || "", - lessons: lessons || "", - supervisorComments: supervisorComments || "", - }); + setFormData((prev) => ({ + ...prev, + ...res.data.report, + })); } }) .catch((err) => { @@ -46,21 +46,53 @@ const WeeklyProgressReportForm = ({ role = "student", readOnly = false }) => { } }, [readOnly, reportId]); - const handleChange = (e) => { - if (readOnly) return; + // Auto-fill A1 data + useEffect(() => { + const fetchA1Data = async () => { + try { + const email = "vikash@example.com"; // TODO: replace with real session email + const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/reports/a1/${email}`); + + if (res.data.success) { + const { + name, + email: userEmail, + supervisorName, + supervisorEmail, + creditHours, + completedHours, + requiredHours, + } = res.data.form; + + setFormData((prev) => ({ + ...prev, + name, + email: userEmail, + supervisorName, + supervisorEmail, + creditHours, + completedHours, + requiredHours: requiredHours || (creditHours ? creditHours * 60 : 0), + })); + } + } catch (err) { + console.error("A1 form not found or failed to fetch."); + setMessage("⚠️ You must submit the A1 form before submitting weekly reports."); + } + }; + + if (!readOnly) fetchA1Data(); + }, []); + const handleChange = (e) => { const { name, value } = e.target; + if (readOnly && !(role === "coordinator" && name === "coordinatorComments")) return; + if (name === "hours") { - const hoursValue = parseInt(value); - if (hoursValue > 40) { - setFormData((prev) => ({ ...prev, [name]: 40 })); - return; - } - if (hoursValue < 1 && value !== "") { - setFormData((prev) => ({ ...prev, [name]: 1 })); - return; - } + const num = parseInt(value); + if (num > 40) return setFormData((prev) => ({ ...prev, hours: 40 })); + if (num < 1 && value !== "") return setFormData((prev) => ({ ...prev, hours: 1 })); } setFormData((prev) => ({ ...prev, [name]: value })); @@ -68,28 +100,35 @@ const WeeklyProgressReportForm = ({ role = "student", readOnly = false }) => { const handleSubmit = async (e) => { e.preventDefault(); + const { week, hours, tasks, lessons, name, email, supervisorName, supervisorEmail } = formData; - const { week, hours, tasks, lessons } = formData; + if (!name || !email || !supervisorName || !supervisorEmail) { + return setMessage("Please complete the A1 form first."); + } if (!week || !hours || !tasks || !lessons) { - setMessage("Please fill in all the required fields."); - return; + return setMessage("Please fill in all the required fields."); } try { - const res = await axios.post( - `${process.env.REACT_APP_API_URL}/api/reports`, - formData - ); - + const res = await axios.post(`${process.env.REACT_APP_API_URL}/api/reports`, formData); setMessage(res.data.message || "Report submitted successfully!"); - setFormData({ + name: "", + email: "", + supervisorName: "", + supervisorEmail: "", + coordinatorName: "Naveena", + coordinatorEmail: "naveena.suddapalli-1@ou.edu", + creditHours: "", + completedHours: 0, + requiredHours: 0, week: "", hours: "", tasks: "", lessons: "", supervisorComments: "", + coordinatorComments: "", }); } catch (err) { console.error(err); @@ -97,33 +136,69 @@ const WeeklyProgressReportForm = ({ role = "student", readOnly = false }) => { } }; + const handleCoordinatorSubmit = async () => { + try { + const res = await axios.put( + `${process.env.REACT_APP_API_URL}/api/reports/${reportId}/coordinator-comment`, + { coordinatorComments: formData.coordinatorComments } + ); + setMessage(res.data.message || "Coordinator comment submitted!"); + } catch (err) { + console.error(err); + setMessage("Failed to submit coordinator comment."); + } + }; + return (

Weekly Progress Report

- + {/* Identity Fields */} + {["name", "email", "supervisorName", "supervisorEmail", "coordinatorName", "coordinatorEmail"].map((field) => ( +
+ + +
+ ))} + + {/* Progress Display */} + {!readOnly && ( +
+

Credit Hours: {formData.creditHours || "--"}

+

Required Hours: {formData.requiredHours || "--"}

+

Completed Hours: {formData.completedHours || "--"}

+ {formData.requiredHours && ( + <> +

+ Progress:{" "} + {Math.min(100, Math.round((formData.completedHours / formData.requiredHours) * 100))}% +

+
+
+
+ + )} +
+ )} + +
+ + {/* Week & Hours */}
- {Array.from({ length: 15 }, (_, i) => ( - + ))}
@@ -144,45 +219,43 @@ const WeeklyProgressReportForm = ({ role = "student", readOnly = false }) => {
-
-