From 32f65c00cc38a913c167dbcdf5a92ac75ce1fa4e Mon Sep 17 00:00:00 2001 From: rithwik-d Date: Sun, 13 Apr 2025 20:12:03 -0500 Subject: [PATCH 1/3] Updated with Schema and API Functionality --- server/index.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/index.js b/server/index.js index 8b71ee0cc..f986ffd24 100644 --- a/server/index.js +++ b/server/index.js @@ -113,6 +113,13 @@ app.post("/api/evaluation", async (req, res) => { res.status(500).json({ error: "Failed to save evaluation" }); } }); + +//Form A.4 + +const presentationRoutes = require("./routes/presentationRoutes"); +app.use("/api/presentation", presentationRoutes); + + // Graceful shutdown (async Mongoose support) process.on("SIGINT", async () => { try { From fc72491f315f96a2a759d39f695912d86ad1b60f Mon Sep 17 00:00:00 2001 From: rithwik-d Date: Sun, 13 Apr 2025 20:14:32 -0500 Subject: [PATCH 2/3] Updated with Schema and API Functionality --- .../pages/A4PresentationEvaluationForm.jsx | 69 +++++++++++++++++-- server/models/PresentationEvaluation.js | 57 +++++++++++++++ server/routes/presentationRoutes.js | 32 +++++++++ 3 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 server/models/PresentationEvaluation.js create mode 100644 server/routes/presentationRoutes.js diff --git a/client/src/pages/A4PresentationEvaluationForm.jsx b/client/src/pages/A4PresentationEvaluationForm.jsx index 1c5f7d868..14d12dca6 100644 --- a/client/src/pages/A4PresentationEvaluationForm.jsx +++ b/client/src/pages/A4PresentationEvaluationForm.jsx @@ -114,16 +114,71 @@ function A4PresentationEvaluationForm() { return Invalid signature; }; - const handleSubmit = (e) => { + const handleSubmit = async (e) => { e.preventDefault(); if (!validateForm()) return; - alert("Form submitted!"); - console.log(formData); - setFormData(initialFormState); - setTypedSignature(""); - sigCanvasRef.current?.clear(); + + // Step 1: Prepare `evaluations` array from formData + const evaluations = [ + { + category: "Presentation Content", + rating: formData["Presentation Content_rating"], + comment: formData["Presentation Content_comments"] || "" + }, + { + category: "Delivery and Communication", + rating: formData["Delivery and Communication_rating"], + comment: formData["Delivery and Communication_comments"] || "" + }, + { + category: "Answering Questions", + rating: formData["Answering Questions_rating"], + comment: formData["Answering Questions_comments"] || "" + } + ]; + + // Step 2: Create payload in the format the backend expects + const payload = { + interneeName: formData.interneeName, + interneeID: formData.interneeID, + interneeEmail: formData.interneeEmail, + companyName: formData.companyName, + companyWebsite: formData.companyWebsite, + companyPhone: formData.companyPhone, + advisorName: formData.advisorName, + advisorTitle: formData.advisorTitle, + advisorEmail: formData.advisorEmail, + presentationDate: formData.presentationDate, + evaluations, + coordinatorSignature: formData.coordinatorSignature + }; + + // Step 3: Make POST request + try { + const response = await fetch("http://localhost:5001/api/presentation/a4/submit", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(payload) + }); + + const result = await response.json(); + if (response.ok) { + alert("✅ Form A.4 submitted successfully!"); + setFormData(initialFormState); + setTypedSignature(""); + sigCanvasRef.current?.clear(); + } else { + alert("❌ Submission failed: " + result.error); + console.error(result); + } + } catch (err) { + console.error("❌ Network error:", err); + alert("❌ Failed to submit. Check console for details."); + } }; - + return (

A.4 – Internship Coordinator Presentation Evaluation Form

diff --git a/server/models/PresentationEvaluation.js b/server/models/PresentationEvaluation.js new file mode 100644 index 000000000..2497f56a4 --- /dev/null +++ b/server/models/PresentationEvaluation.js @@ -0,0 +1,57 @@ +const mongoose = require("mongoose"); + +const signatureSchema = new mongoose.Schema({ + type: { type: String, enum: ["text", "draw"], required: true }, + value: { type: String, required: true }, + font: { type: String } // used only if type is 'text' +}, { _id: false }); + +const evaluationItemSchema = new mongoose.Schema({ + category: { + type: String, + required: true, + enum: ["Presentation Content", "Delivery and Communication", "Answering Questions"] + }, + rating: { + type: String, + enum: ["Satisfactory", "Unsatisfactory"], + required: true + }, + comment: { + type: String, + default: "" + } +}, { _id: false }); + +const presentationEvaluationSchema = new mongoose.Schema({ + interneeName: { type: String, required: true }, + interneeID: { type: String, required: true }, // Sooner ID (9-digit) + interneeEmail: { type: String, required: true }, + + companyName: { type: String, required: true }, + companyWebsite: { type: String }, + companyPhone: { type: String }, + + advisorName: { type: String, required: true }, + advisorTitle: { type: String, required: true }, + advisorEmail: { type: String, required: true }, + + presentationDate: { type: Date, required: true }, + + evaluations: { + type: [evaluationItemSchema], + validate: [arr => arr.length === 3, "All 3 evaluation categories must be filled"] + }, + + coordinatorSignature: { + type: signatureSchema, + required: true + }, + + submittedAt: { + type: Date, + default: Date.now + } +}); + +module.exports = mongoose.model("PresentationEvaluation", presentationEvaluationSchema); diff --git a/server/routes/presentationRoutes.js b/server/routes/presentationRoutes.js new file mode 100644 index 000000000..7ce22e3b3 --- /dev/null +++ b/server/routes/presentationRoutes.js @@ -0,0 +1,32 @@ +const express = require("express"); +const router = express.Router(); +const PresentationEvaluation = require("../models/PresentationEvaluation"); + +// POST: Submit Form A.4 +router.post("/a4/submit", async (req, res) => { + try { + const data = req.body; + const newRecord = new PresentationEvaluation(data); + await newRecord.save(); + res.status(201).json({ message: "Form A.4 submitted successfully!" }); + } catch (error) { + console.error("Error saving A.4:", error); + res.status(500).json({ error: "Failed to save Form A.4" }); + } +}); + +// GET: Fetch A.4 form by Internee Email (or Sooner ID) +router.get("/a4/find", async (req, res) => { + try { + const { interneeEmail, interneeID } = req.query; + const filter = interneeEmail ? { interneeEmail } : { interneeID }; + const record = await PresentationEvaluation.findOne(filter); + if (!record) return res.status(404).json({ message: "No Form A.4 found" }); + res.json(record); + } catch (error) { + console.error("Error retrieving A.4:", error); + res.status(500).json({ error: "Failed to fetch Form A.4" }); + } +}); + +module.exports = router; From 2ed33b11b1422b2a41a090d709f5a8cc162ad908 Mon Sep 17 00:00:00 2001 From: Jessica Lumry Date: Wed, 16 Apr 2025 21:01:34 -0500 Subject: [PATCH 3/3] Internee Details for Form A.3 --- client/src/pages/A3JobEvaluationForm.jsx | 125 +++++++++++++++++----- client/src/styles/A3JobEvaluationForm.css | 6 +- server/index.js | 13 ++- server/models/Evaluation.js | 25 ++++- 4 files changed, 135 insertions(+), 34 deletions(-) diff --git a/client/src/pages/A3JobEvaluationForm.jsx b/client/src/pages/A3JobEvaluationForm.jsx index f04c9b073..cb63ddf3a 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} { }); app.post("/api/evaluation", async (req, res) => { try { - const { formData, ratings, comments } = req.body; + const { interneeName, interneeID, interneeEmail, advisorSignature, advisorAgreement, coordinatorSignature, coordinatorAgreement, ratings, comments } = req.body; const evaluations = Object.keys(ratings).map((category) => ({ category, @@ -104,10 +104,13 @@ app.post("/api/evaluation", async (req, res) => { })); const newEvaluation = new Evaluation({ - advisorSignature: formData.advisorSignature, - advisorAgreement: formData.advisorAgreement, - coordinatorSignature: formData.coordinatorSignature, - coordinatorAgreement: formData.coordinatorAgreement, + interneeName, + interneeID, + interneeEmail, + advisorSignature, + advisorAgreement, + coordinatorSignature, + coordinatorAgreement, evaluations, }); diff --git a/server/models/Evaluation.js b/server/models/Evaluation.js index d104353f5..0ea16fafa 100644 --- a/server/models/Evaluation.js +++ b/server/models/Evaluation.js @@ -20,9 +20,30 @@ const evaluationItemSchema = new mongoose.Schema({ }, { _id: false }); const evaluationSchema = new mongoose.Schema({ - 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 + }, + + interneeID: { + type: String, + required: true, + match: [/^\d{9}$/, 'Sooner ID must be a 9-digit number'] // Sooner ID validation + }, + + interneeEmail: { + type: String, + required: true, + match: [/\S+@\S+\.\S+/, 'Invalid email format'], // Email format validation + lowercase: true, + trim: true + }, + evaluations: { type: [evaluationItemSchema], validate: [arr => arr.length > 0, 'At least one evaluation item is required'] @@ -35,6 +56,6 @@ const evaluationSchema = new mongoose.Schema({ }, { timestamps: true }); -evaluationSchema.index({ interneeId: 1, internshipId: 1 }); +evaluationSchema.index({ interneeID: 1, internshipId: 1 }); module.exports = mongoose.model('Evaluation', evaluationSchema);