From 539baffcfe360af4bebafbf1e7a23bc74bb0f5e7 Mon Sep 17 00:00:00 2001 From: rithwik-d Date: Thu, 17 Apr 2025 16:33:16 -0500 Subject: [PATCH 01/16] Added Id, Name and Address to Form A.3 --- client/src/pages/A3JobEvaluationForm.jsx | 125 +++++++++++++++++----- client/src/styles/A3JobEvaluationForm.css | 6 +- 2 files changed, 104 insertions(+), 27 deletions(-) diff --git a/client/src/pages/A3JobEvaluationForm.jsx b/client/src/pages/A3JobEvaluationForm.jsx index f04c9b07..cb63ddf3 100644 --- a/client/src/pages/A3JobEvaluationForm.jsx +++ b/client/src/pages/A3JobEvaluationForm.jsx @@ -38,11 +38,15 @@ const evaluationItems = [ const A3JobEvaluationForm = () => { // Form state management const [formData, setFormData] = useState({ + interneeName: "", + interneeID: "", + interneeEmail: "", advisorSignature: "", advisorAgreement: false, coordinatorSignature: "", coordinatorAgreement: false, }); + const [errors, setErrors] = useState({}); // Ratings and comments const [ratings, setRatings] = useState({}); @@ -58,6 +62,24 @@ const A3JobEvaluationForm = () => { const [selectedFont, setSelectedFont] = useState(fonts[0]); const [activeTab, setActiveTab] = useState("type"); + // For validation of the form contents + const validateForm = () => { + const newErrors = {}; + if (!formData.interneeName?.trim()) newErrors.interneeName = "Name is required."; + if (!/^\d{9}$/.test(formData.interneeID || "")) newErrors.interneeID = "Enter a valid 9-digit Sooner ID."; + if (!/\S+@\S+\.\S+/.test(formData.interneeEmail || "")) newErrors.interneeEmail = "Invalid email."; + if (!formData.advisorSignature) newErrors.advisorSignature = "Signature is required."; + if (!formData.coordinatorSignature) newErrors.coordinatorSignature = "Signature is required."; + evaluationItems.forEach((item) => { + if (!ratings[item]) { + newErrors[`${item}_rating`] = "Please select one of these"; // Error message + } + }); + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + // Signature canvas ref const sigCanvasRef = useRef(null); @@ -71,6 +93,7 @@ const A3JobEvaluationForm = () => { // Handle form input changes const handleChange = (field, value) => { setFormData((prev) => ({ ...prev, [field]: value })); + setErrors((prev) => ({ ...prev, [field]: undefined })); }; // Rating selection @@ -120,8 +143,8 @@ const A3JobEvaluationForm = () => { // Submit the form to the backend const handleSubmit = async (e) => { e.preventDefault(); - if (!formData.advisorAgreement || !formData.coordinatorAgreement) { - alert("Please confirm both signature agreements before submitting."); + if (!validateForm() || !formData.advisorAgreement || !formData.coordinatorAgreement) { + alert("Please confirm internee details and both signature agreements before submitting."); return; } try { @@ -130,12 +153,25 @@ const A3JobEvaluationForm = () => { { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ formData, ratings, comments }), + body: JSON.stringify({ + interneeName: formData.interneeName, + interneeID: formData.interneeID, + interneeEmail: formData.interneeEmail, + advisorSignature: formData.advisorSignature, + coordinatorSignature: formData.coordinatorSignature, + advisorAgreement: formData.advisorAgreement, + coordinatorAgreement: formData.coordinatorAgreement, + ratings, + comments, + }), } ); if (response.ok) { alert("Evaluation submitted successfully!"); setFormData({ + interneeName: "", + interneeID: "", + interneeEmail: "", advisorSignature: "", advisorAgreement: false, coordinatorSignature: "", @@ -191,10 +227,33 @@ const A3JobEvaluationForm = () => {

A.3 – Job Performance Evaluation

+ + +
+
Internee Details
+ + Name + handleChange("interneeName", e.target.value)} isInvalid={!!errors.interneeName} placeholder="Enter full name" style={{ maxWidth: "300px" }}/> + {errors.interneeName} + + + + Sooner ID + handleChange("interneeID", e.target.value)} isInvalid={!!errors.interneeID} placeholder="Enter 9-digit student ID" style={{ maxWidth: "300px" }}/> + {errors.interneeID} + + + Email + handleChange("interneeEmail", e.target.value)} isInvalid={!!errors.interneeEmail} placeholder="Enter student email" style={{ maxWidth: "300px" }}/> + {errors.interneeEmail} + +
+ +
@@ -207,41 +266,53 @@ const A3JobEvaluationForm = () => { {evaluationItems.map((item, index) => ( - - + + {/* Radios grouped in one cell */} + - - - + + + {/* Show the error below both radio buttons */} + {errors[`${item}_rating`] && ( + + {errors[`${item}_rating`]} + + )} + + + {/* Comments box */} + + ))}
{item} + {item} +
handleRatingChange(item, "Satisfactory")} - required + isInvalid={!!errors[`${item}_rating`]} /> -
- handleRatingChange(item, "Unsatisfactory") - } + onChange={() => handleRatingChange(item, "Unsatisfactory")} + isInvalid={!!errors[`${item}_rating`]} /> - - - handleCommentChange(item, e.target.value) - } - placeholder="Enter comments" - style={{ minWidth: "250px" }} - /> -
+ handleCommentChange(item, e.target.value)} + placeholder="Enter comments" + style={{ minWidth: "250px" }} + /> +
@@ -265,6 +336,7 @@ const A3JobEvaluationForm = () => { > {renderSignaturePreview("advisorSignature")}
+ {errors.advisorSignature} { > {renderSignaturePreview("coordinatorSignature")} + {errors.coordinatorSignature} Date: Thu, 17 Apr 2025 19:35:04 -0500 Subject: [PATCH 02/16] Updated emailIntegration for Form A.3 --- server/jobs/cronJobsConfig.js | 3 ++- server/jobs/reminderEmail.js | 38 ++++++++++++++++++++++++++++++- server/jobs/reminderEmail.test.js | 33 +++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/server/jobs/cronJobsConfig.js b/server/jobs/cronJobsConfig.js index 2cb9e5c6..0ddd5f64 100644 --- a/server/jobs/cronJobsConfig.js +++ b/server/jobs/cronJobsConfig.js @@ -1,10 +1,11 @@ const CronJob = require("../models/CronJob"); -const { coordinatorReminder, supervisorReminder } = require("./reminderEmail"); +const { coordinatorReminder, supervisorReminder, evaluationReminder } = require("./reminderEmail"); // Map of job names to their corresponding functions const jobFunctions = { coordinatorApprovalReminder: coordinatorReminder, supervisorApprovalReminder: supervisorReminder, + evaluationReminderJob: evaluationReminder, // Add more job functions here as needed }; diff --git a/server/jobs/reminderEmail.js b/server/jobs/reminderEmail.js index 9add729f..29d34aa5 100644 --- a/server/jobs/reminderEmail.js +++ b/server/jobs/reminderEmail.js @@ -75,7 +75,43 @@ const supervisorReminder = async () => { } }; +const Evaluation = require("../models/Evaluation"); + +const evaluationReminder = async () => { + try { + const pendingEvals = await Evaluation.find({ + evaluations: { $exists: false }, + advisorAgreement: true, + }); + + for (const evalDoc of pendingEvals) { + const emailHtml = ` +

Dear Supervisor,

+

This is a reminder to complete the Final Job Performance Evaluation (Form A.3) for:

+
    +
  • Name: ${evalDoc.interneeName}
  • +
  • Sooner ID: ${evalDoc.interneeID}
  • +
+

The deadline is approaching. Please use the link below to complete the evaluation:

+

Complete A.3 Evaluation

+

Thanks,
IPMS Team

+ `; + + await emailService.sendEmail({ + to: evalDoc.interneeEmail, // or supervisor's email if you have it + subject: "Reminder: Pending A.3 Evaluation Submission", + html: emailHtml, + }); + + console.log(`✅ Reminder sent for: ${evalDoc.interneeName}`); + } + } catch (error) { + console.error("❌ Error sending A.3 reminders:", error); + } +}; + module.exports = { coordinatorReminder, - supervisorReminder + supervisorReminder, + evaluationReminder, }; diff --git a/server/jobs/reminderEmail.test.js b/server/jobs/reminderEmail.test.js index fef12185..28f71cc1 100644 --- a/server/jobs/reminderEmail.test.js +++ b/server/jobs/reminderEmail.test.js @@ -145,3 +145,36 @@ describe("supervisorReminder escalation", () => { expect(saveSpy).not.toHaveBeenCalled(); }); }); + +const Evaluation = require("../models/Evaluation"); + +describe("evaluationReminder", () => { + beforeEach(() => { + mockingoose.resetAll(); + emailService.sendEmail.mockClear(); + }); + + it("should send evaluation reminder emails to pending A.3 entries", async () => { + const fakeEval = { + _id: new mongoose.Types.ObjectId(), + interneeName: "Test Student", + interneeID: "113689712", + interneeEmail: "student@example.com", + advisorAgreement: true, + }; + + // Mock Evaluation.find to return one pending evaluation + mockingoose(Evaluation).toReturn([fakeEval], "find"); + + const { evaluationReminder } = require("./reminderEmail"); + await evaluationReminder(); + + expect(emailService.sendEmail).toHaveBeenCalledTimes(1); + expect(emailService.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + to: "student@example.com", + subject: expect.stringContaining("Reminder: Pending A.3 Evaluation"), + }) + ); + }); +}); From 04457fe01f3f95e15c68a5fa3c32373017227149 Mon Sep 17 00:00:00 2001 From: Jessica Lumry Date: Sat, 19 Apr 2025 15:28:09 -0500 Subject: [PATCH 03/16] Read Only Fields for Internship Advisor Details --- client/src/pages/A3JobEvaluationForm.jsx | 25 +++++++++++++++++++++--- package.json | 5 +++++ 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 package.json diff --git a/client/src/pages/A3JobEvaluationForm.jsx b/client/src/pages/A3JobEvaluationForm.jsx index cb63ddf3..c7a5843d 100644 --- a/client/src/pages/A3JobEvaluationForm.jsx +++ b/client/src/pages/A3JobEvaluationForm.jsx @@ -46,8 +46,9 @@ const A3JobEvaluationForm = () => { coordinatorSignature: "", coordinatorAgreement: false, }); + const [advisorDetails, setAdvisorDetails] = useState(null); const [errors, setErrors] = useState({}); - + // Ratings and comments const [ratings, setRatings] = useState({}); const [comments, setComments] = useState({}); @@ -172,6 +173,9 @@ const A3JobEvaluationForm = () => { interneeName: "", interneeID: "", interneeEmail: "", + advisorName: "", + advisorJobTitle: "", + advisorEmail: "", advisorSignature: "", advisorAgreement: false, coordinatorSignature: "", @@ -231,7 +235,7 @@ const A3JobEvaluationForm = () => { style={{ backgroundColor: "#fff", maxWidth: "900px", width: "100%" }} > - +
Internee Details
@@ -239,7 +243,6 @@ const A3JobEvaluationForm = () => { Name handleChange("interneeName", e.target.value)} isInvalid={!!errors.interneeName} placeholder="Enter full name" style={{ maxWidth: "300px" }}/> {errors.interneeName} - Sooner ID @@ -252,8 +255,24 @@ const A3JobEvaluationForm = () => { {errors.interneeEmail}
+
+
Internship Advisor Details
+ + Name + + + + Job Title + + + + Email + + +
+ diff --git a/package.json b/package.json new file mode 100644 index 00000000..a1ada9bb --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "cors": "^2.8.5" + } +} From b7806b02e2aa921d4156963b0ab81d653b44b1a4 Mon Sep 17 00:00:00 2001 From: rithwik-d Date: Sun, 20 Apr 2025 18:46:18 -0500 Subject: [PATCH 04/16] Read Only Fields for Internship Advisor Details --- client/package.json | 2 +- client/src/pages/A1InternshipRequestForm.js | 515 ++++++++++-------- client/src/pages/A3JobEvaluationForm.jsx | 25 +- client/src/pages/CoordinatorDashboard.js | 50 +- .../src/pages/CoordinatorRequestDetailView.js | 107 ++++ client/src/pages/Home.js | 87 +-- client/src/router.js | 5 + client/src/styles/A1InternshipRequestForm.css | 114 ++-- .../styles/CoordinatorRequestDetailView.css | 17 + client/src/styles/dashboard.css | 47 ++ package.json | 5 + server/app.js | 4 + server/controllers/approvalController.js | 112 +++- server/controllers/emailController.js | 2 - server/controllers/outcomeAlignController.js | 37 ++ server/index.js | 24 +- server/middleware/authMiddleware.js | 15 +- server/models/Evaluation.js | 25 +- server/models/InternshipRequest.js | 2 +- server/routes/approvalRoutes.js | 27 +- server/routes/formRoutes.js | 68 ++- server/routes/outcomeRoutes.js | 7 + server/services/emailService.js | 42 +- server/services/insertData.js | 29 +- server/utils/cronUtils.test.js | 140 ----- server/utils/cs_outcome_keywords.json | 446 +++++++++++++++ server/utils/logger.test.js | 54 -- 27 files changed, 1392 insertions(+), 616 deletions(-) create mode 100644 client/src/pages/CoordinatorRequestDetailView.js create mode 100644 client/src/styles/CoordinatorRequestDetailView.css create mode 100644 client/src/styles/dashboard.css create mode 100644 package.json create mode 100644 server/controllers/outcomeAlignController.js create mode 100644 server/routes/outcomeRoutes.js delete mode 100644 server/utils/cronUtils.test.js create mode 100644 server/utils/cs_outcome_keywords.json delete mode 100644 server/utils/logger.test.js diff --git a/client/package.json b/client/package.json index ddbbc14c..75898d4e 100644 --- a/client/package.json +++ b/client/package.json @@ -42,6 +42,6 @@ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" - ] + ] } } diff --git a/client/src/pages/A1InternshipRequestForm.js b/client/src/pages/A1InternshipRequestForm.js index a7b719ce..81191c88 100644 --- a/client/src/pages/A1InternshipRequestForm.js +++ b/client/src/pages/A1InternshipRequestForm.js @@ -1,44 +1,60 @@ -import React, { useState } from 'react'; -import '../styles/A1InternshipRequestForm.css'; +import React, { useState } from "react"; +import "../styles/A1InternshipRequestForm.css"; + +const outcomeLabels = [ + "Problem Solving", + "Solution Development", + "Communication", + "Decision-Making", + "Collaboration", + "Application", +]; + +const outcomeDescriptions = [ + "Understand and solve complex computing problems", + "Create, build, and assess computing solutions", + "Communicate clearly and confidently", + "Make responsible decisions", + "Work well within a team", + "Apply computer science algorithms to create practical solutions", +]; const A1InternshipRequestForm = () => { const initialState = { - interneeName: '', - soonerId: '', - interneeEmail: '', - workplaceName: '', - website: '', - phone: '', - startDate: '', - endDate: '', - advisorName: '', - advisorJobTitle: '', - advisorEmail: '', - interneeSignature: '', - advisorSignature: '', - coordinatorApproval: '', - creditHours: '', - tasks: ['', '', '', '', ''], - outcomes: Array(5).fill(Array(6).fill(false)), + interneeName: "", + soonerId: "", + interneeEmail: "", + workplaceName: "", + website: "", + phone: "", + startDate: "", + endDate: "", + advisorName: "", + advisorJobTitle: "", + advisorEmail: "", + interneeSignature: "", + advisorSignature: "", + coordinatorApproval: "", + creditHours: "", + tasks: Array(5).fill({ description: "" }), }; const [formData, setFormData] = useState(initialState); - const [successMsg, setSuccessMsg] = useState(''); - const [errorMsg, setErrorMsg] = useState(''); - const [dateError, setDateError] = useState(''); + const [successMsg, setSuccessMsg] = useState(""); + const [errors, setErrors] = useState({}); + const [dateError, setDateError] = useState(""); const handleInputChange = (e) => { const { id, value } = e.target; setFormData((prev) => ({ ...prev, [id]: value })); - - // Clear date error when either date field changes - if (id === 'startDate' || id === 'endDate') { - setDateError(''); - - // Validate dates when both are filled + + if (id === "startDate" || id === "endDate") { + setDateError(""); if (formData.startDate && formData.endDate) { - validateDates(id === 'startDate' ? value : formData.startDate, - id === 'endDate' ? value : formData.endDate); + validateDates( + id === "startDate" ? value : formData.startDate, + id === "endDate" ? value : formData.endDate + ); } } }; @@ -46,11 +62,10 @@ const A1InternshipRequestForm = () => { const validateDates = (start, end) => { const startDate = new Date(start); const endDate = new Date(end); - if (endDate <= startDate) { - setDateError('End date must be after start date'); + setDateError("End date must be after start date"); } else { - setDateError(''); + setDateError(""); } }; @@ -60,140 +75,173 @@ const A1InternshipRequestForm = () => { const handleTaskChange = (index, value) => { const updatedTasks = [...formData.tasks]; - updatedTasks[index] = value; + updatedTasks[index] = { ...updatedTasks[index], description: value }; setFormData((prev) => ({ ...prev, tasks: updatedTasks })); }; - const handleOutcomeChange = (taskIndex, outcomeIndex) => { - const updatedOutcomes = formData.outcomes.map((row, i) => - i === taskIndex - ? row.map((val, j) => (j === outcomeIndex ? !val : val)) - : row - ); - setFormData((prev) => ({ ...prev, outcomes: updatedOutcomes })); - }; - const validateForm = () => { const namePattern = /^[A-Za-z\s]+$/; const numberPattern = /^[0-9]+$/; const phonePattern = /^[0-9]{10}$/; const emailPattern = /^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$/; - const { - interneeName, soonerId, interneeEmail, workplaceName, phone, - startDate, endDate, advisorName, advisorEmail, - interneeSignature, advisorSignature, coordinatorApproval, - creditHours, tasks, outcomes - } = formData; - - const requiredFieldsFilled = interneeName && soonerId && interneeEmail && - workplaceName && phone && startDate && endDate && - advisorName && advisorEmail && interneeSignature && - advisorSignature && coordinatorApproval && creditHours; - - const patternsValid = namePattern.test(interneeName) && - numberPattern.test(soonerId) && - emailPattern.test(interneeEmail) && - namePattern.test(workplaceName) && - phonePattern.test(phone) && - namePattern.test(advisorName) && - emailPattern.test(advisorEmail) && - namePattern.test(interneeSignature) && - namePattern.test(advisorSignature) && - namePattern.test(coordinatorApproval); - - const tasksFilled = tasks.every(task => task.trim() !== ''); - - const start = new Date(startDate); - const end = new Date(endDate); - const datesValid = end > start; - - if (!datesValid) { - setDateError('End date must be after start date'); - return false; + 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.soonerId) newErrors.soonerId = "Sooner ID is required"; + 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.phone) newErrors.phone = "Phone is required"; + 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"; + else if (formData.startDate && formData.endDate) { + const start = new Date(formData.startDate); + const end = new Date(formData.endDate); + if (end <= start) newErrors.endDate = "End date must be after start date"; } - const outcomesValid = outcomes.every(taskOutcomes => - taskOutcomes.filter(val => val).length >= 4 - ); + if (!formData.advisorName) newErrors.advisorName = "Advisor name is required"; + else if (!namePattern.test(formData.advisorName)) newErrors.advisorName = "Advisor name should contain only letters and spaces"; - return requiredFieldsFilled && patternsValid && tasksFilled && datesValid && outcomesValid; - }; + if (!formData.advisorEmail) newErrors.advisorEmail = "Advisor email is required"; + else if (!emailPattern.test(formData.advisorEmail)) newErrors.advisorEmail = "Invalid advisor email format"; - const handleSubmit = (e) => { - e.preventDefault(); - const isValid = validateForm(); - - if (isValid) { - setSuccessMsg('Form submitted successfully!'); - setErrorMsg(''); - submitFormData(formData); - setTimeout(() => setSuccessMsg(''), 3000); - setFormData(initialState); - } else { - setErrorMsg('Please fill all required fields with valid data. Each task must have at least 4 outcomes selected.'); - setSuccessMsg(''); + 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.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); + return Object.keys(newErrors).length === 0; }; - const submitFormData = async () => { - const outcomeMap = { - 0: 'problemSolving', - 1: 'solutionDevelopment', - 2: 'communication', - 3: 'decisionMaking', - 4: 'collaboration', - 5: 'application' - }; - - const tasksWithOutcomes = formData.tasks.map((taskDesc, i) => { - const selectedOutcomes = formData.outcomes[i] - .map((checked, j) => (checked ? outcomeMap[j] : null)) - .filter(Boolean); - return { - description: taskDesc.trim(), - outcomes: selectedOutcomes - }; - }); - - const payload = { - interneeName: formData.interneeName.trim(), - soonerId: formData.soonerId.trim(), - interneeEmail: formData.interneeEmail.trim(), - workplaceName: formData.workplaceName.trim(), - website: formData.website.trim(), - phone: formData.phone.trim(), - startDate: formData.startDate, - endDate: formData.endDate, - advisorName: formData.advisorName.trim(), - advisorJobTitle: formData.advisorJobTitle.trim(), - advisorEmail: formData.advisorEmail.trim(), - interneeSignature: formData.interneeSignature.trim(), - advisorSignature: formData.advisorSignature.trim(), - coordinatorApproval: formData.coordinatorApproval.trim(), - creditHour: formData.creditHours, - tasks: tasksWithOutcomes - }; + const submitFormData = async () => { try { - const response = await fetch("http://localhost:5001/api/form/submit", { + const response = await fetch(`${process.env.REACT_APP_API_URL}/api/form/submit`, { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify(payload), + body: JSON.stringify(formData), }); - await response.json(); - } catch (err) { - console.error(err); - } -}; + if (!response.ok) { + throw new Error("Failed to submit form", {cause: response}); + } + const data = await response.json(); + console.log("Form submitted successfully:", data); + return data; + } catch (error) { + console.error("Error submitting form:", error); + throw error; + } + }; + + // const handleSubmit = (e) => { + // e.preventDefault(); + // if (validateForm()) { + // // sending descriptions to backend to check if they align with CS outcomes + // const taskDescriptions = formData.tasks + // .map(task => task.description.trim()) + // .filter(Boolean); + // sendTaskDescriptions(taskDescriptions); + // //ending here + // submitFormData().then(data => { + // const recipient = data.manual ? "coordinator for manual review!" : "advisor!"; + // setSuccessMsg("Form submitted successfully and sent to " + recipient); + // setTimeout(() => setSuccessMsg(""), 15000); + // }).catch(err => setErrors("Form submission failed! " + err)) + // .finally(() => setFormData(initialState)); + // } + // }; + + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!validateForm()) return; + + 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!"; + setSuccessMsg("Form submitted successfully and sent to " + recipient); + setTimeout(() => setSuccessMsg(""), 15000); + setFormData(initialState); + } else { + setErrors({ tasks: "Outcome alignment failed or returned no tasks." }); + } + } catch (err) { + console.error("Error during submission:", err); + setErrors({ submit: "Form submission failed! " + err.message }); + } + }; + + //function to send description to backend + 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 }) + }); + + if (!response.ok) { + throw new Error("Failed to send task descriptions"); + } + + const data = await response.json(); + console.log("Alignment result:", data); + formData.tasks = data.results.map(({ task, matched_outcomes }) => ({ + description: task, + outcomes: matched_outcomes + })); - + return formData.tasks; + + } catch (error) { + console.error("Error:", error); + } + }; return (

A.1 - Internship Request Form

-

Internee & Workplace Information:

+

Internee & Workplace Information:

@@ -204,45 +252,70 @@ const A1InternshipRequestForm = () => { - - - + + + - - - + + + - - - + + + - + {[1, 2, 3].map((val) => ( -
Name:
Name:
Name:
+ Name*:
+ +
+ Name*:
+ +
+ Name*:
+ +
Sooner ID:
Website:
Job Title:
+ Sooner ID*:
+ +
+ Website:
+ +
+ Job Title:
+ +
Email:
Phone:
Email:
+ Email*:
+ +
+ Phone*:
+ +
+ Email*:
+ +
Select the Number of Credit Hours + Select the Number of Credit Hours* + - Start Date:
- + Start Date*:
+
- End Date:
- *:
+ - {dateError &&
{dateError}
} + {dateError &&
{dateError}
}
+ {val}
{
- {/* Tasks and Outcomes Section */} -

Task Details & Program Outcomes:

- - - - - - - - - - - - - - - - - - {formData.tasks.map((task, i) => ( - - - {formData.outcomes[i].map((outcome, j) => ( - + ))} + + ))} + +
Job Description DetailsProgram Outcome
-
    -
  1. Tasks need to be filled by the Internship Advisor.
  2. -
  3. Select one or more outcomes per task.
  4. -
  5. All tasks must cover at least 4 outcomes.
  6. -
-
Problem SolvingSolution DevelopmentCommunicationDecision-MakingCollaborationApplication
- Task {i + 1}:
- handleTaskChange(i, e.target.value)} - className="task" - /> -
+

Task Details & Program Outcomes*

+
+
+ Job Description Details: +
    +
  1. Tasks need to be filled by the Internship Advisor.
  2. +
  3. Only task description fields are editable.
  4. +
  5. All tasks should cover a minimum of three outcomes.
  6. +
+
+ + + + + {outcomeLabels.map((label, i) => ( + + ))} + + + + {formData.tasks.map((task, i) => ( + + - ))} - - ))} - -
Task + {label} +
+ ({outcomeDescriptions[i]}) +
handleOutcomeChange(i, j)} - className="outcome" + type="text" + placeholder={`Task ${i + 1}`} + value={task.description} + onChange={(e) => handleTaskChange(i, e.target.value)} + style={{ width: "100%", padding: "4px", boxSizing: "border-box" }} />
+ {outcomeLabels.map((_, j) => ( +
+ +
+ - {/* Signatures */}

Signatures:

- Internee Signature
+ Internee Signature*:
- Internship Advisor Signature
+ Internship Advisor Signature:
- Internship Coordinator Approval
+ Internship Coordinator Approval:
+ {Object.keys(errors).length > 0 && ( +
+
Please correct the following errors:
+ {Object.values(errors).map((err, i) => ( +
{err}
+ ))} +
+ )} +
{successMsg &&
{successMsg}
} - {errorMsg &&
{errorMsg}
} ); diff --git a/client/src/pages/A3JobEvaluationForm.jsx b/client/src/pages/A3JobEvaluationForm.jsx index cb63ddf3..c7a5843d 100644 --- a/client/src/pages/A3JobEvaluationForm.jsx +++ b/client/src/pages/A3JobEvaluationForm.jsx @@ -46,8 +46,9 @@ const A3JobEvaluationForm = () => { coordinatorSignature: "", coordinatorAgreement: false, }); + const [advisorDetails, setAdvisorDetails] = useState(null); const [errors, setErrors] = useState({}); - + // Ratings and comments const [ratings, setRatings] = useState({}); const [comments, setComments] = useState({}); @@ -172,6 +173,9 @@ const A3JobEvaluationForm = () => { interneeName: "", interneeID: "", interneeEmail: "", + advisorName: "", + advisorJobTitle: "", + advisorEmail: "", advisorSignature: "", advisorAgreement: false, coordinatorSignature: "", @@ -231,7 +235,7 @@ const A3JobEvaluationForm = () => { style={{ backgroundColor: "#fff", maxWidth: "900px", width: "100%" }} >
- +
Internee Details
@@ -239,7 +243,6 @@ const A3JobEvaluationForm = () => { Name handleChange("interneeName", e.target.value)} isInvalid={!!errors.interneeName} placeholder="Enter full name" style={{ maxWidth: "300px" }}/> {errors.interneeName} - Sooner ID @@ -252,8 +255,24 @@ const A3JobEvaluationForm = () => { {errors.interneeEmail}
+
+
Internship Advisor Details
+ + Name + + + + Job Title + + + + Email + + +
+ diff --git a/client/src/pages/CoordinatorDashboard.js b/client/src/pages/CoordinatorDashboard.js index 082ff247..8259b650 100644 --- a/client/src/pages/CoordinatorDashboard.js +++ b/client/src/pages/CoordinatorDashboard.js @@ -1,12 +1,48 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import axios from "axios"; +import "../styles/dashboard.css"; + +function CoordinatorDashboard() { + const [requests, setRequests] = useState([]); + const navigate = useNavigate(); + + const fetchRequests = async () => { + try { + const res = await axios.get( + `${process.env.REACT_APP_API_URL}/api/coordinator/requests` + ); + setRequests(res.data); + } catch (err) { + console.error("Failed to fetch requests:", err); + } + }; + + useEffect(() => { + fetchRequests(); + }, []); -const CoordinatorDashboard = () => { return ( -
-

Coordinator Dashboard

-

Welcome, Coordinator!

+
+

Coordinator Dashboard

+ + {requests.length === 0 ? ( +

No Pending Requests

+ ) : ( + requests.map((req) => ( +
navigate(`/coordinator/request/${req._id}`)} + > + {/*

{req.student.userName}

+

Email: {req.student.email}

*/} +

Company: {req.workplace.name}

+
+ )) + )}
); -}; +} -export default CoordinatorDashboard; \ No newline at end of file +export default CoordinatorDashboard; diff --git a/client/src/pages/CoordinatorRequestDetailView.js b/client/src/pages/CoordinatorRequestDetailView.js new file mode 100644 index 00000000..8c8a12f5 --- /dev/null +++ b/client/src/pages/CoordinatorRequestDetailView.js @@ -0,0 +1,107 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import axios from "axios"; +import "../styles/CoordinatorRequestDetailView.css"; + +const CoordinatorRequestDetailView = () => { + const { id } = useParams(); + const navigate = useNavigate(); + const [data, setData] = useState(null); + + useEffect(() => { + axios + .get(`${process.env.REACT_APP_API_URL}/api/coordinator/request/${id}`) + .then((res) => setData(res.data)) + .catch((err) => console.log(err)); + }, [id]); + + const handleApprove = async () => { + try { + const res = await axios.post( + `${process.env.REACT_APP_API_URL}/api/coordinator/request/${id}/approve` + ); + alert(res.data.message); + navigate("/coordinator-dashboard"); + } catch (err) { + console.error("Approval failed:", err); + alert("Error approving request."); + } + }; + + const handleReject = async () => { + const reason = prompt("Please enter a reason for rejection:"); + if (!reason) return alert("Rejection reason required!"); + + try { + const res = await axios.post( + `${process.env.REACT_APP_API_URL}/api/coordinator/request/${id}/reject`, + { reason } + ); + alert(res.data.message); + navigate("/coordinator-dashboard"); + } catch (err) { + console.error("Rejection failed:", err); + alert("Error rejecting request."); + } + }; + + if (!data) return

Loading...

; + + const { requestData, supervisorStatus } = data; + + return ( +
+

Internship Request Details

+ +
+

+ Student: {requestData.student.userName} +

+

+ Email: {requestData.student.email} +

+

+ Company: {requestData.workplace.name} +

+

+ Supervisor Status: {supervisorStatus} +

+ +

Tasks & CS Outcomes

+
+ + + + + + + + {requestData.tasks.map((task, idx) => ( + + + + + ))} + +
TaskOutcomes
{task.description}{task.outcomes.join(", ")}
+ +
+ + + +
+ + + ); +}; + +export default CoordinatorRequestDetailView; diff --git a/client/src/pages/Home.js b/client/src/pages/Home.js index c1fdf9e6..c8ed3df5 100644 --- a/client/src/pages/Home.js +++ b/client/src/pages/Home.js @@ -1,5 +1,4 @@ -import React from 'react'; -import { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Link, useNavigate } from "react-router-dom"; import "../styles/App.css"; import { FaEnvelope, FaLock, FaEye, FaEyeSlash } from "react-icons/fa"; @@ -7,17 +6,23 @@ import "../styles/login.css"; import StudentIcon from "../Icons/StudentIcon"; import CoordinatorIcon from "../Icons/CoordinatorIcon"; import SupervisorIcon from "../Icons/SupervisorIcon"; -import Swal from 'sweetalert2'; +import Swal from "sweetalert2"; function Home() { const navigate = useNavigate(); const [formData, setFormData] = useState({ email: "", password: "", - role: "", + + role: "student", }); const [showPassword, setShowPassword] = useState(false); - + const [role] = useState("student"); + + // Sync role into formData.role + useEffect(() => { + setFormData((prev) => ({ ...prev, role })); + }, [role]); const handleInputChange = (e) => { const { name, value } = e.target; @@ -29,9 +34,10 @@ function Home() { const handleSubmit = async (e) => { e.preventDefault(); - + console.log(`${formData.role} sign in attempted`, formData); + const { email: ouEmail, password, role } = formData; - + if (!ouEmail || !password || !role) { return Swal.fire({ icon: "warning", @@ -39,24 +45,36 @@ function Home() { text: "Please fill in all fields to sign in 💫", }); } - + try { - const response = await fetch(`${process.env.REACT_APP_API_URL}/api/token/user-login`, { - method: "POST", - headers: { - "Content-Type": "application/json", + const response = await fetch( + `${process.env.REACT_APP_API_URL}/api/token/user-login`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ouEmail, password, role }), }, - body: JSON.stringify({ ouEmail, password, role }), - }); - + ); + const data = await response.json(); - + if (response.ok) { Swal.fire({ icon: "success", title: "Login Successful 🌟", text: `Welcome back, ${role}!`, }); + + // Redirect user based on role + if (role === "coordinator") { + navigate("/coordinator-dashboard"); + } else if (role === "student") { + navigate("/student-dashboard"); + } else if (role === "supervisor") { + navigate("/supervisor-dashboard"); + } } else { Swal.fire({ icon: "error", @@ -73,8 +91,6 @@ function Home() { }); } }; - - return (
@@ -101,26 +117,20 @@ function Home() { ].map(({ role: r, Icon }) => (
setFormData({ - ...formData, - role: r, - })} + className={`role-card ${ + formData.role === r ? "selected" : "" + }`} + onClick={() => + setFormData({ + ...formData, + role: r, + }) + } >

{r.charAt(0).toUpperCase() + r.slice(1)}

-
))}
@@ -179,17 +189,20 @@ function Home() { marginBottom: "1rem", }} > -