diff --git a/client/src/pages/SupervisorDashboard.js b/client/src/pages/SupervisorDashboard.js index 18cc054d..23be731a 100644 --- a/client/src/pages/SupervisorDashboard.js +++ b/client/src/pages/SupervisorDashboard.js @@ -12,16 +12,25 @@ const SupervisorDashboard = () => { useEffect(() => { const fetchRequests = async () => { try { - const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/submissions/pending`); - - setRequests(res.data); + const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/form/internshiprequests`); + const formatted = res.data.map(item => ({ + _id: item._id, + name: item.workplace?.name || "N/A", + student_id: item._id, // display _id of InternshipRequest + form_type: "A1", + createdAt: item.createdAt, + supervisor_status: "pending", + fullForm: item + })); + 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 +39,7 @@ 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)); @@ -44,62 +53,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..4727c86d 100644 --- a/client/src/pages/ViewFormModal.js +++ b/client/src/pages/ViewFormModal.js @@ -2,7 +2,6 @@ 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 [error, setError] = useState(""); @@ -11,32 +10,33 @@ const ViewFormModal = ({ formData, onClose, onAction }) => { setError("Comment is required before taking action."); return; } - setError(""); // clear error + setError(""); onAction(formData._id, action, comment); }; return (
-

Form: {formData.form_type}

-

Student: {formData.name}

+

Form: A1

+

Student ID: {formData.student}

+

Workplace: {formData.workplace?.name}

+

Advisor: {formData.internshipAdvisor?.name}

+

Credit Hours: {formData.creditHours}

+

Start Date: {new Date(formData.startDate).toLocaleDateString()}

+

End Date: {new Date(formData.endDate).toLocaleDateString()}

- {form.tasks && ( -
- Tasks: -
    {form.tasks.map((task, i) =>
  • {task}
  • )}
-
- )} - - {form.outcomes && ( -
- Outcomes: -
    {form.outcomes.map((o, i) =>
  • {o}
  • )}
-
- )} - - {form.week &&

Week: {form.week}

} - {form.lessonsLearned &&

Lessons Learned: {form.lessonsLearned}

} +
+ Tasks: +
    + {formData.tasks?.map((task, index) => ( +
  • + Description: {task.description} +
    + Outcomes: {task.outcomes.join(", ")} +
  • + ))} +
+
@@ -47,10 +47,10 @@ const ViewFormModal = ({ formData, onClose, onAction }) => { rows={4} style={{ width: "100%", marginTop: "5px", borderRadius: "4px", padding: "8px" }} /> - {error &&

{error}

} + {error &&

{error}

}
-
+
diff --git a/server/controllers/approvalController.js b/server/controllers/approvalController.js index 32582466..4627f70b 100644 --- a/server/controllers/approvalController.js +++ b/server/controllers/approvalController.js @@ -19,15 +19,22 @@ exports.getPendingSubmissions = async (req, res) => { exports.approveSubmission = async (req, res) => { const { id } = req.params; const { comment } = req.body; + try { - const submission = await Submission.findByIdAndUpdate( + const submission = await InternshipRequest.findByIdAndUpdate( id, { supervisor_status: "Approved", supervisor_comment: comment || "" }, { new: true } ); - if (!submission) + + if (!submission) { return res.status(404).json({ message: "Submission not found" }); - res.json({ message: "Submission Approved", updatedSubmission: submission }); + } + + res.json({ + message: "Submission approved and forwarded to Coordinator", + updatedSubmission: submission, + }); } catch (err) { res.status(500).json({ message: "Approval Failed", error: err }); } @@ -37,15 +44,22 @@ exports.approveSubmission = async (req, res) => { exports.rejectSubmission = async (req, res) => { const { id } = req.params; const { comment } = req.body; + try { - const submission = await Submission.findByIdAndUpdate( + const submission = await InternshipRequest.findByIdAndUpdate( id, { supervisor_status: "Rejected", supervisor_comment: comment || "" }, { new: true } ); - if (!submission) + + if (!submission) { return res.status(404).json({ message: "Submission not found" }); - res.json({ message: "Submission Rejected", updatedSubmission: submission }); + } + + res.json({ + message: "Submission rejected and sent back to student", + updatedSubmission: submission, + }); } catch (err) { res.status(500).json({ message: "Rejection Failed", error: err }); } @@ -63,12 +77,14 @@ exports.getCoordinatorRequests = async (req, res) => { } }; +// Coordinator View Single Request exports.getCoordinatorRequestDetails = async (req, res) => { try { const requestData = await InternshipRequest.findById(req.params.id).lean(); - if (!requestData) + if (!requestData) { return res.status(404).json({ message: "Request not found" }); + } res.status(200).json({ requestData, supervisorStatus: "Not Submitted" }); } catch (err) { @@ -76,6 +92,7 @@ exports.getCoordinatorRequestDetails = async (req, res) => { } }; +// Coordinator Approve Request exports.coordinatorApproveRequest = async (req, res) => { try { const request = await InternshipRequest.findByIdAndUpdate( @@ -84,7 +101,9 @@ exports.coordinatorApproveRequest = async (req, res) => { { new: true } ); - if (!request) return res.status(404).json({ message: "Request not found" }); + if (!request) { + return res.status(404).json({ message: "Request not found" }); + } await EmailService.sendEmail({ to: request.student.email, @@ -94,10 +113,11 @@ exports.coordinatorApproveRequest = async (req, res) => { res.json({ message: "Request Approved Successfully" }); } catch (err) { - res.status(500).json({ message: "Approval failed" }); + res.status(500).json({ message: "Approval failed", error: err.message }); } }; +// Coordinator Reject Request exports.coordinatorRejectRequest = async (req, res) => { const { reason } = req.body; if (!reason) return res.status(400).json({ message: "Reason required" }); @@ -109,7 +129,9 @@ exports.coordinatorRejectRequest = async (req, res) => { { new: true } ); - if (!request) return res.status(404).json({ message: "Request not found" }); + if (!request) { + return res.status(404).json({ message: "Request not found" }); + } await EmailService.sendEmail({ to: request.student.email, @@ -119,6 +141,6 @@ exports.coordinatorRejectRequest = async (req, res) => { res.json({ message: "Request Rejected Successfully" }); } catch (err) { - res.status(500).json({ message: "Rejection failed" }); + res.status(500).json({ message: "Rejection failed", error: err.message }); } }; diff --git a/server/models/InternshipRequest.js b/server/models/InternshipRequest.js index 3732f04d..91fb8936 100644 --- a/server/models/InternshipRequest.js +++ b/server/models/InternshipRequest.js @@ -2,71 +2,89 @@ const mongoose = require("mongoose"); // why are we commonjs const ObjectId = mongoose.Schema.Types.ObjectId; const Task = new mongoose.Schema({ - _id: false, - description: { - type: String, - required: true - }, - outcomes: { - type: [String], - enum: ['problemSolving','solutionDevelopment', 'communication', 'decisionMaking', 'collaboration', 'application'] - } + _id: false, + description: { + type: String, + required: true, + }, + outcomes: { + type: [String], + enum: [ + "problemSolving", + "solutionDevelopment", + "communication", + "decisionMaking", + "collaboration", + "application", + ], + }, }); -const formA1 = new mongoose.Schema({ - student: { // get student's name, email, id from User - type: ObjectId, - required: true, - ref: 'User' +const formA1 = new mongoose.Schema( + { + student: { + // get student's name, email, id from User + type: ObjectId, + required: true, + ref: "User", }, workplace: { - name: { - type: String, - required: true, - }, - website: String, - phone: String, // TODO how to validate this? + name: { + type: String, + required: true, + }, + website: String, + phone: String, // TODO how to validate this? }, internshipAdvisor: { - name: String, - jobTitle: String, - email: { - type: String, - required: true - } + name: String, + jobTitle: String, + email: { + type: String, + required: true, + }, }, creditHours: { - type: Number, - required: true, - enum: [1, 2, 3] + type: Number, + required: true, + enum: [1, 2, 3], }, startDate: { - type: Date, - required: true + type: Date, + required: true, }, - endDate: { // TODO how to make sure endDate is later than startDate? - type: Date, - required: true + endDate: { + // TODO how to make sure endDate is later than startDate? + type: Date, + required: true, }, tasks: { - type: [Task], - required: true + type: [Task], + required: true, }, status: { - type: String, - required: true, - enum: ['draft', 'submitted','pending manual review' ,'approved'] + type: String, + required: true, + enum: ["draft", "submitted", "pending manual review", "approved"], + }, + supervisor_status: { + type: String, + }, + supervisor_comment: { + type: String, }, approvals: { - type: [String], - enum: ['advisor', 'coordinator'] + type: [String], + enum: ["advisor", "coordinator"], }, reminders: [Date], // requiredHours is an easily derived attribute // TODO needs to be a virtual getter that checks this student's WeeklyReports - completedHours: Number -}, { timestamps: true }); -formA1.virtual("requiredHours").get(function() { - return this.creditHours * 60; -}) + completedHours: Number, + }, + { timestamps: true } +); +formA1.virtual("requiredHours").get(function () { + return this.creditHours * 60; +}); -module.exports = mongoose.model("InternshipRequest", formA1); \ No newline at end of file +module.exports = mongoose.model("InternshipRequest", formA1); diff --git a/server/routes/formRoutes.js b/server/routes/formRoutes.js index c80f3ebe..3ed75f52 100644 --- a/server/routes/formRoutes.js +++ b/server/routes/formRoutes.js @@ -1,72 +1,81 @@ -const express = require('express'); +const express = require("express"); const router = express.Router(); -const { insertFormData } = require('../services/insertData'); +const InternshipRequest = require("../models/InternshipRequest"); +const { insertFormData } = require("../services/insertData"); +const { + getPendingSubmissions, + approveSubmission, + rejectSubmission +} = require("../controllers/approvalController"); -let status = ''; +router.post("/internshiprequests/:id/approve", approveSubmission); +router.post("/internshiprequests/:id/reject", rejectSubmission); -// Validate required fields +// GET route to fetch internship requests without supervisor_comment and supervisor_status +router.get("/internshiprequests", async (req, res) => { + try { + const requests = await InternshipRequest.find({ + status: "submitted", + approvals: { $all: ["advisor", "coordinator"] }, + supervisor_comment: { $exists: false }, + supervisor_status: { $exists: false } + }).sort({ createdAt: -1 }); + + res.status(200).json(requests); + } catch (err) { + console.error("Error fetching internship requests:", err); + res.status(500).json({ message: "Server error while fetching internship requests" }); + } +}); + + +// Validate and submit form function validateFormData(formData) { const requiredFields = [ - 'workplaceName', - 'website', - 'phone', - 'advisorName', - 'advisorJobTitle', - 'advisorEmail', - 'creditHours', - 'startDate', - 'endDate', - 'tasks' + "workplaceName", + "website", + "phone", + "advisorName", + "advisorJobTitle", + "advisorEmail", + "creditHours", + "startDate", + "endDate", + "tasks" ]; for (const field of requiredFields) { - if (!formData[field] || formData[field] === '') { + if (!formData[field] || formData[field] === "") { return `Missing or empty required field: ${field}`; } } if (!Array.isArray(formData.tasks) || formData.tasks.length === 0) { - return 'Tasks must be a non-empty array'; + return "Tasks must be a non-empty array"; } - // for (const [index, task] of formData.tasks.entries()) { - // if (!task.description || !task.outcomes) { - // return `Task at index ${index} is missing description or outcomes`; - // } - // } - - // uncomment below if student has to fill in task outcomes - // const filledTasks = formData.tasks.filter((task) => task.description && task.outcomes ); - // if (filledTasks.length < 3) - // return `At least 3 tasks must have description and outcomes; only ${filledTasks.length} do`; - const tasks = formData.tasks; - console.log(tasks); - if (tasks.filter((task) => task.description).length < 3) - return 'At least 3 tasks must be provided'; - const uniqueOutcomes = new Set(); - tasks.forEach((task) => { - if (Array.isArray(task.outcomes)) { - task.outcomes.forEach(outcome => uniqueOutcomes.add(outcome)); - } + const outcomes = new Set(); + formData.tasks.forEach((task) => { + task.outcomes?.forEach(o => outcomes.add(o)); }); - formData.status = uniqueOutcomes.size < 3 ? 'pending manual review' : 'submitted'; + + formData.status = outcomes.size < 3 ? "pending manual review" : "submitted"; return null; } -router.post('/submit', async (req, res) => { +router.post("/submit", async (req, res) => { const formData = req.body; - const validationError = validateFormData(formData); - if (validationError) { - return res.status(400).json({ message: validationError }); - } + const error = validateFormData(formData); + if (error) return res.status(400).json({ message: error }); try { await insertFormData(formData); - res.status(200).json({ message: 'Form received and handled!', status, manual: formData.status !== 'submitted'}); + res.status(200).json({ message: "Form received and stored." }); } catch (error) { - console.error('Error handling form data:', error); - res.status(500).json({ message: 'Something went wrong' }); + console.error("Insert error:", error); + res.status(500).json({ message: "Something went wrong" }); } }); + module.exports = router;