diff --git a/client/src/pages/A1InternshipRequestForm.js b/client/src/pages/A1InternshipRequestForm.js
index 82950c9c..8ab5ac84 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;
}
@@ -134,7 +138,7 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => {
setFormData((prev) => ({
...prev,
interneeName: storedUser.fullName || "",
- interneeEmail: storedUser.email || ""
+ interneeEmail: storedUser.email || "",
}));
}
}, []);
@@ -152,7 +156,7 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => {
}
}
if (errors[id]) {
- setErrors(prev => {
+ setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors[id];
return newErrors;
@@ -179,7 +183,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",
@@ -189,8 +195,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 }));
})
@@ -201,10 +211,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);
@@ -263,11 +305,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;
@@ -279,11 +324,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 }) => ({
@@ -299,13 +347,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);
@@ -319,22 +371,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));
};
}, []);
@@ -355,115 +407,148 @@ 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* :
-
@@ -472,47 +557,59 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => {
2 Credits
3 Credits
- {errors.creditHours && {errors.creditHours}
}
+ {errors.creditHours && (
+
+ {errors.creditHours}
+
+ )}
-
- Start Date* :
-
-
-
-
- {errors.startDate && {errors.startDate}
}
-
-
-
-
- End Date* :
-
-
-
-
- {dateError && {dateError}
}
- {errors.endDate && {errors.endDate}
}
-
+
+ Start Date* :
+
+
+
+
+
+ {errors.startDate && (
+ {errors.startDate}
+ )}
+
+
+
+ End Date* :
+
+
+
+
+
+ {dateError && {dateError}
}
+ {errors.endDate && (
+ {errors.endDate}
+ )}
+
-
Task Details & Program Outcomes*
+
+ Task Details & Program Outcomes
+ *
+
Job Description Details:
@@ -528,7 +625,8 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => {
Task
{outcomeLabels.map((label, j) => (
- {label}
+ {label}
+
({outcomeDescriptions[j]})
))}
@@ -543,16 +641,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:
@@ -560,9 +668,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}
+
+ )}
- {/*
-
- Supervisor Comments:
-
-
-
- Coordinator Comments:
-
-
- */}
@@ -655,4 +736,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.
+
+ )}
+
+
{
+ if (a3Eligibility.eligible) navigate("/evaluation");
+ }}
+ style={{
+ backgroundColor: !a3Eligibility.eligible ? "#ccc" : "",
+ cursor: !a3Eligibility.eligible ? "not-allowed" : "pointer",
+ }}
+ >
+ {a3Eligibility.eligible ? "Open Final Evaluation" : "Locked"}
+
+
);
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
-
navigate("/submitted-reports")}>
+ navigate("/submitted-reports")}
+ >
View Previous Submissions
- {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 c823337c..982b1abb 100644
--- a/server/models/InternshipRequest.js
+++ b/server/models/InternshipRequest.js
@@ -8,91 +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({
- // student: {
- // type: ObjectId,
- // required: true,
- // ref: 'UserTokenRequest'
- // },
- student:{
- name:{
- type: String,
- required: true,
- },
- email:{
- unique: true,
- type: String,
- required:true,
- },
+const formA1 = new mongoose.Schema(
+ {
+ student: {
+ name: {
+ type: String,
+ required: true,
+ },
+ email: {
+ unique: true,
+ type: String,
+ required: true,
+ },
},
...formMetadata,
- // student: {
- // type: ObjectId,
- // required: true,
- // ref: 'UserTokenRequest'
- // },
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 99348e1c..c80119e0 100644
--- a/server/services/insertData.js
+++ b/server/services/insertData.js
@@ -7,22 +7,23 @@ 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
+ student: {
+ name: formData.interneeName,
+ email: formData.interneeEmail,
},
workplace: {
name: formData.workplaceName,
@@ -38,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;
@@ -65,4 +64,4 @@ async function insertFormData(formData) {
module.exports = {
insertFormData,
-};
\ No newline at end of file
+};