From 209c413d9b321864f91d82d6124b9e897ed84358 Mon Sep 17 00:00:00 2001 From: Jayman Date: Mon, 28 Apr 2025 00:16:05 -0500 Subject: [PATCH] Implemented A2 to A3 route --- client/src/pages/A1InternshipRequestForm.js | 470 +++++++++++------- client/src/pages/ProtectedRouteA3.jsx | 47 ++ client/src/pages/StudentDashboard.jsx | 93 +++- client/src/pages/WeeklyProgressReportForm.js | 140 +++++- client/src/router.js | 9 +- .../src/styles/WeeklyProgressReportForm.css | 21 +- .../internshipRequestController.js | 22 +- server/controllers/reportController.js | 192 +++++-- server/jobs/registerCronJobs.test.js | 32 +- server/models/Evaluation.js | 123 +++-- server/models/InternshipRequest.js | 109 ++-- server/routes/weeklyReportRoutes.js | 16 +- server/services/insertData.js | 39 +- 13 files changed, 906 insertions(+), 407 deletions(-) create mode 100644 client/src/pages/ProtectedRouteA3.jsx diff --git a/client/src/pages/A1InternshipRequestForm.js b/client/src/pages/A1InternshipRequestForm.js index 009a0979..5b9dbb6d 100644 --- a/client/src/pages/A1InternshipRequestForm.js +++ b/client/src/pages/A1InternshipRequestForm.js @@ -1,7 +1,6 @@ import React, { useState, useEffect } from "react"; import "../styles/A1InternshipRequestForm.css"; - const outcomeLabels = [ "Problem Solving", "Solution Development", @@ -26,7 +25,7 @@ const signatureFonts = [ { name: "Great Vibes", class: "font-great-vibes" }, { name: "Pacifico", class: "font-pacifico" }, { name: "Satisfy", class: "font-satisfy" }, - { name: "Caveat", class: "font-caveat" } + { name: "Caveat", class: "font-caveat" }, ]; // Signature Font Picker Component @@ -62,9 +61,11 @@ const SignatureInput = ({ id, value, onChange, disabled, placeholder }) => { /> {showFonts && nameInput && (
-
Select a signature style:
+
+ Select a signature style: +
{signatureFonts.map((font) => ( -
selectFont(font.class)} @@ -77,11 +78,7 @@ const SignatureInput = ({ id, value, onChange, disabled, placeholder }) => { {nameInput && (
{nameInput} - +
)}
@@ -118,11 +115,18 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { const isFieldEditable = (fieldType) => { switch (userRole) { case "student": - return !["advisorSignature", "coordinatorApproval", "supervisorComments", "coordinatorComments"].includes(fieldType); + return ![ + "advisorSignature", + "coordinatorApproval", + "supervisorComments", + "coordinatorComments", + ].includes(fieldType); case "supervisor": return ["advisor", "supervisorComments"].includes(fieldType); case "coordinator": - return ["coordinator", "coordinatorComments", "advisor"].includes(fieldType); + return ["coordinator", "coordinatorComments", "advisor"].includes( + fieldType + ); default: return true; } @@ -141,7 +145,7 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { } } if (errors[id]) { - setErrors(prev => { + setErrors((prev) => { const newErrors = { ...prev }; delete newErrors[id]; return newErrors; @@ -168,7 +172,9 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { useEffect(() => { const timeout = setTimeout(() => { - const descriptions = formData.tasks.map((task) => task.description.trim()).filter(Boolean); + const descriptions = formData.tasks + .map((task) => task.description.trim()) + .filter(Boolean); if (descriptions.length > 0) { fetch(`${process.env.REACT_APP_API_URL}/api/align-outcomes`, { method: "POST", @@ -178,8 +184,12 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { .then((res) => res.json()) .then((data) => { const updatedTasks = formData.tasks.map((task) => { - const match = data.results.find((r) => r.task === task.description); - return match ? { ...task, outcomes: match.matched_outcomes } : { ...task, outcomes: [] }; + const match = data.results.find( + (r) => r.task === task.description + ); + return match + ? { ...task, outcomes: match.matched_outcomes } + : { ...task, outcomes: [] }; }); setFormData((prev) => ({ ...prev, tasks: updatedTasks })); }) @@ -190,10 +200,14 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { }, [formData.tasks]); const renderOutcomeCell = (task, outcome, key) => { - const normalizedOutcome = outcome.charAt(0).toLowerCase() + outcome.replace(/\s+/g, "").slice(1); + const normalizedOutcome = + outcome.charAt(0).toLowerCase() + outcome.replace(/\s+/g, "").slice(1); const isMatched = task.outcomes.includes(normalizedOutcome); return ( - + { const emailPattern = /^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$/; const newErrors = {}; - if (!formData.interneeName) newErrors.interneeName = "Internee name is required"; - else if (!namePattern.test(formData.interneeName)) newErrors.interneeName = "Name should contain only letters and spaces"; + if (!formData.interneeName) + newErrors.interneeName = "Internee name is required"; + 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 (!numberPattern.test(formData.soonerId)) + newErrors.soonerId = "Sooner ID should be numeric"; if (!formData.interneeEmail) newErrors.interneeEmail = "Email is required"; - else if (!emailPattern.test(formData.interneeEmail)) newErrors.interneeEmail = "Invalid email format"; - if (!formData.workplaceName) newErrors.workplaceName = "Workplace name is required"; - else if (!namePattern.test(formData.workplaceName)) newErrors.workplaceName = "Workplace name should contain only letters and spaces"; - if (formData.website && !formData.website.includes('.')) newErrors.website = "Please enter a valid website address"; + else if (!emailPattern.test(formData.interneeEmail)) + newErrors.interneeEmail = "Invalid email format"; + if (!formData.workplaceName) + newErrors.workplaceName = "Workplace name is required"; + else if (!namePattern.test(formData.workplaceName)) + newErrors.workplaceName = + "Workplace name should contain only letters and spaces"; + if (formData.website && !formData.website.includes(".")) + newErrors.website = "Please enter a valid website address"; if (!formData.phone) newErrors.phone = "Phone is required"; - else if (!phonePattern.test(formData.phone)) newErrors.phone = "Phone must be 10 digits"; + else if (!phonePattern.test(formData.phone)) + newErrors.phone = "Phone must be 10 digits"; if (!formData.startDate) newErrors.startDate = "Start date is required"; if (!formData.endDate) newErrors.endDate = "End date is required"; - if (!formData.advisorName) newErrors.advisorName = "Supervisor name is required"; - else if (!namePattern.test(formData.advisorName)) newErrors.advisorName = "Supervisor name should contain only letters and spaces"; - if (!formData.advisorEmail) newErrors.advisorEmail = "Supervisor email is required"; - else if (!emailPattern.test(formData.advisorEmail)) newErrors.advisorEmail = "Invalid supervisor email format"; - if (!formData.interneeSignature) newErrors.interneeSignature = "Internee signature is required"; - else if (!namePattern.test(formData.interneeSignature)) newErrors.interneeSignature = "Signature should contain only letters and spaces"; - if (formData.advisorSignature && !namePattern.test(formData.advisorSignature)) { - newErrors.advisorSignature = "Signature should contain only letters and spaces"; + if (!formData.advisorName) + newErrors.advisorName = "Supervisor name is required"; + else if (!namePattern.test(formData.advisorName)) + newErrors.advisorName = + "Supervisor name should contain only letters and spaces"; + if (!formData.advisorEmail) + newErrors.advisorEmail = "Supervisor email is required"; + else if (!emailPattern.test(formData.advisorEmail)) + newErrors.advisorEmail = "Invalid supervisor email format"; + if (!formData.interneeSignature) + newErrors.interneeSignature = "Internee signature is required"; + else if (!namePattern.test(formData.interneeSignature)) + newErrors.interneeSignature = + "Signature should contain only letters and spaces"; + if ( + formData.advisorSignature && + !namePattern.test(formData.advisorSignature) + ) { + newErrors.advisorSignature = + "Signature should contain only letters and spaces"; } - if (formData.coordinatorApproval && !namePattern.test(formData.coordinatorApproval)) { - newErrors.coordinatorApproval = "Approval should contain only letters and spaces"; + if ( + formData.coordinatorApproval && + !namePattern.test(formData.coordinatorApproval) + ) { + newErrors.coordinatorApproval = + "Approval should contain only letters and spaces"; } - if (!formData.creditHours) newErrors.creditHours = "Please select credit hours"; - const tasksFilled = formData.tasks.filter((task) => task.description.trim() !== "").length >= 3; + if (!formData.creditHours) + newErrors.creditHours = "Please select credit hours"; + const tasksFilled = + formData.tasks.filter((task) => task.description.trim() !== "").length >= + 3; if (!tasksFilled) newErrors.tasks = "At least 3 tasks are required"; setErrors(newErrors); @@ -252,11 +294,14 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { const submitFormData = async () => { try { - const response = await fetch(`${process.env.REACT_APP_API_URL}/api/form/submit`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(formData), - }); + const response = await fetch( + `${process.env.REACT_APP_API_URL}/api/form/submit`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData), + } + ); if (!response.ok) throw new Error("Failed to submit form"); const data = await response.json(); return data; @@ -268,11 +313,14 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { const sendTaskDescriptions = async (descriptions) => { try { - const response = await fetch(`${process.env.REACT_APP_API_URL}/api/align-outcomes`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ tasks: descriptions }), - }); + const response = await fetch( + `${process.env.REACT_APP_API_URL}/api/align-outcomes`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ tasks: descriptions }), + } + ); if (!response.ok) throw new Error("Failed to send task descriptions"); const data = await response.json(); return data.results.map(({ task, matched_outcomes }) => ({ @@ -288,13 +336,17 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { const handleSubmit = async (e) => { e.preventDefault(); if (!validateForm()) return; - const taskDescriptions = formData.tasks.map((task) => task.description.trim()).filter(Boolean); + const taskDescriptions = formData.tasks + .map((task) => task.description.trim()) + .filter(Boolean); try { const aligned = await sendTaskDescriptions(taskDescriptions); if (aligned && aligned.length > 0) { setFormData((prev) => ({ ...prev, tasks: aligned })); const submissionResponse = await submitFormData(); - const recipient = submissionResponse.manual ? "coordinator for manual review!" : "supervisor!"; + const recipient = submissionResponse.manual + ? "coordinator for manual review!" + : "supervisor!"; setSuccessMsg(`Form submitted successfully and sent to ${recipient}`); setTimeout(() => setSuccessMsg(""), 15000); setFormData(initialState); @@ -308,22 +360,22 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { useEffect(() => { const fonts = [ - 'https://fonts.googleapis.com/css2?family=Dancing+Script:wght@500&display=swap', - 'https://fonts.googleapis.com/css2?family=Great+Vibes&display=swap', - 'https://fonts.googleapis.com/css2?family=Pacifico&display=swap', - 'https://fonts.googleapis.com/css2?family=Satisfy&display=swap', - 'https://fonts.googleapis.com/css2?family=Caveat:wght@500&display=swap' + "https://fonts.googleapis.com/css2?family=Dancing+Script:wght@500&display=swap", + "https://fonts.googleapis.com/css2?family=Great+Vibes&display=swap", + "https://fonts.googleapis.com/css2?family=Pacifico&display=swap", + "https://fonts.googleapis.com/css2?family=Satisfy&display=swap", + "https://fonts.googleapis.com/css2?family=Caveat:wght@500&display=swap", ]; const links = []; - fonts.forEach(font => { - const link = document.createElement('link'); + fonts.forEach((font) => { + const link = document.createElement("link"); link.href = font; - link.rel = 'stylesheet'; + link.rel = "stylesheet"; document.head.appendChild(link); links.push(link); }); return () => { - links.forEach(link => document.head.removeChild(link)); + links.forEach((link) => document.head.removeChild(link)); }; }, []); @@ -344,113 +396,147 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { Name*:
- - {errors.interneeName &&
{errors.interneeName}
} + {errors.interneeName && ( +
+ {errors.interneeName} +
+ )} Name*:
- - {errors.workplaceName &&
{errors.workplaceName}
} + {errors.workplaceName && ( +
+ {errors.workplaceName} +
+ )} Name*:
- - {errors.advisorName &&
{errors.advisorName}
} + {errors.advisorName && ( +
+ {errors.advisorName} +
+ )} Sooner ID*:
- - {errors.soonerId &&
{errors.soonerId}
} + {errors.soonerId && ( +
+ {errors.soonerId} +
+ )} - Website:
- + - {errors.website &&
{errors.website}
} + {errors.website && ( +
+ {errors.website} +
+ )} - Job Title:
- + Email*:
- - {errors.interneeEmail &&
{errors.interneeEmail}
} + {errors.interneeEmail && ( +
+ {errors.interneeEmail} +
+ )} Phone*:
- - {errors.phone &&
{errors.phone}
} + {errors.phone && ( +
+ {errors.phone} +
+ )} Email*:
- - {errors.advisorEmail &&
{errors.advisorEmail}
} + {errors.advisorEmail && ( +
+ {errors.advisorEmail} +
+ )} Credit Hours*:
- - {errors.creditHours &&
{errors.creditHours}
} + {errors.creditHours && ( +
+ {errors.creditHours} +
+ )} -
-
- -
- {errors.startDate &&
{errors.startDate}
} - - - -
-
- -
- {dateError &&
{dateError}
} - {errors.endDate &&
{errors.endDate}
} - + +
+
+ +
+ {errors.startDate && ( +
{errors.startDate}
+ )} + + + +
+
+ +
+ {dateError &&
{dateError}
} + {errors.endDate && ( +
{errors.endDate}
+ )} + -

Task Details & Program Outcomes*

+

+ Task Details & Program Outcomes + * +

Job Description Details: @@ -515,7 +613,8 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { Task {outcomeLabels.map((label, j) => ( - {label}
+ {label} +
({outcomeDescriptions[j]}) ))} @@ -530,16 +629,26 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { placeholder={`Task ${i + 1}`} value={task.description} onChange={(e) => handleTaskChange(i, e.target.value)} - style={{ width: "100%", padding: "4px", boxSizing: "border-box" }} + style={{ + width: "100%", + padding: "4px", + boxSizing: "border-box", + }} disabled={!isFieldEditable("task")} /> - {outcomeLabels.map((label, j) => renderOutcomeCell(task, label, `${i}-${j}`))} + {outcomeLabels.map((label, j) => + renderOutcomeCell(task, label, `${i}-${j}`) + )} ))} - {errors.tasks &&
{errors.tasks}
} + {errors.tasks && ( +
+ {errors.tasks} +
+ )}

Signatures:

@@ -547,9 +656,10 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { - Internee Signature*:
+ Internee Signature*: +
- { placeholder="Enter your full name" />
- {errors.interneeSignature &&
{errors.interneeSignature}
} + {errors.interneeSignature && ( +
+ {errors.interneeSignature} +
+ )} - Internship Supervisor Signature:
+ Internship Supervisor Signature: +
- { placeholder="Enter your full name" />
- {errors.advisorSignature &&
{errors.advisorSignature}
} + {errors.advisorSignature && ( +
+ {errors.advisorSignature} +
+ )} - Internship Coordinator Approval:
+ Internship Coordinator Approval: +
- { placeholder="Enter your full name" />
- {errors.coordinatorApproval &&
{errors.coordinatorApproval}
} + {errors.coordinatorApproval && ( +
+ {errors.coordinatorApproval} +
+ )} {/* @@ -642,4 +766,4 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { ); }; -export default A1InternshipRequestForm; \ No newline at end of file +export default A1InternshipRequestForm; diff --git a/client/src/pages/ProtectedRouteA3.jsx b/client/src/pages/ProtectedRouteA3.jsx new file mode 100644 index 00000000..024dff3e --- /dev/null +++ b/client/src/pages/ProtectedRouteA3.jsx @@ -0,0 +1,47 @@ +import React, { useEffect, useState } from "react"; +import { Navigate } from "react-router-dom"; + +const ProtectedRouteA3 = ({ children }) => { + const user = JSON.parse(localStorage.getItem("ipmsUser")); + const [eligible, setEligible] = useState(null); // null = loading + const [loading, setLoading] = useState(true); + + useEffect(() => { + const checkEligibility = async () => { + if (!user?.email) { + setEligible(false); + setLoading(false); + return; + } + + try { + const response = await fetch( + `${process.env.REACT_APP_API_URL}/api/reports/A3-eligibility`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email: user.email }), + } + ); + const data = await response.json(); + setEligible(data.eligibleForA3); + } catch (err) { + setEligible(false); + } + setLoading(false); + }; + + checkEligibility(); + }, [user]); + + if (loading) return
Checking eligibility...
; + + if (!eligible) { + // Not eligible, redirecting to student dashboard + return ; + } + + return children; +}; + +export default ProtectedRouteA3; diff --git a/client/src/pages/StudentDashboard.jsx b/client/src/pages/StudentDashboard.jsx index 276bbb65..f7907d09 100644 --- a/client/src/pages/StudentDashboard.jsx +++ b/client/src/pages/StudentDashboard.jsx @@ -8,17 +8,26 @@ const StudentDashboard = () => { const user = JSON.parse(localStorage.getItem("ipmsUser")); const ouEmail = user?.email; const [approvalStatus, setApprovalStatus] = useState("not_submitted"); + const [a3Eligibility, setA3Eligibility] = useState({ + checked: false, + eligible: false, + completedHours: 0, + requiredHours: 0, + }); useEffect(() => { const fetchData = async () => { try { - const res = await fetch(`${process.env.REACT_APP_API_URL}/api/student`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ ouEmail }), - }); + const res = await fetch( + `${process.env.REACT_APP_API_URL}/api/student`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ouEmail }), + } + ); const data = await res.json(); setApprovalStatus(data.approvalStatus); @@ -33,6 +42,38 @@ const StudentDashboard = () => { }, [ouEmail]); console.log(approvalStatus); + useEffect(() => { + const checkA3Eligibility = async () => { + if (!ouEmail) return; + try { + const res = await fetch( + `${process.env.REACT_APP_API_URL}/api/reports/A3-eligibility`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email: ouEmail }), + } + ); + const data = await res.json(); + setA3Eligibility({ + checked: true, + eligible: data.eligibleForA3, + completedHours: data.completedHours, + requiredHours: data.requiredHours, + }); + } catch (err) { + setA3Eligibility({ + checked: true, + eligible: false, + completedHours: 0, + requiredHours: 0, + }); + console.error("Error checking A3 eligibility", err); + } + }; + checkA3Eligibility(); + }, [ouEmail]); + return (
@@ -133,6 +174,44 @@ const StudentDashboard = () => { Request
+ + {/* ------ FORM A3 Card ------ */} +
+
+

Final Evaluation (Form A3)

+ {!a3Eligibility.checked ? ( +

+ Checking eligibility... +

+ ) : a3Eligibility.eligible ? ( +

+ You have completed {a3Eligibility.completedHours} of{" "} + {a3Eligibility.requiredHours} hours. Eligible for final + evaluation. +

+ ) : ( +

+ You have completed {a3Eligibility.completedHours} of{" "} + {a3Eligibility.requiredHours} hours. +
+ Complete all required hours to unlock A3 Final Evaluation. +

+ )} +
+ +
); diff --git a/client/src/pages/WeeklyProgressReportForm.js b/client/src/pages/WeeklyProgressReportForm.js index b2bee0d9..2d38c7b1 100644 --- a/client/src/pages/WeeklyProgressReportForm.js +++ b/client/src/pages/WeeklyProgressReportForm.js @@ -44,14 +44,16 @@ const WeeklyProgressReportForm = ({ role = "student", readOnly = false }) => { console.error("Failed to load report", err); }); } - }, [readOnly, reportId]); + }, [readOnly, reportId]); // Auto-fill A1 data useEffect(() => { const fetchA1Data = async () => { try { - const email = "vikash@example.com"; // TODO: replace with real session email - const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/reports/a1/${email}`); + const email = "Jayman.B.Kalathiya-1@ou.edu"; // TODO: replace with real session email + const res = await axios.get( + `${process.env.REACT_APP_API_URL}/api/reports/a1/${email}` + ); if (res.data.success) { const { @@ -72,12 +74,15 @@ const WeeklyProgressReportForm = ({ role = "student", readOnly = false }) => { supervisorEmail, creditHours, completedHours, - requiredHours: requiredHours || (creditHours ? creditHours * 60 : 0), + requiredHours: + requiredHours || (creditHours ? creditHours * 60 : 0), })); } } catch (err) { console.error("A1 form not found or failed to fetch."); - setMessage("⚠️ You must submit the A1 form before submitting weekly reports."); + setMessage( + "⚠️ You must submit the A1 form before submitting weekly reports." + ); } }; @@ -87,12 +92,14 @@ const WeeklyProgressReportForm = ({ role = "student", readOnly = false }) => { const handleChange = (e) => { const { name, value } = e.target; - if (readOnly && !(role === "coordinator" && name === "coordinatorComments")) return; + if (readOnly && !(role === "coordinator" && name === "coordinatorComments")) + return; if (name === "hours") { const num = parseInt(value); if (num > 40) return setFormData((prev) => ({ ...prev, hours: 40 })); - if (num < 1 && value !== "") return setFormData((prev) => ({ ...prev, hours: 1 })); + if (num < 1 && value !== "") + return setFormData((prev) => ({ ...prev, hours: 1 })); } setFormData((prev) => ({ ...prev, [name]: value })); @@ -100,7 +107,16 @@ const WeeklyProgressReportForm = ({ role = "student", readOnly = false }) => { const handleSubmit = async (e) => { e.preventDefault(); - const { week, hours, tasks, lessons, name, email, supervisorName, supervisorEmail } = formData; + const { + week, + hours, + tasks, + lessons, + name, + email, + supervisorName, + supervisorEmail, + } = formData; if (!name || !email || !supervisorName || !supervisorEmail) { return setMessage("Please complete the A1 form first."); @@ -111,7 +127,10 @@ const WeeklyProgressReportForm = ({ role = "student", readOnly = false }) => { } try { - const res = await axios.post(`${process.env.REACT_APP_API_URL}/api/reports`, formData); + const res = await axios.post( + `${process.env.REACT_APP_API_URL}/api/reports`, + formData + ); setMessage(res.data.message || "Report submitted successfully!"); setFormData({ name: "", @@ -151,37 +170,79 @@ const WeeklyProgressReportForm = ({ role = "student", readOnly = false }) => { return (
-

Weekly Progress Report

+

Weekly Progress Report

-
{/* Identity Fields */} - {["name", "email", "supervisorName", "supervisorEmail", "coordinatorName", "coordinatorEmail"].map((field) => ( + {[ + "name", + "email", + "supervisorName", + "supervisorEmail", + "coordinatorName", + "coordinatorEmail", + ].map((field) => (
- - + +
))} {/* Progress Display */} {!readOnly && (
-

Credit Hours: {formData.creditHours || "--"}

-

Required Hours: {formData.requiredHours || "--"}

-

Completed Hours: {formData.completedHours || "--"}

+

+ Credit Hours: {formData.creditHours || "--"} +

+

+ Required Hours: {formData.requiredHours || "--"} +

+

+ Completed Hours:{" "} + {formData.completedHours || "--"} +

{formData.requiredHours && ( <>

Progress:{" "} - {Math.min(100, Math.round((formData.completedHours / formData.requiredHours) * 100))}% + {Math.min( + 100, + Math.round( + (formData.completedHours / formData.requiredHours) * 100 + ) + )} + %

@@ -195,10 +256,18 @@ const WeeklyProgressReportForm = ({ role = "student", readOnly = false }) => {
- {Array.from({ length: 15 }, (_, i) => ( - + ))}
@@ -230,7 +299,9 @@ const WeeklyProgressReportForm = ({ role = "student", readOnly = false }) => { required readOnly={readOnly} /> - +
{formData[field].length}/300
))} @@ -243,16 +314,27 @@ const WeeklyProgressReportForm = ({ role = "student", readOnly = false }) => { value={formData[field]} onChange={handleChange} placeholder=" " - readOnly={field === "supervisorComments" || !(readOnly && role === "coordinator")} + readOnly={ + field === "supervisorComments" || + !(readOnly && role === "coordinator") + } /> - +
{formData[field].length}/300
))} {/* Buttons */} {readOnly && role === "coordinator" && ( - )} @@ -264,9 +346,13 @@ const WeeklyProgressReportForm = ({ role = "student", readOnly = false }) => { )}
- {message &&

{message}

} + {message && ( +

+ {message} +

+ )}
); }; -export default WeeklyProgressReportForm; \ No newline at end of file +export default WeeklyProgressReportForm; diff --git a/client/src/router.js b/client/src/router.js index 2dba3de2..495f7ddb 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -19,6 +19,7 @@ import CoordinatorRequestDetailView from "./pages/CoordinatorRequestDetailView"; import TokenRenewal from "./pages/TokenRenewal"; import StudentDashboard from "./pages/StudentDashboard"; import ProtectedRouteStudent from "./pages/ProtectedRouteStudent"; +import ProtectedRouteA3 from "./pages/ProtectedRouteA3"; import WeeklyFourWeekReportForm from "./pages/WeeklyFourWeekReportForm"; import SubmittedReports from "./pages/SubmittedReports"; import CumulativeReviewForm from "./pages/CumulativeReviewForm"; @@ -58,7 +59,11 @@ const router = createBrowserRouter([ }, { path: "evaluation", - element: , + element: ( + + + + ), }, { path: "activate/:token", @@ -116,4 +121,4 @@ const router = createBrowserRouter([ }, ]); -export default router; \ No newline at end of file +export default router; diff --git a/client/src/styles/WeeklyProgressReportForm.css b/client/src/styles/WeeklyProgressReportForm.css index a9e6ff2a..e47b6a36 100644 --- a/client/src/styles/WeeklyProgressReportForm.css +++ b/client/src/styles/WeeklyProgressReportForm.css @@ -1,4 +1,3 @@ - .a2-form-container { max-width: 700px; margin: 3rem auto; @@ -20,7 +19,7 @@ } } -h2 { +.a2-form-title { text-align: center; color: #263238; margin-bottom: 1.5rem; @@ -44,7 +43,9 @@ h2 { margin-bottom: 0.5rem; } -input, select, textarea { +input, +select, +textarea { width: 100%; padding: 10px; border-radius: 6px; @@ -55,11 +56,15 @@ input, select, textarea { max-width: 100%; } -input:hover, select:hover, textarea:hover { +input:hover, +select:hover, +textarea:hover { box-shadow: 0 0 8px rgba(155, 17, 30, 0.08); } -input:focus, select:focus, textarea:focus { +input:focus, +select:focus, +textarea:focus { border-color: #9b111e; box-shadow: 0 0 10px rgba(155, 17, 30, 0.2); transform: scale(1.02); @@ -307,8 +312,8 @@ textarea[readonly]:hover { .progress-info:hover { cursor: pointer; } -.form-group:hover,.progress-info:hover - { +.form-group:hover, +.progress-info:hover { box-shadow: 0 0 16px rgba(155, 17, 30, 0.25); /* stronger glow */ background-color: #fffdfd; transform: scale(1.03); @@ -320,7 +325,7 @@ textarea[readonly]:hover { border-left: 5px solid #9b111e; border-radius: 10px; transition: all 0.3s ease; - box-shadow: 0 2px 6px rgba(0,0,0,0.06); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06); } .progress-info:hover { box-shadow: 0 0 15px rgba(155, 17, 30, 0.2); diff --git a/server/controllers/internshipRequestController.js b/server/controllers/internshipRequestController.js index e03c0c07..9910627c 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) => { @@ -6,12 +6,11 @@ exports.getA1ByEmail = async (req, res) => { const { email } = req.params; // Find A1 form by matching the student's email via the populated 'student' reference - const form = await InternshipRequest.findOne() - .populate({ - path: "student", - match: { email }, // only match where user's email matches - select: "name email" - }); + const form = await InternshipRequest.findOne().populate({ + path: "student", + match: { email }, // only match where user's email matches + select: "name email", + }); // If student wasn't matched via population or form doesn't exist if (!form || !form.student) { @@ -25,7 +24,10 @@ exports.getA1ByEmail = async (req, res) => { const reports = await WeeklyReport.find({ email }); // Sum up the completed hours - const completedHours = reports.reduce((sum, report) => sum + (report.hours || 0), 0); + const completedHours = reports.reduce( + (sum, report) => sum + (report.hours || 0), + 0 + ); // Calculate required hours const creditHours = form.creditHours || 0; @@ -40,8 +42,8 @@ exports.getA1ByEmail = async (req, res) => { supervisorEmail: form.internshipAdvisor?.email || "", creditHours, completedHours, - requiredHours - } + requiredHours, + }, }); } catch (err) { console.error("Error fetching A1 form:", err); diff --git a/server/controllers/reportController.js b/server/controllers/reportController.js index c5179546..4683b0a0 100644 --- a/server/controllers/reportController.js +++ b/server/controllers/reportController.js @@ -1,18 +1,53 @@ 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"; +// Calculating completed and required hours for student +async function getStudentHours(email) { + const emailLower = email.toLowerCase(); + + // Getting all weekly reports by student email + const reports = await WeeklyReport.find({ + email: { $regex: new RegExp(`^${emailLower}$`, "i") }, // i = case-insensitive + }); + + const completedHours = reports.reduce((sum, r) => sum + (r.hours || 0), 0); + + // Finding Internship Request (A1) for required hours + const internshipForm = await InternshipRequest.findOne({ + "student.email": { $regex: new RegExp(`^${email}$`, "i") }, + }); + + const requiredHours = internshipForm + ? (internshipForm.creditHours || 0) * 60 + : 0; + return { completedHours, requiredHours }; +} const reportController = { createReport: async (req, res) => { try { - const { week, hours, tasks, lessons, name, email, supervisorName, supervisorEmail, coordinatorName, coordinatorEmail } = req.body; + const { + week, + hours, + tasks, + lessons, + name, + email, + supervisorName, + supervisorEmail, + coordinatorName, + coordinatorEmail, + } = req.body; if (!week || hours === undefined || isNaN(hours) || !tasks || !lessons) { - return res.status(400).json({ success: false, message: "All required fields must be valid." }); + return res.status(400).json({ + success: false, + message: "All required fields must be valid.", + }); } const newReport = new WeeklyReport({ @@ -34,10 +69,7 @@ const reportController = { await newReport.save(); const reports = await WeeklyReport.find({ email }).sort({ week: 1 }); - const completedHours = reports.reduce((sum, r) => sum + (r.hours || 0), 0); - - const internshipForm = await InternshipRequest.findOne({ email }); - const requiredHours = internshipForm ? internshipForm.creditHours * 60 : 0; + const { completedHours, requiredHours } = await getStudentHours(email); // Getting completed and required hours of the student await sendStudentProgressEmail({ name, @@ -49,24 +81,29 @@ const reportController = { return res.status(201).json({ success: true, message: "Weekly report submitted successfully.", - reports + reports, }); - } catch (error) { console.error("Error in createReport:", error); - return res.status(500).json({ success: false, message: "Internal server error." }); + return res + .status(500) + .json({ success: false, message: "Internal server error." }); } }, getReportsByStudent: async (req, res) => { try { const { userId } = req.params; - const reports = await WeeklyReport.find({ studentId: userId }).sort({ week: 1 }); + const reports = await WeeklyReport.find({ studentId: userId }).sort({ + week: 1, + }); return res.status(200).json({ success: true, reports }); } catch (error) { console.error("Error in getReportsByStudent:", error); - return res.status(500).json({ success: false, message: "Failed to fetch reports." }); + return res + .status(500) + .json({ success: false, message: "Failed to fetch reports." }); } }, @@ -90,16 +127,19 @@ const reportController = { })); return res.status(200).json({ success: true, reports: enrichedReports }); - } catch (error) { console.error("Error in getMyReports:", error); - return res.status(500).json({ success: false, message: "Failed to fetch your reports." }); + return res + .status(500) + .json({ success: false, message: "Failed to fetch your reports." }); } }, getCumulativeReports: async (req, res) => { try { - const reports = await WeeklyReport.find({ studentId: STATIC_USER_ID }).sort({ createdAt: 1 }); + const reports = await WeeklyReport.find({ + studentId: STATIC_USER_ID, + }).sort({ createdAt: 1 }); if (!reports.length) { return res.status(200).json({ success: true, cumulativeReports: [] }); @@ -119,16 +159,19 @@ const reportController = { groupedReports.push({ groupIndex, - weeks: reports.slice(i, i + 4).map(r => r.week), + weeks: reports.slice(i, i + 4).map((r) => r.week), reports: reports.slice(i, i + 4), }); } - return res.status(200).json({ success: true, cumulativeReports: groupedReports }); - + return res + .status(200) + .json({ success: true, cumulativeReports: groupedReports }); } catch (error) { console.error("Error in getCumulativeReports:", error); - return res.status(500).json({ success: false, message: "Internal server error." }); + return res + .status(500) + .json({ success: false, message: "Internal server error." }); } }, @@ -137,31 +180,38 @@ const reportController = { const { groupIndex } = req.params; const index = parseInt(groupIndex); - const reports = await WeeklyReport.find({ studentId: STATIC_USER_ID }).sort({ createdAt: 1 }); + const reports = await WeeklyReport.find({ + studentId: STATIC_USER_ID, + }).sort({ createdAt: 1 }); if (!reports.length) { - return res.status(404).json({ success: false, message: "No reports found." }); + return res + .status(404) + .json({ success: false, message: "No reports found." }); } const groupedReports = []; for (let i = 0; i < reports.length; i += 4) { groupedReports.push({ groupIndex: i / 4, - weeks: reports.slice(i, i + 4).map(r => r.week), + weeks: reports.slice(i, i + 4).map((r) => r.week), reports: reports.slice(i, i + 4), }); } const targetGroup = groupedReports[index]; if (!targetGroup) { - return res.status(404).json({ success: false, message: "Group not found." }); + return res + .status(404) + .json({ success: false, message: "Group not found." }); } return res.status(200).json({ success: true, group: targetGroup }); - } catch (error) { console.error("Error in getCumulativeGroup:", error); - return res.status(500).json({ success: false, message: "Internal server error." }); + return res + .status(500) + .json({ success: false, message: "Internal server error." }); } }, @@ -169,13 +219,17 @@ const reportController = { try { const report = await WeeklyReport.findById(req.params.id); if (!report) { - return res.status(404).json({ success: false, message: "Report not found" }); + return res + .status(404) + .json({ success: false, message: "Report not found" }); } return res.status(200).json({ success: true, report }); } catch (error) { console.error("Error in getReportById:", error); - return res.status(500).json({ success: false, message: "Failed to fetch report" }); + return res + .status(500) + .json({ success: false, message: "Failed to fetch report" }); } }, @@ -184,7 +238,9 @@ const reportController = { const { groupIndex, comments, weeks } = req.body; if (!comments || !weeks || weeks.length === 0) { - return res.status(400).json({ success: false, message: "Invalid comment data." }); + return res + .status(400) + .json({ success: false, message: "Invalid comment data." }); } const newReview = new SupervisorReview({ @@ -201,18 +257,22 @@ const reportController = { { $set: { supervisorComments: comments } } ); - return res.status(200).json({ success: true, message: "Supervisor comment submitted successfully." }); - + return res.status(200).json({ + success: true, + message: "Supervisor comment submitted successfully.", + }); } catch (error) { console.error("Error in submitSupervisorComments:", error); - return res.status(500).json({ success: false, message: "Failed to submit comment." }); + return res + .status(500) + .json({ success: false, message: "Failed to submit comment." }); } }, getSupervisorReviewedGroups: async (req, res) => { try { const supervisorReviews = await SupervisorReview.find({ - studentId: STATIC_USER_ID + studentId: STATIC_USER_ID, }); const reviewedGroups = []; @@ -220,7 +280,7 @@ const reportController = { for (const review of supervisorReviews) { const reports = await WeeklyReport.find({ studentId: STATIC_USER_ID, - week: { $in: review.weeks } + week: { $in: review.weeks }, }); const allCoordinatorCommentsPresent = reports.every( @@ -251,11 +311,16 @@ const reportController = { const { groupIndex, comments, weeks } = req.body; if (!comments || !weeks || weeks.length === 0) { - return res.status(400).json({ success: false, message: "Invalid comment data." }); + return res + .status(400) + .json({ success: false, message: "Invalid comment data." }); } const firstWeek = weeks[0]; - const firstReport = await WeeklyReport.findOne({ studentId: STATIC_USER_ID, week: firstWeek }); + const firstReport = await WeeklyReport.findOne({ + studentId: STATIC_USER_ID, + week: firstWeek, + }); const newReview = new CoordinatorReview({ studentId: STATIC_USER_ID, @@ -272,13 +337,60 @@ const reportController = { { $set: { coordinatorComments: comments } } ); - return res.status(200).json({ success: true, message: "Coordinator comment submitted successfully." }); - + return res.status(200).json({ + success: true, + message: "Coordinator comment submitted successfully.", + }); } catch (error) { console.error("Error in submitCoordinatorGroupComments:", error); - return res.status(500).json({ success: false, message: "Failed to submit comment." }); + return res + .status(500) + .json({ success: false, message: "Failed to submit comment." }); } - } + }, + + // Checking if student is eligible for A3 (Final Evaluation) + getStudentProgress: async (req, res) => { + try { + const { email } = req.body; + + if (!email) { + return res + .status(400) + .json({ success: false, message: "Student email is required." }); + } + + // Finding Internship Request (A1) for required hours + const internshipForm = await InternshipRequest.findOne({ + "student.email": { $regex: new RegExp(`^${email}$`, "i") }, + }); + + if (!internshipForm) { + return res + .status(404) + .json({ success: false, message: "Internship (A1) form not found." }); + } + + const { completedHours, requiredHours } = await getStudentHours( + email.toLowerCase() + ); + + // Determining A3 form access eligibility for the student + const eligibleForA3 = + completedHours >= requiredHours && requiredHours > 0; + + return res.json({ + success: true, + completedHours, + requiredHours, + eligibleForA3, + }); + } catch (error) { + return res + .status(500) + .json({ success: false, message: "Internal server error." }); + } + }, }; -module.exports = reportController; \ No newline at end of file +module.exports = reportController; diff --git a/server/jobs/registerCronJobs.test.js b/server/jobs/registerCronJobs.test.js index c9e5d9a1..f13f80a3 100644 --- a/server/jobs/registerCronJobs.test.js +++ b/server/jobs/registerCronJobs.test.js @@ -21,13 +21,13 @@ describe("registerCronJobs", () => { timezone: "Asia/Kolkata", }, }, - supervisorApprovalReminder: { - schedule: "0 8 * * *", - job: jest.fn(), - options: { - timezone: "Asia/Kolkata", - }, - }, + supervisorApprovalReminder: { + schedule: "0 8 * * *", + job: jest.fn(), + options: { + timezone: "Asia/Kolkata", + }, + }, }); await registerAllJobs(); @@ -44,14 +44,14 @@ describe("registerCronJobs", () => { } ); - expect(cronJobManager.registerJob).toHaveBeenCalledWith( - "supervisorApprovalReminder", - "0 8 * * *", - expect.any(Function), - { - timezone: "Asia/Kolkata", - runOnInit: false, - } - ); + expect(cronJobManager.registerJob).toHaveBeenCalledWith( + "supervisorApprovalReminder", + "0 8 * * *", + expect.any(Function), + { + timezone: "Asia/Kolkata", + runOnInit: false, + } + ); }); }); diff --git a/server/models/Evaluation.js b/server/models/Evaluation.js index 4f838107..018649d6 100644 --- a/server/models/Evaluation.js +++ b/server/models/Evaluation.js @@ -1,64 +1,83 @@ -const mongoose = require('mongoose'); -const formMetadata = require('./FormMetadata'); +const mongoose = require("mongoose"); +const formMetadata = require("./FormMetadata"); -const signatureSchema = new mongoose.Schema({ - type: { type: String, enum: ['text', 'draw'], required: true }, - value: { type: String, required: true }, - font: { type: String } -}, { _id: false }); - -const evaluationItemSchema = new mongoose.Schema({ - category: { - type: String, - required: true, - }, - rating: { - type: String, - enum: ['Satisfactory', 'Unsatisfactory'], - required: true +const signatureSchema = new mongoose.Schema( + { + type: { type: String, enum: ["text", "draw"], required: true }, + value: { type: String, required: true }, + font: { type: String }, }, - comment: { type: String, maxlength: 500 } -}, { _id: false }); + { _id: false } +); -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 }, - - interneeName: { - type: String, - required: true, - trim: true, - minlength: 2, - maxlength: 100 +const evaluationItemSchema = new mongoose.Schema( + { + category: { + type: String, + required: true, + }, + rating: { + type: String, + enum: ["Satisfactory", "Unsatisfactory"], + required: true, + }, + comment: { type: String, maxlength: 500 }, }, + { _id: false } +); - interneeID: { - type: String, - required: true, - match: [/^\d{9}$/, 'Sooner ID must be a 9-digit number'] // Sooner ID validation - }, +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, + }, - interneeEmail: { - type: String, - required: true, - match: [/\S+@\S+\.\S+/, 'Invalid email format'], // Email format validation - lowercase: true, - trim: true - }, + interneeName: { + type: String, + required: true, + trim: true, + minlength: 2, + maxlength: 100, + }, - evaluations: { - type: [evaluationItemSchema], - validate: [arr => arr.length > 0, 'At least one evaluation item is required'] - }, + interneeID: { + type: String, + required: true, + match: [/^\d{9}$/, "Sooner ID must be a 9-digit number"], // Sooner ID validation + }, - advisorSignature: { type: signatureSchema, required: true }, - advisorAgreement: { type: Boolean, required: true }, - coordinatorSignature: { type: signatureSchema, required: true }, - coordinatorAgreement: { type: Boolean, required: true } + interneeEmail: { + type: String, + required: true, + match: [/\S+@\S+\.\S+/, "Invalid email format"], // Email format validation + lowercase: true, + trim: true, + }, -}, { timestamps: true }); + evaluations: { + type: [evaluationItemSchema], + validate: [ + (arr) => arr.length > 0, + "At least one evaluation item is required", + ], + }, + + advisorSignature: { type: signatureSchema, required: true }, + advisorAgreement: { type: Boolean, required: true }, + coordinatorSignature: { type: signatureSchema, required: true }, + coordinatorAgreement: { type: Boolean, required: true }, + }, + { timestamps: true } +); 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/InternshipRequest.js b/server/models/InternshipRequest.js index 68dced0e..982b1abb 100644 --- a/server/models/InternshipRequest.js +++ b/server/models/InternshipRequest.js @@ -8,75 +8,82 @@ const Task = new mongoose.Schema({ type: String, required: true, }, - outcomes: [{ - type: String, - enum: [ - "problemSolving", - "solutionDevelopment", - "communication", - "decisionMaking", - "collaboration", - "application" - ] - }] - + outcomes: [ + { + type: String, + enum: [ + "problemSolving", + "solutionDevelopment", + "communication", + "decisionMaking", + "collaboration", + "application", + ], + }, + ], }); -const formA1 = new mongoose.Schema({ - ...formMetadata, - student: { - type: ObjectId, +const formA1 = new mongoose.Schema( + { + student: { + name: { + type: String, required: true, - ref: 'UserTokenRequest' + }, + email: { + unique: true, + type: String, + required: true, + }, }, + ...formMetadata, workplace: { - name: { - type: String, - required: true, - }, - website: String, - phone: String, // TODO how to validate this? + name: { + type: String, + required: true, + }, + website: String, + phone: String, // TODO how to validate this? }, internshipAdvisor: { - name: String, - jobTitle: String, - email: { - type: String, - required: true - } + name: String, + jobTitle: String, + email: { + type: String, + required: true, + }, }, creditHours: { - type: Number, - required: true, - enum: [1, 2, 3] + type: Number, + required: true, + enum: [1, 2, 3], }, startDate: { - type: Date, - required: true + type: Date, + required: true, }, - endDate: { // TODO how to make sure endDate is later than startDate? - type: Date, - required: true + endDate: { + // TODO how to make sure endDate is later than startDate? + type: Date, + required: true, }, tasks: { - type: [Task], - required: true + type: [Task], + required: true, }, - // status: { - // type: String, - // required: true, - // enum: ['draft', 'submitted','pending manual review' ,'approved'] - // }, approvals: { 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.models.InternshipRequest || mongoose.model("InternshipRequest", formA1); \ No newline at end of file +module.exports = + mongoose.models.InternshipRequest || + mongoose.model("InternshipRequest", formA1); diff --git a/server/routes/weeklyReportRoutes.js b/server/routes/weeklyReportRoutes.js index 40b25eb0..94abd092 100644 --- a/server/routes/weeklyReportRoutes.js +++ b/server/routes/weeklyReportRoutes.js @@ -13,16 +13,26 @@ router.get("/a1/:email", getA1ByEmail); // ------------------ Comments: Supervisor & Coordinator ------------------ router.post("/supervisor-comments", reportController.submitSupervisorComments); -router.post("/coordinator-comments", reportController.submitCoordinatorGroupComments); +router.post( + "/coordinator-comments", + reportController.submitCoordinatorGroupComments +); // ---------------------- Cumulative Reports ---------------------- router.get("/cumulative/reports", reportController.getCumulativeReports); -router.get("/cumulative/group/:groupIndex", reportController.getCumulativeGroup); +router.get( + "/cumulative/group/:groupIndex", + reportController.getCumulativeGroup +); // ---------------------- Group Fetches ---------------------- router.get("/supervised-groups", reportController.getSupervisorReviewedGroups); -// ---------------------- Single Report (must remain last!) ---------------------- +// Student access for opening A3 form — needs to be before the catch-all! +router.post("/A3-eligibility", reportController.getStudentProgress); + +// Single Report (must remain last!) +// router.get("/:id", reportController.getReportById); router.get("/:id", reportController.getReportById); module.exports = router; diff --git a/server/services/insertData.js b/server/services/insertData.js index 8d0e9541..c80119e0 100644 --- a/server/services/insertData.js +++ b/server/services/insertData.js @@ -7,19 +7,24 @@ async function insertFormData(formData) { // Assumes global mongoose connection is already established elsewhere in app if (formData.status === "submitted") { - // if tasks are aligned , form will be sent to the supervisor. - formData.supervisor_status="pending" - formData.coordinator_status="not submitted" //TBD + // if tasks are aligned , form will be sent to the supervisor. + formData.supervisor_status = "pending"; + formData.coordinator_status = "not submitted"; //TBD console.log("Submission sent to Supervisor Dashboard."); } else if (formData.status === "pending manual review") { //if tasks are not aligned, form will be sent to coordinator. coordinator approves -> coordinator should forward to supervisor for further approval - formData.coordinator_status="pending" - formData.supervisor_status="not submitted" - console.log("Task not aligned with CS Outcomes. Sent to coordinator for manual review."); + formData.coordinator_status = "pending"; + formData.supervisor_status = "not submitted"; + console.log( + "Task not aligned with CS Outcomes. Sent to coordinator for manual review." + ); } const formattedData = { - student: new mongoose.Types.ObjectId(), // TODO: Replace with actual signed-in student ID + student: { + name: formData.interneeName, + email: formData.interneeEmail, + }, workplace: { name: formData.workplaceName, website: formData.website, @@ -34,25 +39,23 @@ async function insertFormData(formData) { startDate: new Date(formData.startDate), endDate: new Date(formData.endDate), tasks: formData.tasks - .map(task => ({ - description: task.description, - outcomes: task.outcomes, - })).filter(task => task.description.trim() !== ''), // remove empty tasks - // status: "submitted", // Default status — adjust as needed - // status: formData.status, // Default status — adjust as needed + .map((task) => ({ + description: task.description, + outcomes: task.outcomes, + })) + .filter((task) => task.description.trim() !== ""), // remove empty tasks - supervisor_status: formData.supervisor_status ,//function based on if tasks are aligned/not aligned with outcomes + supervisor_status: formData.supervisor_status, //function based on if tasks are aligned/not aligned with outcomes coordinator_status: formData.coordinator_status, approvals: ["advisor", "coordinator"], // TODO: Might be dynamic later reminders: [], // Placeholder for future reminder logic - completedHours: parseInt(formData.creditHours) * 60, // Assuming 1 credit = 60 hours + completedHours: 0, // Assuming 1 credit = 60 hours }; const savedForm = await InternshipRequest.create(formattedData); console.log("Form saved successfully with ID:", savedForm._id); - console.log("saved form",savedForm) + console.log("saved form", savedForm); return savedForm; - } catch (error) { console.error("Error saving form:", error.message); throw error; @@ -61,4 +64,4 @@ async function insertFormData(formData) { module.exports = { insertFormData, -}; \ No newline at end of file +};