From 0a27610af71e8a9b056cad88238c6a4666a44380 Mon Sep 17 00:00:00 2001 From: ICook094 Date: Sat, 26 Apr 2025 16:59:31 -0500 Subject: [PATCH 01/19] add back changes --- client/src/pages/A1InternshipRequestForm.js | 36 +++- client/src/pages/Home.js | 89 ++++----- client/src/pages/StudentDashboard.jsx | 147 +++++++++++---- client/src/styles/StudentDashboard.css | 2 +- server/controllers/approvalController.js | 196 +++++++++++++++++--- server/jobs/reminderEmail.js | 110 +++++------ server/middleware/authMiddleware.js | 59 +++--- server/models/InternshipRequest.js | 18 +- server/routes/approvalRoutes.js | 42 +++-- server/routes/formRoutes.js | 67 +++++++ server/utils/cronUtils.test.js | 164 ++++++++++------ 11 files changed, 650 insertions(+), 280 deletions(-) diff --git a/client/src/pages/A1InternshipRequestForm.js b/client/src/pages/A1InternshipRequestForm.js index 009a0979..dc90a793 100644 --- a/client/src/pages/A1InternshipRequestForm.js +++ b/client/src/pages/A1InternshipRequestForm.js @@ -252,12 +252,41 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { const submitFormData = async () => { try { + // Fetch logged-in student ID from localStorage or context + const studentId = localStorage.getItem("studentId"); // You must store this during login + + const payload = { + student: studentId, + workplace: { + name: formData.workplaceName, + website: formData.website, + phone: formData.phone + }, + internshipAdvisor: { + name: formData.advisorName, + jobTitle: formData.advisorJobTitle, + email: formData.advisorEmail + }, + creditHours: parseInt(formData.creditHours), + startDate: formData.startDate, + endDate: formData.endDate, + tasks: formData.tasks, + status: "submitted", + approvals: ["advisor"] + }; + const response = await fetch(`${process.env.REACT_APP_API_URL}/api/form/submit`, { method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(formData), + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(payload) }); - if (!response.ok) throw new Error("Failed to submit form"); + + if (!response.ok) { + throw new Error("Failed to submit form", { cause: response }); + } + const data = await response.json(); return data; } catch (error) { @@ -265,6 +294,7 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { throw error; } }; + const sendTaskDescriptions = async (descriptions) => { try { diff --git a/client/src/pages/Home.js b/client/src/pages/Home.js index ffe18418..54e5e129 100644 --- a/client/src/pages/Home.js +++ b/client/src/pages/Home.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import "../styles/App.css"; import { FaEnvelope, FaLock, FaEye, FaEyeSlash } from "react-icons/fa"; @@ -13,28 +13,20 @@ function Home() { const [formData, setFormData] = useState({ email: "", password: "", - role: "student", }); const [showPassword, setShowPassword] = useState(false); - const [role] = useState("student"); - - // Sync role into formData.role - useEffect(() => { - setFormData((prev) => ({ ...prev, role })); - }, [role]); const handleInputChange = (e) => { const { name, value } = e.target; - setFormData({ - ...formData, + setFormData((prev) => ({ + ...prev, [name]: value, - }); + })); }; const handleSubmit = async (e) => { e.preventDefault(); - const { email: ouEmail, password, role } = formData; @@ -55,7 +47,7 @@ function Home() { "Content-Type": "application/json", }, body: JSON.stringify({ ouEmail, password, role }), - }, + } ); const data = await response.json(); @@ -64,43 +56,33 @@ function Home() { const user = data.user; if(role === "student"){ // Store only required fields - const limitedUserInfo = { - fullName: user.fullName, - id: user._id, - email:user.ouEmail - }; - - localStorage.setItem("ipmsUser", JSON.stringify(limitedUserInfo)); - navigate("/student-dashboard"); - }else if(role === "supervisor"){ + const limitedUserInfo = { + fullName: user.fullName, + id: user._id, + email:user.ouEmail + }; + + localStorage.setItem("ipmsUser", JSON.stringify(limitedUserInfo)); + navigate("/student-dashboard"); + } else if(role === "supervisor"){ Swal.fire({ icon: "success", title: "Login Successful 🌟", text: `Welcome back, ${role}!`, }); navigate("/supervisor-dashboard"); - }else{ + } else if (role === "coordinator") { Swal.fire({ icon: "success", title: "Login Successful 🌟", text: `Welcome back, ${role}!`, }); - navigate("/coordinator-dashboard"); - } - - - - // Swal.fire({ - // icon: "success", - // title: "Login Successful", - // text: `Welcome back, `, - // }); - - + navigate("/coordinator-dashboard"); } else { Swal.fire({ icon: "error", title: "Login Failed", + text: data.message || "Something went wrong", html: data.message + " " + (data.renewalLink ? `Please click here to request a new token.` @@ -125,9 +107,7 @@ function Home() {
-

- Welcome back -

+

Welcome back

@@ -146,10 +126,7 @@ function Home() { formData.role === r ? "selected" : "" }`} onClick={() => - setFormData({ - ...formData, - role: r, - }) + setFormData((prev) => ({ ...prev, role: r })) } > @@ -157,6 +134,16 @@ function Home() {

{r.charAt(0).toUpperCase() + r.slice(1)}

+
))}
@@ -164,9 +151,7 @@ function Home() {
@@ -206,15 +189,7 @@ function Home() {
-
+
+ + {/* Submissions Table */} +
+

Your Internship Submissions

+ {error &&
{error}
} + + + + + + + + + + + {submissions.map((req) => ( + + + + + + + ))} + +
CompanyStatusSubmitted OnActions
{req.workplace.name}{req.status}{new Date(req.createdAt).toLocaleDateString()} + {req.reminders?.length === 2 && !req.coordinatorResponded ? ( + <> + + + + ) : ( + Waiting + )} +
+
); }; diff --git a/client/src/styles/StudentDashboard.css b/client/src/styles/StudentDashboard.css index 71975b3e..086c76c9 100644 --- a/client/src/styles/StudentDashboard.css +++ b/client/src/styles/StudentDashboard.css @@ -68,4 +68,4 @@ .card-button:hover { background-color: #e6e6e6; } - \ No newline at end of file + diff --git a/server/controllers/approvalController.js b/server/controllers/approvalController.js index 3687d0d4..1830fc3f 100644 --- a/server/controllers/approvalController.js +++ b/server/controllers/approvalController.js @@ -8,8 +8,8 @@ const UserTokenRequest = require("../models/TokenRequest"); // Managing Supervisor Forms // // =========================================== // -exports.getSupervisorForms = async (req, res, filter) => { - try { +const getSupervisorForms = async (req, res, filter) => { + try { // ---------------------------- // Fetching A1 Form // ---------------------------- @@ -19,7 +19,7 @@ exports.getSupervisorForms = async (req, res, filter) => { const typedRequests = requests.map(req => ({ ...req.toObject(), // convert Mongoose doc to plain JS object form_type: "A1" // add the custom type - })); + })); // ---------------------------- // Fetching A2 Form @@ -31,7 +31,7 @@ exports.getSupervisorForms = async (req, res, filter) => { const typedReports = reports.map(report => ({ ...report.toObject(), // convert Mongoose doc to plain JS object form_type: "A2" // add the custom type - })); + })); // ---------------------------- // Fetching A3 Form @@ -43,8 +43,8 @@ exports.getSupervisorForms = async (req, res, filter) => { const typedEvaluations = evaluations.map(evaluation => ({ ...evaluation.toObject(), // convert Mongoose doc to plain JS object form_type: "A3" // add the custom type - })); - + })); + // ---------------------------- // Combine forms // ---------------------------- @@ -55,15 +55,15 @@ exports.getSupervisorForms = async (req, res, filter) => { // Send response res.status(200).json(allRequests); - } catch (err) { + } catch (err) { res.status(500).json({ - message: "Failed to fetch internship requests", + message: "Failed to fetch supervisor forms", error: err.message, }); - } + } } -exports.handleSupervisorFormAction = async (req, res, action) => { +const handleSupervisorFormAction = async (req, res, action) => { try { const form_type = req.params.type; const formId = req.params.id; @@ -105,9 +105,9 @@ exports.handleSupervisorFormAction = async (req, res, action) => { console.warn("⚠️ No student email found for form:", form._id); } else { const emailSubject = `Form ${action === "approve" ? "Approved" : "Rejected"}`; - let emailBody = `

Your ${form_type} form has been ${action}ed by the supervisor.

`; - if (comment) { - emailBody += `

Comment: ${comment}

`; + let emailBody = `

Your ${form_type} form has been ${action}ed by the supervisor.

`; + if (comment) { + emailBody += `

Comment: ${comment}

`; } } @@ -115,14 +115,14 @@ exports.handleSupervisorFormAction = async (req, res, action) => { const student = await UserTokenRequest.findById(student_id); const student_mail = student?.ouEmail || form?.interneeEmail; - try { - await EmailService.sendEmail({ - to: student_mail, - subject: emailSubject, - html: emailBody, - }); + try { + await EmailService.sendEmail({ + to: student_mail, + subject: emailSubject, + html: emailBody, + }); } catch (err) { - console.error("Email sending error:", err); + console.error("Email sending error:", err); } console.log("Email sent to:", student_mail); @@ -141,8 +141,8 @@ exports.handleSupervisorFormAction = async (req, res, action) => { // Coordinator Dashboard // // =========================================== // -exports.getCoordinatorRequests = async (req, res) => { - try { +const getCoordinatorRequests = async (req, res) => { + try { const requests = await InternshipRequest.find({ coordinator_status: "pending", }).populate("student", "userName email"); @@ -153,21 +153,23 @@ exports.getCoordinatorRequests = async (req, res) => { }; // Coordinator View Single Request -exports.getCoordinatorRequestDetails = async (req, res) => { +const getCoordinatorRequestDetails = async (req, res) => { try { const requestData = await InternshipRequest.findById(req.params.id).lean(); if (!requestData) { return res.status(404).json({ message: "Request not found" }); } - res.status(200).json({ requestData, supervisorStatus: "Not Submitted" }); + const supervisorStatus = requestData.supervisor_status || "Not Submitted"; + + res.status(200).json({ requestData, supervisorStatus }); } catch (err) { res.status(500).json({ message: "Failed to fetch details" }); } }; // Coordinator Approve Request -exports.coordinatorApproveRequest = async (req, res) => { +const coordinatorApproveRequest = async (req, res) => { try { const request = await InternshipRequest.findByIdAndUpdate( req.params.id, @@ -192,7 +194,7 @@ exports.coordinatorApproveRequest = async (req, res) => { }; // Coordinator Reject Request -exports.coordinatorRejectRequest = async (req, res) => { +const coordinatorRejectRequest = async (req, res) => { const { reason } = req.body; if (!reason) return res.status(400).json({ message: "Reason required" }); @@ -218,3 +220,145 @@ exports.coordinatorRejectRequest = async (req, res) => { res.status(500).json({ message: "Rejection failed", error: err.message }); } }; + +const coordinatorResendRequest = async (req, res) => { + try { + const submission = await InternshipRequest.findById(req.params.id); + if (!submission) + return res.status(404).json({ message: "Submission not found" }); + + submission.coordinator_reminder_count = 0; + submission.last_coordinator_reminder_at = new Date(); + submission.coordinator_status = "pending"; + await submission.save(); + + return res.status(200).json({ message: "Reminder cycle restarted." }); + } catch (error) { + console.error("Error in coordinatorResendRequest:", error); + return res.status(500).json({ message: "Server error while resending request." }); + } +}; + +const deleteStudentSubmission = async (req, res) => { + try { + const { id } = req.params; + const studentId = req.user._id; + + const submission = await InternshipRequest.findById(id); + if (!submission) + return res.status(404).json({ message: "Submission not found." }); + + if (submission.student.toString() !== studentId.toString()) { + return res.status(403).json({ message: "You are not authorized to delete this submission." }); + } + + if (submission.coordinator_status !== "pending") { + return res.status(400).json({ message: "Submission already reviewed. Cannot delete." }); + } + + await InternshipRequest.findByIdAndDelete(id); + return res.status(200).json({ message: "Submission successfully deleted by student." }); + } catch (err) { + console.error("Error deleting student submission:", err); + return res.status(500).json({ message: "Internal server error." }); + } +}; + +const getStudentSubmissions = async (req, res) => { + try { + const studentId = req.user._id; + const submissions = await InternshipRequest.find({ student: studentId }).sort({ createdAt: -1 }); + res.status(200).json(submissions); + } catch (error) { + console.error("Error fetching student submissions:", error); + res.status(500).json({ message: "Failed to fetch submissions." }); + } +}; + +const getPendingSubmissions = async (req, res) => { + try { + const pendingRequests = await InternshipRequest.find({ + supervisor_status: "pending", + }).populate("student", "fullName ouEmail"); + + res.status(200).json(pendingRequests); + } catch (err) { + res.status(500).json({ + message: "Failed to fetch pending supervisor submissions", + error: err.message, + }); + } +}; + +const approveSubmission = async (req, res) => { + const { id } = req.params; + const { comment } = req.body; + try { + const request = await InternshipRequest.findByIdAndUpdate( + id, + { supervisor_status: "approved", supervisor_comment: comment || "" }, + { new: true } + ); + if (!request) return res.status(404).json({ message: "Submission not found" }); + + res.json({ message: "Submission approved", updated: request }); + } catch (err) { + res.status(500).json({ message: "Approval failed", error: err.message }); + } +}; + +const rejectSubmission = async (req, res) => { + const { id } = req.params; + const { comment } = req.body; + try { + const request = await InternshipRequest.findByIdAndUpdate( + id, + { supervisor_status: "rejected", supervisor_comment: comment || "" }, + { new: true } + ); + if (!request) return res.status(404).json({ message: "Submission not found" }); + + res.json({ message: "Submission rejected", updated: request }); + } catch (err) { + res.status(500).json({ message: "Rejection failed", error: err.message }); + } +}; + +const deleteStalledSubmission = async (req, res) => { + try { + const { id } = req.params; + + const submission = await InternshipRequest.findById(id); + if (!submission) { + return res.status(404).json({ message: "Submission not found." }); + } + + if (submission.coordinator_status !== "pending") { + return res.status(400).json({ message: "Submission already reviewed. Cannot delete." }); + } + + await InternshipRequest.findByIdAndDelete(id); + + return res.status(200).json({ message: "Submission deleted successfully." }); + } catch (error) { + console.error("Error deleting submission:", error); + return res.status(500).json({ message: "Internal server error" }); + } +}; + + +// module.exports = { +// getCoordinatorRequests, +// getCoordinatorRequestDetails, +// coordinatorApproveRequest, +// coordinatorRejectRequest, +// coordinatorResendRequest, +// deleteStudentSubmission, +// getStudentSubmissions, +// getPendingSubmissions, +// getSupervisorForms, +// handleSupervisorFormAction, +// approveSubmission, +// rejectSubmission, +// deleteStalledSubmission, +// }; diff --git a/server/jobs/reminderEmail.js b/server/jobs/reminderEmail.js index c0435d25..ae76dd1f 100644 --- a/server/jobs/reminderEmail.js +++ b/server/jobs/reminderEmail.js @@ -1,5 +1,4 @@ const emailService = require("../services/emailService"); -const Submission = require("../models/InternshipRequest"); const NotificationLog = require("../models/NotifLog"); const User = require("../models/User"); const WeeklyReport = require("../models/WeeklyReport"); @@ -9,57 +8,68 @@ const UserTokenRequest = require("../models/TokenRequest"); const logger = require("../utils/logger"); const dayjs = require("dayjs"); -// Coordinator reminder: weekly report reviewed by supervisor but not yet commented by coordinator +// ================= Coordinator Reminder ================= const coordinatorReminder = async () => { const now = dayjs(); + const fiveWorkingDays = now.subtract(7, "day").toDate(); + try { - const supervisorReviews = await SupervisorReview.find({}); - - for (const review of supervisorReviews) { - const { studentId, weeks } = review; - const reports = await WeeklyReport.find({ - studentId, - week: { $in: weeks }, - }); - - const allCoordinatorCommentsMissing = reports.every( - (r) => !r.coordinatorComments || r.coordinatorComments.trim() === "" - ); - - if (!allCoordinatorCommentsMissing) continue; - - const coordinatorEmail = reports[0]?.coordinatorEmail; - const studentEmail = reports[0]?.email; - - const internship = await InternshipRequest.findOne({ - email: studentEmail, - }); - if (!internship || dayjs().isAfter(dayjs(internship.endDate))) continue; - - await emailService.sendEmail({ - to: coordinatorEmail, - subject: `Reminder: Coordinator Review Pending (Weeks ${weeks.join( - ", " - )})`, - html: `

Supervisor has reviewed weeks ${weeks.join( - ", " - )}.

-

Please add your coordinator comments in IPMS dashboard before the internship ends.

`, - text: `Reminder to review weeks ${weeks.join(", ")} as coordinator.`, - }); - - logger.info( - `[Reminder Sent] Coordinator: "${coordinatorEmail}" for weeks: ${weeks.join( - ", " - )}` - ); + const pendingSubs = await InternshipRequest.find({ + coordinator_status: "pending", + supervisor_status: "approved", + createdAt: { $lt: fiveWorkingDays }, + }); + + for (const submission of pendingSubs) { + const student = await User.findById(submission.student_id); + const coordinator = await User.findById(submission.coordinator_id); + + const reminderCount = submission.coordinator_reminder_count || 0; + const lastReminded = submission.last_coordinator_reminder_at || submission.createdAt; + const nextReminderDue = dayjs(lastReminded).add(5, "day"); + const shouldRemindAgain = now.isAfter(nextReminderDue); + + if (reminderCount >= 2 && shouldRemindAgain && !submission.studentNotified) { + await emailService.sendEmail({ + to: student.email, + subject: `Coordinator Not Responding for "${submission.name}"`, + html: `

Your submission "${submission.name}" has not been approved by the coordinator even after 2 reminders.

+

You can now choose to resend or delete the request.

`, + text: `Your submission "${submission.name}" is still awaiting coordinator approval.`, + }); + + await NotificationLog.create({ + submissionId: submission._id, + type: "studentEscalation", + recipientEmail: student.email, + message: `Student notified about stalled coordinator approval for "${submission.name}"`, + }); + + submission.studentNotified = true; + await submission.save(); + + logger.info(`🔔 Escalation: student notified for "${submission.name}"`); + } else if (shouldRemindAgain) { + await emailService.sendEmail({ + to: coordinator.email, + subject: `Reminder: Please Approve Submission "${submission.name}"`, + html: `

This is a reminder to review and approve the internship submission by ${submission.student_name}.

`, + text: `Reminder to approve submission "${submission.name}".`, + }); + + submission.coordinator_reminder_count = reminderCount + 1; + submission.last_coordinator_reminder_at = new Date(); + await submission.save(); + + logger.info(`📧 Reminder sent to coordinator for "${submission.name}"`); + } } } catch (err) { - logger.error("[CoordinatorReminder Error]:", err.message || err); + logger.error("❌ Error in coordinatorReminder:", err.message); } }; -// Utility to get all forms of type A1, A2, A3 +// ================= Supervisor Reminder ================= const getAllForms = async (filter = {}) => { const models = { A1: require("../models/InternshipRequest"), @@ -67,24 +77,20 @@ const getAllForms = async (filter = {}) => { A3: require("../models/Evaluation"), }; - const formPromises = Object.entries(models).map( - async ([form_type, Model]) => { - const results = await Model.find(filter); - return results; - } - ); + const formPromises = Object.entries(models).map(async ([form_type, Model]) => { + return await Model.find(filter); + }); const allResults = await Promise.all(formPromises); return allResults.flat(); }; -// Supervisor reminder: weekly progress reports pending review const supervisorReminder = async () => { const now = dayjs(); const fiveWorkingDaysAgo = now.subtract(7, "day").toDate(); try { - const pendingSubs = await Submission.find({ + const pendingSubs = await getAllForms({ supervisor_status: "pending", createdAt: { $lt: fiveWorkingDaysAgo }, }); diff --git a/server/middleware/authMiddleware.js b/server/middleware/authMiddleware.js index 6ea8cb9c..736b2f6e 100644 --- a/server/middleware/authMiddleware.js +++ b/server/middleware/authMiddleware.js @@ -1,9 +1,8 @@ const User = require("../models/User"); const UserTokenRequest = require("../models/TokenRequest"); -exports.isSupervisor = (req, res, next) => { - // const supervisor = Sup.find({$id: username}) - +// 🔹 Supervisor Middleware +const isSupervisor = (req, res, next) => { req.user = { role: "supervisor" }; // Mocking user role for demo if (req.user.role === "supervisor") { next(); @@ -12,42 +11,30 @@ exports.isSupervisor = (req, res, next) => { } }; -/* - // This is token management if we'll use it in the future -exports.isSupervisor = async (req, res, next) => { - try { - // Token management - const raw = req.headers.authorization?.split(" ")[1]; // "Bearer " - const token = raw.replace(/^"|"$/g, ""); // removes surrounding quotes - - if (!token) { - return res.status(401).json({ message: "No token provided" }); - } - - const tokenEntry = await UserTokenRequest.findOne({ token }); - if (!tokenEntry) { - return res.status(401).json({ message: "Invalid or expired token" }); - } - - if (tokenEntry.role !== "supervisor") { - return res.status(403).json({ message: "Access denied. Not a supervisor." }); - } - - req.user = tokenEntry; // make user info available to routes - next(); - } catch (err) { - console.error("Supervisor auth error:", err); - res.status(500).json({ message: "Internal server error" }); - } -}; -*/ - -exports.isCoordinator = (req, res, next) => { - req.user = { role: "coordinator" }; // Mocking role for now (or fetch from DB if implemented) - +// 🔹 Coordinator Middleware +const isCoordinator = (req, res, next) => { + req.user = { role: "coordinator" }; // Mocking user role for demo if (req.user.role === "coordinator") { next(); } else { res.status(403).json({ message: "Access denied. Not a coordinator." }); } }; + +// 🔹 Student Middleware +const isStudent = (req, res, next) => { + const ipmsUser = JSON.parse(req.headers["ipms-user"] || "{}"); + if (ipmsUser && ipmsUser.role === "student") { + req.user = ipmsUser; // Includes _id + next(); + } else { + res.status(403).json({ message: "Student access denied" }); + } +}; + +// Export all properly +module.exports = { + isSupervisor, + isCoordinator, + isStudent, +}; diff --git a/server/models/InternshipRequest.js b/server/models/InternshipRequest.js index 68dced0e..58232bbe 100644 --- a/server/models/InternshipRequest.js +++ b/server/models/InternshipRequest.js @@ -49,6 +49,20 @@ const formA1 = new mongoose.Schema({ required: true, enum: [1, 2, 3] }, + + requestedAt: { + type: Date, + default: Date.now, + }, + coordinatorResponded: { + type: Boolean, + default: false, + }, + studentNotified: { + type: Boolean, + default: false, + }, + startDate: { type: Date, required: true @@ -79,4 +93,6 @@ formA1.virtual("requiredHours").get(function() { return this.creditHours * 60; }) -module.exports = mongoose.models.InternshipRequest || mongoose.model("InternshipRequest", formA1); \ No newline at end of file +module.exports = + mongoose.models.InternshipRequest || + mongoose.model("InternshipRequest", formA1); diff --git a/server/routes/approvalRoutes.js b/server/routes/approvalRoutes.js index 0913ac09..f8fde20a 100644 --- a/server/routes/approvalRoutes.js +++ b/server/routes/approvalRoutes.js @@ -8,9 +8,26 @@ const { getCoordinatorRequestDetails, coordinatorApproveRequest, coordinatorRejectRequest, + getStudentSubmissions, + getPendingSubmissions, + coordinatorResendRequest, + deleteStalledSubmission, + deleteStudentSubmission, + rejectSubmission, + approveSubmission, } = require("../controllers/approvalController"); -const { isSupervisor, isCoordinator } = require("../middleware/authMiddleware"); +const { isSupervisor, isCoordinator, isStudent } = require("../middleware/authMiddleware"); + + +// Student API +router.get("/student/submissions", isStudent, getStudentSubmissions); +router.delete("/student/request/:id/delete", isStudent, deleteStudentSubmission); + +// Supervisor APIs +router.get("/submissions/pending", isSupervisor, getPendingSubmissions); +router.post("/submissions/:id/approve", isSupervisor, approveSubmission); +router.post("/submissions/:id/reject", isSupervisor, rejectSubmission); // =========================================== // // Supervisor Approval Routes // @@ -38,22 +55,15 @@ router.post("/supervisor/form/:type/:id/reject", isSupervisor, (req, res) => // Coordinator Approval Routes // // =========================================== // + // Coordinator APIs router.get("/coordinator/requests", isCoordinator, getCoordinatorRequests); -router.get( - "/coordinator/request/:id", - isCoordinator, - getCoordinatorRequestDetails -); -router.post( - "/coordinator/request/:id/approve", - isCoordinator, - coordinatorApproveRequest -); -router.post( - "/coordinator/request/:id/reject", - isCoordinator, - coordinatorRejectRequest -); + +router.get("/coordinator/request/:id", isCoordinator, getCoordinatorRequestDetails); +router.post("/coordinator/request/:id/approve", isCoordinator, coordinatorApproveRequest); +router.post("/coordinator/request/:id/reject", isCoordinator, coordinatorRejectRequest); +router.post("/coordinator/request/:id/resend", isCoordinator, coordinatorResendRequest); +router.delete("/coordinator/request/:id/delete", isCoordinator, deleteStalledSubmission); + module.exports = router; \ No newline at end of file diff --git a/server/routes/formRoutes.js b/server/routes/formRoutes.js index 8fe44896..65cb6aa9 100644 --- a/server/routes/formRoutes.js +++ b/server/routes/formRoutes.js @@ -92,4 +92,71 @@ router.post('/submit', async (req, res) => { } }); +router.get('/pending-requests', async (req, res) => { + try { + const pending = await InternshipRequest.find({ status: { $in: ['submitted', 'pending manual review'] } }); + res.json(pending); + } catch (err) { + console.error("Error fetching pending submissions:", err); + res.status(500).json({ message: "Server error" }); + } +}); + +router.post("/requests/:id/resend", async (req, res) => { + try { + const request = await InternshipRequest.findById(req.params.id); + if (!request) return res.status(404).json({ message: "Request not found" }); + + // Reset reminders + request.reminders = [new Date()]; + request.coordinatorResponded = false; + request.studentNotified = false; + await request.save(); + + // Send email to coordinator + await emailService.sendEmail({ + to: [ + request.internshipAdvisor.email, + request.student.email, + "coordinator@ipms.edu" + ], + subject: "Internship Request Resent", + html: ` +

Hello,

+

The student ${request.student.userName} has resent their internship approval request due to inactivity.

+

Please review and take necessary action.

+ ` + }); + + res.json({ message: "Request resent successfully" }); + } catch (err) { + console.error("Resend error:", err); + res.status(500).json({ message: "Failed to resend request" }); + } +}); +router.delete("/requests/:id", async (req, res) => { + try { + const deleted = await InternshipRequest.findByIdAndDelete(req.params.id); + if (!deleted) return res.status(404).json({ message: "Request not found" }); + res.json({ message: "Request deleted successfully" }); + } catch (err) { + console.error("Delete error:", err); + res.status(500).json({ message: "Failed to delete request" }); + } +}); +router.post("/student", async (req, res) => { + const { ouEmail } = req.body; + if (!ouEmail) return res.status(400).json({ message: "Missing email" }); + + try { + const request = await InternshipRequest.findOne({ "student.email": ouEmail }); + if (!request) return res.json({ approvalStatus: "not_submitted" }); + + return res.json({ approvalStatus: request.status || "draft" }); + } catch (err) { + console.error("Student route error:", err); + res.status(500).json({ message: "Server error" }); + } +}); + module.exports = router; diff --git a/server/utils/cronUtils.test.js b/server/utils/cronUtils.test.js index 346f41ad..53c12db1 100644 --- a/server/utils/cronUtils.test.js +++ b/server/utils/cronUtils.test.js @@ -1,3 +1,4 @@ +// cronUtils.test.js const cron = require("node-cron"); const logger = require("./logger"); const cronJobManager = require("./cronUtils"); @@ -14,86 +15,135 @@ jest.mock("./logger", () => ({ })); describe("cronUtils", () => { - const mockJobFunction = jest.fn().mockResolvedValue(); + let mockJobFunction; beforeEach(() => { + mockJobFunction = jest.fn().mockResolvedValue(); cron.validate.mockClear(); cron.schedule.mockClear(); logger.info.mockClear(); logger.warn.mockClear(); logger.error.mockClear(); - cronJobManager.stopAllJobs(); + cronJobManager.jobs.clear(); }); afterEach(() => { - cronJobManager.stopAllJobs(); jest.clearAllMocks(); }); - test("should create an instance of CronJobManager", () => { + it("create instance of CronJobManager", () => { expect(cronJobManager).toBeDefined(); - expect(cronJobManager.jobs instanceof Map).toBe(true); + expect(cronJobManager.jobs).toEqual(new Map()); + expect(cronJobManager.logger).toEqual(logger); }); - test("should register job with runOnInit = true", () => { - cron.validate.mockReturnValue(true); - cron.schedule.mockReturnValue({ stop: jest.fn() }); - - const result = cronJobManager.registerJob( - "Job1", - "*/1 * * * *", - mockJobFunction, - { runOnInit: true } - ); - - expect(result).toBe(true); - expect(logger.info).toHaveBeenCalledWith( - `Running job Job1 immediately on init` - ); + describe("registerJob", () => { + + beforeEach(() => { + cron.validate.mockClear(); + cron.schedule.mockClear(); + logger.info.mockClear(); + logger.warn.mockClear(); + logger.error.mockClear(); + }) + + 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).toHaveBeenCalledTimes(0); + }); + + 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"); + }); }); - test("should register job with runOnInit = false", () => { + it("stopJob stops a given job", () => { cron.validate.mockReturnValue(true); cron.schedule.mockReturnValue({ stop: jest.fn() }); - - const result = cronJobManager.registerJob( - "Job2", - "*/1 * * * *", - mockJobFunction - ); - - expect(result).toBe(true); + // 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"); }); - test("should not register job with invalid cron", () => { - cron.validate.mockReturnValue(false); - - const result = cronJobManager.registerJob("InvalidJob", "invalid", mockJobFunction); - - expect(result).toBe(false); - expect(logger.error).toHaveBeenCalledWith( - "Invalid cron expression: invalid" - ); - }); - - test("should stop a specific job", () => { + it("listJobs prints out jobs", () => { cron.validate.mockReturnValue(true); - const stopFn = jest.fn(); - cron.schedule.mockReturnValue({ stop: stopFn }); - - cronJobManager.registerJob("Job3", "*/1 * * * *", mockJobFunction); - cronJobManager.stopJob("Job3"); - - expect(stopFn).toHaveBeenCalled(); + // Create Job to list + cronJobManager.registerJob("testJob", "*/5 * * * *", mockJobFunction); + const result = cronJobManager.listJobs(); + expect(result[0].name).toBe("testJob"); }); - test("should list registered jobs", () => { - cron.validate.mockReturnValue(true); - cron.schedule.mockReturnValue({ stop: jest.fn() }); - - cronJobManager.registerJob("Job4", "*/1 * * * *", mockJobFunction); - - const jobs = cronJobManager.listJobs(); - expect(jobs.length).toBeGreaterThan(0); - }); }); From c86409ed912c1b886a9ec111df4972cbd0280c27 Mon Sep 17 00:00:00 2001 From: ICook094 Date: Sat, 26 Apr 2025 17:14:01 -0500 Subject: [PATCH 02/19] fix error in Home.js --- client/src/pages/Home.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/client/src/pages/Home.js b/client/src/pages/Home.js index 54e5e129..4eefac5c 100644 --- a/client/src/pages/Home.js +++ b/client/src/pages/Home.js @@ -78,16 +78,17 @@ function Home() { text: `Welcome back, ${role}!`, }); navigate("/coordinator-dashboard"); - } else { - Swal.fire({ - icon: "error", - title: "Login Failed", - text: data.message || "Something went wrong", - html: data.message + " " + - (data.renewalLink - ? `Please click here to request a new token.` - : "Something went wrong."), - }); + } else { + Swal.fire({ + icon: "error", + title: "Login Failed", + text: data.message || "Something went wrong", + html: data.message + " " + + (data.renewalLink + ? `Please click here to request a new token.` + : "Something went wrong."), + }); + } } } catch (error) { console.error("Login error:", error); From 99c1b426b2c42c2aaca7097ce7ef14c704f714ca Mon Sep 17 00:00:00 2001 From: ICook094 Date: Sat, 26 Apr 2025 19:05:57 -0500 Subject: [PATCH 03/19] Update approvalController --- server/controllers/approvalController.js | 146 +++++++++-------------- 1 file changed, 59 insertions(+), 87 deletions(-) diff --git a/server/controllers/approvalController.js b/server/controllers/approvalController.js index 1830fc3f..cf87481c 100644 --- a/server/controllers/approvalController.js +++ b/server/controllers/approvalController.js @@ -10,58 +10,37 @@ const UserTokenRequest = require("../models/TokenRequest"); const getSupervisorForms = async (req, res, filter) => { try { - // ---------------------------- - // Fetching A1 Form - // ---------------------------- - const requests = await InternshipRequest.find(filter) - .populate("_id", "fullName ouEmail soonerId"); - - const typedRequests = requests.map(req => ({ - ...req.toObject(), // convert Mongoose doc to plain JS object - form_type: "A1" // add the custom type + const InternshipRequest = require("../models/InternshipRequest"); + const WeeklyReport = require("../models/WeeklyReport"); + const Evaluation = require("../models/Evaluation"); + + const a1Forms = await InternshipRequest.find(filter).populate("student", "fullName ouEmail soonerId"); + const typedA1 = a1Forms.map((form) => ({ + ...form.toObject(), + form_type: "A1", })); - // ---------------------------- - // Fetching A2 Form - // ---------------------------- - const reports = await WeeklyReport.find(filter) - .populate("student_id", "fullName ouEmail soonerId"); - - // Adding custom type to A2 Form - const typedReports = reports.map(report => ({ - ...report.toObject(), // convert Mongoose doc to plain JS object - form_type: "A2" // add the custom type + const a2Forms = await WeeklyReport.find(filter).populate("student_id", "fullName ouEmail soonerId"); + const typedA2 = a2Forms.map((form) => ({ + ...form.toObject(), + form_type: "A2", })); - // ---------------------------- - // Fetching A3 Form - // ---------------------------- - const evaluations = await Evaluation.find(filter) - .populate("student_id", "fullName ouEmail soonerId"); - - // Adding custom type to A3 Form - const typedEvaluations = evaluations.map(evaluation => ({ - ...evaluation.toObject(), // convert Mongoose doc to plain JS object - form_type: "A3" // add the custom type + const a3Forms = await Evaluation.find(filter).populate("student_id", "fullName ouEmail soonerId"); + const typedA3 = a3Forms.map((form) => ({ + ...form.toObject(), + form_type: "A3", })); - // ---------------------------- - // Combine forms - // ---------------------------- - const allRequests = [...typedRequests, ...typedReports, ...typedEvaluations]; + const allForms = [...typedA1, ...typedA2, ...typedA3]; + allForms.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - // Sort by createdAt date - allRequests.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - - // Send response - res.status(200).json(allRequests); + return res.status(200).json(allForms); } catch (err) { - res.status(500).json({ - message: "Failed to fetch supervisor forms", - error: err.message, - }); + console.error("Error in getSupervisorForms:", err.message); + return res.status(500).json({ message: "Failed to fetch supervisor forms", error: err.message }); } -} +}; const handleSupervisorFormAction = async (req, res, action) => { try { @@ -89,26 +68,18 @@ const handleSupervisorFormAction = async (req, res, action) => { supervisor_comment: comment, }; - const form = await FormModel.findByIdAndUpdate(formId, update, { new: true }).populate("student_id", "userName email"); + const form = await FormModel.findByIdAndUpdate(formId, update, { new: true }) + .populate("student_id", "userName email"); if (!form) { return res.status(404).json({ message: "Form not found" }); } - const studentEmail = - form.student_id?.email || - form.interneeEmail || - form.studentEmail || - null; - - if (!studentEmail) { - console.warn("⚠️ No student email found for form:", form._id); - } else { - const emailSubject = `Form ${action === "approve" ? "Approved" : "Rejected"}`; + const studentEmail = form.student_id?.email || form.interneeEmail || form.studentEmail || null; + let emailSubject = `Form ${action === "approve" ? "Approved" : "Rejected"}`; let emailBody = `

Your ${form_type} form has been ${action}ed by the supervisor.

`; if (comment) { emailBody += `

Comment: ${comment}

`; - } } const student_id = form.student_id || form.internee_id || form.student; @@ -126,36 +97,31 @@ const handleSupervisorFormAction = async (req, res, action) => { } console.log("Email sent to:", student_mail); - - res.status(200).json({ - message: `Form ${action}ed successfully`, - updatedForm: form, - }); + res.status(200).json({ message: `Form ${action}ed successfully`, updatedForm: form }); } catch (err) { console.error("SupervisorFormAction error:", err); res.status(500).json({ message: "Error processing form", error: err.message }); } }; -// =========================================== // -// Coordinator Dashboard // -// =========================================== // - const getCoordinatorRequests = async (req, res) => { try { const requests = await InternshipRequest.find({ coordinator_status: "pending", }).populate("student", "userName email"); + res.status(200).json(requests); } catch (err) { res.status(500).json({ message: "Failed to fetch requests" }); } }; -// Coordinator View Single Request const getCoordinatorRequestDetails = async (req, res) => { try { - const requestData = await InternshipRequest.findById(req.params.id).lean(); + const requestData = await InternshipRequest.findById(req.params.id) + .populate("student", "userName email") + .lean(); + if (!requestData) { return res.status(404).json({ message: "Request not found" }); } @@ -168,19 +134,22 @@ const getCoordinatorRequestDetails = async (req, res) => { } }; -// Coordinator Approve Request const coordinatorApproveRequest = async (req, res) => { try { const request = await InternshipRequest.findByIdAndUpdate( req.params.id, - { coordinator_status: "approved" }, + { status: "approved" }, { new: true } - ); + ).populate("student", "userName email"); if (!request) { return res.status(404).json({ message: "Request not found" }); } + request.coordinator_status = "Approved"; + request.coordinator_comment = "Approved by Coordinator"; + await request.save(); + await EmailService.sendEmail({ to: request.student.email, subject: "Internship Request Approved", @@ -193,7 +162,6 @@ const coordinatorApproveRequest = async (req, res) => { } }; -// Coordinator Reject Request const coordinatorRejectRequest = async (req, res) => { const { reason } = req.body; if (!reason) return res.status(400).json({ message: "Reason required" }); @@ -201,14 +169,18 @@ const coordinatorRejectRequest = async (req, res) => { try { const request = await InternshipRequest.findByIdAndUpdate( req.params.id, - { coordinator_status: "rejected" }, + { status: "rejected" }, { new: true } - ); + ).populate("student", "userName email"); if (!request) { return res.status(404).json({ message: "Request not found" }); } + request.coordinator_status = "Rejected"; + request.coordinator_comment = reason; + await request.save(); + await EmailService.sendEmail({ to: request.student.email, subject: "Internship Request Rejected", @@ -347,18 +319,18 @@ const deleteStalledSubmission = async (req, res) => { }; -// module.exports = { -// getCoordinatorRequests, -// getCoordinatorRequestDetails, -// coordinatorApproveRequest, -// coordinatorRejectRequest, -// coordinatorResendRequest, -// deleteStudentSubmission, -// getStudentSubmissions, -// getPendingSubmissions, -// getSupervisorForms, -// handleSupervisorFormAction, -// approveSubmission, -// rejectSubmission, -// deleteStalledSubmission, -// }; +module.exports = { + getCoordinatorRequests, + getCoordinatorRequestDetails, + coordinatorApproveRequest, + coordinatorRejectRequest, + coordinatorResendRequest, + deleteStudentSubmission, + getStudentSubmissions, + getPendingSubmissions, + getSupervisorForms, + handleSupervisorFormAction, + approveSubmission, + rejectSubmission, + deleteStalledSubmission, +}; \ No newline at end of file From e1763a1f343368216dd76dd430a8f3130c0da6c5 Mon Sep 17 00:00:00 2001 From: ICook094 Date: Sun, 27 Apr 2025 12:13:48 -0500 Subject: [PATCH 04/19] Initial Weekly Progress Coordinator Additions --- .../pages/CoordinatorCumulativeReviewForm.js | 25 +++++++++++ client/src/pages/CoordinatorDashboard.js | 12 +++--- client/src/pages/StudentDashboard.jsx | 2 +- .../CoordinatorCumulativeReviewForm.css | 6 ++- server/controllers/reportController.js | 42 +++++++++++-------- server/jobs/reminderEmail.js | 7 +--- server/models/FormMetadata.js | 2 + server/models/InternshipRequest.js | 8 ---- server/models/WeeklyReport.js | 5 +-- server/routes/formRoutes.js | 4 +- 10 files changed, 70 insertions(+), 43 deletions(-) diff --git a/client/src/pages/CoordinatorCumulativeReviewForm.js b/client/src/pages/CoordinatorCumulativeReviewForm.js index 0dd496df..f96ecf86 100644 --- a/client/src/pages/CoordinatorCumulativeReviewForm.js +++ b/client/src/pages/CoordinatorCumulativeReviewForm.js @@ -47,6 +47,7 @@ const CoordinatorCumulativeReviewForm = () => { await axios.post(`${process.env.REACT_APP_API_URL}/api/reports/coordinator-comments`, { groupIndex: parseInt(groupIndex), + coordinator_status: "approved", comments: coordinatorComment.trim(), weeks, }); @@ -60,7 +61,28 @@ const CoordinatorCumulativeReviewForm = () => { Swal.fire("Error", "Failed to submit comment. Please try again.", "error"); } }; + + // Submit coordinator coordinator rejction + const handleReject = async () => { + try { + const weeks = reports.map((report) => report.week); // extract weeks + + await axios.post(`${process.env.REACT_APP_API_URL}/api/reports/coordinator-comments`, { + groupIndex: parseInt(groupIndex), + coordinator_status: "rejected", + comments: "", + weeks, + }); + + Swal.fire("Success", "Coordinator rejected weekly update FORM A2.", "success"); + localStorage.setItem("reviewedGroupIndex", groupIndex); // ✅ For dashboard refresh + navigate("/coordinator-dashboard"); + } catch (err) { + console.error("[CoordinatorCumulativeReviewForm] Failed to reject weekly report", err); + Swal.fire("Error", "Failed to reject weekly report. Please try again.", "error"); + } + }; if (loading) { return

Loading...

; @@ -108,6 +130,9 @@ const CoordinatorCumulativeReviewForm = () => {
+ diff --git a/client/src/pages/CoordinatorDashboard.js b/client/src/pages/CoordinatorDashboard.js index ed712155..27650c70 100644 --- a/client/src/pages/CoordinatorDashboard.js +++ b/client/src/pages/CoordinatorDashboard.js @@ -9,6 +9,7 @@ const CoordinatorDashboard = () => { const [requests, setRequests] = useState([]); const [loadingRequests, setLoadingRequests] = useState(true); + // == INTERNSHIP REQUESTS (FORM A1) == useEffect(() => { if (activeTab === "requests") { fetchInternshipRequests(); @@ -25,7 +26,8 @@ const CoordinatorDashboard = () => { setLoadingRequests(false); } }; - // Group D's Weekly Report Review Logic + + // == WEEKLY REPORT (FORM A2) == const [reportGroups, setReportGroups] = useState([]); const [loadingReports, setLoadingReports] = useState(true); @@ -85,10 +87,10 @@ const CoordinatorDashboard = () => { {requests.map(req => ( - {req.studentName} - {req.studentId} - {req.companyName} - {req.status} + {req.student.studentName} + {req.student_id} + {req.workplace.name} + {req.coordinator_status} ))} diff --git a/client/src/pages/StudentDashboard.jsx b/client/src/pages/StudentDashboard.jsx index eba9fbb0..0f3f50e8 100644 --- a/client/src/pages/StudentDashboard.jsx +++ b/client/src/pages/StudentDashboard.jsx @@ -169,7 +169,7 @@ const StudentDashboard = () => { {req.status} {new Date(req.createdAt).toLocaleDateString()} - {req.reminders?.length === 2 && !req.coordinatorResponded ? ( + {req.reminders?.length === 2 && !req.coordinator_responded ? ( <>
))} diff --git a/client/src/pages/StudentDashboard.jsx b/client/src/pages/StudentDashboard.jsx index 59c2747a..feaf1635 100644 --- a/client/src/pages/StudentDashboard.jsx +++ b/client/src/pages/StudentDashboard.jsx @@ -33,6 +33,54 @@ const StudentDashboard = () => { }, [ouEmail]); console.log(approvalStatus); + useEffect(() => { + const fetchSubmissions = async () => { + try { + const res = await fetch(`${backendUrl}/api/form/pending-requests`); + const data = await res.json(); + const studentSubmissions = data.filter( + (req) => + req?.student?.email?.toLowerCase().trim() === + user.email.toLowerCase().trim() + ); + setSubmissions(studentSubmissions); + } catch (err) { + console.error("Error fetching submissions", err); + setError("Unable to load your submissions right now."); + } + }; + + if (user?.email) fetchSubmissions(); + }, [backendUrl, user?.email]); + + const handleResend = async (id) => { + try { + const res = await fetch(`${backendUrl}/api/form/requests/${id}/resend`, { + method: "POST", + }); + const data = await res.json(); + alert(data.message); + } catch { + alert("Failed to resend request."); + } + }; + + const handleDelete = async (id) => { + const confirmDelete = window.confirm("Delete this request?"); + if (!confirmDelete) return; + + try { + const res = await fetch(`${backendUrl}/api/form/requests/${id}`, { + method: "DELETE", + }); + const data = await res.json(); + alert(data.message); + setSubmissions((prev) => prev.filter((sub) => sub._id !== id)); + } catch { + alert("Failed to delete request."); + } + }; + return (
diff --git a/server/controllers/approvalController.js b/server/controllers/approvalController.js index 3687d0d4..96f1b1de 100644 --- a/server/controllers/approvalController.js +++ b/server/controllers/approvalController.js @@ -8,62 +8,41 @@ const UserTokenRequest = require("../models/TokenRequest"); // Managing Supervisor Forms // // =========================================== // -exports.getSupervisorForms = async (req, res, filter) => { - try { - // ---------------------------- - // Fetching A1 Form - // ---------------------------- - const requests = await InternshipRequest.find(filter) - .populate("_id", "fullName ouEmail soonerId"); - - const typedRequests = requests.map(req => ({ - ...req.toObject(), // convert Mongoose doc to plain JS object - form_type: "A1" // add the custom type - })); - - // ---------------------------- - // Fetching A2 Form - // ---------------------------- - const reports = await WeeklyReport.find(filter) - .populate("student_id", "fullName ouEmail soonerId"); - - // Adding custom type to A2 Form - const typedReports = reports.map(report => ({ - ...report.toObject(), // convert Mongoose doc to plain JS object - form_type: "A2" // add the custom type - })); - - // ---------------------------- - // Fetching A3 Form - // ---------------------------- - const evaluations = await Evaluation.find(filter) - .populate("student_id", "fullName ouEmail soonerId"); - - // Adding custom type to A3 Form - const typedEvaluations = evaluations.map(evaluation => ({ - ...evaluation.toObject(), // convert Mongoose doc to plain JS object - form_type: "A3" // add the custom type - })); - - // ---------------------------- - // Combine forms - // ---------------------------- - const allRequests = [...typedRequests, ...typedReports, ...typedEvaluations]; - - // Sort by createdAt date - allRequests.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - - // Send response - res.status(200).json(allRequests); - } catch (err) { - res.status(500).json({ - message: "Failed to fetch internship requests", - error: err.message, - }); - } -} +const getSupervisorForms = async (req, res, filter) => { + try { + const InternshipRequest = require("../models/InternshipRequest"); + const WeeklyReport = require("../models/WeeklyReport"); + const Evaluation = require("../models/Evaluation"); + + const a1Forms = await InternshipRequest.find(filter).populate("student", "fullName ouEmail soonerId"); + const typedA1 = a1Forms.map((form) => ({ + ...form.toObject(), + form_type: "A1", + })); + + const a2Forms = await WeeklyReport.find(filter).populate("student_id", "fullName ouEmail soonerId"); + const typedA2 = a2Forms.map((form) => ({ + ...form.toObject(), + form_type: "A2", + })); + + const a3Forms = await Evaluation.find(filter).populate("student_id", "fullName ouEmail soonerId"); + const typedA3 = a3Forms.map((form) => ({ + ...form.toObject(), + form_type: "A3", + })); + + const allForms = [...typedA1, ...typedA2, ...typedA3]; + allForms.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + + return res.status(200).json(allForms); + } catch (err) { + console.error("Error in getSupervisorForms:", err.message); + return res.status(500).json({ message: "Failed to fetch supervisor forms", error: err.message }); + } +}; -exports.handleSupervisorFormAction = async (req, res, action) => { +const handleSupervisorFormAction = async (req, res, action) => { try { const form_type = req.params.type; const formId = req.params.id; @@ -89,96 +68,88 @@ exports.handleSupervisorFormAction = async (req, res, action) => { supervisor_comment: comment, }; - const form = await FormModel.findByIdAndUpdate(formId, update, { new: true }).populate("student_id", "userName email"); + const form = await FormModel.findByIdAndUpdate(formId, update, { new: true }) + .populate("student_id", "userName email"); if (!form) { return res.status(404).json({ message: "Form not found" }); } - const studentEmail = - form.student_id?.email || - form.interneeEmail || - form.studentEmail || - null; - - if (!studentEmail) { - console.warn("⚠️ No student email found for form:", form._id); - } else { - const emailSubject = `Form ${action === "approve" ? "Approved" : "Rejected"}`; - let emailBody = `

Your ${form_type} form has been ${action}ed by the supervisor.

`; - if (comment) { - emailBody += `

Comment: ${comment}

`; - } + const studentEmail = form.student_id?.email || form.interneeEmail || form.studentEmail || null; + let emailSubject = `Form ${action === "approve" ? "Approved" : "Rejected"}`; + let emailBody = `

Your ${form_type} form has been ${action}ed by the supervisor.

`; + if (comment) { + emailBody += `

Comment: ${comment}

`; } const student_id = form.student_id || form.internee_id || form.student; const student = await UserTokenRequest.findById(student_id); const student_mail = student?.ouEmail || form?.interneeEmail; - try { - await EmailService.sendEmail({ - to: student_mail, - subject: emailSubject, - html: emailBody, - }); + try { + await EmailService.sendEmail({ + to: student_mail, + subject: emailSubject, + html: emailBody, + }); } catch (err) { - console.error("Email sending error:", err); + console.error("Email sending error:", err); } console.log("Email sent to:", student_mail); - - res.status(200).json({ - message: `Form ${action}ed successfully`, - updatedForm: form, - }); + res.status(200).json({ message: `Form ${action}ed successfully`, updatedForm: form }); } catch (err) { console.error("SupervisorFormAction error:", err); res.status(500).json({ message: "Error processing form", error: err.message }); } }; -// =========================================== // -// Coordinator Dashboard // -// =========================================== // - -exports.getCoordinatorRequests = async (req, res) => { - try { +const getCoordinatorRequests = async (req, res) => { + try { const requests = await InternshipRequest.find({ coordinator_status: "pending", }).populate("student", "userName email"); + res.status(200).json(requests); } catch (err) { res.status(500).json({ message: "Failed to fetch requests" }); } }; -// Coordinator View Single Request -exports.getCoordinatorRequestDetails = async (req, res) => { +const getCoordinatorRequestDetails = async (req, res) => { try { - const requestData = await InternshipRequest.findById(req.params.id).lean(); + const requestData = await InternshipRequest.findById(req.params.id) + .populate("student", "userName email") + .lean(); + if (!requestData) { return res.status(404).json({ message: "Request not found" }); } - res.status(200).json({ requestData, supervisorStatus: "Not Submitted" }); + const supervisorStatus = requestData.supervisor_status || "Not Submitted"; + + res.status(200).json({ requestData, supervisorStatus }); } catch (err) { res.status(500).json({ message: "Failed to fetch details" }); } }; -// Coordinator Approve Request -exports.coordinatorApproveRequest = async (req, res) => { +const coordinatorApproveRequest = async (req, res) => { try { const request = await InternshipRequest.findByIdAndUpdate( req.params.id, - { coordinator_status: "approved" }, + { status: "approved" }, { new: true } - ); + ).populate("student", "userName email"); if (!request) { return res.status(404).json({ message: "Request not found" }); } + request.coordinator_status = "Approved"; + request.coordinator_comment = "Approved by Coordinator"; + await request.save(); + await EmailService.sendEmail({ to: request.student.email, subject: "Internship Request Approved", @@ -191,22 +162,25 @@ exports.coordinatorApproveRequest = async (req, res) => { } }; -// Coordinator Reject Request -exports.coordinatorRejectRequest = async (req, res) => { +const coordinatorRejectRequest = async (req, res) => { const { reason } = req.body; if (!reason) return res.status(400).json({ message: "Reason required" }); try { const request = await InternshipRequest.findByIdAndUpdate( req.params.id, - { coordinator_status: "rejected" }, + { status: "rejected" }, { new: true } - ); + ).populate("student", "userName email"); if (!request) { return res.status(404).json({ message: "Request not found" }); } + request.coordinator_status = "Rejected"; + request.coordinator_comment = reason; + await request.save(); + await EmailService.sendEmail({ to: request.student.email, subject: "Internship Request Rejected", @@ -218,3 +192,145 @@ exports.coordinatorRejectRequest = async (req, res) => { res.status(500).json({ message: "Rejection failed", error: err.message }); } }; + +const coordinatorResendRequest = async (req, res) => { + try { + const submission = await InternshipRequest.findById(req.params.id); + if (!submission) + return res.status(404).json({ message: "Submission not found" }); + + submission.coordinator_reminder_count = 0; + submission.last_coordinator_reminder_at = new Date(); + submission.coordinator_status = "pending"; + await submission.save(); + + return res.status(200).json({ message: "Reminder cycle restarted." }); + } catch (error) { + console.error("Error in coordinatorResendRequest:", error); + return res.status(500).json({ message: "Server error while resending request." }); + } +}; + +const deleteStudentSubmission = async (req, res) => { + try { + const { id } = req.params; + const studentId = req.user._id; + + const submission = await InternshipRequest.findById(id); + if (!submission) + return res.status(404).json({ message: "Submission not found." }); + + if (submission.student.toString() !== studentId.toString()) { + return res.status(403).json({ message: "You are not authorized to delete this submission." }); + } + + if (submission.coordinator_status !== "pending") { + return res.status(400).json({ message: "Submission already reviewed. Cannot delete." }); + } + + await InternshipRequest.findByIdAndDelete(id); + return res.status(200).json({ message: "Submission successfully deleted by student." }); + } catch (err) { + console.error("Error deleting student submission:", err); + return res.status(500).json({ message: "Internal server error." }); + } +}; + +const getStudentSubmissions = async (req, res) => { + try { + const studentId = req.user._id; + const submissions = await InternshipRequest.find({ student: studentId }).sort({ createdAt: -1 }); + res.status(200).json(submissions); + } catch (error) { + console.error("Error fetching student submissions:", error); + res.status(500).json({ message: "Failed to fetch submissions." }); + } +}; + +const getPendingSubmissions = async (req, res) => { + try { + const pendingRequests = await InternshipRequest.find({ + supervisor_status: "pending", + }).populate("student", "fullName ouEmail"); + + res.status(200).json(pendingRequests); + } catch (err) { + res.status(500).json({ + message: "Failed to fetch pending supervisor submissions", + error: err.message, + }); + } +}; + +const approveSubmission = async (req, res) => { + const { id } = req.params; + const { comment } = req.body; + try { + const request = await InternshipRequest.findByIdAndUpdate( + id, + { supervisor_status: "approved", supervisor_comment: comment || "" }, + { new: true } + ); + if (!request) return res.status(404).json({ message: "Submission not found" }); + + res.json({ message: "Submission approved", updated: request }); + } catch (err) { + res.status(500).json({ message: "Approval failed", error: err.message }); + } +}; + +const rejectSubmission = async (req, res) => { + const { id } = req.params; + const { comment } = req.body; + try { + const request = await InternshipRequest.findByIdAndUpdate( + id, + { supervisor_status: "rejected", supervisor_comment: comment || "" }, + { new: true } + ); + if (!request) return res.status(404).json({ message: "Submission not found" }); + + res.json({ message: "Submission rejected", updated: request }); + } catch (err) { + res.status(500).json({ message: "Rejection failed", error: err.message }); + } +}; + +const deleteStalledSubmission = async (req, res) => { + try { + const { id } = req.params; + + const submission = await InternshipRequest.findById(id); + if (!submission) { + return res.status(404).json({ message: "Submission not found." }); + } + + if (submission.coordinator_status !== "pending") { + return res.status(400).json({ message: "Submission already reviewed. Cannot delete." }); + } + + await InternshipRequest.findByIdAndDelete(id); + + return res.status(200).json({ message: "Submission deleted successfully." }); + } catch (error) { + console.error("Error deleting submission:", error); + return res.status(500).json({ message: "Internal server error" }); + } +}; + + +module.exports = { + getCoordinatorRequests, + getCoordinatorRequestDetails, + coordinatorApproveRequest, + coordinatorRejectRequest, + coordinatorResendRequest, + deleteStudentSubmission, + getStudentSubmissions, + getPendingSubmissions, + getSupervisorForms, + handleSupervisorFormAction, + approveSubmission, + rejectSubmission, + deleteStalledSubmission, +}; diff --git a/server/jobs/reminderEmail.js b/server/jobs/reminderEmail.js index e7e553d3..d323247b 100644 --- a/server/jobs/reminderEmail.js +++ b/server/jobs/reminderEmail.js @@ -88,7 +88,7 @@ const sendCoordinatorReminder = async (subs, nextDueIn, now) => { } }; -// Utility to get all forms of type A1, A2, A3 +// ================= Supervisor Reminder ================= const getAllForms = async (filter = {}) => { const models = { A1: require("../models/InternshipRequest"), @@ -96,26 +96,22 @@ const getAllForms = async (filter = {}) => { A3: require("../models/Evaluation"), }; - const formPromises = Object.entries(models).map( - async ([form_type, Model]) => { - const results = await Model.find(filter); - return results; - } - ); + const formPromises = Object.entries(models).map(async ([form_type, Model]) => { + return await Model.find(filter); + }); const allResults = await Promise.all(formPromises); return allResults.flat(); }; -// Supervisor reminder: weekly progress reports pending review const supervisorReminder = async () => { const now = dayjs(); - const fiveWorkingDaysAgo = now.subtract(7, "day").toDate(); + const fiveWorkingDays = now.subtract(7, "day").toDate(); try { - const pendingSubs = await Submission.find({ + const pendingSubs = await getAllForms({ supervisor_status: "pending", - createdAt: { $lt: fiveWorkingDaysAgo }, + last_supervisor_reminder_at: { $lt: fiveWorkingDays }, }); const supervisors = await UserTokenRequest.find({ @@ -124,40 +120,38 @@ const supervisorReminder = async () => { }); for (const submission of pendingSubs) { - const student = await User.findById(submission.student_id); - const supervisor = await User.findById(submission.supervisor_id); if (!student || !supervisor) continue; - + + const student = await UserTokenRequest.findById(submission.student_id); const reminderCount = submission.supervisor_reminder_count || 0; - const lastReminded = - submission.last_supervisor_reminder_at || submission.createdAt; + const lastReminded = submission.last_supervisor_reminder_at || submission.createdAt; const nextReminderDue = dayjs(lastReminded).add(5, "day"); const shouldRemindAgain = now.isAfter(nextReminderDue); if (reminderCount >= 2 && shouldRemindAgain) { await emailService.sendEmail({ - to: student.email, - subject: `Supervisor Not Responding for "${submission.name}"`, - html: `

Your submission "${submission.name}" has not been reviewed by your supervisor after multiple reminders.

-

Please consider resending or deleting the request.

`, - text: `Your submission "${submission.name}" is still awaiting supervisor review.`, + to: student.ouEmail, + subject: `Supervisor Not Responding for "${submission._id}"`, + html: `

Your submission "${submission._id}" has not been reviewed by the supervisor after multiple reminders.

+

Please consider resending the form or deleting the request.

`, + text: `Your submission "${submission._id}" is still awaiting supervisor review.`, }); await NotificationLog.create({ - submissionId: submission._id, + submission_id: submission._id, type: "studentEscalation", - recipientEmail: student.email, - message: `Student notified about supervisor inaction for "${submission.name}".`, + recipient_email: student.ouEmail, + message: `Student notified about supervisor status on: "${submission._id}"`, }); - logger.info(`[Escalated] Student notified for: "${submission.name}"`); + logger.info(`Returned to student for resubmit/delete: "${submission._id}"`); } else if (shouldRemindAgain) { - for (const sup of supervisors) { + for (const supervisor of supervisors) { await emailService.sendEmail({ - to: sup.ouEmail, + to: supervisor.ouEmail, subject: `Reminder: Please Review Submission "${submission._id}"`, - html: `

This is a reminder to review the submission by ${student.email}.

`, + html: `

This is a reminder to review the submission by ${student.ouEmail}.

`, text: `Reminder to review submission "${submission._id}".`, }); } @@ -170,13 +164,12 @@ const supervisorReminder = async () => { } catch (err) { logger.error(`Failed to save submission: ${err.message}`); } - logger.info( - `[Reminder Sent] Supervisor: "${supervisor.email}" for "${submission.name}"` - ); + + logger.info(`[Reminder Sent] Supervisor: "${supervisor.email}" for "${submission.name}"`); } } } catch (err) { - logger.error("[SupervisorReminder Error]:", err.message || err); + logger.error("Error in supervisorReminder:", err.message || err); } }; diff --git a/server/middleware/authMiddleware.js b/server/middleware/authMiddleware.js index 6ea8cb9c..736b2f6e 100644 --- a/server/middleware/authMiddleware.js +++ b/server/middleware/authMiddleware.js @@ -1,9 +1,8 @@ const User = require("../models/User"); const UserTokenRequest = require("../models/TokenRequest"); -exports.isSupervisor = (req, res, next) => { - // const supervisor = Sup.find({$id: username}) - +// 🔹 Supervisor Middleware +const isSupervisor = (req, res, next) => { req.user = { role: "supervisor" }; // Mocking user role for demo if (req.user.role === "supervisor") { next(); @@ -12,42 +11,30 @@ exports.isSupervisor = (req, res, next) => { } }; -/* - // This is token management if we'll use it in the future -exports.isSupervisor = async (req, res, next) => { - try { - // Token management - const raw = req.headers.authorization?.split(" ")[1]; // "Bearer " - const token = raw.replace(/^"|"$/g, ""); // removes surrounding quotes - - if (!token) { - return res.status(401).json({ message: "No token provided" }); - } - - const tokenEntry = await UserTokenRequest.findOne({ token }); - if (!tokenEntry) { - return res.status(401).json({ message: "Invalid or expired token" }); - } - - if (tokenEntry.role !== "supervisor") { - return res.status(403).json({ message: "Access denied. Not a supervisor." }); - } - - req.user = tokenEntry; // make user info available to routes - next(); - } catch (err) { - console.error("Supervisor auth error:", err); - res.status(500).json({ message: "Internal server error" }); - } -}; -*/ - -exports.isCoordinator = (req, res, next) => { - req.user = { role: "coordinator" }; // Mocking role for now (or fetch from DB if implemented) - +// 🔹 Coordinator Middleware +const isCoordinator = (req, res, next) => { + req.user = { role: "coordinator" }; // Mocking user role for demo if (req.user.role === "coordinator") { next(); } else { res.status(403).json({ message: "Access denied. Not a coordinator." }); } }; + +// 🔹 Student Middleware +const isStudent = (req, res, next) => { + const ipmsUser = JSON.parse(req.headers["ipms-user"] || "{}"); + if (ipmsUser && ipmsUser.role === "student") { + req.user = ipmsUser; // Includes _id + next(); + } else { + res.status(403).json({ message: "Student access denied" }); + } +}; + +// Export all properly +module.exports = { + isSupervisor, + isCoordinator, + isStudent, +}; diff --git a/server/models/InternshipRequest.js b/server/models/InternshipRequest.js index 2d71a2e9..a4649818 100644 --- a/server/models/InternshipRequest.js +++ b/server/models/InternshipRequest.js @@ -22,28 +22,12 @@ const Task = new mongoose.Schema({ }); const formA1 = new mongoose.Schema({ - // student: { - // type: ObjectId, - // required: true, - // ref: 'UserTokenRequest' - // }, - student:{ - name:{ - type: String, - required: true, - }, - email:{ - unique: true, - type: String, - required:true, - }, - }, ...formMetadata, - // student: { - // type: ObjectId, - // required: true, - // ref: 'UserTokenRequest' - // }, + student: { + type: ObjectId, + required: true, + ref: 'UserTokenRequest' + }, workplace: { name: { type: String, @@ -65,10 +49,12 @@ const formA1 = new mongoose.Schema({ required: true, enum: [1, 2, 3] }, + requestedAt: { type: Date, default: Date.now, }, + startDate: { type: Date, required: true @@ -99,4 +85,6 @@ formA1.virtual("requiredHours").get(function() { return this.creditHours * 60; }) -module.exports = mongoose.models.InternshipRequest || mongoose.model("InternshipRequest", formA1); \ No newline at end of file +module.exports = + mongoose.models.InternshipRequest || + mongoose.model("InternshipRequest", formA1); diff --git a/server/routes/approvalRoutes.js b/server/routes/approvalRoutes.js index 0913ac09..f8fde20a 100644 --- a/server/routes/approvalRoutes.js +++ b/server/routes/approvalRoutes.js @@ -8,9 +8,26 @@ const { getCoordinatorRequestDetails, coordinatorApproveRequest, coordinatorRejectRequest, + getStudentSubmissions, + getPendingSubmissions, + coordinatorResendRequest, + deleteStalledSubmission, + deleteStudentSubmission, + rejectSubmission, + approveSubmission, } = require("../controllers/approvalController"); -const { isSupervisor, isCoordinator } = require("../middleware/authMiddleware"); +const { isSupervisor, isCoordinator, isStudent } = require("../middleware/authMiddleware"); + + +// Student API +router.get("/student/submissions", isStudent, getStudentSubmissions); +router.delete("/student/request/:id/delete", isStudent, deleteStudentSubmission); + +// Supervisor APIs +router.get("/submissions/pending", isSupervisor, getPendingSubmissions); +router.post("/submissions/:id/approve", isSupervisor, approveSubmission); +router.post("/submissions/:id/reject", isSupervisor, rejectSubmission); // =========================================== // // Supervisor Approval Routes // @@ -38,22 +55,15 @@ router.post("/supervisor/form/:type/:id/reject", isSupervisor, (req, res) => // Coordinator Approval Routes // // =========================================== // + // Coordinator APIs router.get("/coordinator/requests", isCoordinator, getCoordinatorRequests); -router.get( - "/coordinator/request/:id", - isCoordinator, - getCoordinatorRequestDetails -); -router.post( - "/coordinator/request/:id/approve", - isCoordinator, - coordinatorApproveRequest -); -router.post( - "/coordinator/request/:id/reject", - isCoordinator, - coordinatorRejectRequest -); + +router.get("/coordinator/request/:id", isCoordinator, getCoordinatorRequestDetails); +router.post("/coordinator/request/:id/approve", isCoordinator, coordinatorApproveRequest); +router.post("/coordinator/request/:id/reject", isCoordinator, coordinatorRejectRequest); +router.post("/coordinator/request/:id/resend", isCoordinator, coordinatorResendRequest); +router.delete("/coordinator/request/:id/delete", isCoordinator, deleteStalledSubmission); + module.exports = router; \ No newline at end of file From ff55490d4708bb424f2dceee595e71387642eaea Mon Sep 17 00:00:00 2001 From: ICook094 Date: Sun, 27 Apr 2025 14:28:28 -0500 Subject: [PATCH 09/19] fix merge error --- client/src/pages/StudentDashboard.jsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/client/src/pages/StudentDashboard.jsx b/client/src/pages/StudentDashboard.jsx index feaf1635..a2bf0001 100644 --- a/client/src/pages/StudentDashboard.jsx +++ b/client/src/pages/StudentDashboard.jsx @@ -4,34 +4,33 @@ import "../styles/StudentDashboard.css"; // Make sure you create this CSS const StudentDashboard = () => { const navigate = useNavigate(); - const user = JSON.parse(localStorage.getItem("ipmsUser")); + const backendUrl = process.env.REACT_APP_API_URL; const ouEmail = user?.email; + const [approvalStatus, setApprovalStatus] = useState("not_submitted"); + const [submissions, setSubmissions] = useState([]); + const [error, setError] = useState(""); useEffect(() => { - const fetchData = async () => { + const fetchStatus = async () => { try { - const res = await fetch(`${process.env.REACT_APP_API_URL}/api/student`, { + const res = await fetch(`${backendUrl}/api/student`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ ouEmail }), }); - const data = await res.json(); setApprovalStatus(data.approvalStatus); } catch (err) { - console.error("Error fetching internship data", err); + console.error("Error fetching approval status", err); } }; - if (ouEmail) { - fetchData(); - } - }, [ouEmail]); - console.log(approvalStatus); + if (ouEmail) fetchStatus(); + }, [ouEmail, backendUrl]); useEffect(() => { const fetchSubmissions = async () => { From 8632e455c39637d5c7c492b577f90403434423e5 Mon Sep 17 00:00:00 2001 From: Vijay Date: Sun, 27 Apr 2025 15:47:39 -0500 Subject: [PATCH 10/19] showing pending a3 forms to coordinator --- client/src/pages/CoordinatorDashboard.js | 171 +++++++++++++----- .../src/pages/CoordinatorEvaluationReview.jsx | 115 ++++++++++++ .../src/pages/CoordinatorRequestDetailView.js | 49 +++-- client/src/router.js | 7 +- .../styles/CoordinatorRequestDetailView.css | 50 +++++ 5 files changed, 328 insertions(+), 64 deletions(-) create mode 100644 client/src/pages/CoordinatorEvaluationReview.jsx diff --git a/client/src/pages/CoordinatorDashboard.js b/client/src/pages/CoordinatorDashboard.js index ed712155..e849f934 100644 --- a/client/src/pages/CoordinatorDashboard.js +++ b/client/src/pages/CoordinatorDashboard.js @@ -1,11 +1,13 @@ import React, { useEffect, useState } from "react"; import axios from "axios"; import { useNavigate } from "react-router-dom"; -import "../styles/SupervisorDashboard.css"; +import "../styles/SupervisorDashboard.css"; + const CoordinatorDashboard = () => { - const [activeTab, setActiveTab] = useState("requests"); // 'requests' or 'reports' + const [activeTab, setActiveTab] = useState("requests"); const navigate = useNavigate(); - // TEAM A's Internship Requests Logic + + // Internship Requests const [requests, setRequests] = useState([]); const [loadingRequests, setLoadingRequests] = useState(true); @@ -17,7 +19,9 @@ const CoordinatorDashboard = () => { const fetchInternshipRequests = async () => { try { - const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/coordinator/requests`); + const res = await axios.get( + `${process.env.REACT_APP_API_URL}/api/coordinator/requests` + ); setRequests(res.data || []); } catch (err) { console.error("Error fetching internship requests:", err); @@ -25,25 +29,23 @@ const CoordinatorDashboard = () => { setLoadingRequests(false); } }; - // Group D's Weekly Report Review Logic - const [reportGroups, setReportGroups] = useState([]); + // Weekly Reports + const [reports, setReports] = useState([]); const [loadingReports, setLoadingReports] = useState(true); useEffect(() => { if (activeTab === "reports") { - fetchReportGroups(); + fetchWeeklyReports(); } }, [activeTab]); - const fetchReportGroups = async () => { + const fetchWeeklyReports = async () => { try { - const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/reports/supervised-groups`); - const filtered = res.data?.groups?.filter(group => { - const key = `coordinator_reviewed_${group.groupIndex}`; - return !localStorage.getItem(key); - }); - setReportGroups(filtered || []); + const res = await axios.get( + `${process.env.REACT_APP_API_URL}/api/coordinator/reports` + ); + setReports(res.data.reports || []); } catch (err) { console.error("Error fetching reports:", err); } finally { @@ -51,44 +53,86 @@ const CoordinatorDashboard = () => { } }; - const handleReviewClick = (group) => { - localStorage.setItem(`coordinator_reviewed_${group.groupIndex}`, "true"); - navigate(`/review-cumulative/${group.groupIndex}`); + // Job Evaluations (A3 forms) + const [evaluations, setEvaluations] = useState([]); + const [loadingEvaluations, setLoadingEvaluations] = useState(true); + + useEffect(() => { + if (activeTab === "evaluations") { + fetchEvaluations(); + } + }, [activeTab]); + + const fetchEvaluations = async () => { + try { + const res = await axios.get( + `${process.env.REACT_APP_API_URL}/api/coordinator/evaluations` + ); + setEvaluations(res.data || []); + } catch (err) { + console.error("Error fetching evaluations:", err); + } finally { + setLoadingEvaluations(false); + } }; - - // Render UI - return (

Coordinator Dashboard

{/* Tabs */}
- - + + +
- {/* Tab: Internship Requests */} + {/* Internship Requests Tab */} {activeTab === "requests" && ( <> - {loadingRequests ?

Loading...

: ( + {loadingRequests ? ( +

Loading requests...

+ ) : ( - + - {requests.map(req => ( + {requests.map((req) => ( - - - - + + + + ))} @@ -97,23 +141,60 @@ const CoordinatorDashboard = () => { )} - {/* Tab: Weekly Reports Review */} + {/* Weekly Reports Tab */} {activeTab === "reports" && ( <> - {loadingReports ?

Loading reports...

: ( - reportGroups.length === 0 - ?

No reports to review

- : reportGroups.map(group => ( -
-

Weeks: {group.weeks?.join(", ")}

-
    - {group.reports.map((r, i) => ( -
  • Week {r.week} — Hours: {r.hours} — Tasks: {r.tasks}
  • - ))} -
- -
- )) + {loadingReports ? ( +

Loading reports...

+ ) : reports.length === 0 ? ( +

No reports to review

+ ) : ( + reports.map((report) => ( +
+

Week: {report.week}

+

Hours: {report.hours}

+

Tasks: {report.tasks}

+
+ )) + )} + + )} + + {/* Job Evaluations Tab */} + {activeTab === "evaluations" && ( + <> + {loadingEvaluations ? ( +

Loading evaluations...

+ ) : evaluations.length === 0 ? ( +

No evaluations pending

+ ) : ( +
Student NameStudent ID Company StatusAction
{req.studentName}{req.studentId}{req.companyName}{req.status}{req.student?.fullName || "-"}{req.workplace?.name || "-"}{req.coordinator_status || "-"} + +
+ + + + + + + + + {evaluations.map((evalItem) => ( + + + + + + ))} + +
Internee NameInternee EmailAction
{evalItem.interneeName}{evalItem.interneeEmail} + +
)} )} diff --git a/client/src/pages/CoordinatorEvaluationReview.jsx b/client/src/pages/CoordinatorEvaluationReview.jsx new file mode 100644 index 00000000..57755fe8 --- /dev/null +++ b/client/src/pages/CoordinatorEvaluationReview.jsx @@ -0,0 +1,115 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import axios from "axios"; +import "../styles/CoordinatorRequestDetailView.css"; + +const CoordinatorEvaluationReview = () => { + const { id } = useParams(); + const navigate = useNavigate(); + const [evaluation, setEvaluation] = useState(null); + + useEffect(() => { + fetchEvaluationDetails(); + }, [id]); + + const fetchEvaluationDetails = async () => { + try { + const res = await axios.get( + `${process.env.REACT_APP_API_URL}/api/coordinator/evaluations` + ); + const matchedEvaluation = res.data.find((form) => form._id === id); + setEvaluation(matchedEvaluation || null); + } catch (err) { + console.error("Error fetching evaluation form:", err); + } + }; + + const handleApprove = async () => { + try { + const res = await axios.post( + `${process.env.REACT_APP_API_URL}/api/coordinator/evaluation/${id}/approve` + ); + alert(res.data.message); + navigate("/coordinator-dashboard"); + } catch (err) { + console.error("Error approving evaluation:", err); + alert("Error approving evaluation form."); + } + }; + + const handleReject = async () => { + const reason = prompt("Please enter a reason for rejection:"); + if (!reason) { + alert("Rejection reason is required!"); + return; + } + try { + const res = await axios.post( + `${process.env.REACT_APP_API_URL}/api/coordinator/evaluation/${id}/reject`, + { reason } + ); + alert(res.data.message); + navigate("/coordinator-dashboard"); + } catch (err) { + console.error("Error rejecting evaluation:", err); + alert("Error rejecting evaluation form."); + } + }; + + if (!evaluation) return

Loading evaluation details...

; + + return ( +
+

Job Evaluation (Form A3) Review

+ +
+

+ Internee Name: {evaluation.interneeName} +

+

+ Internee Email: {evaluation.interneeEmail} +

+ +

Evaluation Categories

+ + + + + + + + + + {evaluation.evaluations.map((item, idx) => ( + + + + + + ))} + +
CategoryRatingComments
{item.category}{item.rating}{item.comment || "N/A"}
+ +
+ + + +
+
+
+ ); +}; + +export default CoordinatorEvaluationReview; diff --git a/client/src/pages/CoordinatorRequestDetailView.js b/client/src/pages/CoordinatorRequestDetailView.js index 8c8a12f5..aa2e26c7 100644 --- a/client/src/pages/CoordinatorRequestDetailView.js +++ b/client/src/pages/CoordinatorRequestDetailView.js @@ -1,20 +1,30 @@ import React, { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; import axios from "axios"; -import "../styles/CoordinatorRequestDetailView.css"; +import "../styles/CoordinatorRequestDetailView.css"; // Reuse same styling const CoordinatorRequestDetailView = () => { const { id } = useParams(); const navigate = useNavigate(); - const [data, setData] = useState(null); + const [requestData, setRequestData] = useState(null); + const [supervisorStatus, setSupervisorStatus] = useState(null); useEffect(() => { - axios - .get(`${process.env.REACT_APP_API_URL}/api/coordinator/request/${id}`) - .then((res) => setData(res.data)) - .catch((err) => console.log(err)); + fetchRequestDetails(); }, [id]); + const fetchRequestDetails = async () => { + try { + const res = await axios.get( + `${process.env.REACT_APP_API_URL}/api/coordinator/request/${id}` + ); + setRequestData(res.data.requestData); + setSupervisorStatus(res.data.supervisorStatus); + } catch (err) { + console.error("Error fetching request details:", err); + } + }; + const handleApprove = async () => { try { const res = await axios.post( @@ -24,14 +34,16 @@ const CoordinatorRequestDetailView = () => { navigate("/coordinator-dashboard"); } catch (err) { console.error("Approval failed:", err); - alert("Error approving request."); + alert("Error approving the request."); } }; const handleReject = async () => { const reason = prompt("Please enter a reason for rejection:"); - if (!reason) return alert("Rejection reason required!"); - + if (!reason) { + alert("Rejection reason is required!"); + return; + } try { const res = await axios.post( `${process.env.REACT_APP_API_URL}/api/coordinator/request/${id}/reject`, @@ -41,13 +53,11 @@ const CoordinatorRequestDetailView = () => { navigate("/coordinator-dashboard"); } catch (err) { console.error("Rejection failed:", err); - alert("Error rejecting request."); + alert("Error rejecting the request."); } }; - if (!data) return

Loading...

; - - const { requestData, supervisorStatus } = data; + if (!requestData) return

Loading request details...

; return (
@@ -55,13 +65,13 @@ const CoordinatorRequestDetailView = () => {

- Student: {requestData.student.userName} + Student: {requestData.student?.fullName || "N/A"}

- Email: {requestData.student.email} + Email: {requestData.student?.ouEmail || "N/A"}

- Company: {requestData.workplace.name} + Company: {requestData.workplace?.name || "N/A"}

Supervisor Status: {supervisorStatus} @@ -79,13 +89,16 @@ const CoordinatorRequestDetailView = () => { {requestData.tasks.map((task, idx) => ( {task.description} - {task.outcomes.join(", ")} + {task.outcomes?.join(", ") || "N/A"} ))} -

+
diff --git a/client/src/router.js b/client/src/router.js index 2dba3de2..0cd5d33e 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -24,6 +24,7 @@ import SubmittedReports from "./pages/SubmittedReports"; import CumulativeReviewForm from "./pages/CumulativeReviewForm"; import CoordinatorReviewForm from "./pages/CoordinatorReviewForm"; import CoordinatorCumulativeReviewForm from "./pages/CoordinatorCumulativeReviewForm"; +import CoordinatorEvaluationReview from "./pages/CoordinatorEvaluationReview"; // Create and export the router configuration const router = createBrowserRouter([ @@ -112,8 +113,12 @@ const router = createBrowserRouter([ path: "weekly-report/:groupIndex/week-:weekNumber/:studentName", element: , }, + { + path: "coordinator/evaluation/:id", + element: , + }, ], }, ]); -export default router; \ No newline at end of file +export default router; diff --git a/client/src/styles/CoordinatorRequestDetailView.css b/client/src/styles/CoordinatorRequestDetailView.css index f47f167d..220c53be 100644 --- a/client/src/styles/CoordinatorRequestDetailView.css +++ b/client/src/styles/CoordinatorRequestDetailView.css @@ -15,3 +15,53 @@ font-weight: 600; font-size: 1.1rem; } +/* Add this if not already present */ +.approve-btn { + background-color: #28a745; + color: white; + padding: 10px 20px; + border: none; + font-weight: bold; + border-radius: 5px; + cursor: pointer; +} + +.reject-btn { + background-color: #dc3545; + color: white; + padding: 10px 20px; + border: none; + font-weight: bold; + border-radius: 5px; + cursor: pointer; +} + +.back-btn { + background-color: #841617; /* OU Maroon */ + color: white; + padding: 10px 20px; + border: none; + font-weight: bold; + border-radius: 5px; + cursor: pointer; +} + +.view-details-btn { + background-color: #007bff; /* Blue */ + color: white; + padding: 8px 16px; + border: none; + font-weight: bold; + border-radius: 5px; + cursor: pointer; +} + +.review-btn { + background-color: #007bff; /* Blue */ + color: white; + padding: 8px 16px; + border: none; + font-weight: bold; + border-radius: 5px; + cursor: pointer; +} From 055f83e9c6eec8605b0cfbbc4294299cd8ec4702 Mon Sep 17 00:00:00 2001 From: Vijay Date: Sun, 27 Apr 2025 15:49:47 -0500 Subject: [PATCH 11/19] saving my changes before merging --- server/controllers/approvalController.js | 389 ++++++++++------------- server/index.js | 22 +- server/routes/approvalRoutes.js | 114 ++++--- 3 files changed, 241 insertions(+), 284 deletions(-) diff --git a/server/controllers/approvalController.js b/server/controllers/approvalController.js index cf87481c..47204e65 100644 --- a/server/controllers/approvalController.js +++ b/server/controllers/approvalController.js @@ -4,148 +4,154 @@ const Evaluation = require("../models/Evaluation"); const EmailService = require("../services/emailService"); const UserTokenRequest = require("../models/TokenRequest"); -// =========================================== // -// Managing Supervisor Forms // -// =========================================== // +// --------------------------------------------------- +// Student + Supervisor methods (NO CHANGES) +// --------------------------------------------------- -const getSupervisorForms = async (req, res, filter) => { +const getStudentSubmissions = async (req, res) => { try { - const InternshipRequest = require("../models/InternshipRequest"); - const WeeklyReport = require("../models/WeeklyReport"); - const Evaluation = require("../models/Evaluation"); - - const a1Forms = await InternshipRequest.find(filter).populate("student", "fullName ouEmail soonerId"); - const typedA1 = a1Forms.map((form) => ({ - ...form.toObject(), - form_type: "A1", - })); - - const a2Forms = await WeeklyReport.find(filter).populate("student_id", "fullName ouEmail soonerId"); - const typedA2 = a2Forms.map((form) => ({ - ...form.toObject(), - form_type: "A2", - })); - - const a3Forms = await Evaluation.find(filter).populate("student_id", "fullName ouEmail soonerId"); - const typedA3 = a3Forms.map((form) => ({ - ...form.toObject(), - form_type: "A3", - })); - - const allForms = [...typedA1, ...typedA2, ...typedA3]; - allForms.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - - return res.status(200).json(allForms); + const submissions = await InternshipRequest.find({ + student: req.user._id, + }).sort({ createdAt: -1 }); + res.status(200).json(submissions); } catch (err) { - console.error("Error in getSupervisorForms:", err.message); - return res.status(500).json({ message: "Failed to fetch supervisor forms", error: err.message }); + res.status(500).json({ message: "Failed to fetch submissions." }); } }; -const handleSupervisorFormAction = async (req, res, action) => { +const deleteStudentSubmission = async (req, res) => { try { - const form_type = req.params.type; - const formId = req.params.id; - const { comment = "", signature = "" } = req.body; - - const models = { - A1: require("../models/InternshipRequest"), - A2: require("../models/WeeklyReport"), - A3: require("../models/Evaluation"), - }; - - const FormModel = models[form_type]; - if (!FormModel) { - return res.status(400).json({ message: "Invalid form type" }); + const { id } = req.params; + const submission = await InternshipRequest.findById(id); + if (!submission) + return res.status(404).json({ message: "Submission not found." }); + + if (submission.student.toString() !== req.user._id.toString()) { + return res + .status(403) + .json({ message: "Unauthorized to delete this submission." }); } - if (!["approve", "reject"].includes(action)) { - return res.status(400).json({ message: "Invalid action" }); + if (submission.coordinator_status !== "pending") { + return res + .status(400) + .json({ message: "Cannot delete reviewed submission." }); } - const update = { - supervisor_status: action === "approve" ? "approved" : "rejected", - supervisor_comment: comment, - }; + await InternshipRequest.findByIdAndDelete(id); + res.status(200).json({ message: "Submission deleted successfully." }); + } catch (err) { + res.status(500).json({ message: "Internal server error." }); + } +}; - const form = await FormModel.findByIdAndUpdate(formId, update, { new: true }) - .populate("student_id", "userName email"); +const getPendingSubmissions = async (req, res) => { + try { + const pendingRequests = await InternshipRequest.find({ + supervisor_status: "pending", + }).populate("student", "fullName email"); + res.status(200).json(pendingRequests); + } catch (err) { + res.status(500).json({ message: "Failed to fetch pending submissions." }); + } +}; - if (!form) { - return res.status(404).json({ message: "Form not found" }); - } +const approveSubmission = async (req, res) => { + try { + const { id } = req.params; + const { comment } = req.body; + const request = await InternshipRequest.findByIdAndUpdate( + id, + { supervisor_status: "approved", supervisor_comment: comment || "" }, + { new: true } + ); + if (!request) + return res.status(404).json({ message: "Submission not found." }); - const studentEmail = form.student_id?.email || form.interneeEmail || form.studentEmail || null; - let emailSubject = `Form ${action === "approve" ? "Approved" : "Rejected"}`; - let emailBody = `

Your ${form_type} form has been ${action}ed by the supervisor.

`; - if (comment) { - emailBody += `

Comment: ${comment}

`; - } + res.json({ message: "Submission approved successfully." }); + } catch (err) { + res.status(500).json({ message: "Approval failed." }); + } +}; - const student_id = form.student_id || form.internee_id || form.student; - const student = await UserTokenRequest.findById(student_id); - const student_mail = student?.ouEmail || form?.interneeEmail; - - try { - await EmailService.sendEmail({ - to: student_mail, - subject: emailSubject, - html: emailBody, - }); - } catch (err) { - console.error("Email sending error:", err); +const rejectSubmission = async (req, res) => { + try { + const { id } = req.params; + const { comment } = req.body; + const request = await InternshipRequest.findByIdAndUpdate( + id, + { supervisor_status: "rejected", supervisor_comment: comment || "" }, + { new: true } + ); + if (!request) + return res.status(404).json({ message: "Submission not found." }); + + res.json({ message: "Submission rejected successfully." }); + } catch (err) { + res.status(500).json({ message: "Rejection failed." }); + } +}; + +const deleteStalledSubmission = async (req, res) => { + try { + const { id } = req.params; + const submission = await InternshipRequest.findById(id); + if (!submission) + return res.status(404).json({ message: "Submission not found." }); + + if (submission.coordinator_status !== "pending") { + return res + .status(400) + .json({ message: "Cannot delete reviewed submission." }); } - console.log("Email sent to:", student_mail); - res.status(200).json({ message: `Form ${action}ed successfully`, updatedForm: form }); + await InternshipRequest.findByIdAndDelete(id); + res.status(200).json({ message: "Submission deleted successfully." }); } catch (err) { - console.error("SupervisorFormAction error:", err); - res.status(500).json({ message: "Error processing form", error: err.message }); + res.status(500).json({ message: "Internal server error." }); } }; +// --------------------------------------------------- +// Coordinator-specific methods (UPDATED) +// --------------------------------------------------- + const getCoordinatorRequests = async (req, res) => { try { const requests = await InternshipRequest.find({ coordinator_status: "pending", - }).populate("student", "userName email"); - + }).populate("student", "fullName email"); res.status(200).json(requests); } catch (err) { - res.status(500).json({ message: "Failed to fetch requests" }); + res.status(500).json({ message: "Failed to fetch internship requests." }); } }; const getCoordinatorRequestDetails = async (req, res) => { try { const requestData = await InternshipRequest.findById(req.params.id) - .populate("student", "userName email") + .populate("student", "fullName ouEmail") .lean(); - - if (!requestData) { - return res.status(404).json({ message: "Request not found" }); - } + if (!requestData) + return res.status(404).json({ message: "Request not found." }); const supervisorStatus = requestData.supervisor_status || "Not Submitted"; - res.status(200).json({ requestData, supervisorStatus }); } catch (err) { - res.status(500).json({ message: "Failed to fetch details" }); + res.status(500).json({ message: "Failed to fetch request details." }); } }; const coordinatorApproveRequest = async (req, res) => { try { - const request = await InternshipRequest.findByIdAndUpdate( - req.params.id, - { status: "approved" }, - { new: true } - ).populate("student", "userName email"); - - if (!request) { - return res.status(404).json({ message: "Request not found" }); - } + const request = await InternshipRequest.findById(req.params.id).populate( + "student", + "fullName email" + ); + if (!request) + return res.status(404).json({ message: "Request not found." }); + request.status = "approved"; request.coordinator_status = "Approved"; request.coordinator_comment = "Approved by Coordinator"; await request.save(); @@ -156,27 +162,26 @@ const coordinatorApproveRequest = async (req, res) => { html: `

Your internship request has been approved by the Coordinator.

`, }); - res.json({ message: "Request Approved Successfully" }); + res.json({ message: "Request approved successfully." }); } catch (err) { - res.status(500).json({ message: "Approval failed", error: err.message }); + res.status(500).json({ message: "Approval failed." }); } }; const coordinatorRejectRequest = async (req, res) => { const { reason } = req.body; - if (!reason) return res.status(400).json({ message: "Reason required" }); + if (!reason) + return res.status(400).json({ message: "Rejection reason required." }); try { - const request = await InternshipRequest.findByIdAndUpdate( - req.params.id, - { status: "rejected" }, - { new: true } - ).populate("student", "userName email"); - - if (!request) { - return res.status(404).json({ message: "Request not found" }); - } + const request = await InternshipRequest.findById(req.params.id).populate( + "student", + "fullName email" + ); + if (!request) + return res.status(404).json({ message: "Request not found." }); + request.status = "rejected"; request.coordinator_status = "Rejected"; request.coordinator_comment = reason; await request.save(); @@ -184,153 +189,93 @@ const coordinatorRejectRequest = async (req, res) => { await EmailService.sendEmail({ to: request.student.email, subject: "Internship Request Rejected", - html: `

Your internship request has been rejected.
Reason: ${reason}

`, + html: `

Your internship request has been rejected.
Reason: ${reason}

`, }); - res.json({ message: "Request Rejected Successfully" }); + res.json({ message: "Request rejected successfully." }); } catch (err) { - res.status(500).json({ message: "Rejection failed", error: err.message }); + res.status(500).json({ message: "Rejection failed." }); } }; -const coordinatorResendRequest = async (req, res) => { +const getCoordinatorReports = async (req, res) => { try { - const submission = await InternshipRequest.findById(req.params.id); - if (!submission) - return res.status(404).json({ message: "Submission not found" }); - - submission.coordinator_reminder_count = 0; - submission.last_coordinator_reminder_at = new Date(); - submission.coordinator_status = "pending"; - await submission.save(); - - return res.status(200).json({ message: "Reminder cycle restarted." }); - } catch (error) { - console.error("Error in coordinatorResendRequest:", error); - return res.status(500).json({ message: "Server error while resending request." }); - } -}; - -const deleteStudentSubmission = async (req, res) => { - try { - const { id } = req.params; - const studentId = req.user._id; - - const submission = await InternshipRequest.findById(id); - if (!submission) - return res.status(404).json({ message: "Submission not found." }); - - if (submission.student.toString() !== studentId.toString()) { - return res.status(403).json({ message: "You are not authorized to delete this submission." }); - } - - if (submission.coordinator_status !== "pending") { - return res.status(400).json({ message: "Submission already reviewed. Cannot delete." }); - } - - await InternshipRequest.findByIdAndDelete(id); - return res.status(200).json({ message: "Submission successfully deleted by student." }); + const reports = await WeeklyReport.find({}).sort({ submittedAt: -1 }); + res.status(200).json({ reports }); } catch (err) { - console.error("Error deleting student submission:", err); - return res.status(500).json({ message: "Internal server error." }); + res.status(500).json({ message: "Failed to fetch weekly reports." }); } }; -const getStudentSubmissions = async (req, res) => { +const getCoordinatorEvaluations = async (req, res) => { try { - const studentId = req.user._id; - const submissions = await InternshipRequest.find({ student: studentId }).sort({ createdAt: -1 }); - res.status(200).json(submissions); - } catch (error) { - console.error("Error fetching student submissions:", error); - res.status(500).json({ message: "Failed to fetch submissions." }); - } -}; - -const getPendingSubmissions = async (req, res) => { - try { - const pendingRequests = await InternshipRequest.find({ - supervisor_status: "pending", - }).populate("student", "fullName ouEmail"); - - res.status(200).json(pendingRequests); + const evaluations = await Evaluation.find({ advisorAgreement: true }); + res.status(200).json(evaluations); } catch (err) { - res.status(500).json({ - message: "Failed to fetch pending supervisor submissions", - error: err.message, - }); + res.status(500).json({ message: "Failed to fetch evaluations." }); } }; -const approveSubmission = async (req, res) => { - const { id } = req.params; - const { comment } = req.body; +const approveJobEvaluation = async (req, res) => { try { - const request = await InternshipRequest.findByIdAndUpdate( - id, - { supervisor_status: "approved", supervisor_comment: comment || "" }, - { new: true } - ); - if (!request) return res.status(404).json({ message: "Submission not found" }); + const { id } = req.params; + const evaluation = await Evaluation.findById(id); + if (!evaluation) + return res.status(404).json({ message: "Evaluation not found." }); - res.json({ message: "Submission approved", updated: request }); - } catch (err) { - res.status(500).json({ message: "Approval failed", error: err.message }); - } -}; + evaluation.coordinatorAgreement = true; + evaluation.updatedAt = new Date(); + await evaluation.save(); -const rejectSubmission = async (req, res) => { - const { id } = req.params; - const { comment } = req.body; - try { - const request = await InternshipRequest.findByIdAndUpdate( - id, - { supervisor_status: "rejected", supervisor_comment: comment || "" }, - { new: true } - ); - if (!request) return res.status(404).json({ message: "Submission not found" }); + await EmailService.sendEmail({ + to: evaluation.interneeEmail, + subject: "Job Evaluation Approved", + html: `

Your Job Evaluation (Form A3) has been approved by the Coordinator.

`, + }); - res.json({ message: "Submission rejected", updated: request }); + res.json({ message: "Job Evaluation approved successfully." }); } catch (err) { - res.status(500).json({ message: "Rejection failed", error: err.message }); + res.status(500).json({ message: "Approval failed." }); } }; -const deleteStalledSubmission = async (req, res) => { +const rejectJobEvaluation = async (req, res) => { try { const { id } = req.params; + const { reason } = req.body; + const evaluation = await Evaluation.findById(id); + if (!evaluation) + return res.status(404).json({ message: "Evaluation not found." }); - const submission = await InternshipRequest.findById(id); - if (!submission) { - return res.status(404).json({ message: "Submission not found." }); - } + evaluation.coordinatorAgreement = false; + evaluation.updatedAt = new Date(); + await evaluation.save(); - if (submission.coordinator_status !== "pending") { - return res.status(400).json({ message: "Submission already reviewed. Cannot delete." }); - } - - await InternshipRequest.findByIdAndDelete(id); + await EmailService.sendEmail({ + to: evaluation.interneeEmail, + subject: "Job Evaluation Rejected", + html: `

Your Job Evaluation (Form A3) was rejected.
Reason: ${reason}

`, + }); - return res.status(200).json({ message: "Submission deleted successfully." }); - } catch (error) { - console.error("Error deleting submission:", error); - return res.status(500).json({ message: "Internal server error" }); + res.json({ message: "Job Evaluation rejected successfully." }); + } catch (err) { + res.status(500).json({ message: "Rejection failed." }); } }; - module.exports = { - getCoordinatorRequests, - getCoordinatorRequestDetails, - coordinatorApproveRequest, - coordinatorRejectRequest, - coordinatorResendRequest, - deleteStudentSubmission, getStudentSubmissions, + deleteStudentSubmission, getPendingSubmissions, - getSupervisorForms, - handleSupervisorFormAction, approveSubmission, rejectSubmission, deleteStalledSubmission, -}; \ No newline at end of file + getCoordinatorRequests, + getCoordinatorRequestDetails, + coordinatorApproveRequest, + coordinatorRejectRequest, + getCoordinatorReports, + getCoordinatorEvaluations, + approveJobEvaluation, + rejectJobEvaluation, +}; diff --git a/server/index.js b/server/index.js index 80b7fe97..70bc76d6 100644 --- a/server/index.js +++ b/server/index.js @@ -5,27 +5,26 @@ const express = require("express"); const mongoose = require("mongoose"); const cors = require("cors"); const User = require("./models/User"); + const formRoutes = require("./routes/formRoutes"); const emailRoutes = require("./routes/emailRoutes"); const tokenRoutes = require("./routes/token"); const approvalRoutes = require("./routes/approvalRoutes"); -const studentRoutes = require("./routes/studentRoutes"); const outcomeRoutes = require("./routes/outcomeRoutes"); // Import cron job manager and register jobs -const cronJobManager = require("./utils/cronUtils").cronJobManager; +const cronJobManager = require("./utils/cronUtils"); const { registerAllJobs } = require("./jobs/registerCronJobs"); const Evaluation = require("./models/Evaluation"); -const fourWeekReportRoutes = require("./routes/fourWeekReportRoutes"); -const path = require("path"); +const cronJobRoutes = require("./routes/cronJobRoutes"); const app = express(); app.use(express.json()); app.use(cors()); -app.use("/api/form", formRoutes); +app.use("/api/form", formRoutes); // register route as /api/form/submit app.use("/api/email", emailRoutes); app.use("/api/token", tokenRoutes); app.use("/api", outcomeRoutes); @@ -42,8 +41,9 @@ mongoose .connect(process.env.MONGO_URI, mongoConfig) .then(async () => { console.log("Connected to Local MongoDB"); + // Initialize cron jobs after database connection is established try { - await registerAllJobs(); + await registerAllJobs(); // Register cronjobs console.log("Cron jobs initialized successfully"); } catch (error) { console.error("Failed to initialize cron jobs:", error); @@ -81,8 +81,6 @@ app.use("/api/token", tokenRoutes); app.use("/api", approvalRoutes); app.use("/api/reports", weeklyReportRoutes); -app.use("/api/student", studentRoutes); -app.use("/api/fourWeekReports", fourWeekReportRoutes); app.post("/api/createUser", async (req, res) => { try { @@ -99,7 +97,6 @@ app.post("/api/createUser", async (req, res) => { .json({ message: "Failed to create user", error: error.message }); } }); - app.post("/api/evaluation", async (req, res) => { try { const { @@ -139,13 +136,12 @@ app.post("/api/evaluation", async (req, res) => { } }); - - - //Form A.4 + const presentationRoutes = require("./routes/presentationRoutes"); app.use("/api/presentation", presentationRoutes); +// Graceful shutdown (async Mongoose support) process.on("SIGINT", async () => { try { cronJobManager.stopAllJobs(); @@ -159,4 +155,4 @@ process.on("SIGINT", async () => { }); const PORT = process.env.PORT || 5001; -app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); \ No newline at end of file +app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); diff --git a/server/routes/approvalRoutes.js b/server/routes/approvalRoutes.js index f8fde20a..ed6e2034 100644 --- a/server/routes/approvalRoutes.js +++ b/server/routes/approvalRoutes.js @@ -1,69 +1,85 @@ const express = require("express"); const router = express.Router(); - const { - getSupervisorForms, - handleSupervisorFormAction, + getStudentSubmissions, + deleteStudentSubmission, + getPendingSubmissions, + approveSubmission, + rejectSubmission, + deleteStalledSubmission, getCoordinatorRequests, getCoordinatorRequestDetails, coordinatorApproveRequest, coordinatorRejectRequest, - getStudentSubmissions, - getPendingSubmissions, - coordinatorResendRequest, - deleteStalledSubmission, - deleteStudentSubmission, - rejectSubmission, - approveSubmission, + getCoordinatorReports, + getCoordinatorEvaluations, + approveJobEvaluation, + rejectJobEvaluation, } = require("../controllers/approvalController"); -const { isSupervisor, isCoordinator, isStudent } = require("../middleware/authMiddleware"); - +const { + isSupervisor, + isCoordinator, + isStudent, +} = require("../middleware/authMiddleware"); -// Student API +// ----------------------------------------------- +// Student Routes +// ----------------------------------------------- router.get("/student/submissions", isStudent, getStudentSubmissions); -router.delete("/student/request/:id/delete", isStudent, deleteStudentSubmission); +router.delete( + "/student/request/:id/delete", + isStudent, + deleteStudentSubmission +); -// Supervisor APIs +// ----------------------------------------------- +// Supervisor Routes +// ----------------------------------------------- router.get("/submissions/pending", isSupervisor, getPendingSubmissions); router.post("/submissions/:id/approve", isSupervisor, approveSubmission); router.post("/submissions/:id/reject", isSupervisor, rejectSubmission); -// =========================================== // -// Supervisor Approval Routes // -// =========================================== // - -// Supervisor APIs -router.get("/supervisor/forms", isSupervisor, (req, res) => { - // const supervisorId = req.user._id, - return getSupervisorForms(req, res, { - // supervisor_id: supervisorId, - supervisor_status: { $in: ["pending"] }, - }) -}); -// Approve route -router.post("/supervisor/form/:type/:id/approve", isSupervisor, (req, res) => - handleSupervisorFormAction(req, res, "approve") +// ----------------------------------------------- +// Coordinator Routes +// ----------------------------------------------- +router.get("/coordinator/requests", isCoordinator, getCoordinatorRequests); +router.get( + "/coordinator/request/:id", + isCoordinator, + getCoordinatorRequestDetails ); - -// Reject route -router.post("/supervisor/form/:type/:id/reject", isSupervisor, (req, res) => - handleSupervisorFormAction(req, res, "reject") +router.post( + "/coordinator/request/:id/approve", + isCoordinator, + coordinatorApproveRequest +); +router.post( + "/coordinator/request/:id/reject", + isCoordinator, + coordinatorRejectRequest +); +router.delete( + "/coordinator/request/:id/delete", + isCoordinator, + deleteStalledSubmission ); -// =========================================== // -// Coordinator Approval Routes // -// =========================================== // - - -// Coordinator APIs -router.get("/coordinator/requests", isCoordinator, getCoordinatorRequests); - -router.get("/coordinator/request/:id", isCoordinator, getCoordinatorRequestDetails); -router.post("/coordinator/request/:id/approve", isCoordinator, coordinatorApproveRequest); -router.post("/coordinator/request/:id/reject", isCoordinator, coordinatorRejectRequest); -router.post("/coordinator/request/:id/resend", isCoordinator, coordinatorResendRequest); -router.delete("/coordinator/request/:id/delete", isCoordinator, deleteStalledSubmission); - +router.get("/coordinator/reports", isCoordinator, getCoordinatorReports); +router.get( + "/coordinator/evaluations", + isCoordinator, + getCoordinatorEvaluations +); +router.post( + "/coordinator/evaluation/:id/approve", + isCoordinator, + approveJobEvaluation +); +router.post( + "/coordinator/evaluation/:id/reject", + isCoordinator, + rejectJobEvaluation +); -module.exports = router; \ No newline at end of file +module.exports = router; From 6400e46793f2ad8b2339808e07cf36616b785870 Mon Sep 17 00:00:00 2001 From: Vijay Date: Sun, 27 Apr 2025 17:06:05 -0500 Subject: [PATCH 12/19] Finalize Coordinator Dashboard fixes: Updated model, controllers, request detail view --- client/src/pages/CoordinatorDashboard.js | 2 +- .../src/pages/CoordinatorRequestDetailView.js | 4 +-- server/controllers/approvalController.js | 26 ++++++------------- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/client/src/pages/CoordinatorDashboard.js b/client/src/pages/CoordinatorDashboard.js index a43b4511..073d1414 100644 --- a/client/src/pages/CoordinatorDashboard.js +++ b/client/src/pages/CoordinatorDashboard.js @@ -120,7 +120,7 @@ const CoordinatorDashboard = () => { {requests.map((req) => ( - {req.student?.fullName || "-"} + {req.student?.name || "-"} {req.workplace?.name || "-"} {req.coordinator_status || "-"} diff --git a/client/src/pages/CoordinatorRequestDetailView.js b/client/src/pages/CoordinatorRequestDetailView.js index aa2e26c7..55acf7b2 100644 --- a/client/src/pages/CoordinatorRequestDetailView.js +++ b/client/src/pages/CoordinatorRequestDetailView.js @@ -65,10 +65,10 @@ const CoordinatorRequestDetailView = () => {

- Student: {requestData.student?.fullName || "N/A"} + Student: {requestData.student?.name || "N/A"}

- Email: {requestData.student?.ouEmail || "N/A"} + Email: {requestData.student?.email || "N/A"}

Company: {requestData.workplace?.name || "N/A"} diff --git a/server/controllers/approvalController.js b/server/controllers/approvalController.js index 5f9b81c9..65b83daa 100644 --- a/server/controllers/approvalController.js +++ b/server/controllers/approvalController.js @@ -2,7 +2,6 @@ const InternshipRequest = require("../models/InternshipRequest"); const WeeklyReport = require("../models/WeeklyReport"); const Evaluation = require("../models/Evaluation"); const EmailService = require("../services/emailService"); -const UserTokenRequest = require("../models/TokenRequest"); // ======================================= // Student-Side Controllers @@ -10,9 +9,9 @@ const UserTokenRequest = require("../models/TokenRequest"); const getStudentSubmissions = async (req, res) => { try { - const studentId = req.user._id; + const studentEmail = req.user.ouEmail; const submissions = await InternshipRequest.find({ - student: studentId, + "student.email": studentEmail, }).sort({ createdAt: -1 }); res.status(200).json(submissions); } catch (error) { @@ -28,7 +27,7 @@ const deleteStudentSubmission = async (req, res) => { if (!submission) return res.status(404).json({ message: "Submission not found." }); - if (submission.student.toString() !== req.user._id.toString()) { + if (submission.student.email !== req.user.ouEmail) { return res .status(403) .json({ message: "Unauthorized to delete this submission." }); @@ -52,7 +51,7 @@ const getPendingSubmissions = async (req, res) => { try { const pendingRequests = await InternshipRequest.find({ supervisor_status: "pending", - }).populate("student", "fullName ouEmail"); + }); res.status(200).json(pendingRequests); } catch (err) { console.error("Error fetching pending submissions:", err); @@ -125,7 +124,7 @@ const getCoordinatorRequests = async (req, res) => { try { const requests = await InternshipRequest.find({ coordinator_status: "pending", - }).populate("student", "fullName email"); + }); res.status(200).json(requests); } catch (err) { res.status(500).json({ message: "Failed to fetch internship requests." }); @@ -134,9 +133,7 @@ const getCoordinatorRequests = async (req, res) => { const getCoordinatorRequestDetails = async (req, res) => { try { - const requestData = await InternshipRequest.findById(req.params.id) - .populate("student", "fullName ouEmail") - .lean(); + const requestData = await InternshipRequest.findById(req.params.id).lean(); if (!requestData) return res.status(404).json({ message: "Request not found." }); @@ -149,10 +146,7 @@ const getCoordinatorRequestDetails = async (req, res) => { const coordinatorApproveRequest = async (req, res) => { try { - const request = await InternshipRequest.findById(req.params.id).populate( - "student", - "fullName email" - ); + const request = await InternshipRequest.findById(req.params.id); if (!request) return res.status(404).json({ message: "Request not found." }); @@ -179,10 +173,7 @@ const coordinatorRejectRequest = async (req, res) => { return res.status(400).json({ message: "Rejection reason required." }); try { - const request = await InternshipRequest.findById(req.params.id).populate( - "student", - "fullName email" - ); + const request = await InternshipRequest.findById(req.params.id); if (!request) return res.status(404).json({ message: "Request not found." }); @@ -250,7 +241,6 @@ const approveJobEvaluation = async (req, res) => { evaluation.updatedAt = new Date(); await evaluation.save(); - // Send A3 Final Form to Student (for Canvas) await EmailService.sendEmail({ to: evaluation.interneeEmail, subject: "Your Job Evaluation (Form A3) is Approved!", From 12b441711df8a8b73b9a28bc2537a723a723b5c8 Mon Sep 17 00:00:00 2001 From: Vijay Date: Sun, 27 Apr 2025 18:22:19 -0500 Subject: [PATCH 13/19] approve , reject and send mail functionality fixed --- server/controllers/approvalController.js | 47 ++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/server/controllers/approvalController.js b/server/controllers/approvalController.js index 65b83daa..d26c3d53 100644 --- a/server/controllers/approvalController.js +++ b/server/controllers/approvalController.js @@ -223,7 +223,10 @@ const getCoordinatorReports = async (req, res) => { const getCoordinatorEvaluations = async (req, res) => { try { - const evaluations = await Evaluation.find({ advisorAgreement: true }); + const evaluations = await Evaluation.find({ + advisorAgreement: true, + coordinatorAgreement: { $ne: true }, // Only fetch not-yet-approved evaluations + }); res.status(200).json(evaluations); } catch (err) { res.status(500).json({ message: "Failed to fetch evaluations." }); @@ -241,15 +244,50 @@ const approveJobEvaluation = async (req, res) => { evaluation.updatedAt = new Date(); await evaluation.save(); + // Build Clean HTML Table without newlines + const evaluationTable = ` + + + + + + + + + + ${evaluation.evaluations + .map( + (item) => ` + + + + + + ` + ) + .join("")} + +
CategoryRatingComment
${item.category}${item.rating}${item.comment ? item.comment : "N/A"}
+ `; + await EmailService.sendEmail({ to: evaluation.interneeEmail, subject: "Your Job Evaluation (Form A3) is Approved!", - html: `

Dear ${evaluation.interneeName},

-

Your Job Evaluation (Form A3) has been approved by the Coordinator. Please upload it to Canvas.

`, + html: ` +
+

Dear ${evaluation.interneeName},

+

Your Job Evaluation (Form A3) has been approved by the Coordinator. Please find the evaluation details below. Kindly upload this to Canvas:

+ ${evaluationTable} +

Best regards,
Internship Program Management System

+
+ `, }); - res.json({ message: "A3 Job Evaluation approved successfully." }); + res.json({ + message: "A3 Job Evaluation approved and emailed successfully.", + }); } catch (err) { + console.error("Approval failed:", err); res.status(500).json({ message: "Approval failed." }); } }; @@ -275,6 +313,7 @@ const rejectJobEvaluation = async (req, res) => { res.json({ message: "A3 Job Evaluation rejected successfully." }); } catch (err) { + console.error("Rejection failed:", err); res.status(500).json({ message: "Rejection failed." }); } }; From 947b171e5020f76c6a560fec2e60a133d1b542c7 Mon Sep 17 00:00:00 2001 From: Kamal Poshala Date: Sun, 27 Apr 2025 19:20:31 -0500 Subject: [PATCH 14/19] Finalize Coordinator Sprint 4 fixes by kamal --- client/src/pages/CoordinatorDashboard.js | 212 +++++++++--------- .../src/pages/CoordinatorEvaluationReview.jsx | 54 ++--- .../src/pages/CoordinatorManualReviewView.js | 90 ++++++++ .../src/pages/CoordinatorRequestDetailView.js | 64 ++---- client/src/router.js | 101 ++------- server/controllers/approvalController.js | 188 +++++++++++----- server/routes/approvalRoutes.js | 22 ++ 7 files changed, 412 insertions(+), 319 deletions(-) create mode 100644 client/src/pages/CoordinatorManualReviewView.js diff --git a/client/src/pages/CoordinatorDashboard.js b/client/src/pages/CoordinatorDashboard.js index 073d1414..70f2fc31 100644 --- a/client/src/pages/CoordinatorDashboard.js +++ b/client/src/pages/CoordinatorDashboard.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; -import axios from "axios"; import { useNavigate } from "react-router-dom"; +import axios from "axios"; import "../styles/SupervisorDashboard.css"; const CoordinatorDashboard = () => { @@ -11,17 +11,28 @@ const CoordinatorDashboard = () => { const [requests, setRequests] = useState([]); const [loadingRequests, setLoadingRequests] = useState(true); + // Weekly Reports (Form A2) + const [reports, setReports] = useState([]); + const [loadingReports, setLoadingReports] = useState(true); + + // Job Evaluations (Form A3) + const [evaluations, setEvaluations] = useState([]); + const [loadingEvaluations, setLoadingEvaluations] = useState(true); + + // Manual Reviews (Failed A1) + const [manualReviews, setManualReviews] = useState([]); + const [loadingManualReviews, setLoadingManualReviews] = useState(true); + useEffect(() => { - if (activeTab === "requests") { - fetchInternshipRequests(); - } + if (activeTab === "requests") fetchInternshipRequests(); + if (activeTab === "reports") fetchWeeklyReports(); + if (activeTab === "evaluations") fetchEvaluations(); + if (activeTab === "manualReviews") fetchManualReviews(); }, [activeTab]); const fetchInternshipRequests = async () => { try { - const res = await axios.get( - `${process.env.REACT_APP_API_URL}/api/coordinator/requests` - ); + const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/coordinator/requests`); setRequests(res.data || []); } catch (err) { console.error("Error fetching internship requests:", err); @@ -30,21 +41,9 @@ const CoordinatorDashboard = () => { } }; - // Weekly Reports (Form A2) - const [reports, setReports] = useState([]); - const [loadingReports, setLoadingReports] = useState(true); - - useEffect(() => { - if (activeTab === "reports") { - fetchWeeklyReports(); - } - }, [activeTab]); - const fetchWeeklyReports = async () => { try { - const res = await axios.get( - `${process.env.REACT_APP_API_URL}/api/coordinator/reports` - ); + const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/coordinator/reports`); setReports(res.data.reports || []); } catch (err) { console.error("Error fetching reports:", err); @@ -53,21 +52,9 @@ const CoordinatorDashboard = () => { } }; - // Job Evaluations (Form A3) - const [evaluations, setEvaluations] = useState([]); - const [loadingEvaluations, setLoadingEvaluations] = useState(true); - - useEffect(() => { - if (activeTab === "evaluations") { - fetchEvaluations(); - } - }, [activeTab]); - const fetchEvaluations = async () => { try { - const res = await axios.get( - `${process.env.REACT_APP_API_URL}/api/coordinator/evaluations` - ); + const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/coordinator/evaluations`); setEvaluations(res.data || []); } catch (err) { console.error("Error fetching evaluations:", err); @@ -76,38 +63,33 @@ const CoordinatorDashboard = () => { } }; + const fetchManualReviews = async () => { + try { + const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/coordinator/manual-review-a1`); + setManualReviews(res.data || []); + } catch (err) { + console.error("Error fetching manual review forms:", err); + } finally { + setLoadingManualReviews(false); + } + }; + return (

Coordinator Dashboard

{/* Tabs */}
- - - + + + +
- {/* Internship Requests Tab */} + {/* Internship Requests */} {activeTab === "requests" && ( <> - {loadingRequests ? ( -

Loading requests...

- ) : ( + {loadingRequests ?

Loading requests...

: ( @@ -124,14 +106,7 @@ const CoordinatorDashboard = () => { ))} @@ -141,60 +116,81 @@ const CoordinatorDashboard = () => { )} - {/* Weekly Reports Tab */} + {/* Weekly Reports */} {activeTab === "reports" && ( <> - {loadingReports ? ( -

Loading reports...

- ) : reports.length === 0 ? ( -

No reports to review

- ) : ( - reports.map((report) => ( -
-

Week: {report.week}

-

Hours: {report.hours}

-

Tasks: {report.tasks}

-
- )) + {loadingReports ?

Loading reports...

: ( + reports.length === 0 ?

No reports to review

: ( + reports.map((report) => ( +
+

Week: {report.week}

+

Hours: {report.hours}

+

Tasks: {report.tasks}

+
+ )) + ) )} )} - {/* Job Evaluations Tab */} + {/* Job Evaluations */} {activeTab === "evaluations" && ( <> - {loadingEvaluations ? ( -

Loading evaluations...

- ) : evaluations.length === 0 ? ( -

No evaluations pending

- ) : ( -
{req.workplace?.name || "-"} {req.coordinator_status || "-"} - +
- - - - - - - - - {evaluations.map((evalItem) => ( - - - - + {loadingEvaluations ?

Loading evaluations...

: ( + evaluations.length === 0 ?

No evaluations pending

: ( +
Internee NameInternee EmailAction
{evalItem.interneeName}{evalItem.interneeEmail} - -
+ + + + + - ))} - -
Internee NameInternee EmailAction
+ + + {evaluations.map((evalItem) => ( + + {evalItem.interneeName} + {evalItem.interneeEmail} + + + + + ))} + + + ) + )} + + )} + + {/* Manual Reviews */} + {activeTab === "manualReviews" && ( + <> + {loadingManualReviews ?

Loading manual reviews...

: ( + manualReviews.length === 0 ?

No manual review forms.

: ( + + + + + + + + + + + {manualReviews.map((form) => ( + + + + + + + ))} + +
Student NameEmailCompanyActions
{form.student?.userName || "N/A"}{form.student?.email || "N/A"}{form.workplace?.name || "N/A"} + +
+ ) )} )} diff --git a/client/src/pages/CoordinatorEvaluationReview.jsx b/client/src/pages/CoordinatorEvaluationReview.jsx index 57755fe8..81b57616 100644 --- a/client/src/pages/CoordinatorEvaluationReview.jsx +++ b/client/src/pages/CoordinatorEvaluationReview.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; import axios from "axios"; -import "../styles/CoordinatorRequestDetailView.css"; +import "../styles/CoordinatorRequestDetailView.css"; // Reuse styles const CoordinatorEvaluationReview = () => { const { id } = useParams(); @@ -9,21 +9,21 @@ const CoordinatorEvaluationReview = () => { const [evaluation, setEvaluation] = useState(null); useEffect(() => { + const fetchEvaluationDetails = async () => { + try { + const res = await axios.get( + `${process.env.REACT_APP_API_URL}/api/coordinator/evaluations` + ); + const matchedEvaluation = res.data.find((form) => form._id === id); + setEvaluation(matchedEvaluation || null); + } catch (err) { + console.error("Error fetching evaluation form:", err); + } + }; + fetchEvaluationDetails(); }, [id]); - const fetchEvaluationDetails = async () => { - try { - const res = await axios.get( - `${process.env.REACT_APP_API_URL}/api/coordinator/evaluations` - ); - const matchedEvaluation = res.data.find((form) => form._id === id); - setEvaluation(matchedEvaluation || null); - } catch (err) { - console.error("Error fetching evaluation form:", err); - } - }; - const handleApprove = async () => { try { const res = await axios.post( @@ -63,12 +63,8 @@ const CoordinatorEvaluationReview = () => {

Job Evaluation (Form A3) Review

-

- Internee Name: {evaluation.interneeName} -

-

- Internee Email: {evaluation.interneeEmail} -

+

Internee Name: {evaluation.interneeName}

+

Internee Email: {evaluation.interneeEmail}

Evaluation Categories

@@ -90,22 +86,10 @@ const CoordinatorEvaluationReview = () => {
-
- - - +
+ + +
diff --git a/client/src/pages/CoordinatorManualReviewView.js b/client/src/pages/CoordinatorManualReviewView.js new file mode 100644 index 00000000..50a9ed31 --- /dev/null +++ b/client/src/pages/CoordinatorManualReviewView.js @@ -0,0 +1,90 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import axios from "axios"; +import "../styles/CoordinatorRequestDetailView.css"; // Reuse same styling + +const CoordinatorManualReviewView = () => { + const { id } = useParams(); + const navigate = useNavigate(); + const [formData, setFormData] = useState(null); + + useEffect(() => { + const fetchManualReviewForm = async () => { + try { + const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/coordinator/manual-review-a1`); + const matchedForm = res.data.find((form) => form._id === id); + setFormData(matchedForm || null); + } catch (err) { + console.error("Error fetching manual review form:", err); + } + }; + fetchManualReviewForm(); + }, [id]); + + const handleApprove = async () => { + try { + const res = await axios.post(`${process.env.REACT_APP_API_URL}/api/coordinator/manual-review-a1/${id}/approve`); + alert(res.data.message); + navigate("/coordinator-dashboard"); + } catch (err) { + console.error("Approval failed:", err); + alert("Error approving manual review form."); + } + }; + + const handleReject = async () => { + const reason = prompt("Please enter a reason for rejection:"); + if (!reason) { + alert("Rejection reason is required!"); + return; + } + try { + const res = await axios.post(`${process.env.REACT_APP_API_URL}/api/coordinator/manual-review-a1/${id}/reject`, { reason }); + alert(res.data.message); + navigate("/coordinator-dashboard"); + } catch (err) { + console.error("Rejection failed:", err); + alert("Error rejecting manual review form."); + } + }; + + if (!formData) return

Loading form details...

; + + return ( +
+

Manual Review (Failed A1 Form)

+ +
+

Student: {formData.student?.userName || "N/A"}

+

Email: {formData.student?.email || "N/A"}

+

Company: {formData.workplace?.name || "N/A"}

+ +

Tasks & CS Outcomes

+ + + + + + + + + {formData.tasks.map((task, idx) => ( + + + + + ))} + +
TaskOutcomes
{task.description}{task.outcomes?.join(", ") || "N/A"}
+ +
+ + + +
+
+
+ ); +}; + +export default CoordinatorManualReviewView; diff --git a/client/src/pages/CoordinatorRequestDetailView.js b/client/src/pages/CoordinatorRequestDetailView.js index 55acf7b2..f9e6827e 100644 --- a/client/src/pages/CoordinatorRequestDetailView.js +++ b/client/src/pages/CoordinatorRequestDetailView.js @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; import axios from "axios"; -import "../styles/CoordinatorRequestDetailView.css"; // Reuse same styling +import "../styles/CoordinatorRequestDetailView.css"; const CoordinatorRequestDetailView = () => { const { id } = useParams(); @@ -10,20 +10,20 @@ const CoordinatorRequestDetailView = () => { const [supervisorStatus, setSupervisorStatus] = useState(null); useEffect(() => { - fetchRequestDetails(); - }, [id]); + const fetchRequestDetails = async () => { + try { + const res = await axios.get( + `${process.env.REACT_APP_API_URL}/api/coordinator/request/${id}` + ); + setRequestData(res.data.requestData); + setSupervisorStatus(res.data.supervisorStatus); + } catch (err) { + console.error("Error fetching request details:", err); + } + }; - const fetchRequestDetails = async () => { - try { - const res = await axios.get( - `${process.env.REACT_APP_API_URL}/api/coordinator/request/${id}` - ); - setRequestData(res.data.requestData); - setSupervisorStatus(res.data.supervisorStatus); - } catch (err) { - console.error("Error fetching request details:", err); - } - }; + fetchRequestDetails(); + }, [id]); // ✅ Only depends on id const handleApprove = async () => { try { @@ -64,18 +64,10 @@ const CoordinatorRequestDetailView = () => {

Internship Request Details

-

- Student: {requestData.student?.name || "N/A"} -

-

- Email: {requestData.student?.email || "N/A"} -

-

- Company: {requestData.workplace?.name || "N/A"} -

-

- Supervisor Status: {supervisorStatus} -

+

Student: {requestData.student?.name || "N/A"}

+

Email: {requestData.student?.email || "N/A"}

+

Company: {requestData.workplace?.name || "N/A"}

+

Supervisor Status: {supervisorStatus}

Tasks & CS Outcomes

@@ -95,22 +87,10 @@ const CoordinatorRequestDetailView = () => {
-
- - - +
+ + +
diff --git a/client/src/router.js b/client/src/router.js index 0cd5d33e..96fe4ee1 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -16,6 +16,8 @@ import A4PresentationEvaluationForm from "./pages/A4PresentationEvaluationForm"; import SupervisorDashboard from "./pages/SupervisorDashboard"; import CoordinatorDashboard from "./pages/CoordinatorDashboard"; import CoordinatorRequestDetailView from "./pages/CoordinatorRequestDetailView"; +import CoordinatorManualReviewView from "./pages/CoordinatorManualReviewView"; // ✅ Newly added! +import CoordinatorEvaluationReview from "./pages/CoordinatorEvaluationReview"; import TokenRenewal from "./pages/TokenRenewal"; import StudentDashboard from "./pages/StudentDashboard"; import ProtectedRouteStudent from "./pages/ProtectedRouteStudent"; @@ -24,7 +26,6 @@ import SubmittedReports from "./pages/SubmittedReports"; import CumulativeReviewForm from "./pages/CumulativeReviewForm"; import CoordinatorReviewForm from "./pages/CoordinatorReviewForm"; import CoordinatorCumulativeReviewForm from "./pages/CoordinatorCumulativeReviewForm"; -import CoordinatorEvaluationReview from "./pages/CoordinatorEvaluationReview"; // Create and export the router configuration const router = createBrowserRouter([ @@ -33,19 +34,10 @@ const router = createBrowserRouter([ element: , errorElement: , children: [ - { - index: true, - element: , - }, - { - path: "signup", - element: , - }, - { - path: "weekly-report", - element: , - }, - { + { index: true, element: }, + { path: "signup", element: }, + { path: "weekly-report", element: }, + { path: "student-dashboard", element: ( @@ -53,70 +45,23 @@ const router = createBrowserRouter([ ), }, - { - path: "a1-form", - element: , - }, - { - path: "evaluation", - element: , - }, - { - path: "activate/:token", - element: , - }, - { - path: "presentation", - element: , - }, - { - path: "supervisor-dashboard", - element: , - }, - { - path: "coordinator-dashboard", - element: , - }, - { - path: "coordinator/request/:id", - element: , - }, - { - path: "renew-token/:token", - element: , - }, - { - path: "four-week-report", - element: , - }, - { - path: "submitted-reports", - element: , - }, - { - path: "submitted-reports/view/:reportId", - element: , - }, - { - path: "review-cumulative/:groupIndex", - element: , - }, - { - path: "coordinator-review/:groupIndex", - element: , - }, - { - path: "review-cumulative/:groupIndex/coordinator", - element: , - }, - { - path: "weekly-report/:groupIndex/week-:weekNumber/:studentName", - element: , - }, - { - path: "coordinator/evaluation/:id", - element: , - }, + { path: "a1-form", element: }, + { path: "evaluation", element: }, + { path: "activate/:token", element: }, + { path: "presentation", element: }, + { path: "supervisor-dashboard", element: }, + { path: "coordinator-dashboard", element: }, + { path: "coordinator/request/:id", element: }, + { path: "coordinator/manual-review/:id", element: }, + { path: "coordinator/evaluation/:id", element: }, + { path: "renew-token/:token", element: }, + { path: "four-week-report", element: }, + { path: "submitted-reports", element: }, + { path: "submitted-reports/view/:reportId", element: }, + { path: "review-cumulative/:groupIndex", element: }, + { path: "coordinator-review/:groupIndex", element: }, + { path: "review-cumulative/:groupIndex/coordinator", element: }, + { path: "weekly-report/:groupIndex/week-:weekNumber/:studentName", element: }, ], }, ]); diff --git a/server/controllers/approvalController.js b/server/controllers/approvalController.js index d26c3d53..1f1b0f28 100644 --- a/server/controllers/approvalController.js +++ b/server/controllers/approvalController.js @@ -124,9 +124,13 @@ const getCoordinatorRequests = async (req, res) => { try { const requests = await InternshipRequest.find({ coordinator_status: "pending", - }); + "approvals.0": "advisor", + csValidationPassed: true, + }).populate("student", "userName email"); + res.status(200).json(requests); } catch (err) { + console.error("Error fetching coordinator requests:", err); res.status(500).json({ message: "Failed to fetch internship requests." }); } }; @@ -140,6 +144,7 @@ const getCoordinatorRequestDetails = async (req, res) => { const supervisorStatus = requestData.supervisor_status || "Not Submitted"; res.status(200).json({ requestData, supervisorStatus }); } catch (err) { + console.error("Error fetching coordinator request details:", err); res.status(500).json({ message: "Failed to fetch request details." }); } }; @@ -147,53 +152,152 @@ const getCoordinatorRequestDetails = async (req, res) => { const coordinatorApproveRequest = async (req, res) => { try { const request = await InternshipRequest.findById(req.params.id); - if (!request) + if (!request) { return res.status(404).json({ message: "Request not found." }); + } request.status = "approved"; request.coordinator_status = "Approved"; request.coordinator_comment = "Approved by Coordinator"; await request.save(); - await EmailService.sendEmail({ - to: request.student.email, - subject: "Internship Request Approved", - html: `

Your internship request has been approved by the Coordinator.

`, - }); + if (request.student?.email) { + try { + await EmailService.sendEmail({ + to: request.student.email, + subject: "Internship Request Approved", + html: `

Your internship request has been approved by the Coordinator.

`, + }); + } catch (emailError) { + console.error("Failed to send approval email:", emailError.message); + // Continue even if email fails + } + } else { + console.warn("No student email found. Skipping email notification."); + } res.json({ message: "Request approved successfully." }); } catch (err) { + console.error("Approval failed:", err); res.status(500).json({ message: "Approval failed." }); } }; const coordinatorRejectRequest = async (req, res) => { const { reason } = req.body; - if (!reason) + if (!reason) { return res.status(400).json({ message: "Rejection reason required." }); + } try { const request = await InternshipRequest.findById(req.params.id); - if (!request) + if (!request) { return res.status(404).json({ message: "Request not found." }); + } request.status = "rejected"; request.coordinator_status = "Rejected"; request.coordinator_comment = reason; await request.save(); - await EmailService.sendEmail({ - to: request.student.email, - subject: "Internship Request Rejected", - html: `

Your internship request has been rejected.
Reason: ${reason}

`, - }); + if (request.student?.email) { + try { + await EmailService.sendEmail({ + to: request.student.email, + subject: "Internship Request Rejected", + html: `

Your internship request has been rejected.
Reason: ${reason}

`, + }); + } catch (emailError) { + console.error("Failed to send rejection email:", emailError.message); + } + } else { + console.warn("No student email found. Skipping email notification."); + } res.json({ message: "Request rejected successfully." }); } catch (err) { + console.error("Rejection failed:", err); res.status(500).json({ message: "Rejection failed." }); } }; + +const getManualReviewForms = async (req, res) => { + try { + const forms = await InternshipRequest.find({ + csValidationPassed: false, + manualReviewStatus: "pending", + }).populate("student", "userName email"); + + res.status(200).json(forms); + } catch (error) { + console.error("Error fetching manual review forms:", error); + res.status(500).send("Server Error"); + } +}; + +const coordinatorApproveManualReview = async (req, res) => { + try { + const formId = req.params.id; + const request = await InternshipRequest.findByIdAndUpdate( + formId, + { coordinator_status: "approved", manualReviewStatus: "approved" }, + { new: true } + ).populate("student"); + + if (!request) + return res.status(404).json({ message: "Request not found" }); + + if (request.student?.email) { + await EmailService.sendEmail({ + to: request.student.email, + subject: "Internship Request Approved (Manual Review)", + html: `

Your internship request has been manually reviewed and approved by the Coordinator.

`, + }); + } + + res.json({ message: "Manual Review Request Approved Successfully" }); + } catch (err) { + console.error("Manual review approval failed:", err); + res.status(500).json({ message: "Approval failed", error: err.message }); + } +}; + +const coordinatorRejectManualReview = async (req, res) => { + const { reason } = req.body; + if (!reason) + return res.status(400).json({ message: "Rejection reason required." }); + + try { + const formId = req.params.id; + const request = await InternshipRequest.findByIdAndUpdate( + formId, + { coordinator_status: "rejected", manualReviewStatus: "rejected" }, + { new: true } + ).populate("student"); + + if (!request) + return res.status(404).json({ message: "Request not found" }); + + if (request.student?.email) { + await EmailService.sendEmail({ + to: request.student.email, + subject: "Internship Request Rejected (Manual Review)", + html: `

Your internship request has been manually reviewed and rejected.
Reason: ${reason}

`, + }); + } + + res.json({ message: "Manual Review Request Rejected Successfully" }); + } catch (err) { + console.error("Manual review rejection failed:", err); + res.status(500).json({ message: "Rejection failed.", error: err.message }); + } +}; + +// ======================================= +// Coordinator Resend Feature +// ======================================= + const coordinatorResendRequest = async (req, res) => { try { const submission = await InternshipRequest.findById(req.params.id); @@ -212,6 +316,10 @@ const coordinatorResendRequest = async (req, res) => { } }; +// ======================================= +// Coordinator Evaluation +// ======================================= + const getCoordinatorReports = async (req, res) => { try { const reports = await WeeklyReport.find({}).sort({ submittedAt: -1 }); @@ -225,7 +333,7 @@ const getCoordinatorEvaluations = async (req, res) => { try { const evaluations = await Evaluation.find({ advisorAgreement: true, - coordinatorAgreement: { $ne: true }, // Only fetch not-yet-approved evaluations + coordinatorAgreement: { $ne: true }, }); res.status(200).json(evaluations); } catch (err) { @@ -244,50 +352,14 @@ const approveJobEvaluation = async (req, res) => { evaluation.updatedAt = new Date(); await evaluation.save(); - // Build Clean HTML Table without newlines - const evaluationTable = ` - - - - - - - - - - ${evaluation.evaluations - .map( - (item) => ` - - - - - - ` - ) - .join("")} - -
CategoryRatingComment
${item.category}${item.rating}${item.comment ? item.comment : "N/A"}
- `; - await EmailService.sendEmail({ to: evaluation.interneeEmail, subject: "Your Job Evaluation (Form A3) is Approved!", - html: ` -
-

Dear ${evaluation.interneeName},

-

Your Job Evaluation (Form A3) has been approved by the Coordinator. Please find the evaluation details below. Kindly upload this to Canvas:

- ${evaluationTable} -

Best regards,
Internship Program Management System

-
- `, + html: `

Dear ${evaluation.interneeName}, your evaluation is approved! Kindly upload this to Canvas.

`, }); - res.json({ - message: "A3 Job Evaluation approved and emailed successfully.", - }); + res.json({ message: "A3 Job Evaluation approved and emailed successfully." }); } catch (err) { - console.error("Approval failed:", err); res.status(500).json({ message: "Approval failed." }); } }; @@ -307,13 +379,11 @@ const rejectJobEvaluation = async (req, res) => { await EmailService.sendEmail({ to: evaluation.interneeEmail, subject: "Your Job Evaluation (Form A3) Needs Attention", - html: `

Dear ${evaluation.interneeName},

-

Your Job Evaluation (Form A3) was not approved.
Reason: ${reason}

`, + html: `

Dear ${evaluation.interneeName}, your A3 evaluation was not approved.
Reason: ${reason}

`, }); res.json({ message: "A3 Job Evaluation rejected successfully." }); } catch (err) { - console.error("Rejection failed:", err); res.status(500).json({ message: "Rejection failed." }); } }; @@ -323,6 +393,7 @@ const rejectJobEvaluation = async (req, res) => { // ======================================= module.exports = { + // Student-Side getStudentSubmissions, deleteStudentSubmission, getPendingSubmissions, @@ -330,12 +401,17 @@ module.exports = { rejectSubmission, deleteStalledSubmission, + // Coordinator-Side getCoordinatorRequests, getCoordinatorRequestDetails, coordinatorApproveRequest, coordinatorRejectRequest, coordinatorResendRequest, + getManualReviewForms, + coordinatorApproveManualReview, + coordinatorRejectManualReview, + // Coordinator Reports and Evaluations getCoordinatorReports, getCoordinatorEvaluations, approveJobEvaluation, diff --git a/server/routes/approvalRoutes.js b/server/routes/approvalRoutes.js index 397efc72..ada326ec 100644 --- a/server/routes/approvalRoutes.js +++ b/server/routes/approvalRoutes.js @@ -16,6 +16,9 @@ const { approveJobEvaluation, rejectJobEvaluation, coordinatorResendRequest, + getManualReviewForms, + coordinatorApproveManualReview, + coordinatorRejectManualReview } = require("../controllers/approvalController"); const { @@ -88,4 +91,23 @@ router.post( rejectJobEvaluation ); +// ----------------------------------------------- +// Coordinator Manual Review Routes (NEW) +// ----------------------------------------------- +router.get( + "/coordinator/manual-review-a1", + isCoordinator, + getManualReviewForms +); +router.post( + "/coordinator/manual-review-a1/:id/approve", + isCoordinator, + coordinatorApproveManualReview +); +router.post( + "/coordinator/manual-review-a1/:id/reject", + isCoordinator, + coordinatorRejectManualReview +); + module.exports = router; From aee82616514f84420640955112b30805b3764267 Mon Sep 17 00:00:00 2001 From: Kamal Poshala Date: Mon, 28 Apr 2025 19:59:36 -0500 Subject: [PATCH 15/19] Resolved index.js --- server/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/index.js b/server/index.js index 70bc76d6..c01641db 100644 --- a/server/index.js +++ b/server/index.js @@ -11,7 +11,7 @@ const formRoutes = require("./routes/formRoutes"); const emailRoutes = require("./routes/emailRoutes"); const tokenRoutes = require("./routes/token"); const approvalRoutes = require("./routes/approvalRoutes"); - +const studentRoutes = require("./routes/studentRoutes"); const outcomeRoutes = require("./routes/outcomeRoutes"); // Import cron job manager and register jobs @@ -81,7 +81,7 @@ app.use("/api/token", tokenRoutes); app.use("/api", approvalRoutes); app.use("/api/reports", weeklyReportRoutes); - +app.use("/api/student", studentRoutes); app.post("/api/createUser", async (req, res) => { try { const { userName, email, password, role } = req.body; From e7bd3690abc36c50c194f513bf711b5286cb2ee7 Mon Sep 17 00:00:00 2001 From: KamalPoshala <108420996+Kamal-Poshala@users.noreply.github.com> Date: Mon, 28 Apr 2025 20:45:55 -0500 Subject: [PATCH 16/19] Update A1InternshipRequestForm.js --- client/src/pages/A1InternshipRequestForm.js | 38 +++------------------ 1 file changed, 4 insertions(+), 34 deletions(-) diff --git a/client/src/pages/A1InternshipRequestForm.js b/client/src/pages/A1InternshipRequestForm.js index f4583977..cc11d362 100644 --- a/client/src/pages/A1InternshipRequestForm.js +++ b/client/src/pages/A1InternshipRequestForm.js @@ -254,41 +254,12 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { const submitFormData = async () => { try { - // Fetch logged-in student ID from localStorage or context - const studentId = localStorage.getItem("studentId"); // You must store this during login - - const payload = { - student: studentId, - workplace: { - name: formData.workplaceName, - website: formData.website, - phone: formData.phone - }, - internshipAdvisor: { - name: formData.advisorName, - jobTitle: formData.advisorJobTitle, - email: formData.advisorEmail - }, - creditHours: parseInt(formData.creditHours), - startDate: formData.startDate, - endDate: formData.endDate, - tasks: formData.tasks, - status: "submitted", - approvals: ["advisor"] - }; - const response = await fetch(`${process.env.REACT_APP_API_URL}/api/form/submit`, { method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify(payload) + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData), }); - - if (!response.ok) { - throw new Error("Failed to submit form", { cause: response }); - } - + if (!response.ok) throw new Error("Failed to submit form"); const data = await response.json(); return data; } catch (error) { @@ -296,7 +267,6 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { throw error; } }; - const sendTaskDescriptions = async (descriptions) => { try { @@ -611,4 +581,4 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { ); }; -export default A1InternshipRequestForm; \ No newline at end of file +export default A1InternshipRequestForm; From c8eb6645605ea83adda7785ec269b473b680167b Mon Sep 17 00:00:00 2001 From: KamalPoshala <108420996+Kamal-Poshala@users.noreply.github.com> Date: Mon, 28 Apr 2025 21:18:13 -0500 Subject: [PATCH 17/19] Update StudentDashboard.jsx --- client/src/pages/StudentDashboard.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/pages/StudentDashboard.jsx b/client/src/pages/StudentDashboard.jsx index 2ee5e5f1..8f20255d 100644 --- a/client/src/pages/StudentDashboard.jsx +++ b/client/src/pages/StudentDashboard.jsx @@ -11,7 +11,7 @@ const StudentDashboard = () => { const backendUrl = process.env.REACT_APP_API_URL; const ouEmail = user?.email; - const backendUrl = process.env.REACT_APP_API_URL; + const studentId = localStorage.getItem("studentId"); const [approvalStatus, setApprovalStatus] = useState("not_submitted"); @@ -394,4 +394,4 @@ console.log(approvalStatus) ); }; -export default StudentDashboard; \ No newline at end of file +export default StudentDashboard; From dc3c52f2f54abc2cd8e4fcb0fd8943d6599500d9 Mon Sep 17 00:00:00 2001 From: KamalPoshala <108420996+Kamal-Poshala@users.noreply.github.com> Date: Mon, 28 Apr 2025 21:21:39 -0500 Subject: [PATCH 18/19] Update StudentDashboard.jsx --- client/src/pages/StudentDashboard.jsx | 234 +++++--------------------- 1 file changed, 44 insertions(+), 190 deletions(-) diff --git a/client/src/pages/StudentDashboard.jsx b/client/src/pages/StudentDashboard.jsx index 8f20255d..c4e609f2 100644 --- a/client/src/pages/StudentDashboard.jsx +++ b/client/src/pages/StudentDashboard.jsx @@ -1,26 +1,20 @@ import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import "../styles/StudentDashboard.css"; - import Swal from "sweetalert2"; - +import "../styles/StudentDashboard.css"; const StudentDashboard = () => { const navigate = useNavigate(); const user = JSON.parse(localStorage.getItem("ipmsUser")); const backendUrl = process.env.REACT_APP_API_URL; - const ouEmail = user?.email; - - const studentId = localStorage.getItem("studentId"); const [approvalStatus, setApprovalStatus] = useState("not_submitted"); const [submissions, setSubmissions] = useState([]); + const [error, setError] = useState(""); useEffect(() => { - const fetchA1Status = async () => { - try { const res = await fetch(`${backendUrl}/api/student`, { method: "POST", @@ -28,9 +22,10 @@ const StudentDashboard = () => { body: JSON.stringify({ ouEmail: user?.email }), }); const data = await res.json(); - setApprovalStatus(data.approvalStatus); + setApprovalStatus(data.approvalStatus || "not_submitted"); } catch (err) { console.error("Error fetching A1 status:", err); + setError("Failed to fetch A1 status."); } }; @@ -47,10 +42,10 @@ const StudentDashboard = () => { }, }); const data = await res.json(); - setSubmissions(data); + setSubmissions(data || []); } catch (err) { - console.error("Error fetching submissions:", err); + setError("Failed to fetch submissions."); } }; @@ -65,9 +60,11 @@ const StudentDashboard = () => { const res = await fetch(`${backendUrl}/api/coordinator/request/${id}/resend`, { method: "POST", }); - if (res.ok) alert("Resent to coordinator!"); + if (res.ok) { + Swal.fire("Success", "Resent to coordinator!", "success"); + } } catch (err) { - alert("Error resending."); + Swal.fire("Error", "Error resending.", "error"); } }; @@ -82,15 +79,14 @@ const StudentDashboard = () => { }, }); if (res.ok) { - alert("Deleted successfully."); + Swal.fire("Deleted!", "Submission deleted successfully.", "success"); setSubmissions((prev) => prev.filter((s) => s._id !== id)); } } catch (err) { - alert("Error deleting."); + Swal.fire("Error", "Error deleting submission.", "error"); } }; - const handleAccountDelete = async () => { Swal.fire({ title: "Are you sure?", @@ -99,26 +95,20 @@ const StudentDashboard = () => { showCancelButton: true, confirmButtonColor: "#e45858", cancelButtonColor: "#3085d6", - confirmButtonText: "Yes, delete my account" + confirmButtonText: "Yes, delete my account", }).then(async (result) => { if (result.isConfirmed) { try { - // Make API call to delete account const res = await fetch(`${backendUrl}/api/token/delete`, { method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ ouEmail }), + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ouEmail: user?.email }), }); const data = await res.json(); if (res.ok) { - Swal.fire("Deleted!", data.message || "Your account has been deleted.", "success") .then(() => { - // Redirect to login page navigate("/", { replace: true }); - // Optionally, force reload to clear history stack window.location.reload(); }); } else { @@ -131,65 +121,36 @@ const StudentDashboard = () => { }); }; -console.log(approvalStatus) - - return (
- - + {/* Top Profile Section */}
-
+
- {/* Avatar Icon */} -
- {/* Bootstrap "person" icon, can use font-awesome as well */} - +
+
- {/* Student Info */}

- {user.fullName} + {user?.fullName || "Student Name"}

- - Advisor: [{user.academicAdvisor}] + + Advisor: [{user?.academicAdvisor || "Advisor"}]
- - Semester: [{user.semester}] + + Semester: [{user?.semester || "Semester"}]
- {/* FORM A1 Card */} + + {/* Form A1 Card */}

Request Internship (FORM A1)

@@ -197,65 +158,29 @@ console.log(approvalStatus)
- {/* FORM A2 Card */} + {/* Form A2 Card */}

Weekly Report (FORM A2)

- - - {approvalStatus === "not_submitted" && ( -

- Please fill your Form A1 first -

- )} - - {approvalStatus === "draft" && ( -

- Finish your Form A1 first -

- )} - - {(approvalStatus === "submitted" || - approvalStatus === "pending manual review") && ( -

- Wait for your Form A1 to be approved -

- )} -

- {approvalStatus === "approved" - ? "You may now submit weekly reports" - : "Finish Form A1 approval first"} + {approvalStatus === "approved" ? "You may now submit weekly reports" : "Finish Form A1 approval first"}

-
@@ -284,13 +209,12 @@ console.log(approvalStatus) {s.supervisor_status} {s.coordinator_status} - {s.supervisor_status === "approved" && - s.coordinator_status === "pending" ? ( + {s.supervisor_status === "approved" && s.coordinator_status === "pending" ? ( <> - - @@ -303,93 +227,23 @@ console.log(approvalStatus) )} -
+ + {/* Delete Account Button */} +
- {/* Submissions Table */} -
-

Your Internship Submissions

- {error &&
{error}
} - - - - - - - - - - - {submissions.map((req) => ( - - - - - - - ))} - -
CompanyStatusSubmitted OnActions
{req.workplace?.name || "-"}{req.status}{new Date(req.createdAt).toLocaleDateString()} - {req.reminders?.length === 2 && !req.coordinatorResponded ? ( - <> - - - - ) : ( - Waiting - )} -
-
+ {error &&
{error}
}
); }; From ea3b6555cb26057e8c06ef6ba44d3923192ffdda Mon Sep 17 00:00:00 2001 From: ICook094 Date: Mon, 28 Apr 2025 23:36:36 -0500 Subject: [PATCH 19/19] ReAdd A2 Detail Form Link --- client/src/pages/CoordinatorDashboard.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/src/pages/CoordinatorDashboard.js b/client/src/pages/CoordinatorDashboard.js index 70f2fc31..d33c06c5 100644 --- a/client/src/pages/CoordinatorDashboard.js +++ b/client/src/pages/CoordinatorDashboard.js @@ -126,6 +126,9 @@ const CoordinatorDashboard = () => {

Week: {report.week}

Hours: {report.hours}

Tasks: {report.tasks}

+ + +
)) )