From 186ca8007d57805e0bb66e05f4fada0c796c6c60 Mon Sep 17 00:00:00 2001 From: Doruk Ayhan Date: Wed, 16 Apr 2025 00:35:39 -0500 Subject: [PATCH 01/56] InternshipRequest.student is now sooner id instead of User reference --- client/src/pages/A1InternshipRequestForm.js | 4 ++-- server/models/InternshipRequest.js | 7 ++++--- server/routes/formRoutes.js | 4 ++++ server/services/insertData.js | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/client/src/pages/A1InternshipRequestForm.js b/client/src/pages/A1InternshipRequestForm.js index 81191c88..0b63a159 100644 --- a/client/src/pages/A1InternshipRequestForm.js +++ b/client/src/pages/A1InternshipRequestForm.js @@ -81,7 +81,7 @@ const A1InternshipRequestForm = () => { const validateForm = () => { const namePattern = /^[A-Za-z\s]+$/; - const numberPattern = /^[0-9]+$/; + const soonerIdPattern = /^[0-9]{9}$/; const phonePattern = /^[0-9]{10}$/; const emailPattern = /^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$/; @@ -91,7 +91,7 @@ const A1InternshipRequestForm = () => { else if (!namePattern.test(formData.interneeName)) newErrors.interneeName = "Name should contain only letters and spaces"; if (!formData.soonerId) newErrors.soonerId = "Sooner ID is required"; - else if (!numberPattern.test(formData.soonerId)) newErrors.soonerId = "Sooner ID should be numeric"; + else if (!soonerIdPattern.test(formData.soonerId)) newErrors.soonerId = "Sooner ID should be a 9-digit number"; if (!formData.interneeEmail) newErrors.interneeEmail = "Email is required"; else if (!emailPattern.test(formData.interneeEmail)) newErrors.interneeEmail = "Invalid email format"; diff --git a/server/models/InternshipRequest.js b/server/models/InternshipRequest.js index 3732f04d..df9ac416 100644 --- a/server/models/InternshipRequest.js +++ b/server/models/InternshipRequest.js @@ -13,10 +13,11 @@ const Task = new mongoose.Schema({ } }); const formA1 = new mongoose.Schema({ - student: { // get student's name, email, id from User - type: ObjectId, + student: { + type: Number, required: true, - ref: 'User' + min: [0, 'Sooner ID can\'t be negative'], + max: [999999999, 'Sooner ID should be a 9-digit number'] }, workplace: { name: { diff --git a/server/routes/formRoutes.js b/server/routes/formRoutes.js index c80f3ebe..b465f448 100644 --- a/server/routes/formRoutes.js +++ b/server/routes/formRoutes.js @@ -7,6 +7,7 @@ let status = ''; // Validate required fields function validateFormData(formData) { const requiredFields = [ + 'soonerId', 'workplaceName', 'website', 'phone', @@ -25,6 +26,9 @@ function validateFormData(formData) { } } + if (!/^[0-9]{9}$/.test(formData.soonerId)) + return `Sooner ID must be a 9-digit number, not ${formData.soonerId}`; + if (!Array.isArray(formData.tasks) || formData.tasks.length === 0) { return 'Tasks must be a non-empty array'; } diff --git a/server/services/insertData.js b/server/services/insertData.js index 7668f7c6..75c5d616 100644 --- a/server/services/insertData.js +++ b/server/services/insertData.js @@ -9,7 +9,7 @@ async function insertFormData(formData) { // Assumes global mongoose connection is already established elsewhere in app const formattedData = { - student: new mongoose.Types.ObjectId(), // TODO: Replace with actual signed-in student ID + student: parseInt(formData.soonerId), workplace: { name: formData.workplaceName, website: formData.website, From 16d21161b627f6815918ac5f7e2f9b4a8be54afc Mon Sep 17 00:00:00 2001 From: TheJob21 Date: Wed, 16 Apr 2025 22:40:47 -0500 Subject: [PATCH 02/56] tokenExpiryChecks and reminder emails implemented --- client/package.json | 3 +- client/src/pages/TokenRenewal.jsx | 73 +++++++++++++++++++++++++++++++ client/src/router.js | 5 +++ server/jobs/cronJobsConfig.js | 2 + server/jobs/tokenExpiryCheck.js | 57 ++++++++++++++++++++++++ server/package.json | 3 +- server/routes/token.js | 34 ++++++++++++++ 7 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 client/src/pages/TokenRenewal.jsx create mode 100644 server/jobs/tokenExpiryCheck.js diff --git a/client/package.json b/client/package.json index 75898d4e..1884b6fa 100644 --- a/client/package.json +++ b/client/package.json @@ -9,6 +9,7 @@ "@testing-library/user-event": "^13.5.0", "axios": "^1.8.2", "bootstrap": "^5.3.5", + "client": "file:", "react": "^19.0.0", "react-bootstrap": "^2.10.9", "react-dom": "^19.0.0", @@ -42,6 +43,6 @@ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" - ] + ] } } diff --git a/client/src/pages/TokenRenewal.jsx b/client/src/pages/TokenRenewal.jsx new file mode 100644 index 00000000..f83b15a3 --- /dev/null +++ b/client/src/pages/TokenRenewal.jsx @@ -0,0 +1,73 @@ +import React, { useState } from 'react'; + +const TokenRenewal = () => { + const [token, setToken] = useState(''); + const [responseMessage, setResponseMessage] = useState(''); + const [loading, setLoading] = useState(false); + + const handleInputChange = (event) => { + setToken(event.target.value); + }; + + const handleFormSubmit = async (event) => { + event.preventDefault(); + setLoading(true); + setResponseMessage(''); + + try { + const response = await fetch(`${process.env.REACT_APP_API_URL}/api/token/renew`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ token }), + }); + + const data = await response.json(); + + if (response.ok) { + setResponseMessage({ text: 'Success: Your token has been renewed!', success: true }); + } else { + setResponseMessage({ text: `Error: ${data.message}`, success: false }); + } + } catch (error) { + setResponseMessage({ text: 'Error: Unable to process your request.', success: false }); + } finally { + setLoading(false); + } + }; + + return ( +
+

Token Renewal

+
+
+ + +
+ +
+ + {responseMessage && ( +
+ {responseMessage.text} +
+ )} +
+ ); +}; + +export default TokenRenewal; diff --git a/client/src/router.js b/client/src/router.js index c1bfe4ef..c1e9bbe6 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -17,6 +17,7 @@ import A4PresentationEvaluationForm from "./pages/A4PresentationEvaluationForm"; import SupervisorDashboard from "./pages/SupervisorDashboard"; import CoordinatorDashboard from "./pages/CoordinatorDashboard"; import CoordinatorRequestDetailView from "./pages/CoordinatorRequestDetailView"; +import TokenRenewal from "./pages/TokenRenewal"; // Create and export the router configuration const router = createBrowserRouter([ @@ -65,6 +66,10 @@ const router = createBrowserRouter([ path: "coordinator/request/:id", element: , }, + { + path: "renew-token", + element: , + }, ], }, ]); diff --git a/server/jobs/cronJobsConfig.js b/server/jobs/cronJobsConfig.js index 2cb9e5c6..ae83c923 100644 --- a/server/jobs/cronJobsConfig.js +++ b/server/jobs/cronJobsConfig.js @@ -1,10 +1,12 @@ const CronJob = require("../models/CronJob"); const { coordinatorReminder, supervisorReminder } = require("./reminderEmail"); +const { checkAndSendReminders } = require("./tokenExpiryCheck"); // Map of job names to their corresponding functions const jobFunctions = { coordinatorApprovalReminder: coordinatorReminder, supervisorApprovalReminder: supervisorReminder, + tokenExpiryReminder: checkAndSendReminders, // Add more job functions here as needed }; diff --git a/server/jobs/tokenExpiryCheck.js b/server/jobs/tokenExpiryCheck.js new file mode 100644 index 00000000..3607ccd2 --- /dev/null +++ b/server/jobs/tokenExpiryCheck.js @@ -0,0 +1,57 @@ +const mongoose = require("mongoose"); +const UserTokenRequest = require("../models/TokenRequest"); // Adjust the path if needed +const emailService = require("../services/emailService"); +require("dotenv").config(); + +async function checkAndSendReminders() { + try { + await mongoose.connect(process.env.MONGO_URI); + const now = new Date(); + + // Tokens expiring in 3 and 7 days + const daysToCheck = [3, 7]; + + for (const days of daysToCheck) { + const targetDate = new Date(now); + targetDate.setDate(now.getDate() + days); + + const expiringTokens = await UserTokenRequest.find({ + isActivated: true, + status: "activated", + expiresAt: { + $gte: new Date(targetDate.setHours(0, 0, 0, 0)), + $lt: new Date(targetDate.setHours(23, 59, 59, 999)), + }, + }); + + for (const token of expiringTokens) { + const subject = `Reminder: Your Internship Token Expires in ${days} Day${days > 1 ? "s" : ""}`; + const renewalLink = `${process.env.FRONTEND_URL}/renew-token/${token.token}`; // Update with actual renewal path + + const html = ` +

Hello ${token.fullName},

+

This is a reminder that your internship access token will expire in ${days} day${days > 1 ? "s" : ""} on ${token.expiresAt.toDateString()}.

+

If your token expires, you will lose access to the internship management system.

+

Click here to renew your token securely.

+

Thank you,
Internship Program Management Team

+ `; + + const emailResponse = await emailService.sendEmail({ + to: token.ouEmail, + subject, + html, + }); + + console.log(`Email sent to ${token.ouEmail} for ${days}-day reminder:`, emailResponse); + } + } + + await mongoose.disconnect(); + } catch (error) { + console.error("Error during token expiry check:", error); + } +} + +module.exports = { + checkAndSendReminders, +}; diff --git a/server/package.json b/server/package.json index 20f54234..2e950867 100644 --- a/server/package.json +++ b/server/package.json @@ -24,7 +24,8 @@ "mongodb": "^6.14.2", "mongoose": "^8.13.2", "node-cron": "^3.0.3", - "nodemailer": "^6.10.0" + "nodemailer": "^6.10.0", + "server": "file:" }, "devDependencies": { "jest": "^29.7.0" diff --git a/server/routes/token.js b/server/routes/token.js index 247fda86..eb5e2173 100644 --- a/server/routes/token.js +++ b/server/routes/token.js @@ -233,4 +233,38 @@ router.delete("/deactivate", async (req, res) => { } }); +router.post('/renew', async (req, res) => { + const { token } = req.body; + + try { + const userToken = await UserTokenRequest.findOne({ token }); + + if (!userToken) { + return res.status(404).json({ message: 'Token not found or invalid.' }); + } + + if (userToken.status !== 'activated') { + return res.status(400).json({ message: 'Token is not activated, cannot renew.' }); + } + + // Renew the token logic (extend expiry, etc.) + const currentDate = new Date(); + userToken.expiresAt = new Date(currentDate.setDate(currentDate.getDate() + 30)); + await userToken.save(); + + // Send confirmation email (optional) + await emailService.sendEmail({ + to: userToken.ouEmail, + subject: 'Your Token Has Been Renewed', + html: `

Your token has been successfully renewed and will now expire on ${userToken.expiresAt.toLocaleDateString()}.

`, + }); + + return res.status(200).json({ message: 'Token successfully renewed!' }); + + } catch (error) { + console.error('Error renewing token:', error); + return res.status(500).json({ message: 'An error occurred while renewing the token.' }); + } +}); + module.exports = router; From a7a64cd48a34255512d2e4f64d9c815b80e6ef4f Mon Sep 17 00:00:00 2001 From: TheJob21 Date: Thu, 17 Apr 2025 20:16:38 -0500 Subject: [PATCH 03/56] Fixed some bugs, Tested functionality --- client/src/pages/TokenRenewal.jsx | 88 +++++++++++++------------------ client/src/router.js | 2 +- server/jobs/tokenExpiryCheck.js | 2 +- server/routes/token.js | 10 ++-- 4 files changed, 46 insertions(+), 56 deletions(-) diff --git a/client/src/pages/TokenRenewal.jsx b/client/src/pages/TokenRenewal.jsx index f83b15a3..8db7edeb 100644 --- a/client/src/pages/TokenRenewal.jsx +++ b/client/src/pages/TokenRenewal.jsx @@ -1,64 +1,50 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; const TokenRenewal = () => { - const [token, setToken] = useState(''); + const { token } = useParams(); const [responseMessage, setResponseMessage] = useState(''); - const [loading, setLoading] = useState(false); - - const handleInputChange = (event) => { - setToken(event.target.value); - }; - - const handleFormSubmit = async (event) => { - event.preventDefault(); - setLoading(true); - setResponseMessage(''); - - try { - const response = await fetch(`${process.env.REACT_APP_API_URL}/api/token/renew`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ token }), - }); - - const data = await response.json(); - - if (response.ok) { - setResponseMessage({ text: 'Success: Your token has been renewed!', success: true }); - } else { - setResponseMessage({ text: `Error: ${data.message}`, success: false }); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const renewToken = async () => { + try { + const response = await fetch(`${process.env.REACT_APP_API_URL}/api/token/renew`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ token }), + }); + + const data = await response.json(); + + if (response.ok) { + setResponseMessage({ text: '✅ Success: Your token has been renewed!', success: true }); + } else { + setResponseMessage({ text: `❌ Error: ${data.message}`, success: false }); + } + } catch (error) { + setResponseMessage({ text: '❌ Error: Unable to process your request.', success: false }); + } finally { + setLoading(false); } - } catch (error) { - setResponseMessage({ text: 'Error: Unable to process your request.', success: false }); - } finally { + }; + + if (token) { + renewToken(); + } else { + setResponseMessage({ text: '❌ Error: No token found in the URL.', success: false }); setLoading(false); } - }; + }, [token]); return (

Token Renewal

-
-
- - -
- -
- - {responseMessage && ( + {loading ? ( +

⏳ Processing your token renewal...

+ ) : (
, }, { - path: "renew-token", + path: "renew-token/:token", element: , }, ], diff --git a/server/jobs/tokenExpiryCheck.js b/server/jobs/tokenExpiryCheck.js index 3607ccd2..ebdc6374 100644 --- a/server/jobs/tokenExpiryCheck.js +++ b/server/jobs/tokenExpiryCheck.js @@ -3,7 +3,7 @@ const UserTokenRequest = require("../models/TokenRequest"); // Adjust the path i const emailService = require("../services/emailService"); require("dotenv").config(); -async function checkAndSendReminders() { +const checkAndSendReminders = async () => { try { await mongoose.connect(process.env.MONGO_URI); const now = new Date(); diff --git a/server/routes/token.js b/server/routes/token.js index eb5e2173..e9bbc8bb 100644 --- a/server/routes/token.js +++ b/server/routes/token.js @@ -237,7 +237,10 @@ router.post('/renew', async (req, res) => { const { token } = req.body; try { - const userToken = await UserTokenRequest.findOne({ token }); + + if (!token) return res.status(400).json({ error: "Token is missing." }); + console.log("Received token:", token); + const userToken = await TokenRequest.findOne({ token: token }); if (!userToken) { return res.status(404).json({ message: 'Token not found or invalid.' }); @@ -249,7 +252,7 @@ router.post('/renew', async (req, res) => { // Renew the token logic (extend expiry, etc.) const currentDate = new Date(); - userToken.expiresAt = new Date(currentDate.setDate(currentDate.getDate() + 30)); + userToken.expiresAt = new Date(currentDate.setMonth(currentDate.getMonth() + 6)); await userToken.save(); // Send confirmation email (optional) @@ -258,7 +261,8 @@ router.post('/renew', async (req, res) => { subject: 'Your Token Has Been Renewed', html: `

Your token has been successfully renewed and will now expire on ${userToken.expiresAt.toLocaleDateString()}.

`, }); - + + console.log('Token successfully renewed!') return res.status(200).json({ message: 'Token successfully renewed!' }); } catch (error) { From be80fffd29b3377b41e3e1bd173e7920225c10cd Mon Sep 17 00:00:00 2001 From: DHANUSHWI Date: Thu, 17 Apr 2025 22:36:06 -0500 Subject: [PATCH 04/56] Token Renewal API with secure validation and expiry extension --- package.json | 5 ++ server/routes/token.js | 106 +++++++++++++++++++++++++++-------------- 2 files changed, 74 insertions(+), 37 deletions(-) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 00000000..1bb6abe9 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "node-cron": "^3.0.3" + } +} diff --git a/server/routes/token.js b/server/routes/token.js index 247fda86..b6809181 100644 --- a/server/routes/token.js +++ b/server/routes/token.js @@ -16,8 +16,7 @@ const hashToken = (token) => { router.post("/request", async (req, res) => { try { - const { fullName, ouEmail, password, semester, academicAdvisor, role } = - req.body; + const { fullName, ouEmail, password, semester, academicAdvisor, role } = req.body; if (!fullName || !ouEmail || !password || !semester) { return res.status(400).json({ error: "All fields are required." }); @@ -25,9 +24,7 @@ router.post("/request", async (req, res) => { const existing = await TokenRequest.findOne({ ouEmail }); if (existing) { - return res - .status(400) - .json({ error: "Token request already exists for this email." }); + return res.status(400).json({ error: "Token request already exists for this email." }); } const plainToken = jwt.sign({ ouEmail }, JWT_SECRET, { expiresIn: "180d" }); @@ -35,7 +32,7 @@ router.post("/request", async (req, res) => { const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS); const requestedAt = new Date(); - const expiresAt = new Date(requestedAt.getTime() + 5 * 24 * 60 * 60 * 1000); // 5 days + const expiresAt = new Date(requestedAt.getTime() + 5 * 24 * 60 * 60 * 1000); const request = new TokenRequest({ fullName, @@ -85,16 +82,12 @@ router.post("/activate", async (req, res) => { const { token } = req.body; if (!token) return res.status(400).json({ error: "Token is missing." }); const hashedToken = hashToken(token); - console.log("Received token:", token); const user = await TokenRequest.findOne({ token: hashedToken }); if (!user) return res.status(404).json({ error: "Token not found." }); - if (user.deletedAt) - return res.status(403).json({ error: "Token has been deactivated." }); - if (user.isActivated) - return res.status(400).json({ error: "Token already activated." }); - if (new Date() > user.expiresAt) - return res.status(400).json({ error: "Token has expired." }); + if (user.deletedAt) return res.status(403).json({ error: "Token has been deactivated." }); + if (user.isActivated) return res.status(400).json({ error: "Token already activated." }); + if (new Date() > user.expiresAt) return res.status(400).json({ error: "Token has expired." }); user.isActivated = true; user.activatedAt = new Date(); @@ -120,10 +113,8 @@ router.post("/login", async (req, res) => { const user = await TokenRequest.findOne({ token: hashedToken }); if (!user) return res.status(404).json({ error: "Invalid token." }); - if (user.deletedAt) - return res.status(403).json({ error: "Token is deactivated." }); - if (!user.isActivated) - return res.status(403).json({ error: "Token not activated." }); + if (user.deletedAt) return res.status(403).json({ error: "Token is deactivated." }); + if (!user.isActivated) return res.status(403).json({ error: "Token not activated." }); res.json({ message: "Login successful", user }); } catch (err) { @@ -131,40 +122,32 @@ router.post("/login", async (req, res) => { } }); -// login api router.post("/user-login", async (req, res) => { const { ouEmail, password, role } = req.body; - console.log(role); + if (!ouEmail || !password || !role) { return res.status(400).json({ message: "All fields are required" }); } + try { const user = await TokenRequest.findOne({ ouEmail }); if (!user) { - return res - .status(401) - .json({ message: "Email or password is incorrect" }); + return res.status(401).json({ message: "Email or password is incorrect" }); } const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { - return res - .status(401) - .json({ message: "Email or password is incorrect" }); + return res.status(401).json({ message: "Email or password is incorrect" }); } - // First, check if the entered role matches the user's actual role if (user.role.toLowerCase() !== role.toLowerCase()) { return res.status(403).json({ message: "User role mismatch." }); } - // If the role is student, do additional token checks if (role.toLowerCase() === "student") { if (!user.isStudent) { - return res - .status(403) - .json({ message: "User is not registered as a student." }); + return res.status(403).json({ message: "User is not registered as a student." }); } if (!user.token || user.token === "") { @@ -179,9 +162,9 @@ router.post("/user-login", async (req, res) => { const tokenExpiry = new Date(user.expiresAt); if (tokenExpiry < now) { - return res - .status(403) - .json({ message: "Token has expired. Please request a new one." }); + return res.status(403).json({ + message: "Token has expired. Please request a new one.", + }); } } @@ -196,14 +179,11 @@ router.delete("/deactivate", async (req, res) => { try { const { token, ouEmail } = req.body; if (!token && !ouEmail) { - return res - .status(400) - .json({ error: "Token or Email is required for deactivation." }); + return res.status(400).json({ error: "Token or Email is required for deactivation." }); } let filter = {}; - // Only hash the token if it exists if (token) { if (typeof token !== "string") { return res.status(400).json({ error: "Token must be a string." }); @@ -213,6 +193,7 @@ router.delete("/deactivate", async (req, res) => { } else { filter = { ouEmail }; } + const user = await TokenRequest.findOne(filter); if (!user) { return res.status(404).json({ error: "Token not found." }); @@ -233,4 +214,55 @@ router.delete("/deactivate", async (req, res) => { } }); +router.post("/renew", async (req, res) => { + try { + const { token } = req.body; + + if (!token) { + return res.status(400).json({ error: "Token is required." }); + } + + const hashedToken = hashToken(token); + const user = await TokenRequest.findOne({ token: hashedToken }); + + if (!user) { + return res.status(404).json({ error: "Token not found." }); + } + + if (user.deletedAt || user.status === "deleted") { + return res.status(403).json({ error: "Token has been deactivated." }); + } + + if (!user.isActivated || user.status !== "activated") { + return res.status(403).json({ error: "Token is not activated." }); + } + + if (new Date() > user.expiresAt) { + return res.status(403).json({ error: "Token has already expired." }); + } + + const newToken = jwt.sign({ ouEmail: user.ouEmail }, JWT_SECRET, { expiresIn: "180d" }); + const hashedNewToken = hashToken(newToken); + + const newExpiresAt = new Date(); + newExpiresAt.setMonth(newExpiresAt.getMonth() + 6); + + user.token = hashedNewToken; + user.expiresAt = newExpiresAt; + user.status = "activated"; + + await user.save(); + + res.status(200).json({ + message: "Your token has been updated. You can now securely login.", + redirectUrl: `${FRONTEND_URL}/renewal-success`, + token: newToken, + expiresAt: newExpiresAt, + }); + } catch (error) { + console.error("Token renewal error:", error); + res.status(500).json({ error: "Internal server error." }); + } +}); + module.exports = router; From 3fa4ce5fa7353b4bfbfb46afee32ca83d3b4e291 Mon Sep 17 00:00:00 2001 From: nethra4321 <108627909+nethra4321@users.noreply.github.com> Date: Fri, 18 Apr 2025 00:52:56 -0500 Subject: [PATCH 05/56] saving changes --- client/src/pages/SupervisorDashboard.js | 124 +++++++++++++----------- client/src/pages/ViewFormModal.js | 88 ++++++++++++----- server/routes/formRoutes.js | 32 +++++- server/services/insertData.js | 4 + 4 files changed, 163 insertions(+), 85 deletions(-) diff --git a/client/src/pages/SupervisorDashboard.js b/client/src/pages/SupervisorDashboard.js index 18cc054d..077cdeef 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"; @@ -12,16 +13,31 @@ const SupervisorDashboard = () => { useEffect(() => { const fetchRequests = async () => { try { - const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/submissions/pending`); + const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/form/internshiprequests`); + console.log("Fetched internship requests:", res.data); // debug log + + const formatted = res.data + .map(item => ({ + _id: item._id, + name: item.student?.userName || item.student?.name || "N/A", + student_id: item.student?._id || item._id, - setRequests(res.data); + form_type: "A1", + createdAt: item.createdAt, + supervisor_status: item.supervisor_status || "pending", + fullForm: item + })) + .sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)); // oldest first + + setRequests(formatted); setLoading(false); } catch (err) { - console.error("Error fetching requests:", err); - setMessage("Error fetching requests."); + console.error("Error fetching Internship A1 forms:", err); + setMessage("Error fetching Internship A1 forms."); setLoading(false); } }; + fetchRequests(); }, []); @@ -30,7 +46,10 @@ const SupervisorDashboard = () => { if (!confirmed) return; try { - const res = await axios.post(`${process.env.REACT_APP_API_URL}/api/submissions/${id}/${action}`, { comment }); + const res = await axios.post( + `${process.env.REACT_APP_API_URL}/api/form/internshiprequests/${id}/${action}`, + { comment } + ); setMessage(res.data.message || `${action} successful`); setRequests(prev => prev.filter(req => req._id !== id)); @@ -43,63 +62,52 @@ const SupervisorDashboard = () => { const openFormView = (form) => setSelectedForm(form); const closeFormView = () => setSelectedForm(null); - - const formatDate = (dateStr) => new Date(dateStr).toLocaleDateString(); - - const sortedRequests = [...requests].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) => ( - - - - - - - - ))} - -
Student NameStudent IDForm TypeDate SubmittedStatus
{req.name} - - {req.form_type}{formatDate(req.createdAt)} - - {req.supervisor_status} - -
- ); - } + const formatDate = (date) => new Date(date).toLocaleDateString(); return (

Supervisor Dashboard

{message &&

{message}

} - {content} + + {loading ? ( +

Loading...

+ ) : requests.length === 0 ? ( +
+
No pending approvals.
+
+ ) : ( + + + + + + + + + + + + {requests.map((req) => ( + + + + + + + + ))} + +
Student NameStudent IDForm TypeDate SubmittedStatus
{req.name} + + {req.form_type}{formatDate(req.createdAt)} + + {req.supervisor_status} + +
+ )} + {selectedForm && ( { ); }; -export default SupervisorDashboard; \ No newline at end of file +export default SupervisorDashboard; diff --git a/client/src/pages/ViewFormModal.js b/client/src/pages/ViewFormModal.js index e913db19..55cc551e 100644 --- a/client/src/pages/ViewFormModal.js +++ b/client/src/pages/ViewFormModal.js @@ -2,8 +2,8 @@ import React, { useState } from "react"; import "../styles/SupervisorDashboard.css"; const ViewFormModal = ({ formData, onClose, onAction }) => { - const form = typeof formData.details === "string" ? JSON.parse(formData.details) : formData.details; const [comment, setComment] = useState(""); + const [signature, setSignature] = useState(""); const [error, setError] = useState(""); const handleDecision = (action) => { @@ -11,34 +11,75 @@ const ViewFormModal = ({ formData, onClose, onAction }) => { setError("Comment is required before taking action."); return; } - setError(""); // clear error - onAction(formData._id, action, comment); + if (!signature.trim()) { + setError("Signature is required before approval/rejection."); + return; + } + + const payloadComment = `${comment.trim()} | Supervisor Signature: ${signature.trim()}`; + setError(""); + onAction(formData._id, action, payloadComment); }; return (
-

Form: {formData.form_type}

-

Student: {formData.name}

+

A.1 Internship Request Form

- {form.tasks && ( -
- Tasks: -
    {form.tasks.map((task, i) =>
  • {task}
  • )}
-
- )} + + + + + + + + + + + + + + + + + + + + + + + +
Student Name: {formData.interneeName}Sooner ID: {formData.soonerId}
Email: {formData.interneeEmail}Phone: {formData.phone}
Workplace Name: {formData.workplaceName}Website: {formData.website}
Advisor Name: {formData.advisorName}Advisor Email: {formData.advisorEmail}
Credit Hours: {formData.creditHours} + Start Date: {new Date(formData.startDate).toLocaleDateString()} +
+ End Date: {new Date(formData.endDate).toLocaleDateString()} +
- {form.outcomes && ( -
- Outcomes: -
    {form.outcomes.map((o, i) =>
  • {o}
  • )}
-
- )} - - {form.week &&

Week: {form.week}

} - {form.lessonsLearned &&

Lessons Learned: {form.lessonsLearned}

} +
+ Task Descriptions & Outcomes: +
    + {formData.tasks?.map((task, index) => ( +
  • + Task {index + 1}: {task.description} +
    + Outcomes: {task.outcomes?.join(", ") || "N/A"} +
  • + ))} +
+
+ + setSignature(e.target.value)} + placeholder="Enter your full name" + style={{ width: "100%", padding: "6px", marginTop: "5px", borderRadius: "4px" }} + /> +
+ +