diff --git a/client/src/pages/Home.js b/client/src/pages/Home.js
index 5f27617b..5f95adbe 100644
--- a/client/src/pages/Home.js
+++ b/client/src/pages/Home.js
@@ -71,14 +71,25 @@ function Home() {
semester: user.semester,
};
+ localStorage.clear();
localStorage.setItem("ipmsUser", JSON.stringify(limitedUserInfo));
localStorage.setItem("ouEmail", user.ouEmail);
navigate("/student-dashboard");
} else if (role === "supervisor") {
+ // Store only required fields
+ const limitedUserInfo = {
+ role: role,
+ };
+ const token = data.user.token;
+
+ localStorage.clear();
+ localStorage.setItem("ipmsUser", JSON.stringify(limitedUserInfo));
+ localStorage.setItem("token", token);
+
Swal.fire({
- icon: "success",
- title: "Login Successful 🌟",
- text: `Welcome back, ${role}!`,
+ icon: "success",
+ title: "Login Successful 🌟",
+ text: `Welcome back, ${role}!`,
});
navigate("/supervisor-dashboard");
} else {
@@ -249,4 +260,4 @@ function Home() {
);
}
-export default Home;
\ No newline at end of file
+export default Home;
diff --git a/client/src/pages/ProtectedSupervisor.jsx b/client/src/pages/ProtectedSupervisor.jsx
new file mode 100644
index 00000000..1480b154
--- /dev/null
+++ b/client/src/pages/ProtectedSupervisor.jsx
@@ -0,0 +1,9 @@
+import React from "react";
+import { Navigate } from "react-router-dom";
+
+const ProtectedSupervisor = ({ children }) => {
+ const user = JSON.parse(localStorage.getItem("ipmsUser"));
+ return user && user.role === "supervisor" ? children : ;
+ };
+
+ export default ProtectedSupervisor;
diff --git a/client/src/pages/SupervisorDashboard.js b/client/src/pages/SupervisorDashboard.js
index 4141a2ae..76045c32 100644
--- a/client/src/pages/SupervisorDashboard.js
+++ b/client/src/pages/SupervisorDashboard.js
@@ -1,3 +1,4 @@
+
import React, { useEffect, useState } from "react";
import axios from "axios";
import "../styles/SupervisorDashboard.css";
@@ -9,28 +10,25 @@ const SupervisorDashboard = () => {
const [selectedForm, setSelectedForm] = useState(null);
const [loading, setLoading] = useState(true);
const [message, setMessage] = useState("");
-
- useEffect(() => {
- const token = localStorage.getItem("token") || "";
-
- const fetchRequests = async () => {
+ const token = localStorage.getItem("token") || "";
+
+ useEffect(() => {
+ const fetchRequests = async () => {
try {
- const res = await axios.get(
+ const response = await axios.get(
`${process.env.REACT_APP_API_URL}/api/supervisor/forms`,
{
headers: {
Authorization: `Bearer ${token}`,
},
- }
- );
+ });
- const formatted = res.data.map((item) => ({
+ const formatted = response.data.map(item => ({
_id: item._id,
- interneeName: item.interneeName || item.student_id?.userName || "N/A",
- soonerId: item.soonerId || item.student_id?.soonerId || "N/A",
- interneeEmail: item.interneeEmail || item.student_id?.email || "N/A",
+ interneeName: item.student?.name || item.studentId?.fullName || item.interneeName || "N/A",
+ interneeEmail: item.student?.email || item.studentId?.ouEmail || item.interneeEmail || "N/A",
form_type: item.form_type,
- createdAt: item.createdAt,
+ createdAt: item.createdAt || item.submittedAt,
supervisor_status: item.supervisor_status || "pending",
fullForm: item,
workplace: {
@@ -48,27 +46,44 @@ const SupervisorDashboard = () => {
endDate: item.endDate || "N/A",
tasks: item.tasks || [],
status: item.status || "pending",
- supervisor_comment: item.supervisor_comment || "N/A",
+ supervisor_comment: item.supervisor_comment || "N/A"
}));
+ setLoading(false);
setRequests(formatted);
+
} catch (err) {
- console.error("Error fetching forms:", err);
- setMessage("Error fetching forms.");
- } finally {
- setLoading(false);
+ if (err.response) {
+ if (err.response.status === 401) {
+ console.error("Unauthorized access. Redirecting to login...");
+ setMessage("Unauthorized access. Redirecting to login...");
+ localStorage.removeItem("token");
+ window.location.href = "/";
+ }
+ else if (err.response.status === 403) {
+ console.error("Forbidden access. Redirecting to login...");
+ setMessage("Forbidden access. Redirecting to login...");
+ window.location.href = "/";
+ }
+ else if (err.response.status === 500) {
+ console.error("Server error. Please try again later.");
+ setMessage("Server error. Please try again later.");
+ }
+ else {
+ console.error("Unexpected error:", err.message);
+ setMessage("Unexpected error. Please try again.");
+ }
+ }
+
+ setLoading(false);
}
};
fetchRequests();
- }, []);
-
- const handleAction = async (form_type, id, action, comment, signature) => {
- const token = localStorage.getItem("token");
+ }, [token, setLoading]);
- const confirmed = window.confirm(
- `Are you sure you want to ${action} this request?`
- );
+ const handleAction = async (id, form_type, action, comment, signature) => {
+ const confirmed = window.confirm(`Are you sure you want to ${action} this request?`);
if (!confirmed) return;
try {
@@ -94,78 +109,77 @@ const SupervisorDashboard = () => {
const openFormView = (form) => setSelectedForm(form);
const closeFormView = () => setSelectedForm(null);
+
const formatDate = (date) => new Date(date).toLocaleDateString();
const sortedRequests = [...requests]
.filter((res) => res.supervisor_status?.toLowerCase() === "pending")
.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
-
- let content;
-
- if (loading) {
- content =
Loading...
;
- } else if (sortedRequests.length === 0) {
- content = (
-
-
- {formData.form_type === "A1" ? renderA1() : renderA3()}
- {renderSignaturesAndActions()}
-
+ let renderedComponent;
+
+ if (formData.form_type === "A1") {
+ renderedComponent = renderA1();
+ } else if (formData.form_type === "A2") {
+ renderedComponent = renderA2();
+ } else {
+ renderedComponent = renderA3();
+ }
+
+ return (
+
+
+ {renderedComponent}
+ {renderSignaturesAndActions()}
- );
+
+ );
+
};
-export default ViewFormModal;
+export default ViewFormModal;
\ No newline at end of file
diff --git a/client/src/router.js b/client/src/router.js
index a41675cf..530e11b8 100644
--- a/client/src/router.js
+++ b/client/src/router.js
@@ -26,6 +26,7 @@ import SubmittedReports from "./pages/SubmittedReports";
import CumulativeReviewForm from "./pages/CumulativeReviewForm";
import CoordinatorReviewForm from "./pages/CoordinatorReviewForm";
import CoordinatorCumulativeReviewForm from "./pages/CoordinatorCumulativeReviewForm";
+import ProtectedSupervisor from "./pages/ProtectedSupervisor";
// Create and export the router configuration
const router = createBrowserRouter([
@@ -72,7 +73,11 @@ const router = createBrowserRouter([
},
{
path: "supervisor-dashboard",
- element:
,
+ element: (
+
+
+
+ ),
},
{
path: "coordinator-dashboard",
@@ -126,4 +131,4 @@ const router = createBrowserRouter([
},
]);
-export default router;
\ No newline at end of file
+export default router;
diff --git a/client/src/styles/A1InternshipRequestForm.css b/client/src/styles/A1InternshipRequestForm.css
index 4355861e..deed2e01 100644
--- a/client/src/styles/A1InternshipRequestForm.css
+++ b/client/src/styles/A1InternshipRequestForm.css
@@ -193,6 +193,10 @@
margin-top: 10px;
}
+.required-asterisk {
+ color: #ff6666; /* Light red */
+}
+
.modal-overlay {
position: fixed;
top: 0;
@@ -216,6 +220,7 @@
width: 90%;
max-width: 1200px;
border-radius: 12px;
+
}
.success-msg, .error-msg {
diff --git a/client/src/styles/SupervisorDashboard.css b/client/src/styles/SupervisorDashboard.css
index 7abe75c9..07e8cdd9 100644
--- a/client/src/styles/SupervisorDashboard.css
+++ b/client/src/styles/SupervisorDashboard.css
@@ -1,229 +1,240 @@
.dashboard-container {
- padding: 40px 20px;
- max-width: 1200px;
- margin: auto;
+ padding: 20px;
background-color: #f9f9f9;
- border-radius: 12px;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.dashboard-container h2 {
- color: #263238;
- margin-bottom: 30px;
+ font-size: 20px;
+ margin-bottom: 20px;
+ color: #333;
+}
+
+.toggle-button {
+ position: absolute;
+ right: 50px;
+ top: 100px;
+ transform: translateY(-50%);
+ width: 0;
+ height: 0;
+ border-left: 6px solid transparent;
+ border-right: 6px solid transparent;
+ border-top: 6px solid #cddbea8a;
+ cursor: pointer;
+ transition: transform 0.2s;
}
-.section-heading {
- font-size: 22px;
- font-weight: 700;
- color: #222;
- margin-top: 40px;
- margin-bottom: 25px;
+.toggle-button.collapsed {
+ transform: translateY(-50%) rotate(180deg);
}
-.status-msg {
- margin-bottom: 20px;
- padding: 12px 18px;
- background: #e8f5e9;
- border-left: 5px solid #4caf50;
- color: #2e7d32;
- font-weight: 500;
+.forms-list {
+ margin-top: 10px;
+ overflow: hidden;
+ transition: max-height 0.3s ease;
+}
+
+.form-item {
+ border-bottom: 1px solid #eee;
+ padding: 5px 0;
+}
+
+.clickable-row {
+ cursor: pointer;
+}
+
+.clickable-row:hover {
+ background-color: #f5f5f5;
}
.dashboard-table {
width: 100%;
border-collapse: collapse;
- margin-bottom: 40px;
+ margin-top: 20px;
+ transition: max-height 0.3s ease;
+}
+
+.dashboard-table.collapsed {
+ max-height: 0;
+ overflow: hidden;
}
.dashboard-table th,
.dashboard-table td {
- padding: 14px 18px;
+ padding: 12px 16px;
+ text-align: left;
border-bottom: 1px solid #ddd;
}
.dashboard-table th {
- background-color: #263238;
- color: #fff;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- font-size: 13px;
+ background-color: #861f1f;
+ color: white;
+ font-weight: bold;
}
.link-button {
background: none;
border: none;
- color: #1565c0;
+ color: #333;
cursor: pointer;
- font-weight: 500;
+ font-size: 15px;
+ padding: 0;
+ text-align: left;
}
.link-button:hover {
+ background: none !important;
+ /* Prevents red background */
+ color: #007bff;
+ /* Optional: subtle hover color, or use 'inherit' */
text-decoration: underline;
}
+
+/* Action buttons */
+.action-buttons {
+ display: flex;
+ gap: 10px;
+}
+
+.approve {
+ background-color: #28a745;
+ color: white;
+ padding: 6px 12px;
+ border: none;
+ border-radius: 5px;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+.reject {
+ background-color: #dc3545;
+ color: white;
+ padding: 6px 12px;
+ border: none;
+ border-radius: 5px;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+.approve:hover {
+ background-color: #218838;
+}
+
+.reject:hover {
+ background-color: #c82333;
+}
+
.status-badge {
- padding: 6px 10px;
- border-radius: 16px;
+ padding: 4px 8px;
+ border-radius: 4px;
font-size: 12px;
- font-weight: 600;
+ font-weight: bold;
text-transform: capitalize;
+ display: inline-block;
}
.status-badge.pending {
- background: #fff3cd;
+ background-color: #fff3cd;
color: #856404;
}
.status-badge.approved {
- background: #d4edda;
+ background-color: #d4edda;
color: #155724;
}
.status-badge.rejected {
- background: #f8d7da;
+ background-color: #f8d7da;
color: #721c24;
}
-.cumulative-report-card {
- background: #fff;
- border-radius: 12px;
- padding: 25px;
- margin: 25px 0;
- box-shadow: 0 0 15px rgba(0, 123, 255, 0.1);
- border-left: 5px solid #007bff;
- transition: 0.3s ease all;
-}
-
-.cumulative-report-card:hover {
- transform: translateY(-3px);
- box-shadow: 0 0 25px rgba(0, 123, 255, 0.3);
-}
-
-.weeks-covered {
- font-size: 16px;
- color: #222;
- font-weight: 600;
- margin-bottom: 15px;
+/* Empty state */
+.empty-message-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 20vh;
}
-.week-report-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
- gap: 12px;
- margin-bottom: 20px;
- padding: 0;
- list-style: none;
+.empty-message {
+ font-size: 28px;
+ color: #000000;
+ text-align: center;
+ margin: 20px 0;
+ font-weight: bold;
}
-.week-report-item {
- background: #f1f8ff;
- border: 1px solid #dfefff;
- padding: 12px 16px;
- border-radius: 8px;
+/* Modal override styles (if needed) */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 100vh;
+ width: 100vw;
+ background-color: rgba(0, 0, 0, 0.5);
display: flex;
- flex-direction: column;
- gap: 6px;
- box-shadow: 0 0 5px rgba(0, 123, 255, 0.08);
- transition: box-shadow 0.2s ease, transform 0.2s ease;
+ justify-content: center;
+ align-items: center;
+ z-index: 999;
}
-.week-report-item:hover {
- box-shadow: 0 0 15px rgba(0, 123, 255, 0.25);
- transform: translateY(-2px);
- background: #e3f2fd;
+.modal-box {
+ background-color: #fff;
+ padding: 60px;
+ border-radius: 30px;
+ width: 1000px;
+ max-width: 95%;
+ max-height: 90vh;
+ overflow-y: auto;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
-.week-report-item span {
- font-size: 14px;
- color: #555;
+.modal-box h3 {
+ margin-top: 0;
+ color: hsl(0, 0%, 20%);
}
-.week-report-item strong {
- font-size: 15px;
- color: #212121;
+.form-details {
+ background: #f4f4f4;
+ padding: 15px;
+ border-radius: 5px;
+ font-family: monospace;
+ white-space: pre-wrap;
+ margin-bottom: 15px;
}
-/* Change the color of the "Review & Comment" button to match the top bar red */
-.review-button.red-btn {
- background: #9b111e; /* Red color */
- color: white;
- padding: 10px 20px;
- border: none;
- border-radius: 6px;
- cursor: pointer;
- font-weight: 600;
- transition: 0.3s ease all;
+/* Make modal section headings red, matching dashboard-table header */
+.modal-box .section-title,
+.modal-box h3 {
+ color: #861f1f !important;
+ font-weight: bold;
}
-/* Shine effect when hovering over the "Review & Comment" button */
-.review-button.red-btn:hover {
- background: #b71c1c; /* Darker red for hover effect */
- box-shadow: 0 0 10px rgba(211, 47, 47, 0.6); /* Glow effect */
- transform: scale(1.05); /* Slightly enlarge the button */
-}
-/* Change the side color (weeks section) to the same red as the button */
-.cumulative-report-card {
- background: #fff;
- border-radius: 12px;
- padding: 25px;
- margin: 25px 0;
- box-shadow: 0 0 15px rgba(0, 123, 255, 0.1);
- border-left: 5px solid #9b111e; /* Red color for border */
- transition: 0.3s ease all;
+.signature-modal-overlay {
+ position: fixed;
+ z-index: 9999;
+ top: 0; left: 0; width: 100vw; height: 100vh;
+ background: rgba(40, 40, 40, 0.32);
+ display: flex; align-items: center; justify-content: center;
}
-
-/* Shine effect when hovering over the week blocks */
-.cumulative-report-card:hover {
- transform: translateY(-3px);
- box-shadow: 0 0 25px rgba(155, 17, 30, 0.3); /* Glow effect */
-}
-
-/* Update the hover effect for the week report items (individual weeks) */
-.week-report-item {
- background: #f1f8ff;
- border: 1px solid #dfefff;
- padding: 12px 16px;
- border-radius: 8px;
+.signature-modal-popup {
+ background: #fff;
+ padding: 32px 36px 28px 36px;
+ border-radius: 14px;
+ min-width: 390px;
+ max-width: 97vw;
+ max-height: 95vh;
+ box-shadow: 0 8px 40px rgba(0,0,0,0.13);
display: flex;
flex-direction: column;
- gap: 6px;
- box-shadow: 0 0 5px rgba(155, 17, 30, 0.08); /* Red shadow */
- transition: box-shadow 0.2s ease, transform 0.2s ease;
+ align-items: stretch;
}
-
-/* Shine and glow effect when hovering over individual weeks */
-.week-report-item:hover {
- box-shadow: 0 0 15px rgba(155, 17, 30, 0.25); /* Glow effect */
- transform: translateY(-2px); /* Lift the item slightly */
- background: #e3f2fd;
-}
-
-.week-report-item span {
- font-size: 14px;
- color: #555;
-}
-
-.week-report-item strong {
- font-size: 15px;
- color: #212121;
-}
-.tab-toggle button {
- margin-right: 10px;
- padding: 8px 16px;
- border: none;
- background: #ddd;
- cursor: pointer;
-}
-.tab-toggle .active {
- background: #007bff;
- color: white;
- font-weight: bold;
-}
-.report-group-card {
- border: 1px solid #ccc;
- padding: 16px;
- margin: 20px 0;
- border-radius: 12px;
+.signature-modal-title {
+ color: #861f1f;
+ font-size: 1.6rem;
+ font-weight: 700;
+ margin: 0;
}
diff --git a/client/src/styles/ViewFormModal.css b/client/src/styles/ViewFormModal.css
index 7acf4e7e..07dc08c8 100644
--- a/client/src/styles/ViewFormModal.css
+++ b/client/src/styles/ViewFormModal.css
@@ -1,5 +1,3 @@
-/* ViewFormModal.css */
-
.view-modal {
background: #ffffff !important;
padding: 2rem;
@@ -140,4 +138,6 @@
.action-buttons .close-btn:hover {
background-color: #5a6268;
}
+
+
\ No newline at end of file
diff --git a/server/controllers/approvalController.js b/server/controllers/approvalController.js
index 3687d0d4..d6c5aa85 100644
--- a/server/controllers/approvalController.js
+++ b/server/controllers/approvalController.js
@@ -3,77 +3,139 @@ const WeeklyReport = require("../models/WeeklyReport");
const Evaluation = require("../models/Evaluation");
const EmailService = require("../services/emailService");
const UserTokenRequest = require("../models/TokenRequest");
+const { supervisorReminder } = require("../jobs/reminderEmail");
// =========================================== //
// Managing Supervisor Forms //
// =========================================== //
-exports.getSupervisorForms = async (req, res, filter) => {
+const findSupervisorFromForm = async (form) => {
+ let supervisor = null;
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,
- });
+ if (form.form_type === "A1") {
+ supervisor = await UserTokenRequest.findOne({ ouEmail: form.internshipAdvisor.email });
+ }
+ else if (form.form_type === "A2") {
+ supervisor = await UserTokenRequest.findOne({ ouEmail: form.supervisorEmail });
+ }
+ else if (form.form_type === "A3") {
+ const internship_a1 = await InternshipRequest.findById(form.internshipId);
+ supervisor = await UserTokenRequest.findOne({ ouEmail: internship_a1.internshipAdvisor.email });
+ }
+ else {
+ logger.error(`Unknown form type: ${form.form_type}`);
+ }
+ }
+ catch (err) {
+ logger.error(`Error retrieving supervisor: ${err.message}`);
}
+ return supervisor;
}
-exports.handleSupervisorFormAction = async (req, res, action) => {
+const getSupervisorForms = async (req, res) => {
+ const supervisor = req.user;
+ const InternshipRequest = require("../models/InternshipRequest");
+ const WeeklyReport = require("../models/WeeklyReport");
+ const Evaluation = require("../models/Evaluation");
+
+ supervisorReminder();
+
try {
+ // ----------------------------
+ // Fetching A1 Forms
+ // ----------------------------
+ const filterA1 = {
+ "internshipAdvisor.email": supervisor.ouEmail,
+ supervisor_status: { $in: ["pending"] },
+ };
+
+ const a1Forms = await InternshipRequest.find(filterA1);
+
+ const typedA1 = a1Forms.map((form) => ({
+ ...form.toObject(),
+ form_type: "A1",
+ }));
+
+ // ----------------------------
+ // Fetching A2 Forms
+ // ----------------------------
+ const filterA2 = {
+ supervisorEmail: supervisor.ouEmail,
+ supervisor_status: { $in: ["pending"] },
+ };
+
+
+ const a2Forms = await WeeklyReport.find(filterA2)
+ .populate("studentId", "fullName ouEmail");
+
+ const typedA2 = a2Forms.map((form) => ({
+ ...form.toObject(),
+ form_type: "A2",
+ }));
+
+ // ----------------------------
+ // Fetching A3 Forms
+ // ----------------------------
+ const allA2Forms = await WeeklyReport.find();
+ const studentIdsWithA2 = allA2Forms.map((form) => form.email);
+
+ const allA1Forms = await InternshipRequest.find({}, "student.email internshipAdvisor.email");
+
+ const emailToAdvisorMap = new Map();
+ allA1Forms.forEach(form => {
+ if (form.student?.email && form.internshipAdvisor?.email) {
+ emailToAdvisorMap.set(form.student.email, form.internshipAdvisor.email);
+ }
+ });
+
+ const pendingA3Forms = await Evaluation.find({
+ supervisor_status: { $in: ["pending"] },
+ interneeEmail: { $in: studentIdsWithA2 },
+ });
+
+ const a3Forms = pendingA3Forms.filter(form => {
+ const interneeEmail = form.interneeEmail;
+ const advisorEmail = emailToAdvisorMap.get(interneeEmail);
+ return advisorEmail === supervisor.ouEmail;
+ });
+
+ const typedA3 = a3Forms.map((form) => ({
+ ...form.toObject(),
+ form_type: "A3",
+ }));
+
+ // ----------------------------
+ // Combine All Forms
+ // ----------------------------
+ const allForms = [...typedA1, ...typedA2, ...typedA3];
+
+ // Sort by createdAt
+ allForms.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
+
+ // Respond
+ 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 handleSupervisorFormAction = async (req, res, action) => {
+ try {
const form_type = req.params.type;
const formId = req.params.id;
- const { comment = "", signature = "" } = req.body;
+ const { comment = "", signature = "" } = req.body;
- const models = {
- A1: require("../models/InternshipRequest"),
- A2: require("../models/WeeklyReport"),
- A3: require("../models/Evaluation"),
- };
+ const models = {
+ A1: require("../models/InternshipRequest"),
+ A2: require("../models/WeeklyReport"),
+ A3: require("../models/Evaluation"),
+ };
const FormModel = models[form_type];
if (!FormModel) {
@@ -87,33 +149,34 @@ exports.handleSupervisorFormAction = async (req, res, action) => {
const update = {
supervisor_status: action === "approve" ? "approved" : "rejected",
supervisor_comment: comment,
+ supervisor_signature: signature,
};
- const form = await FormModel.findByIdAndUpdate(formId, update, { new: true }).populate("student_id", "userName email");
+ const form = await FormModel.findByIdAndUpdate(formId, update, { new: true })
if (!form) {
return res.status(404).json({ message: "Form not found" });
}
- const studentEmail =
- form.student_id?.email ||
- form.interneeEmail ||
- form.studentEmail ||
- null;
-
- if (!studentEmail) {
- console.warn("⚠️ No student email found for form:", form._id);
- } else {
- const emailSubject = `Form ${action === "approve" ? "Approved" : "Rejected"}`;
- let emailBody = `
Your ${form_type} form has been ${action}ed by the supervisor.
`;
- if (comment) {
+ const emailSubject = `Form ${action === "approve" ? "Approved" : "Rejected"}`;
+ let emailBody = `
Your ${form_type} form has been ${action === "approve" ? "approved": "rejected"} by the supervisor.
`;
+ if (comment) {
emailBody += `
Comment: ${comment}
`;
- }
}
-
- const student_id = form.student_id || form.internee_id || form.student;
- const student = await UserTokenRequest.findById(student_id);
- const student_mail = student?.ouEmail || form?.interneeEmail;
+
+ let student_mail = null;
+ if (form_type === "A1") {
+ student_mail = form.student.email;
+ }
+ else if (form_type === "A2") {
+ student_mail = form.studentId?.ouEmail;
+ }
+ else if (form_type === "A3") {
+ student_mail = form.interneeEmail;
+ }
+ else {
+ console.error(`Unknown form type: ${form_type}`);
+ }
try {
await EmailService.sendEmail({
@@ -125,8 +188,6 @@ exports.handleSupervisorFormAction = async (req, res, action) => {
console.error("Email sending error:", err);
}
- console.log("Email sent to:", student_mail);
-
res.status(200).json({
message: `Form ${action}ed successfully`,
updatedForm: form,
@@ -141,7 +202,7 @@ exports.handleSupervisorFormAction = async (req, res, action) => {
// Coordinator Dashboard //
// =========================================== //
-exports.getCoordinatorRequests = async (req, res) => {
+const getCoordinatorRequests = async (req, res) => {
try {
const requests = await InternshipRequest.find({
coordinator_status: "pending",
@@ -153,7 +214,7 @@ exports.getCoordinatorRequests = async (req, res) => {
};
// Coordinator View Single Request
-exports.getCoordinatorRequestDetails = async (req, res) => {
+const getCoordinatorRequestDetails = async (req, res) => {
try {
const requestData = await InternshipRequest.findById(req.params.id).lean();
if (!requestData) {
@@ -167,7 +228,7 @@ exports.getCoordinatorRequestDetails = async (req, res) => {
};
// Coordinator Approve Request
-exports.coordinatorApproveRequest = async (req, res) => {
+const coordinatorApproveRequest = async (req, res) => {
try {
const request = await InternshipRequest.findByIdAndUpdate(
req.params.id,
@@ -192,7 +253,7 @@ exports.coordinatorApproveRequest = async (req, res) => {
};
// Coordinator Reject Request
-exports.coordinatorRejectRequest = async (req, res) => {
+const coordinatorRejectRequest = async (req, res) => {
const { reason } = req.body;
if (!reason) return res.status(400).json({ message: "Reason required" });
@@ -218,3 +279,93 @@ exports.coordinatorRejectRequest = async (req, res) => {
res.status(500).json({ message: "Rejection failed", error: err.message });
}
};
+
+const coordinatorResendRequest = async (req, res) => {
+ try {
+ const submission = await InternshipRequest.findById(req.params.id);
+ if (!submission)
+ return res.status(404).json({ message: "Submission not found" });
+
+ submission.coordinator_reminder_count = 0;
+ submission.last_coordinator_reminder_at = new Date();
+ submission.coordinator_status = "pending";
+ await submission.save();
+
+ return res.status(200).json({ message: "Reminder cycle restarted." });
+ } catch (error) {
+ console.error("Error in coordinatorResendRequest:", error);
+ return res.status(500).json({ message: "Server error while resending request." });
+ }
+};
+
+const deleteStudentSubmission = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const studentId = req.user._id;
+
+ const submission = await InternshipRequest.findById(id);
+ if (!submission)
+ return res.status(404).json({ message: "Submission not found." });
+
+ if (submission.student.toString() !== studentId.toString()) {
+ return res.status(403).json({ message: "You are not authorized to delete this submission." });
+ }
+
+ if (submission.coordinator_status !== "pending") {
+ return res.status(400).json({ message: "Submission already reviewed. Cannot delete." });
+ }
+
+ await InternshipRequest.findByIdAndDelete(id);
+ return res.status(200).json({ message: "Submission successfully deleted by student." });
+ } catch (err) {
+ console.error("Error deleting student submission:", err);
+ return res.status(500).json({ message: "Internal server error." });
+ }
+};
+
+const getStudentSubmissions = async (req, res) => {
+ try {
+ const studentId = req.user._id;
+ const submissions = await InternshipRequest.find({ student: studentId }).sort({ createdAt: -1 });
+ res.status(200).json(submissions);
+ } catch (error) {
+ console.error("Error fetching student submissions:", error);
+ res.status(500).json({ message: "Failed to fetch submissions." });
+ }
+};
+
+const deleteStalledSubmission = async (req, res) => {
+ try {
+ const { id } = req.params;
+
+ const submission = await InternshipRequest.findById(id);
+ if (!submission) {
+ return res.status(404).json({ message: "Submission not found." });
+ }
+
+ if (submission.coordinator_status !== "pending") {
+ return res.status(400).json({ message: "Submission already reviewed. Cannot delete." });
+ }
+
+ await InternshipRequest.findByIdAndDelete(id);
+
+ return res.status(200).json({ message: "Submission deleted successfully." });
+ } catch (error) {
+ console.error("Error deleting submission:", error);
+ return res.status(500).json({ message: "Internal server error" });
+ }
+};
+
+
+module.exports = {
+ getCoordinatorRequests,
+ getCoordinatorRequestDetails,
+ coordinatorApproveRequest,
+ coordinatorRejectRequest,
+ coordinatorResendRequest,
+ deleteStudentSubmission,
+ getStudentSubmissions,
+ getSupervisorForms,
+ handleSupervisorFormAction,
+ deleteStalledSubmission,
+};
diff --git a/server/controllers/internshipRequestController.js b/server/controllers/internshipRequestController.js
index e03c0c07..f6929ea9 100644
--- a/server/controllers/internshipRequestController.js
+++ b/server/controllers/internshipRequestController.js
@@ -1,4 +1,4 @@
-const InternshipRequest = require("../models/internshiprequest");
+const InternshipRequest = require("../models/InternshipRequest");
const WeeklyReport = require("../models/WeeklyReport");
exports.getA1ByEmail = async (req, res) => {
diff --git a/server/controllers/reportController.js b/server/controllers/reportController.js
index c5179546..c62f0616 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";
@@ -281,4 +281,4 @@ const reportController = {
}
};
-module.exports = reportController;
\ No newline at end of file
+module.exports = reportController;
diff --git a/server/jobs/cronJobsConfig.js b/server/jobs/cronJobsConfig.js
index 69829511..9cad1595 100644
--- a/server/jobs/cronJobsConfig.js
+++ b/server/jobs/cronJobsConfig.js
@@ -8,7 +8,6 @@ const jobFunctions = {
coordinatorApprovalReminder: coordinatorReminder,
supervisorApprovalReminder: supervisorReminder,
// Add future cron jobs here
- supervisorApprovalReminder: supervisorReminder,
tokenExpiryReminder: checkAndSendReminders,
autoDeactivateCronjobs: autoDeactivateCronjobs,
// Add more job functions here as needed
diff --git a/server/jobs/reminderEmail.js b/server/jobs/reminderEmail.js
index c0435d25..bc5530f0 100644
--- a/server/jobs/reminderEmail.js
+++ b/server/jobs/reminderEmail.js
@@ -67,88 +67,131 @@ const getAllForms = async (filter = {}) => {
A3: require("../models/Evaluation"),
};
- const formPromises = Object.entries(models).map(
- async ([form_type, Model]) => {
- const results = await Model.find(filter);
- return results;
- }
- );
-
- const allResults = await Promise.all(formPromises);
- return allResults.flat();
+ const formPromises = Object.entries(models).map(async ([formType, Model]) => {
+ const forms = await Model.find(filter);
+ return forms.map(form => ({
+ ...form.toObject(),
+ form_type: formType // dynamically add the type
+ }));
+ });
+
+ const allForms = await Promise.all(formPromises);
+ return allForms.flat(); // flatten into a single array
};
+const findSupervisorFromForm = async (form) => {
+ let supervisor = null;
+ try {
+ if (form.form_type === "A1") {
+ supervisor = await UserTokenRequest.findOne({ ouEmail: form.internshipAdvisor.email });
+ }
+ else if (form.form_type === "A2") {
+ supervisor = await UserTokenRequest.findOne({ ouEmail: form.supervisorEmail });
+ }
+ else if (form.form_type === "A3") {
+ const internship_a1 = await InternshipRequest.findById(form.internshipId);
+ supervisor = await UserTokenRequest.findOne({ ouEmail: internship_a1.internshipAdvisor.email });
+ }
+ else {
+ logger.error(`Unknown form type: ${form.form_type}`);
+ }
+ }
+ catch (err) {
+ logger.error(`Error retrieving supervisor: ${err.message}`);
+ }
+ return supervisor;
+}
+
+const getStudentFromForm = async (form) => {
+ let student = null;
+ try {
+ if (form.form_type === "A1") {
+ student = await UserTokenRequest.findById(form.student);
+ }
+ else if (form.form_type === "A2") {
+ student = await UserTokenRequest.findById(form.studentId);
+ }
+ else if (form.form_type === "A3") {
+ student = await UserTokenRequest.findById(form.interneeId);
+ }
+ else {
+ logger.error(`Unknown form type: ${form.form_type}`);
+ }
+ }
+ catch (err) {
+ logger.error(`Error retrieving student: ${err.message}`);
+ }
+
+ return student;
+}
+
// Supervisor reminder: weekly progress reports pending review
const supervisorReminder = async () => {
const now = dayjs();
const fiveWorkingDaysAgo = now.subtract(7, "day").toDate();
- try {
- const pendingSubs = await Submission.find({
- supervisor_status: "pending",
- createdAt: { $lt: fiveWorkingDaysAgo },
- });
-
- const supervisors = await UserTokenRequest.find({
- role: "supervisor",
- isActivated: true,
- });
+ try {
+ const models = {
+ A1: require("../models/InternshipRequest"),
+ A2: require("../models/WeeklyReport"),
+ A3: require("../models/Evaluation"),
+ };
+
+ const pendingSubs = await getAllForms({
+ supervisor_status: "pending",
+ last_supervisor_reminder_at: { $lt: fiveWorkingDaysAgo },
+ });
- for (const submission of pendingSubs) {
- const student = await User.findById(submission.student_id);
- const supervisor = await User.findById(submission.supervisor_id);
+ for (const submission of pendingSubs) {
- if (!student || !supervisor) continue;
+ const student = await getStudentFromForm(submission);
- 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);
+ 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 (reminderCount >= 2 && shouldRemindAgain) {
await emailService.sendEmail({
- to: student.email,
- subject: `Supervisor Not Responding for "${submission.name}"`,
- html: `
Your submission "${submission.name}" has not been reviewed by your supervisor after multiple reminders.
-
Please consider resending or deleting the request.
`,
- text: `Your submission "${submission.name}" is still awaiting supervisor review.`,
+ to: student.ouEmail,
+ subject: `Supervisor Not Responding for "${submission.form_type}"`,
+ html: `
Your submission "${submission.form_type}" has not been reviewed by the supervisor after multiple reminders.
+
Please consider resending the form or deleting the request.
`,
+ text: `Your submission "${submission.form_type}" is still awaiting supervisor review.`,
});
await NotificationLog.create({
submissionId: submission._id,
type: "studentEscalation",
recipientEmail: student.email,
- message: `Student notified about supervisor inaction for "${submission.name}".`,
+ message: `Student notified about supervisor inaction for "${submission._id}".`,
});
- logger.info(`[Escalated] Student notified for: "${submission.name}"`);
+ logger.info(`[Escalated] Student notified for: "${submission._id}"`);
} else if (shouldRemindAgain) {
- for (const sup of supervisors) {
+
+ const supervisor = await findSupervisorFromForm(submission);
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: supervisor?.ouEmail,
+ subject: `Reminder: Please Review Submission ${submission.form_type} of ${student.fullName}`,
+ html: `
This is a reminder to review the submission by ${student.ouEmail}.
`,
+ text: `Reminder: Please Review Submission ${submission.form_type} of ${student.fullName}`,
});
- }
-
- submission.supervisor_reminder_count = reminderCount + 1;
- submission.last_supervisor_reminder_at = new Date();
-
- try {
- await submission.save();
- } catch (err) {
- logger.error(`Failed to save submission: ${err.message}`);
- }
-
- logger.info(
- `[Reminder Sent] Supervisor: "${supervisor.email}" for "${submission.name}"`
- );
+
+ const updatedSubmission = await models[submission.form_type].findByIdAndUpdate(
+ submission._id,
+ {
+ supervisor_status: "pending",
+ supervisor_reminder_count: reminderCount + 1,
+ last_supervisor_reminder_at: new Date(),
+ },
+ );
+
+ logger.info(`[Reminder Sent] Supervisor: "${supervisor.ouEmail}" for "${submission._id}"`);
}
}
} catch (err) {
- logger.error("[SupervisorReminder Error]:", err.message || err);
+ logger.error("[SupervisorReminder Error]:", err.message);
}
};
diff --git a/server/middleware/authMiddleware.js b/server/middleware/authMiddleware.js
index 6ea8cb9c..19871eae 100644
--- a/server/middleware/authMiddleware.js
+++ b/server/middleware/authMiddleware.js
@@ -1,24 +1,11 @@
const User = require("../models/User");
const UserTokenRequest = require("../models/TokenRequest");
-exports.isSupervisor = (req, res, next) => {
- // const supervisor = Sup.find({$id: username})
-
- req.user = { role: "supervisor" }; // Mocking user role for demo
- if (req.user.role === "supervisor") {
- next();
- } else {
- res.status(403).json({ message: "Access denied. Not a supervisor." });
- }
-};
-
-/*
- // This is token management if we'll use it in the future
-exports.isSupervisor = async (req, res, next) => {
+// Supervisor authentication middleware
+const isSupervisor = async (req, res, next) => {
try {
// Token management
- const raw = req.headers.authorization?.split(" ")[1]; // "Bearer
"
- const token = raw.replace(/^"|"$/g, ""); // removes surrounding quotes
+ const token = req.headers.authorization?.split(" ")[1]; // "Bearer "
if (!token) {
return res.status(401).json({ message: "No token provided" });
@@ -40,14 +27,30 @@ exports.isSupervisor = async (req, res, next) => {
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)
- if (req.user.role === "coordinator") {
+// 🔹 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" });
+ }
+};
+
+module.exports = {
+ isStudent,
+ isSupervisor,
+ isCoordinator,
+};
diff --git a/server/models/Evaluation.js b/server/models/Evaluation.js
index 4f838107..8ac82dfd 100644
--- a/server/models/Evaluation.js
+++ b/server/models/Evaluation.js
@@ -22,8 +22,8 @@ const evaluationItemSchema = new mongoose.Schema({
const evaluationSchema = new mongoose.Schema({
...formMetadata,
- interneeId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: false },
- internshipId: { type: mongoose.Schema.Types.ObjectId, ref: 'Internship', required: false },
+ interneeId: { type: mongoose.Schema.Types.ObjectId, ref: 'UserTokenRequest', required: false },
+ internshipId: { type: mongoose.Schema.Types.ObjectId, ref: 'InternshipRequest', required: false },
interneeName: {
type: String,
@@ -61,4 +61,4 @@ const evaluationSchema = new mongoose.Schema({
evaluationSchema.index({ interneeID: 1, internshipId: 1 });
-module.exports = mongoose.model('Evaluation', evaluationSchema);
\ No newline at end of file
+module.exports = mongoose.model('Evaluation', evaluationSchema);
diff --git a/server/models/FormMetadata.js b/server/models/FormMetadata.js
index 0e54089e..3b26c2b6 100644
--- a/server/models/FormMetadata.js
+++ b/server/models/FormMetadata.js
@@ -2,12 +2,9 @@ const mongoose = require("mongoose");
const UserTokenRequest = require("../models/TokenRequest");
const formMetadata = {
- student_id: { type: mongoose.Schema.Types.ObjectId, ref: "UserTokenRequest"},
- supervisor_id: { type: mongoose.Schema.Types.ObjectId, ref: "UserTokenRequest"},
- coordinator_id: { type: mongoose.Schema.Types.ObjectId, ref: "UserTokenRequest"},
-
supervisor_status: { type: String, default: "pending" },
- supervisor_comment: String,
+ supervisor_comment: String,
+ supervisor_signature: String,
supervisor_reminder_count: { type: Number, default: 0 },
last_supervisor_reminder_at: Date,
diff --git a/server/models/TokenRequest.js b/server/models/TokenRequest.js
index 2b76e0a7..d9008e55 100644
--- a/server/models/TokenRequest.js
+++ b/server/models/TokenRequest.js
@@ -54,8 +54,6 @@ const userTokenRequestSchema = new mongoose.Schema(
required: function () {
return this.role === 'student';
},
- unique: true,
- match: [/^\d{9}$/, 'Sooner ID must be exactly 9 digits'],
},
role: {
type: String,
diff --git a/server/models/WeeklyReport.js b/server/models/WeeklyReport.js
index f6c06313..e6db3213 100644
--- a/server/models/WeeklyReport.js
+++ b/server/models/WeeklyReport.js
@@ -2,8 +2,11 @@ const mongoose = require("mongoose");
const formMetadata = require("./FormMetadata");
const weeklyReportSchema = new mongoose.Schema({
+ ...formMetadata,
+
studentId: {
- type: String,
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "UserTokenRequest",
required: true,
},
diff --git a/server/routes/approvalRoutes.js b/server/routes/approvalRoutes.js
index 0913ac09..2d45ab69 100644
--- a/server/routes/approvalRoutes.js
+++ b/server/routes/approvalRoutes.js
@@ -1,6 +1,6 @@
const express = require("express");
const router = express.Router();
-
+const { isSupervisor, isCoordinator, isStudent } = require("../middleware/authMiddleware");
const {
getSupervisorForms,
handleSupervisorFormAction,
@@ -8,21 +8,27 @@ const {
getCoordinatorRequestDetails,
coordinatorApproveRequest,
coordinatorRejectRequest,
+ getStudentSubmissions,
+ coordinatorResendRequest,
+ deleteStalledSubmission,
+ deleteStudentSubmission,
} = require("../controllers/approvalController");
-const { isSupervisor, isCoordinator } = require("../middleware/authMiddleware");
+// Student API
+router.get("/student/submissions", isStudent, getStudentSubmissions);
+router.delete("/student/request/:id/delete", isStudent, deleteStudentSubmission);
// =========================================== //
// Supervisor Approval Routes //
// =========================================== //
// Supervisor APIs
+router.get("/supervisor-dashboard", isSupervisor, (req,res)=>{
+ res.render("supervisorDashboard");
+})
router.get("/supervisor/forms", isSupervisor, (req, res) => {
- // const supervisorId = req.user._id,
- return getSupervisorForms(req, res, {
- // supervisor_id: supervisorId,
- supervisor_status: { $in: ["pending"] },
- })
+ // req.user supposed exists in the request
+ return getSupervisorForms(req, res)
});
// Approve route
router.post("/supervisor/form/:type/:id/approve", isSupervisor, (req, res) =>
@@ -56,4 +62,4 @@ router.post(
coordinatorRejectRequest
);
-module.exports = router;
\ No newline at end of file
+module.exports = router;
diff --git a/server/routes/token.js b/server/routes/token.js
index 6c838799..3f0a3f38 100644
--- a/server/routes/token.js
+++ b/server/routes/token.js
@@ -24,7 +24,7 @@ router.post("/request", async (req, res) => {
if (!fullName || !ouEmail || !password || !semester || !role) {
return res.status(400).json({ error: "All fields are required." });
}
-
+
const existing = await TokenRequest.findOne({ ouEmail });
if (existing) {
return res.status(401).json({ error: "Token request already exists for this email." });