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 = ( -
-
No pending approvals.
-
- ); - } else { - content = ( - - - - - - - - - - - - - {sortedRequests.map((req) => ( - - - - - - - + + let content; + if (loading) { + content =

Loading...

; + } else if (sortedRequests.length === 0) { + content = ( +
+
No pending approvals.
+
+ ); + } else { + content = ( +
Student NameSooner IDStudent EmailForm TypeSubmittedStatus
{req.interneeName} - - {req.interneeEmail}{req.form_type}{formatDate(req.createdAt)} - - {req.supervisor_status} - -
+ + + + + + + - ))} - -
Student NameStudent EmailForm TypeSubmittedStatus
+ + + {sortedRequests.map((req) => { + return ( + openFormView(req)} + style={{ cursor: "pointer" }} + > + {req.interneeName || "N/A"} + {req.interneeEmail || "N/A"} + {req.form_type} + {formatDate(req.createdAt)} + + + {req.supervisor_status || req.status} + + + ); - } + })} + - return ( -
-

Supervisor Dashboard

- {message &&

{message}

} - {content} - {selectedForm && ( - - handleAction(selectedForm.form_type, id, action, comment, signature) - } - /> - )} -
- ); -}; + + ); + } + + return ( +
+

Supervisor Dashboard

+ {message &&

{message}

} + {content} + {selectedForm && ( + + handleAction(id, selectedForm.form_type, action, comment, signature) + } + /> + )} +
+ ); + }; export default SupervisorDashboard; diff --git a/client/src/pages/ViewFormModal.js b/client/src/pages/ViewFormModal.js index a83a3fa3..f724358b 100644 --- a/client/src/pages/ViewFormModal.js +++ b/client/src/pages/ViewFormModal.js @@ -1,11 +1,10 @@ import React, { useState } from "react"; -import "../styles/SupervisorDashboard.css"; +import "../styles/A1InternshipRequestForm.css"; -const ViewFormModal = ({ formData, onClose, onAction }) => { - const [comment, setComment] = useState(""); +const ViewFormModal = ({ formData, onClose, onAction, onActionComplete }) => { + const [comment, setComment] = useState(""); const [signature, setSignature] = useState(""); const [error, setError] = useState(""); - const handleDecision = (action) => { if (!comment.trim()) return setError("Comment is required."); if (!signature.trim()) return setError("Signature is required."); @@ -13,7 +12,6 @@ const ViewFormModal = ({ formData, onClose, onAction }) => { onAction(formData._id, action, comment.trim(), signature.trim()); }; - // ✅ Inserted rendering helpers const renderA1 = () => ( <>

A1 – Internship Request Form

@@ -27,14 +25,16 @@ const ViewFormModal = ({ formData, onClose, onAction }) => { Student Name: {formData.interneeName || "N/A"} - Student ID: {formData.soonerId || "N/A"} Email: {formData.interneeEmail || "N/A"} Workplace Name: {formData.workplace?.name || "N/A"} - Phone: {formData.workplace?.phone || "N/A"} Website: {formData.workplace?.website || "N/A"} + + Workplace Phone: {formData.workplace?.phone || "N/A"} + + Advisor Name: {formData.internshipAdvisor?.name || "N/A"} Advisor Email: {formData.internshipAdvisor?.email || "N/A"} @@ -65,32 +65,77 @@ const ViewFormModal = ({ formData, onClose, onAction }) => { ); + const renderA2 = () => ( + <> +

A2 – Weekly Evaluation

+ + + + + + + + + + + + + + + + + +
Name:

{formData.interneeName || "N/A"}

Email:

{formData.interneeEmail || "N/A"}

Current Week:

{formData.fullForm.week|| "N/A"}

Hours:

{formData.fullForm.hours|| "N/A"}

+ +
+ Tasks Performed +

{formData.tasks || "No tasks provided"}

+
+
+ Lessons Learned +

{formData.lessons || "No lessons provided"}

+
+ + ); + const renderA3 = () => ( <> -

A3 – Final Job Performance Evaluation

-

Name: {formData.interneeName}

-

Email: {formData.interneeEmail}

-

Sooner ID: {formData.interneeID}

+

A3 – Final Performance Eval

+ + + + + + + + + + + + + +

Name: {formData.interneeName}

Email: {formData.interneeEmail}

+

Evaluation Items

- - - - - - - - - {formData.evaluations?.map((item, i) => ( - - - - + + + + + - ))} - -
CategoryRatingComment
{item.category}{item.rating}{item.comment || "-"}
CategoryRatingComment
+ + + {formData.fullForm.evaluations?.map((item, i) => ( + + {item.category} + {item.rating} + {item.comment || "-"} + + ))} + + ); @@ -120,14 +165,25 @@ const ViewFormModal = ({ formData, onClose, onAction }) => { ); - return ( -
-
- {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." });