- | {req.workplace.name} |
+ {req.workplace?.name || "-"} |
{req.status} |
{new Date(req.createdAt).toLocaleDateString()} |
- {req.reminders?.length === 2 && !req.coordinator_responded ? (
+ {req.reminders?.length === 2 && !req.coordinatorResponded ? (
<>
,
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: (
@@ -52,68 +45,25 @@ 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: "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: },
],
},
]);
-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;
+}
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 96f1b1de..1f1b0f28 100644
--- a/server/controllers/approvalController.js
+++ b/server/controllers/approvalController.js
@@ -2,335 +2,418 @@ 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");
-// =========================================== //
-// Managing Supervisor Forms //
-// =========================================== //
+// =======================================
+// Student-Side Controllers
+// =======================================
-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);
- } catch (err) {
- console.error("Error in getSupervisorForms:", err.message);
- return res.status(500).json({ message: "Failed to fetch supervisor forms", error: err.message });
+ const studentEmail = req.user.ouEmail;
+ const submissions = await InternshipRequest.find({
+ "student.email": studentEmail,
+ }).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 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 { id } = req.params;
+ const submission = await InternshipRequest.findById(id);
+ if (!submission)
+ return res.status(404).json({ message: "Submission not found." });
- const FormModel = models[form_type];
- if (!FormModel) {
- return res.status(400).json({ message: "Invalid form type" });
+ if (submission.student.email !== req.user.ouEmail) {
+ 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) {
+ console.error("Error deleting student submission:", err);
+ res.status(500).json({ message: "Internal server error." });
+ }
+};
+
+const getPendingSubmissions = async (req, res) => {
+ try {
+ const pendingRequests = await InternshipRequest.find({
+ supervisor_status: "pending",
+ });
+ res.status(200).json(pendingRequests);
+ } catch (err) {
+ console.error("Error fetching pending submissions:", err);
+ res.status(500).json({ message: "Failed to fetch pending submissions." });
+ }
+};
- const form = await FormModel.findByIdAndUpdate(formId, update, { new: true })
- .populate("student_id", "userName email");
+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." });
- if (!form) {
- return res.status(404).json({ message: "Form not found" });
- }
+ res.json({ message: "Submission approved", updated: request });
+ } catch (err) {
+ res.status(500).json({ message: "Approval failed", error: err.message });
+ }
+};
- 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 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." });
- 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;
+ res.json({ message: "Submission rejected", updated: request });
+ } catch (err) {
+ res.status(500).json({ message: "Rejection failed", error: err.message });
+ }
+};
- try {
- await EmailService.sendEmail({
- to: student_mail,
- subject: emailSubject,
- html: emailBody,
- });
- } catch (err) {
- console.error("Email sending error:", err);
+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 });
- } catch (err) {
- console.error("SupervisorFormAction error:", err);
- res.status(500).json({ message: "Error processing form", error: err.message });
+ await InternshipRequest.findByIdAndDelete(id);
+ res.status(200).json({ message: "Submission deleted successfully." });
+ } catch (error) {
+ console.error("Error deleting submission:", error);
+ res.status(500).json({ message: "Internal server error" });
}
};
+// =======================================
+// Coordinator-Side Controllers
+// =======================================
+
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) {
- res.status(500).json({ message: "Failed to fetch requests" });
+ console.error("Error fetching coordinator requests:", err);
+ 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")
- .lean();
-
- if (!requestData) {
- return res.status(404).json({ message: "Request not found" });
- }
+ const requestData = await InternshipRequest.findById(req.params.id).lean();
+ 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" });
+ console.error("Error fetching coordinator request details:", err);
+ 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");
-
+ const request = await InternshipRequest.findById(req.params.id);
if (!request) {
- return res.status(404).json({ message: "Request not found" });
+ 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" });
+ res.json({ message: "Request approved successfully." });
} catch (err) {
- res.status(500).json({ message: "Approval failed", error: err.message });
+ console.error("Approval failed:", err);
+ 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");
-
+ const request = await InternshipRequest.findById(req.params.id);
if (!request) {
- return res.status(404).json({ message: "Request not found" });
+ 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" });
+ res.json({ message: "Request rejected successfully." });
} catch (err) {
- res.status(500).json({ message: "Rejection failed", error: err.message });
+ console.error("Rejection failed:", err);
+ res.status(500).json({ message: "Rejection failed." });
}
};
-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();
+const getManualReviewForms = async (req, res) => {
+ try {
+ const forms = await InternshipRequest.find({
+ csValidationPassed: false,
+ manualReviewStatus: "pending",
+ }).populate("student", "userName email");
- return res.status(200).json({ message: "Reminder cycle restarted." });
+ res.status(200).json(forms);
} catch (error) {
- console.error("Error in coordinatorResendRequest:", error);
- return res.status(500).json({ message: "Server error while resending request." });
+ console.error("Error fetching manual review forms:", error);
+ res.status(500).send("Server Error");
}
};
-const deleteStudentSubmission = async (req, res) => {
+const coordinatorApproveManualReview = async (req, res) => {
try {
- const { id } = req.params;
- const studentId = req.user._id;
+ const formId = req.params.id;
+ const request = await InternshipRequest.findByIdAndUpdate(
+ formId,
+ { coordinator_status: "approved", manualReviewStatus: "approved" },
+ { new: true }
+ ).populate("student");
- const submission = await InternshipRequest.findById(id);
- if (!submission)
- return res.status(404).json({ message: "Submission not found." });
+ if (!request)
+ return res.status(404).json({ message: "Request not found" });
- if (submission.student.toString() !== studentId.toString()) {
- return res.status(403).json({ message: "You are not authorized to delete this submission." });
+ 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. `,
+ });
}
- if (submission.coordinator_status !== "pending") {
- return res.status(400).json({ message: "Submission already reviewed. Cannot delete." });
+ 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} `,
+ });
}
- await InternshipRequest.findByIdAndDelete(id);
- return res.status(200).json({ message: "Submission successfully deleted by student." });
+ res.json({ message: "Manual Review Request Rejected Successfully" });
} catch (err) {
- console.error("Error deleting student submission:", err);
- return res.status(500).json({ message: "Internal server error." });
+ console.error("Manual review rejection failed:", err);
+ res.status(500).json({ message: "Rejection failed.", error: err.message });
}
};
-const getStudentSubmissions = async (req, res) => {
+// =======================================
+// Coordinator Resend Feature
+// =======================================
+
+const coordinatorResendRequest = async (req, res) => {
try {
- const studentId = req.user._id;
- const submissions = await InternshipRequest.find({ student: studentId }).sort({ createdAt: -1 });
- res.status(200).json(submissions);
+ 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();
+
+ res.status(200).json({ message: "Reminder cycle restarted." });
} catch (error) {
- console.error("Error fetching student submissions:", error);
- res.status(500).json({ message: "Failed to fetch submissions." });
+ console.error("Error in coordinatorResendRequest:", error);
+ res.status(500).json({ message: "Server error while resending request." });
}
};
-const getPendingSubmissions = async (req, res) => {
- try {
- const pendingRequests = await InternshipRequest.find({
- supervisor_status: "pending",
- }).populate("student", "fullName ouEmail");
+// =======================================
+// Coordinator Evaluation
+// =======================================
- res.status(200).json(pendingRequests);
+const getCoordinatorReports = async (req, res) => {
+ try {
+ const reports = await WeeklyReport.find({}).sort({ submittedAt: -1 });
+ res.status(200).json({ reports });
} catch (err) {
- res.status(500).json({
- message: "Failed to fetch pending supervisor submissions",
- error: err.message,
- });
+ res.status(500).json({ message: "Failed to fetch weekly reports." });
}
};
-const approveSubmission = async (req, res) => {
- const { id } = req.params;
- const { comment } = req.body;
+const getCoordinatorEvaluations = 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" });
-
- res.json({ message: "Submission approved", updated: request });
+ const evaluations = await Evaluation.find({
+ advisorAgreement: true,
+ coordinatorAgreement: { $ne: true },
+ });
+ res.status(200).json(evaluations);
} catch (err) {
- res.status(500).json({ message: "Approval failed", error: err.message });
+ res.status(500).json({ message: "Failed to fetch evaluations." });
}
};
-const rejectSubmission = 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: "rejected", 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 rejected", updated: request });
+ evaluation.coordinatorAgreement = true;
+ evaluation.updatedAt = new Date();
+ await evaluation.save();
+
+ await EmailService.sendEmail({
+ to: evaluation.interneeEmail,
+ subject: "Your Job Evaluation (Form A3) is Approved!",
+ html: `Dear ${evaluation.interneeName}, your evaluation is approved! Kindly upload this to Canvas. `,
+ });
+
+ res.json({ message: "A3 Job Evaluation approved and emailed 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." });
- }
-
- if (submission.coordinator_status !== "pending") {
- return res.status(400).json({ message: "Submission already reviewed. Cannot delete." });
- }
+ evaluation.coordinatorAgreement = false;
+ evaluation.updatedAt = new Date();
+ await evaluation.save();
- await InternshipRequest.findByIdAndDelete(id);
+ await EmailService.sendEmail({
+ to: evaluation.interneeEmail,
+ subject: "Your Job Evaluation (Form A3) Needs Attention",
+ html: `Dear ${evaluation.interneeName}, your A3 evaluation was not approved. 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: "A3 Job Evaluation rejected successfully." });
+ } catch (err) {
+ res.status(500).json({ message: "Rejection failed." });
}
};
+// =======================================
+// EXPORTS
+// =======================================
module.exports = {
- getCoordinatorRequests,
- getCoordinatorRequestDetails,
- coordinatorApproveRequest,
- coordinatorRejectRequest,
- coordinatorResendRequest,
- deleteStudentSubmission,
+ // Student-Side
getStudentSubmissions,
+ deleteStudentSubmission,
getPendingSubmissions,
- getSupervisorForms,
- handleSupervisorFormAction,
approveSubmission,
rejectSubmission,
deleteStalledSubmission,
+
+ // Coordinator-Side
+ getCoordinatorRequests,
+ getCoordinatorRequestDetails,
+ coordinatorApproveRequest,
+ coordinatorRejectRequest,
+ coordinatorResendRequest,
+ getManualReviewForms,
+ coordinatorApproveManualReview,
+ coordinatorRejectManualReview,
+
+ // Coordinator Reports and Evaluations
+ 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/jobs/reminderEmail.js b/server/jobs/reminderEmail.js
index d323247b..cb057232 100644
--- a/server/jobs/reminderEmail.js
+++ b/server/jobs/reminderEmail.js
@@ -2,177 +2,203 @@ const emailService = require("../services/emailService");
const NotificationLog = require("../models/NotifLog");
const User = require("../models/User");
const UserTokenRequest = require("../models/TokenRequest");
+const InternshipRequest = require("../models/InternshipRequest");
+const WeeklyReport = require("../models/WeeklyReport");
+const Evaluation = require("../models/Evaluation");
const logger = require("../utils/logger");
const dayjs = require("dayjs");
-// ================= Coordinator Reminder =================
-// Sends reminders for
-// A1 (InternshipRequest) every 5 working days
-// A2 (Weekly Reports) every 5 working days
-// A3 (Evaluations) every 3 days
+// ================================================
+// Utility: Fetch All Forms for Reminders
+// ================================================
+const getAllForms = async (filter = {}) => {
+ const formPromises = [
+ InternshipRequest.find(filter),
+ WeeklyReport.find(filter),
+ Evaluation.find(filter),
+ ];
+ const allResults = await Promise.all(formPromises);
+ return allResults.flat();
+};
+
+// ================================================
+// Coordinator Reminders
+// ================================================
const coordinatorReminder = async () => {
const now = dayjs();
- const fiveWorkingDays = now.subtract(7, "day").toDate();
- const threeDays = now.subtract(3, 'day').toDate();
+ const fiveDaysAgo = now.subtract(7, "day").toDate(); // ~5 working days
+ const threeDaysAgo = now.subtract(3, "day").toDate(); // 3 days for A3
try {
- const pendingSubs = await getAllForms({
+ // A1 and A2 pending forms (5 days)
+ const pendingA1A2 = await InternshipRequest.find({
coordinator_status: "pending",
supervisor_status: "approved",
- createdAt: { $lt: fiveWorkingDays },
- evaluations: { $size: 0 },
+ createdAt: { $lt: fiveDaysAgo },
});
- const pendingEvals = await getAllForms({
+ const pendingWeeklyReports = await WeeklyReport.find({
coordinator_status: "pending",
supervisor_status: "approved",
- createdAt: { $lt: threeDays },
- evaluations: { $ne: [] },
+ createdAt: { $lt: fiveDaysAgo },
});
- await sendCoordinatorReminder(pendingSubs, 5, now);
- await sendCoordinatorReminder(pendingEvals, 3, now);
+ // A3 pending evaluations (3 days)
+ const pendingEvaluations = await Evaluation.find({
+ advisorAgreement: true,
+ coordinatorAgreement: { $exists: false },
+ createdAt: { $lt: threeDaysAgo },
+ });
+
+ await sendCoordinatorReminder(pendingA1A2, 5, now, "A1 Internship Request");
+ await sendCoordinatorReminder(
+ pendingWeeklyReports,
+ 5,
+ now,
+ "A2 Weekly Report"
+ );
+ await sendCoordinatorReminder(
+ pendingEvaluations,
+ 3,
+ now,
+ "A3 Job Evaluation"
+ );
} catch (err) {
logger.error("❌ Error in coordinatorReminder:", err.message);
}
};
-const sendCoordinatorReminder = async (subs, nextDueIn, now) => {
- try {
- for (const submission of subs) {
- 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(nextDueIn, "day");
+const sendCoordinatorReminder = async (forms, nextDueInDays, now, formType) => {
+ for (const form of forms) {
+ try {
+ const reminderCount = form.coordinator_reminder_count || 0;
+ const lastReminded = form.last_coordinator_reminder_at || form.createdAt;
+ const nextReminderDue = dayjs(lastReminded).add(nextDueInDays, "day");
const shouldRemindAgain = now.isAfter(nextReminderDue);
- if (reminderCount >= 2 && shouldRemindAgain && !submission.studentNotified) {
+ if (!shouldRemindAgain) continue;
+
+ const student = await UserTokenRequest.findById(form.student_id);
+ const coordinator = await UserTokenRequest.findById(form.coordinator_id);
+
+ if (!student || !coordinator) continue;
+
+ if (reminderCount >= 2 && !form.studentNotified) {
+ // Escalate to student
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.`,
+ to: student.ouEmail,
+ subject: `Coordinator Not Responding - ${formType}`,
+ html: `Your ${formType} has not been approved even after multiple reminders.
+ You can now resend or delete your submission if needed. `,
});
await NotificationLog.create({
- submissionId: submission._id,
+ submission_id: form._id,
type: "studentEscalation",
- recipientEmail: student.email,
- message: `Student notified about stalled coordinator approval for "${submission.name}"`,
+ recipient_email: student.ouEmail,
+ message: `Escalation: Student notified about delayed ${formType} approval`,
});
- submission.coordinator_studentNotified = true;
- await submission.save();
-
- logger.info(`🔔 Escalation: student notified for "${submission.name}"`);
- } else if (shouldRemindAgain) {
+ form.studentNotified = true;
+ } else {
+ // Send reminder to Coordinator
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}".`,
+ to: coordinator.ouEmail,
+ subject: `Reminder: Action Needed on ${formType}`,
+ html: `Please review and approve the pending ${formType} form submitted by the student. `,
});
- 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}"`);
+ form.coordinator_reminder_count = reminderCount + 1;
}
+
+ form.last_coordinator_reminder_at = new Date();
+ await form.save();
+ logger.info(`✅ Reminder sent for ${formType}: Form ID ${form._id}`);
+ } catch (err) {
+ logger.error(
+ `❌ Error sending coordinator reminder for ${formType}:`,
+ err.message
+ );
}
- } catch (err) {
- logger.error("❌ Error in sendCoordinatorReminder:", err.message);
}
};
-// ================= Supervisor Reminder =================
-const getAllForms = async (filter = {}) => {
- const models = {
- A1: require("../models/InternshipRequest"),
- A2: require("../models/WeeklyReport"),
- A3: require("../models/Evaluation"),
- };
-
- 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 Reminders
+// ================================================
const supervisorReminder = async () => {
const now = dayjs();
- const fiveWorkingDays = now.subtract(7, "day").toDate();
+ const fiveDaysAgo = now.subtract(7, "day").toDate();
try {
const pendingSubs = await getAllForms({
supervisor_status: "pending",
- last_supervisor_reminder_at: { $lt: fiveWorkingDays },
+ last_supervisor_reminder_at: { $lt: fiveDaysAgo },
});
- const supervisors = await UserTokenRequest.find({
- role: "supervisor",
- isActivated: true,
- });
-
- for (const submission of pendingSubs) {
+ for (const form of pendingSubs) {
+ try {
+ const student = await UserTokenRequest.findById(form.student_id);
+ const reminderCount = form.supervisor_reminder_count || 0;
+ const lastReminded = form.last_supervisor_reminder_at || form.createdAt;
+ const nextReminderDue = dayjs(lastReminded).add(5, "day");
+ const shouldRemindAgain = now.isAfter(nextReminderDue);
- 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 nextReminderDue = dayjs(lastReminded).add(5, "day");
- const shouldRemindAgain = now.isAfter(nextReminderDue);
+ if (!shouldRemindAgain) continue;
+ if (!student) continue;
- if (reminderCount >= 2 && shouldRemindAgain) {
- await emailService.sendEmail({
- 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.`,
- });
+ if (reminderCount >= 2) {
+ // Escalate to student
+ await emailService.sendEmail({
+ to: student.ouEmail,
+ subject: `Supervisor Not Responding`,
+ html: `Your internship form has not been reviewed by the supervisor after multiple reminders.
+ You may consider resending or deleting the form. `,
+ });
- await NotificationLog.create({
- submission_id: submission._id,
- type: "studentEscalation",
- recipient_email: student.ouEmail,
- message: `Student notified about supervisor status on: "${submission._id}"`,
- });
+ await NotificationLog.create({
+ submission_id: form._id,
+ type: "studentEscalation",
+ recipient_email: student.ouEmail,
+ message: `Student notified about supervisor inaction`,
+ });
- logger.info(`Returned to student for resubmit/delete: "${submission._id}"`);
- } else if (shouldRemindAgain) {
- for (const supervisor of supervisors) {
- await emailService.sendEmail({
- to: supervisor.ouEmail,
- subject: `Reminder: Please Review Submission "${submission._id}"`,
- html: `This is a reminder to review the submission by ${student.ouEmail}. `,
- text: `Reminder to review submission "${submission._id}".`,
+ logger.info(
+ `⚠️ Escalation sent to student for supervisor delay: Form ${form._id}`
+ );
+ } else {
+ // Reminder to Supervisor
+ const supervisors = await UserTokenRequest.find({
+ role: "supervisor",
+ isActivated: true,
});
- }
- submission.supervisor_reminder_count = reminderCount + 1;
- submission.last_supervisor_reminder_at = new Date();
+ for (const supervisor of supervisors) {
+ await emailService.sendEmail({
+ to: supervisor.ouEmail,
+ subject: `Reminder: Please Review Internship Form`,
+ html: `Kindly review and approve the student's internship submission pending your action. `,
+ });
+ }
- try {
- await submission.save();
- } catch (err) {
- logger.error(`Failed to save submission: ${err.message}`);
+ logger.info(`📧 Reminder sent to supervisors for Form ${form._id}`);
}
- logger.info(`[Reminder Sent] Supervisor: "${supervisor.email}" for "${submission.name}"`);
+ form.supervisor_reminder_count = reminderCount + 1;
+ form.last_supervisor_reminder_at = new Date();
+ await form.save();
+ } catch (err) {
+ logger.error(`❌ Error processing supervisor reminder:`, err.message);
}
}
} catch (err) {
- logger.error("Error in supervisorReminder:", err.message || err);
+ logger.error("❌ Error in supervisorReminder:", err.message);
}
};
+// ================================================
+// Exports
+// ================================================
module.exports = {
coordinatorReminder,
supervisorReminder,
diff --git a/server/models/InternshipRequest.js b/server/models/InternshipRequest.js
index a4649818..ba8905e9 100644
--- a/server/models/InternshipRequest.js
+++ b/server/models/InternshipRequest.js
@@ -8,64 +8,77 @@ const Task = new mongoose.Schema({
type: String,
required: true,
},
- outcomes: [{
- type: String,
- enum: [
- "problemSolving",
- "solutionDevelopment",
- "communication",
- "decisionMaking",
- "collaboration",
- "application"
- ]
- }]
-
+ outcomes: [
+ {
+ type: String,
+ enum: [
+ "problemSolving",
+ "solutionDevelopment",
+ "communication",
+ "decisionMaking",
+ "collaboration",
+ "application",
+ ],
+ },
+ ],
});
-const formA1 = new mongoose.Schema({
- ...formMetadata,
- student: {
- type: ObjectId,
+const formA1 = new mongoose.Schema(
+ {
+ // student: {
+ // type: ObjectId,
+ // required: true,
+ // ref: 'UserTokenRequest'
+ // },
+ student: {
+ name: {
+ type: String,
required: true,
- ref: 'UserTokenRequest'
+ },
+ email: {
+ unique: true,
+ type: String,
+ required: true,
+ },
},
+ ...formMetadata,
+ // student: {
+ // type: ObjectId,
+ // required: true,
+ // ref: 'UserTokenRequest'
+ // },
workplace: {
- name: {
- type: String,
- required: true,
- },
- website: String,
- phone: String, // TODO how to validate this?
+ name: {
+ type: String,
+ required: true,
+ },
+ website: String,
+ phone: String, // TODO how to validate this?
},
internshipAdvisor: {
- name: String,
- jobTitle: String,
- email: {
- type: String,
- required: true
- }
+ name: String,
+ jobTitle: String,
+ email: {
+ type: String,
+ required: true,
+ },
},
creditHours: {
- type: Number,
- required: true,
- enum: [1, 2, 3]
+ type: Number,
+ required: true,
+ enum: [1, 2, 3],
},
-
- requestedAt: {
- type: Date,
- default: Date.now,
- },
-
startDate: {
- type: Date,
- required: true
+ type: Date,
+ required: true,
},
- endDate: { // TODO how to make sure endDate is later than startDate?
- type: Date,
- required: true
+ endDate: {
+ // TODO how to make sure endDate is later than startDate?
+ type: Date,
+ required: true,
},
tasks: {
- type: [Task],
- required: true
+ type: [Task],
+ required: true,
},
// status: {
// type: String,
@@ -79,11 +92,13 @@ const formA1 = new mongoose.Schema({
reminders: [Date],
// requiredHours is an easily derived attribute
// TODO needs to be a virtual getter that checks this student's WeeklyReports
- completedHours: Number
-}, { timestamps: true });
-formA1.virtual("requiredHours").get(function() {
- return this.creditHours * 60;
-})
+ completedHours: Number,
+ },
+ { timestamps: true }
+);
+formA1.virtual("requiredHours").get(function () {
+ return this.creditHours * 60;
+});
module.exports =
mongoose.models.InternshipRequest ||
diff --git a/server/routes/approvalRoutes.js b/server/routes/approvalRoutes.js
index f8fde20a..ada326ec 100644
--- a/server/routes/approvalRoutes.js
+++ b/server/routes/approvalRoutes.js
@@ -1,69 +1,113 @@
const express = require("express");
const router = express.Router();
-
const {
- getSupervisorForms,
- handleSupervisorFormAction,
+ getStudentSubmissions,
+ deleteStudentSubmission,
+ getPendingSubmissions,
+ approveSubmission,
+ rejectSubmission,
+ deleteStalledSubmission,
getCoordinatorRequests,
getCoordinatorRequestDetails,
coordinatorApproveRequest,
coordinatorRejectRequest,
- getStudentSubmissions,
- getPendingSubmissions,
+ getCoordinatorReports,
+ getCoordinatorEvaluations,
+ approveJobEvaluation,
+ rejectJobEvaluation,
coordinatorResendRequest,
- deleteStalledSubmission,
- deleteStudentSubmission,
- rejectSubmission,
- approveSubmission,
+ getManualReviewForms,
+ coordinatorApproveManualReview,
+ coordinatorRejectManualReview
} = 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.post(
+ "/coordinator/request/:id/resend",
+ isCoordinator,
+ coordinatorResendRequest
+);
+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
+);
+// -----------------------------------------------
+// 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;
\ No newline at end of file
+module.exports = router;
diff --git a/server/routes/formRoutes.js b/server/routes/formRoutes.js
index 04ebee4d..9c100fd5 100644
--- a/server/routes/formRoutes.js
+++ b/server/routes/formRoutes.js
@@ -2,44 +2,49 @@ const express = require("express");
const router = express.Router();
const InternshipRequest = require("../models/InternshipRequest");
const { insertFormData } = require("../services/insertData");
+const emailService = require("../services/emailService"); // Missing import added
-// router.post("/internshiprequests/:id/approve", approveSubmission);
-// router.post("/internshiprequests/:id/reject", rejectSubmission);
-
-// UPDATED: GET route to fetch internship requests pending supervisor action
+// -----------------------------------------
+// GET internship requests (pending supervisor action)
+// -----------------------------------------
router.get("/internshiprequests", async (req, res) => {
try {
const requests = await InternshipRequest.find({
- supervisor_status: "pending",
- // approvals: "advisor", // advisor has approved
- supervisor_status: { $in: [null, "pending"] } // not yet reviewed by supervisor
- }).sort({ createdAt: 1 }) .populate("student", "userName") // oldest first
-
+ supervisor_status: { $in: [null, "pending"] },
+ })
+ .sort({ createdAt: 1 }) // oldest first
+ .select(
+ "student workplace name supervisor_status coordinator_status createdAt"
+ );
res.status(200).json(requests);
} catch (err) {
console.error("Error fetching internship requests:", err);
- res.status(500).json({ message: "Server error while fetching internship requests" });
+ res
+ .status(500)
+ .json({ message: "Server error while fetching internship requests" });
}
});
-// Validate required fields
+// -----------------------------------------
+// Validate Form Data (Before Submit)
+// -----------------------------------------
function validateFormData(formData) {
const requiredFields = [
- 'soonerId',
- 'workplaceName',
- 'website',
- 'phone',
- 'advisorName',
- 'advisorJobTitle',
- 'advisorEmail',
- 'creditHours',
- 'startDate',
- 'endDate',
- 'tasks'
+ "soonerId",
+ "workplaceName",
+ "website",
+ "phone",
+ "advisorName",
+ "advisorJobTitle",
+ "advisorEmail",
+ "creditHours",
+ "startDate",
+ "endDate",
+ "tasks",
];
for (const field of requiredFields) {
- if (!formData[field] || formData[field] === '') {
+ if (!formData[field] || formData[field] === "") {
return `Missing or empty required field: ${field}`;
}
}
@@ -48,35 +53,32 @@ function validateFormData(formData) {
return `Sooner ID must be a 9-digit number, not ${formData.soonerId}`;
if (!Array.isArray(formData.tasks) || formData.tasks.length === 0) {
- return 'Tasks must be a non-empty array';
+ return "Tasks must be a non-empty array.";
}
- // for (const [index, task] of formData.tasks.entries()) {
- // if (!task.description || !task.outcomes) {
- // return `Task at index ${index} is missing description or outcomes`;
- // }
- // }
-
- // uncomment below if student has to fill in task outcomes
- // const filledTasks = formData.tasks.filter((task) => task.description && task.outcomes );
- // if (filledTasks.length < 3)
- // return `At least 3 tasks must have description and outcomes; only ${filledTasks.length} do`;
const tasks = formData.tasks;
- console.log(tasks);
- if (tasks.filter((task) => task.description && task.description.trim() !== '').length < 3)
- return 'At least 3 tasks must be provided';
+ if (
+ tasks.filter((task) => task.description && task.description.trim() !== "")
+ .length < 3
+ )
+ return "At least 3 tasks must be provided.";
+
const uniqueOutcomes = new Set();
tasks.forEach((task) => {
if (Array.isArray(task.outcomes)) {
- task.outcomes.forEach(outcome => uniqueOutcomes.add(outcome));
- }
+ task.outcomes.forEach((outcome) => uniqueOutcomes.add(outcome));
+ }
});
- formData.status = uniqueOutcomes.size < 3 ? 'pending manual review' : 'submitted';
+
+ formData.status =
+ uniqueOutcomes.size < 3 ? "pending manual review" : "submitted";
return null;
}
-
-router.post('/submit', async (req, res) => {
+// -----------------------------------------
+// Submit Form A1
+// -----------------------------------------
+router.post("/submit", async (req, res) => {
const formData = req.body;
const validationError = validateFormData(formData);
if (validationError) {
@@ -85,16 +87,24 @@ router.post('/submit', async (req, res) => {
try {
await insertFormData(formData);
- res.status(200).json({ message: 'Form received and handled!', manual: formData.status !== 'submitted'});
+ res.status(200).json({
+ message: "Form received and handled!",
+ manual: formData.status !== "submitted",
+ });
} catch (error) {
- console.error('Error handling form data:', error);
- res.status(500).json({ message: 'Something went wrong' });
+ console.error("Error handling form data:", error);
+ res.status(500).json({ message: "Something went wrong" });
}
});
-router.get('/pending-requests', async (req, res) => {
+// -----------------------------------------
+// Get pending internship requests for Student Dashboard
+// -----------------------------------------
+router.get("/pending-requests", async (req, res) => {
try {
- const pending = await InternshipRequest.find({ status: { $in: ['submitted', 'pending manual review'] } });
+ const pending = await InternshipRequest.find({
+ status: { $in: ["submitted", "pending manual review"] },
+ });
res.json(pending);
} catch (err) {
console.error("Error fetching pending submissions:", err);
@@ -102,30 +112,28 @@ router.get('/pending-requests', async (req, res) => {
}
});
+// -----------------------------------------
+// Resend Request (Reset reminders)
+// -----------------------------------------
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.coordinator_responded = false;
- request.coordinator_studentNotified = false;
+ request.coordinatorResponded = false;
+ request.studentNotified = false;
await request.save();
- // Send email to coordinator
+ // Send email to internship advisor and student
await emailService.sendEmail({
- to: [
- request.internshipAdvisor.email,
- request.student.email,
- "coordinator@ipms.edu"
- ],
+ to: [request.internshipAdvisor.email, request.student.email],
subject: "Internship Request Resent",
html: `
Hello,
- The student ${request.student.userName} has resent their internship approval request due to inactivity.
+ The student ${request.student.name} has resent their internship approval request due to coordinator inactivity.
Please review and take necessary action.
- `
+ `,
});
res.json({ message: "Request resent successfully" });
@@ -134,22 +142,33 @@ router.post("/requests/:id/resend", async (req, res) => {
res.status(500).json({ message: "Failed to resend request" });
}
});
+
+// -----------------------------------------
+// Delete 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" });
}
});
+
+// -----------------------------------------
+// Fetch student's approval status
+// -----------------------------------------
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 });
+ const request = await InternshipRequest.findOne({
+ "student.email": ouEmail,
+ });
if (!request) return res.json({ approvalStatus: "not_submitted" });
return res.json({ approvalStatus: request.status || "draft" });
@@ -158,4 +177,5 @@ router.post("/student", async (req, res) => {
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 6408eb40..a000f895 100644
--- a/server/utils/cronUtils.test.js
+++ b/server/utils/cronUtils.test.js
@@ -14,85 +14,123 @@ 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("should 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", () => {
+ it("should register a job successfully with runOnInit", () => {
+ cron.validate.mockReturnValue(true);
+ cron.schedule.mockReturnValue({ stop: jest.fn() });
+
+ const result = cronJobManager.registerJob(
+ "testJob",
+ "*/5 * * * *",
+ mockJobFunction,
+ { runOnInit: true }
+ );
+
+ expect(result).toBe(true);
+ expect(cron.schedule).toHaveBeenCalledTimes(1);
+ expect(logger.info).toHaveBeenCalledWith(
+ `Running job testJob immediately on init`
+ );
+ });
+
+ it("should register a job successfully 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(logger.info).not.toHaveBeenCalledWith(
+ `Running job testJob immediately on init`
+ );
+ });
+
+ it("should NOT register job with invalid cron expression", () => {
+ cron.validate.mockReturnValue(false);
+
+ const result = cronJobManager.registerJob(
+ "InvalidJob",
+ "invalid",
+ mockJobFunction
+ );
+
+ expect(result).toBe(false);
+ expect(logger.error).toHaveBeenCalledWith(
+ "Invalid cron expression: invalid"
+ );
+ });
+
+ it("should warn and replace an already existing job", () => {
+ cron.validate.mockReturnValue(true);
+ cron.schedule.mockReturnValue({ stop: jest.fn() });
+
+ cronJobManager.registerJob("testJob", "*/5 * * * *", mockJobFunction);
+ const result = cronJobManager.registerJob(
+ "testJob",
+ "*/10 * * * *",
+ mockJobFunction
+ );
+
+ 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", () => {
- cron.validate.mockReturnValue(true);
- cron.schedule.mockReturnValue({ stop: jest.fn() });
+ describe("stopJob", () => {
+ it("should stop a running job", () => {
+ cron.validate.mockReturnValue(true);
+ const stopFn = jest.fn();
+ cron.schedule.mockReturnValue({ stop: stopFn });
- const result = cronJobManager.registerJob(
- "Job2",
- "*/1 * * * *",
- mockJobFunction
- );
+ cronJobManager.registerJob("JobToStop", "*/5 * * * *", mockJobFunction);
+ cronJobManager.stopJob("JobToStop");
- expect(result).toBe(true);
+ expect(stopFn).toHaveBeenCalled();
+ expect(logger.info).toHaveBeenCalledWith("Stopped job: JobToStop");
+ });
});
- test("should not register job with invalid cron", () => {
- cron.validate.mockReturnValue(false);
+ describe("listJobs", () => {
+ it("should list all registered jobs", () => {
+ cron.validate.mockReturnValue(true);
+ cron.schedule.mockReturnValue({ stop: jest.fn() });
- const result = cronJobManager.registerJob("InvalidJob", "invalid", mockJobFunction);
+ cronJobManager.registerJob("JobToList", "*/1 * * * *", mockJobFunction);
- expect(result).toBe(false);
- expect(logger.error).toHaveBeenCalledWith(
- "Invalid cron expression: invalid"
- );
- });
- test("should stop a specific job", () => {
- cron.validate.mockReturnValue(true);
- const stopFn = jest.fn();
- cron.schedule.mockReturnValue({ stop: stopFn });
-
- cronJobManager.registerJob("Job3", "*/1 * * * *", mockJobFunction);
- cronJobManager.stopJob("Job3");
-
- expect(stopFn).toHaveBeenCalled();
- });
-
- 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);
+ const jobs = cronJobManager.listJobs();
+ expect(jobs.length).toBeGreaterThan(0);
+ expect(jobs[0].name).toBe("JobToList");
+ });
});
});
|