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..d33c06c5 100644
--- a/client/src/pages/CoordinatorDashboard.js
+++ b/client/src/pages/CoordinatorDashboard.js
@@ -1,18 +1,33 @@
import React, { useEffect, useState } from "react";
-import axios from "axios";
import { useNavigate } from "react-router-dom";
-import "../styles/SupervisorDashboard.css";
+import axios from "axios";
+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 (Form A1)
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 () => {
@@ -25,25 +40,11 @@ const CoordinatorDashboard = () => {
setLoadingRequests(false);
}
};
- // Group D's Weekly Report Review Logic
- const [reportGroups, setReportGroups] = useState([]);
- const [loadingReports, setLoadingReports] = useState(true);
-
- useEffect(() => {
- if (activeTab === "reports") {
- fetchReportGroups();
- }
- }, [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,14 +52,28 @@ const CoordinatorDashboard = () => {
}
};
- const handleReviewClick = (group) => {
- localStorage.setItem(`coordinator_reviewed_${group.groupIndex}`, "true");
- navigate(`/review-cumulative/${group.groupIndex}`);
+ 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);
+ }
+ };
+
+ 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);
+ }
};
-
- // Render UI
-
return (
Coordinator Dashboard
@@ -66,29 +81,33 @@ const CoordinatorDashboard = () => {
{/* Tabs */}
-
+
+
+
- {/* Tab: Internship Requests */}
+ {/* Internship Requests */}
{activeTab === "requests" && (
<>
- {loadingRequests ?
Loading...
: (
+ {loadingRequests ?
Loading requests...
: (
| Student Name |
- Student ID |
Company |
Status |
+ Action |
- {requests.map(req => (
+ {requests.map((req) => (
- | {req.studentName} |
- {req.studentId} |
- {req.companyName} |
- {req.status} |
+ {req.student?.name || "-"} |
+ {req.workplace?.name || "-"} |
+ {req.coordinator_status || "-"} |
+
+
+ |
))}
@@ -97,23 +116,84 @@ const CoordinatorDashboard = () => {
>
)}
- {/* Tab: Weekly Reports Review */}
+ {/* Weekly Reports */}
{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}
- ))}
-
-
+ reports.length === 0 ?
No reports to review
: (
+ reports.map((report) => (
+
+
Week: {report.week}
+
Hours: {report.hours}
+
Tasks: {report.tasks}
+
+
+ |
))
+ )
+ )}
+ >
+ )}
+
+ {/* Job Evaluations */}
+ {activeTab === "evaluations" && (
+ <>
+ {loadingEvaluations ?
Loading evaluations...
: (
+ evaluations.length === 0 ?
No evaluations pending
: (
+
+
+
+ | Internee Name |
+ Internee Email |
+ Action |
+
+
+
+ {evaluations.map((evalItem) => (
+
+ | {evalItem.interneeName} |
+ {evalItem.interneeEmail} |
+
+
+ |
+
+ ))}
+
+
+ )
+ )}
+ >
+ )}
+
+ {/* Manual Reviews */}
+ {activeTab === "manualReviews" && (
+ <>
+ {loadingManualReviews ?
Loading manual reviews...
: (
+ manualReviews.length === 0 ?
No manual review forms.
: (
+
+
+
+ | Student Name |
+ Email |
+ Company |
+ Actions |
+
+
+
+ {manualReviews.map((form) => (
+
+ | {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
new file mode 100644
index 00000000..81b57616
--- /dev/null
+++ b/client/src/pages/CoordinatorEvaluationReview.jsx
@@ -0,0 +1,99 @@
+import React, { useEffect, useState } from "react";
+import { useParams, useNavigate } from "react-router-dom";
+import axios from "axios";
+import "../styles/CoordinatorRequestDetailView.css"; // Reuse styles
+
+const CoordinatorEvaluationReview = () => {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ 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 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
+
+
+
+ | Category |
+ Rating |
+ Comments |
+
+
+
+ {evaluation.evaluations.map((item, idx) => (
+
+ | {item.category} |
+ {item.rating} |
+ {item.comment || "N/A"} |
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default 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
+
+
+
+ | Task |
+ Outcomes |
+
+
+
+ {formData.tasks.map((task, idx) => (
+
+ | {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 8c8a12f5..f9e6827e 100644
--- a/client/src/pages/CoordinatorRequestDetailView.js
+++ b/client/src/pages/CoordinatorRequestDetailView.js
@@ -6,14 +6,24 @@ import "../styles/CoordinatorRequestDetailView.css";
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));
- }, [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);
+ }
+ };
+
+ fetchRequestDetails();
+ }, [id]); // ✅ Only depends on id
const handleApprove = async () => {
try {
@@ -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,31 +53,21 @@ 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 (
Internship Request Details
-
- Student: {requestData.student.userName}
-
-
- Email: {requestData.student.email}
-
-
- Company: {requestData.workplace.name}
-
-
- 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
@@ -79,25 +81,16 @@ const CoordinatorRequestDetailView = () => {
{requestData.tasks.map((task, idx) => (
| {task.description} |
- {task.outcomes.join(", ")} |
+ {task.outcomes?.join(", ") || "N/A"} |
))}
-
-
-
-
+
+
+
+
diff --git a/client/src/pages/Home.js b/client/src/pages/Home.js
index 5f27617b..4b4912f0 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;
if (!ouEmail || !password || !role) {
@@ -62,49 +54,50 @@ function Home() {
if (response.ok) {
const user = data.user;
if (role === "student") {
- // Store only required fields
+
const limitedUserInfo = {
fullName: user.fullName,
id: user._id,
email: user.ouEmail,
+
academicAdvisor: user.academicAdvisor,
semester: user.semester,
};
localStorage.setItem("ipmsUser", JSON.stringify(limitedUserInfo));
localStorage.setItem("ouEmail", user.ouEmail);
+
navigate("/student-dashboard");
} else if (role === "supervisor") {
Swal.fire({
icon: "success",
title: "Login Successful 🌟",
- text: `Welcome back, ${role}!`,
+ text: `Welcome back, Supervisor!`,
});
navigate("/supervisor-dashboard");
+
} else {
+
Swal.fire({
icon: "success",
title: "Login Successful 🌟",
- text: `Welcome back, ${role}!`,
+ text: `Welcome back, Coordinator!`,
});
navigate("/coordinator-dashboard");
}
- // Swal.fire({
- // icon: "success",
- // title: "Login Successful",
- // text: `Welcome back, `,
- // });
} else {
Swal.fire({
icon: "error",
title: "Login Failed",
html:
data.message +
+
" " +
(data.renewalLink
? `Please click
here to request a new token.`
: "Something went wrong."),
+
});
}
} catch (error) {
@@ -146,10 +139,7 @@ function Home() {
formData.role === r ? "selected" : ""
}`}
onClick={() =>
- setFormData({
- ...formData,
- role: r,
- })
+ setFormData((prev) => ({ ...prev, role: r }))
}
>
@@ -205,15 +195,7 @@ function Home() {
-
+
)}
-
+
+ {/* Delete Account Button */}
+
- {/* Trash SVG icon */}
-
+
+ {error &&
{error}
}
);
};
-export default StudentDashboard;
\ No newline at end of file
+export default StudentDashboard;
diff --git a/client/src/router.js b/client/src/router.js
index a41675cf..76d33dee 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";
+import CoordinatorEvaluationReview from "./pages/CoordinatorEvaluationReview";
import TokenRenewal from "./pages/TokenRenewal";
import StudentDashboard from "./pages/StudentDashboard";
import ProtectedRouteStudent from "./pages/ProtectedRouteStudent";
@@ -34,19 +36,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: (
@@ -54,38 +47,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: "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:
},
+
{
path: "about",
element:
,
@@ -94,36 +74,9 @@ const router = createBrowserRouter([
path: "contact",
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/CoordinatorCumulativeReviewForm.css b/client/src/styles/CoordinatorCumulativeReviewForm.css
index 10f0e47e..9da63931 100644
--- a/client/src/styles/CoordinatorCumulativeReviewForm.css
+++ b/client/src/styles/CoordinatorCumulativeReviewForm.css
@@ -51,13 +51,17 @@
font-weight: bold;
cursor: pointer;
}
+
+ .button-group .reject-btn {
+ background: #aa1e2e;;
+ }
.button-group .cancel-btn {
background: #e0e0e0;
}
.button-group .submit-btn {
- background: #aa1e2e;
+ background: #35aa1e;
color: white;
}
\ No newline at end of file
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 3687d0d4..1f1b0f28 100644
--- a/server/controllers/approvalController.js
+++ b/server/controllers/approvalController.js
@@ -2,219 +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 //
-// =========================================== //
-
-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,
- });
- }
-}
-exports.handleSupervisorFormAction = async (req, res, action) => {
- try {
- const form_type = req.params.type;
- const formId = req.params.id;
- const { comment = "", signature = "" } = req.body;
+// =======================================
+// Student-Side Controllers
+// =======================================
- const models = {
- A1: require("../models/InternshipRequest"),
- A2: require("../models/WeeklyReport"),
- A3: require("../models/Evaluation"),
- };
+const getStudentSubmissions = async (req, res) => {
+ try {
+ 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 FormModel = models[form_type];
- if (!FormModel) {
- return res.status(400).json({ message: "Invalid form type" });
+const deleteStudentSubmission = 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.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 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",
+ });
+ res.status(200).json(pendingRequests);
+ } catch (err) {
+ console.error("Error fetching pending submissions:", 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) => {
+ 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." });
- const studentEmail =
- form.student_id?.email ||
- form.interneeEmail ||
- form.studentEmail ||
- null;
+ res.json({ message: "Submission approved", updated: request });
+ } catch (err) {
+ res.status(500).json({ message: "Approval failed", error: err.message });
+ }
+};
- 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 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 Dashboard //
-// =========================================== //
+// =======================================
+// Coordinator-Side Controllers
+// =======================================
-exports.getCoordinatorRequests = async (req, res) => {
- try {
+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." });
}
};
-// 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" });
+ 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) {
+ 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.findById(req.params.id);
+ 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();
+
+ 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) {
+ return res.status(400).json({ message: "Rejection reason required." });
+ }
+
+ try {
+ const request = await InternshipRequest.findById(req.params.id);
+ if (!request) {
+ return res.status(404).json({ message: "Request not found." });
}
- res.status(200).json({ requestData, supervisorStatus: "Not Submitted" });
+ request.status = "rejected";
+ request.coordinator_status = "Rejected";
+ request.coordinator_comment = reason;
+ await request.save();
+
+ 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) {
- res.status(500).json({ message: "Failed to fetch details" });
+ console.error("Rejection failed:", err);
+ res.status(500).json({ message: "Rejection failed." });
}
};
-// Coordinator Approve Request
-exports.coordinatorApproveRequest = async (req, res) => {
+
+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(
- req.params.id,
- { coordinator_status: "approved" },
+ formId,
+ { coordinator_status: "approved", manualReviewStatus: "approved" },
{ new: true }
- );
+ ).populate("student");
- if (!request) {
+ if (!request)
return res.status(404).json({ message: "Request not found" });
- }
- 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) {
+ 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: "Request Approved Successfully" });
+ 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 });
}
};
-// Coordinator Reject Request
-exports.coordinatorRejectRequest = async (req, res) => {
+const coordinatorRejectManualReview = 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 formId = req.params.id;
const request = await InternshipRequest.findByIdAndUpdate(
- req.params.id,
- { coordinator_status: "rejected" },
+ formId,
+ { coordinator_status: "rejected", manualReviewStatus: "rejected" },
{ new: true }
- );
+ ).populate("student");
- if (!request) {
+ 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);
+ 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 in coordinatorResendRequest:", error);
+ res.status(500).json({ message: "Server error while resending request." });
+ }
+};
+
+// =======================================
+// Coordinator Evaluation
+// =======================================
+
+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 weekly reports." });
+ }
+};
+
+const getCoordinatorEvaluations = async (req, res) => {
+ try {
+ const evaluations = await Evaluation.find({
+ advisorAgreement: true,
+ coordinatorAgreement: { $ne: true },
+ });
+ res.status(200).json(evaluations);
+ } catch (err) {
+ res.status(500).json({ message: "Failed to fetch evaluations." });
+ }
+};
+
+const approveJobEvaluation = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const evaluation = await Evaluation.findById(id);
+ if (!evaluation)
+ return res.status(404).json({ message: "Evaluation not found." });
+
+ evaluation.coordinatorAgreement = true;
+ evaluation.updatedAt = new Date();
+ await evaluation.save();
+
await EmailService.sendEmail({
- to: request.student.email,
- subject: "Internship Request Rejected",
- html: `
Your internship request has been rejected.
Reason: ${reason}
`,
+ 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: "Request Rejected Successfully" });
+ 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 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." });
+
+ evaluation.coordinatorAgreement = false;
+ evaluation.updatedAt = new Date();
+ await evaluation.save();
+
+ 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}
`,
+ });
+
+ res.json({ message: "A3 Job Evaluation rejected successfully." });
+ } catch (err) {
+ res.status(500).json({ message: "Rejection failed." });
}
};
+
+// =======================================
+// EXPORTS
+// =======================================
+
+module.exports = {
+ // Student-Side
+ getStudentSubmissions,
+ deleteStudentSubmission,
+ getPendingSubmissions,
+ 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/controllers/reportController.js b/server/controllers/reportController.js
index c5179546..3c291aa0 100644
--- a/server/controllers/reportController.js
+++ b/server/controllers/reportController.js
@@ -1,7 +1,7 @@
const WeeklyReport = require("../models/WeeklyReport");
const SupervisorReview = require("../models/SupervisorReview");
const CoordinatorReview = require("../models/CoordinatorReview");
-const InternshipRequest = require("../models/internshiprequest");
+const InternshipRequest = require("../models/internshipRequest");
const { sendStudentProgressEmail } = require("../jobs/reminderEmail");
const STATIC_USER_ID = "vikash123";
@@ -16,7 +16,7 @@ const reportController = {
}
const newReport = new WeeklyReport({
- studentId: STATIC_USER_ID,
+ student_id: STATIC_USER_ID,
name,
email,
supervisorName,
@@ -61,7 +61,7 @@ const reportController = {
getReportsByStudent: async (req, res) => {
try {
const { userId } = req.params;
- const reports = await WeeklyReport.find({ studentId: userId }).sort({ week: 1 });
+ const reports = await WeeklyReport.find({ student_id: userId }).sort({ week: 1 });
return res.status(200).json({ success: true, reports });
} catch (error) {
@@ -72,10 +72,10 @@ const reportController = {
getMyReports: async (req, res) => {
try {
- const studentId = req.user?.id || STATIC_USER_ID;
- const reports = await WeeklyReport.find({ studentId }).sort({ week: 1 });
+ const student_id = req.user?.id || STATIC_USER_ID;
+ const reports = await WeeklyReport.find({ student_id }).sort({ week: 1 });
- const reviews = await SupervisorReview.find({ studentId });
+ const reviews = await SupervisorReview.find({ student_id });
const weekToComment = {};
reviews.forEach((review) => {
@@ -99,7 +99,7 @@ const reportController = {
getCumulativeReports: async (req, res) => {
try {
- const reports = await WeeklyReport.find({ studentId: STATIC_USER_ID }).sort({ createdAt: 1 });
+ const reports = await WeeklyReport.find({ student_id: STATIC_USER_ID }).sort({ createdAt: 1 });
if (!reports.length) {
return res.status(200).json({ success: true, cumulativeReports: [] });
@@ -111,7 +111,7 @@ const reportController = {
const groupIndex = i / 4;
const isReviewed = await SupervisorReview.findOne({
- studentId: STATIC_USER_ID,
+ student_id: STATIC_USER_ID,
groupIndex,
});
@@ -137,7 +137,7 @@ const reportController = {
const { groupIndex } = req.params;
const index = parseInt(groupIndex);
- const reports = await WeeklyReport.find({ studentId: STATIC_USER_ID }).sort({ createdAt: 1 });
+ const reports = await WeeklyReport.find({ student_id: STATIC_USER_ID }).sort({ createdAt: 1 });
if (!reports.length) {
return res.status(404).json({ success: false, message: "No reports found." });
@@ -181,14 +181,22 @@ const reportController = {
submitSupervisorComments: async (req, res) => {
try {
- const { groupIndex, comments, weeks } = req.body;
+ const { coordinator_status, groupIndex, comments, weeks } = req.body;
+
+ if (coordinator_status == "reject") {
+ await WeeklyReport.updateMany(
+ { student_id: STATIC_USER_ID, week: { $in: weeks }, coordinator_status, coordinator_responded: true },
+ { $set: { supervisorComments: comments } }
+ );
+ return res.status(200).json({ success: true, message: "Supervisor successfully rejected weekly report." });
+ }
if (!comments || !weeks || weeks.length === 0) {
return res.status(400).json({ success: false, message: "Invalid comment data." });
}
const newReview = new SupervisorReview({
- studentId: STATIC_USER_ID,
+ student_id: STATIC_USER_ID,
groupIndex,
weeks,
comments,
@@ -197,7 +205,7 @@ const reportController = {
await newReview.save();
await WeeklyReport.updateMany(
- { studentId: STATIC_USER_ID, week: { $in: weeks } },
+ { student_id: STATIC_USER_ID, week: { $in: weeks }, coordinator_status, coordinator_responded: true },
{ $set: { supervisorComments: comments } }
);
@@ -212,14 +220,14 @@ const reportController = {
getSupervisorReviewedGroups: async (req, res) => {
try {
const supervisorReviews = await SupervisorReview.find({
- studentId: STATIC_USER_ID
+ student_id: STATIC_USER_ID
});
const reviewedGroups = [];
for (const review of supervisorReviews) {
const reports = await WeeklyReport.find({
- studentId: STATIC_USER_ID,
+ student_id: STATIC_USER_ID,
week: { $in: review.weeks }
});
@@ -255,10 +263,10 @@ const reportController = {
}
const firstWeek = weeks[0];
- const firstReport = await WeeklyReport.findOne({ studentId: STATIC_USER_ID, week: firstWeek });
+ const firstReport = await WeeklyReport.findOne({ student_id: STATIC_USER_ID, week: firstWeek });
const newReview = new CoordinatorReview({
- studentId: STATIC_USER_ID,
+ student_id: STATIC_USER_ID,
groupIndex,
weeks,
supervisorComments: firstReport?.supervisorComments || "",
@@ -268,7 +276,7 @@ const reportController = {
await newReview.save();
await WeeklyReport.updateMany(
- { studentId: STATIC_USER_ID, week: { $in: weeks } },
+ { student_id: STATIC_USER_ID, week: { $in: weeks } },
{ $set: { coordinatorComments: comments } }
);
diff --git a/server/index.js b/server/index.js
index 80b7fe97..c01641db 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);
@@ -82,8 +82,6 @@ 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 {
const { userName, email, password, role } = req.body;
@@ -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 c0435d25..cb057232 100644
--- a/server/jobs/reminderEmail.js
+++ b/server/jobs/reminderEmail.js
@@ -1,157 +1,204 @@
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");
-const SupervisorReview = require("../models/SupervisorReview");
-const InternshipRequest = require("../models/InternshipRequest");
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: weekly report reviewed by supervisor but not yet commented by coordinator
-const coordinatorReminder = async () => {
- const now = dayjs();
- 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(
- ", "
- )}`
- );
- }
- } catch (err) {
- logger.error("[CoordinatorReminder Error]:", err.message || err);
- }
-};
-
-// Utility to get all forms of type A1, A2, A3
+// ================================================
+// Utility: Fetch All Forms for Reminders
+// ================================================
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]) => {
- const results = await Model.find(filter);
- return results;
- }
- );
-
+ const formPromises = [
+ InternshipRequest.find(filter),
+ WeeklyReport.find(filter),
+ Evaluation.find(filter),
+ ];
const allResults = await Promise.all(formPromises);
return allResults.flat();
};
-// Supervisor reminder: weekly progress reports pending review
-const supervisorReminder = async () => {
+// ================================================
+// Coordinator Reminders
+// ================================================
+const coordinatorReminder = async () => {
const now = dayjs();
- const fiveWorkingDaysAgo = now.subtract(7, "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 Submission.find({
- supervisor_status: "pending",
- createdAt: { $lt: fiveWorkingDaysAgo },
+ // A1 and A2 pending forms (5 days)
+ const pendingA1A2 = await InternshipRequest.find({
+ coordinator_status: "pending",
+ supervisor_status: "approved",
+ createdAt: { $lt: fiveDaysAgo },
});
- const supervisors = await UserTokenRequest.find({
- role: "supervisor",
- isActivated: true,
+ const pendingWeeklyReports = await WeeklyReport.find({
+ coordinator_status: "pending",
+ supervisor_status: "approved",
+ createdAt: { $lt: fiveDaysAgo },
});
- for (const submission of pendingSubs) {
- const student = await User.findById(submission.student_id);
- const supervisor = await User.findById(submission.supervisor_id);
+ // A3 pending evaluations (3 days)
+ const pendingEvaluations = await Evaluation.find({
+ advisorAgreement: true,
+ coordinatorAgreement: { $exists: false },
+ createdAt: { $lt: threeDaysAgo },
+ });
- if (!student || !supervisor) continue;
+ 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 reminderCount = submission.supervisor_reminder_count || 0;
- const lastReminded =
- submission.last_supervisor_reminder_at || submission.createdAt;
- const nextReminderDue = dayjs(lastReminded).add(5, "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) {
+ 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: `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: `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 supervisor inaction for "${submission.name}".`,
+ recipient_email: student.ouEmail,
+ message: `Escalation: Student notified about delayed ${formType} approval`,
});
- logger.info(`[Escalated] Student notified for: "${submission.name}"`);
- } else if (shouldRemindAgain) {
- for (const sup of supervisors) {
+ form.studentNotified = true;
+ } else {
+ // Send reminder to Coordinator
+ await emailService.sendEmail({
+ to: coordinator.ouEmail,
+ subject: `Reminder: Action Needed on ${formType}`,
+ html: `
Please review and approve the pending ${formType} form submitted by the student.
`,
+ });
+
+ 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
+ );
+ }
+ }
+};
+
+// ================================================
+// Supervisor Reminders
+// ================================================
+const supervisorReminder = async () => {
+ const now = dayjs();
+ const fiveDaysAgo = now.subtract(7, "day").toDate();
+
+ try {
+ const pendingSubs = await getAllForms({
+ supervisor_status: "pending",
+ last_supervisor_reminder_at: { $lt: fiveDaysAgo },
+ });
+
+ 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 (!shouldRemindAgain) continue;
+ if (!student) continue;
+
+ if (reminderCount >= 2) {
+ // Escalate to student
await emailService.sendEmail({
- to: sup.ouEmail,
- subject: `Reminder: Please Review Submission "${submission._id}"`,
- html: `
This is a reminder to review the submission by ${student.email}.
`,
- text: `Reminder to review submission "${submission._id}".`,
+ 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: form._id,
+ type: "studentEscalation",
+ recipient_email: student.ouEmail,
+ message: `Student notified about supervisor inaction`,
+ });
+
+ 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("[SupervisorReminder Error]:", err.message || err);
+ logger.error("❌ Error in supervisorReminder:", err.message);
}
};
+// ================================================
+// Exports
+// ================================================
module.exports = {
coordinatorReminder,
supervisorReminder,
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/FormMetadata.js b/server/models/FormMetadata.js
index 0e54089e..71e99b2c 100644
--- a/server/models/FormMetadata.js
+++ b/server/models/FormMetadata.js
@@ -15,6 +15,8 @@ const formMetadata = {
coordinator_comment: String,
coordinator_reminder_count: { type: Number, default: 0 },
last_coordinator_reminder_at: Date,
+ coordinator_responded: {type: Boolean, default: false },
+ coordinator_studentNotified: {type: Boolean, default: false },
};
module.exports = formMetadata;
diff --git a/server/models/InternshipRequest.js b/server/models/InternshipRequest.js
index e0842b83..c23b5fed 100644
--- a/server/models/InternshipRequest.js
+++ b/server/models/InternshipRequest.js
@@ -8,19 +8,21 @@ 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({
student:{
@@ -36,38 +38,40 @@ const formA1 = new mongoose.Schema({
},
...formMetadata,
+
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],
},
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,
},
approvals: {
@@ -77,10 +81,14 @@ 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 || mongoose.model("InternshipRequest", formA1);
\ No newline at end of file
+module.exports =
+ mongoose.models.InternshipRequest ||
+ mongoose.model("InternshipRequest", formA1);
diff --git a/server/models/WeeklyReport.js b/server/models/WeeklyReport.js
index f6c06313..52222ffb 100644
--- a/server/models/WeeklyReport.js
+++ b/server/models/WeeklyReport.js
@@ -2,10 +2,7 @@ const mongoose = require("mongoose");
const formMetadata = require("./FormMetadata");
const weeklyReportSchema = new mongoose.Schema({
- studentId: {
- type: String,
- required: true,
- },
+ ...formMetadata,
// New Fields from A1
name: {
diff --git a/server/routes/approvalRoutes.js b/server/routes/approvalRoutes.js
index 0913ac09..ada326ec 100644
--- a/server/routes/approvalRoutes.js
+++ b/server/routes/approvalRoutes.js
@@ -1,49 +1,57 @@
const express = require("express");
const router = express.Router();
-
const {
- getSupervisorForms,
- handleSupervisorFormAction,
+ getStudentSubmissions,
+ deleteStudentSubmission,
+ getPendingSubmissions,
+ approveSubmission,
+ rejectSubmission,
+ deleteStalledSubmission,
getCoordinatorRequests,
getCoordinatorRequestDetails,
coordinatorApproveRequest,
coordinatorRejectRequest,
+ getCoordinatorReports,
+ getCoordinatorEvaluations,
+ approveJobEvaluation,
+ rejectJobEvaluation,
+ coordinatorResendRequest,
+ getManualReviewForms,
+ coordinatorApproveManualReview,
+ coordinatorRejectManualReview
} = require("../controllers/approvalController");
-const { isSupervisor, isCoordinator } = require("../middleware/authMiddleware");
-
-// =========================================== //
-// 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")
-);
+const {
+ isSupervisor,
+ isCoordinator,
+ isStudent,
+} = require("../middleware/authMiddleware");
-// Reject route
-router.post("/supervisor/form/:type/:id/reject", isSupervisor, (req, res) =>
- handleSupervisorFormAction(req, res, "reject")
+// -----------------------------------------------
+// Student Routes
+// -----------------------------------------------
+router.get("/student/submissions", isStudent, getStudentSubmissions);
+router.delete(
+ "/student/request/:id/delete",
+ isStudent,
+ deleteStudentSubmission
);
-// =========================================== //
-// Coordinator Approval Routes //
-// =========================================== //
+// -----------------------------------------------
+// Supervisor Routes
+// -----------------------------------------------
+router.get("/submissions/pending", isSupervisor, getPendingSubmissions);
+router.post("/submissions/:id/approve", isSupervisor, approveSubmission);
+router.post("/submissions/:id/reject", isSupervisor, rejectSubmission);
-// Coordinator APIs
+// -----------------------------------------------
+// Coordinator Routes
+// -----------------------------------------------
router.get("/coordinator/requests", isCoordinator, getCoordinatorRequests);
router.get(
- "/coordinator/request/:id",
- isCoordinator,
- getCoordinatorRequestDetails
+ "/coordinator/request/:id",
+ isCoordinator,
+ getCoordinatorRequestDetails
);
router.post(
"/coordinator/request/:id/approve",
@@ -55,5 +63,51 @@ router.post(
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 7540747d..9bb56d88 100644
--- a/server/routes/formRoutes.js
+++ b/server/routes/formRoutes.js
@@ -2,6 +2,8 @@ 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.get("/internshiprequests", async (req, res) => {
@@ -12,16 +14,22 @@ router.get("/internshiprequests", async (req, res) => {
supervisor_status: { $in: [null, "pending"] } // not yet reviewed by supervisor
}).sort({ createdAt: 1 }) .populate("student", "userName") // oldest first
+
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 = [
+
'workplaceName',
'phone',
'advisorName',
@@ -30,35 +38,43 @@ function validateFormData(formData) {
'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}`;
}
}
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.";
}
-
+
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) {
@@ -67,10 +83,94 @@ 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" });
+ }
+});
+
+// -----------------------------------------
+// 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"] },
+ });
+ res.json(pending);
+ } catch (err) {
+ console.error("Error fetching pending submissions:", err);
+ res.status(500).json({ message: "Server error" });
+ }
+});
+
+// -----------------------------------------
+// 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" });
+
+ request.reminders = [new Date()];
+ request.coordinatorResponded = false;
+ request.studentNotified = false;
+ await request.save();
+
+ // Send email to internship advisor and student
+ await emailService.sendEmail({
+ to: [request.internshipAdvisor.email, request.student.email],
+ subject: "Internship Request Resent",
+ html: `
+ Hello,
+ 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" });
+ } catch (err) {
+ console.error("Resend error:", err);
+ 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,
+ });
+ 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" });
}
});
diff --git a/server/utils/cronUtils.test.js b/server/utils/cronUtils.test.js
index 346f41ad..a000f895 100644
--- a/server/utils/cronUtils.test.js
+++ b/server/utils/cronUtils.test.js
@@ -14,86 +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");
+ });
});
});