diff --git a/client/package.json b/client/package.json
index 102348e2b..f9599a871 100644
--- a/client/package.json
+++ b/client/package.json
@@ -3,22 +3,21 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@testing-library/dom": "^10.4.0",
- "@testing-library/jest-dom": "^6.6.3",
- "@testing-library/react": "^16.2.0",
- "@testing-library/user-event": "^13.5.0",
- "axios": "^1.8.2",
- "bootstrap": "^5.3.5",
- "react": "^19.0.0",
- "react-bootstrap": "^2.10.9",
- "react-dom": "^19.0.0",
- "react-router-dom": "^7.4.1",
- "react-scripts": "5.0.1",
- "react-icons": "^5.5.0",
- "react-signature-canvas": "^1.1.0-alpha.2",
- "web-vitals": "^2.1.4"
-},
-
+ "@testing-library/dom": "^10.4.0",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.2.0",
+ "@testing-library/user-event": "^13.5.0",
+ "axios": "^1.8.2",
+ "bootstrap": "^5.3.5",
+ "react": "^19.0.0",
+ "react-bootstrap": "^2.10.9",
+ "react-dom": "^19.0.0",
+ "react-icons": "^5.5.0",
+ "react-router-dom": "^7.4.1",
+ "react-scripts": "5.0.1",
+ "react-signature-canvas": "^1.1.0-alpha.2",
+ "web-vitals": "^2.1.4"
+ },
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
diff --git a/client/src/App.js b/client/src/App.js
index aec9d81c4..67991b7cb 100644
--- a/client/src/App.js
+++ b/client/src/App.js
@@ -1,5 +1,6 @@
import React from 'react';
import { RouterProvider } from "react-router-dom";
+
import router from "./router";
import "./styles/App.css";
diff --git a/client/src/pages/A1InternshipRequestForm.js b/client/src/pages/A1InternshipRequestForm.js
new file mode 100644
index 000000000..a7b719ce9
--- /dev/null
+++ b/client/src/pages/A1InternshipRequestForm.js
@@ -0,0 +1,356 @@
+import React, { useState } from 'react';
+import '../styles/A1InternshipRequestForm.css';
+
+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)),
+ };
+
+ const [formData, setFormData] = useState(initialState);
+ const [successMsg, setSuccessMsg] = useState('');
+ const [errorMsg, setErrorMsg] = 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 (formData.startDate && formData.endDate) {
+ validateDates(id === 'startDate' ? value : formData.startDate,
+ id === 'endDate' ? value : formData.endDate);
+ }
+ }
+ };
+
+ 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');
+ } else {
+ setDateError('');
+ }
+ };
+
+ const handleCreditHourChange = (value) => {
+ setFormData((prev) => ({ ...prev, creditHours: value }));
+ };
+
+ const handleTaskChange = (index, value) => {
+ const updatedTasks = [...formData.tasks];
+ updatedTasks[index] = 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 outcomesValid = outcomes.every(taskOutcomes =>
+ taskOutcomes.filter(val => val).length >= 4
+ );
+
+ return requiredFieldsFilled && patternsValid && tasksFilled && datesValid && outcomesValid;
+ };
+
+ 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('');
+ }
+ };
+ 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
+ };
+
+ try {
+ const response = await fetch("http://localhost:5001/api/form/submit", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(payload),
+ });
+ await response.json();
+ } catch (err) {
+ console.error(err);
+ }
+};
+
+
+ return (
+
+
A.1 - Internship Request Form
+
Internee & Workplace Information:
+
+
+ );
+};
+
+export default A1InternshipRequestForm;
\ No newline at end of file
diff --git a/client/src/pages/Home.js b/client/src/pages/Home.js
index c9334218b..449478f73 100644
--- a/client/src/pages/Home.js
+++ b/client/src/pages/Home.js
@@ -172,4 +172,4 @@ function Home() {
);
}
-export default Home;
+export default Home;
\ No newline at end of file
diff --git a/client/src/pages/SignUp.js b/client/src/pages/SignUp.js
index 085a8277c..5f4a42166 100644
--- a/client/src/pages/SignUp.js
+++ b/client/src/pages/SignUp.js
@@ -354,4 +354,4 @@ function SignUp() {
);
}
-export default SignUp;
+export default SignUp;
\ No newline at end of file
diff --git a/client/src/router.js b/client/src/router.js
index 69479cdfd..3664e5fcf 100644
--- a/client/src/router.js
+++ b/client/src/router.js
@@ -1,6 +1,8 @@
import React from "react";
import { createBrowserRouter } from "react-router-dom";
+import A1InternshipRequestForm from "./pages/A1InternshipRequestForm";
+
// Layout
import Layout from "./components/Layout";
@@ -28,6 +30,10 @@ const router = createBrowserRouter([
path: "signup",
element: ,
},
+ {
+ path: "a1-form",
+ element: ,
+ },
{
path: "evaluation",
element: ,
diff --git a/client/src/styles/A1InternshipRequestForm.css b/client/src/styles/A1InternshipRequestForm.css
new file mode 100644
index 000000000..602322465
--- /dev/null
+++ b/client/src/styles/A1InternshipRequestForm.css
@@ -0,0 +1,118 @@
+.form-container {
+ font-family: 'Roboto', sans-serif;
+ margin: 30px;
+ background-color: #f5f5f5;
+ padding: 20px;
+ }
+
+ h2 {
+ text-align: center;
+ color: #841617;
+ }
+
+ .section-title {
+ font-size: 16px;
+ color: #841617;
+ margin-top: 30px;
+ margin-bottom: 10px;
+ font-weight: bold;
+ }
+
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-bottom: 20px;
+ background-color: #fff;
+ }
+
+ th {
+ background-color: #841617;
+ color: #fff;
+ }
+
+ th, td {
+ padding: 8px;
+ border: 1px solid #999;
+ vertical-align: top;
+ }
+
+ input[type="text"],
+ input[type="email"],
+ input[type="date"] {
+ width: 95%;
+ padding: 6px;
+ font-size: 13px;
+ border: 1px solid #999;
+ border-radius: 3px;
+ }
+
+ input[type="checkbox"] {
+ appearance: none;
+ width: 16px;
+ height: 16px;
+ border: 2px solid #333;
+ border-radius: 50%;
+ background-color: white;
+ cursor: pointer;
+ }
+
+ input[type="checkbox"]:checked {
+ background-color: #841617;
+ }
+
+ .signature-cell {
+ text-align: center;
+ background-color: #841617;
+ color: #fff;
+ }
+
+ .signature-cell input {
+ margin-top: 6px;
+ padding: 6px;
+ width: 90%;
+ }
+
+ span.description {
+ font-size: 11.5px;
+ color: #fff;
+ display: block;
+ margin-top: 4px;
+ }
+
+ ol {
+ padding-left: 18px;
+ font-size: 13px;
+ }
+
+ .submit-section {
+ text-align: center;
+ margin-top: 20px;
+ }
+
+ button {
+ padding: 10px 25px;
+ background-color: #841617;
+ color: white;
+ border: none;
+ font-size: 14px;
+ border-radius: 5px;
+ cursor: pointer;
+ }
+
+ button:hover {
+ background-color: #6e1212;
+ }
+
+ .success-msg {
+ color: green;
+ font-weight: bold;
+ text-align: center;
+ margin-top: 10px;
+ }
+
+ .error-msg {
+ color: red;
+ font-weight: bold;
+ text-align: center;
+ margin-top: 10px;
+ }
\ No newline at end of file
diff --git a/server/.env b/server/.env
index 32d995d42..db7ef5551 100644
--- a/server/.env
+++ b/server/.env
@@ -2,6 +2,8 @@
PORT=5001
MONGO_URI=mongodb://localhost:27017/IPMS
+
+
# Email Configuration
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
diff --git a/server/index.js b/server/index.js
index cddf72aa6..d431a6aaf 100644
--- a/server/index.js
+++ b/server/index.js
@@ -2,6 +2,8 @@ const express = require("express");
const mongoose = require("mongoose");
const cors = require("cors");
const User = require("./models/User");
+const formRoutes = require("./routes/formRoutes");
+
require("dotenv").config();
const emailRoutes = require("./routes/emailRoutes");
@@ -16,6 +18,7 @@ const Evaluation = require("./models/Evaluation");
const app = express();
app.use(express.json());
app.use(cors());
+app.use("/api/form", formRoutes); // register route as /api/form/submit
const mongoConfig = {
serverSelectionTimeoutMS: 5000,
diff --git a/server/models/InternshipRequest.js b/server/models/InternshipRequest.js
new file mode 100644
index 000000000..b22a5ac62
--- /dev/null
+++ b/server/models/InternshipRequest.js
@@ -0,0 +1,72 @@
+const mongoose = require("mongoose"); // why are we commonjs
+const ObjectId = mongoose.Schema.Types.ObjectId;
+
+const Task = new mongoose.Schema({
+ _id: false,
+ description: {
+ type: String,
+ required: true
+ },
+ outcomes: {
+ type: [String],
+ enum: ['problemSolving','solutionDevelopment', 'communication', 'decisionMaking', 'collaboration', 'application']
+ }
+});
+const formA1 = new mongoose.Schema({
+ student: { // get student's name, email, id from User
+ type: ObjectId,
+ required: true,
+ ref: 'User'
+ },
+ workplace: {
+ name: {
+ type: String,
+ required: true,
+ },
+ website: String,
+ phone: String, // TODO how to validate this?
+ },
+ internshipAdvisor: {
+ name: String,
+ jobTitle: String,
+ email: {
+ type: String,
+ required: true
+ }
+ },
+ creditHours: {
+ type: Number,
+ required: true,
+ enum: [1, 2, 3]
+ },
+ 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
+ },
+ status: {
+ type: String,
+ required: true,
+ enum: ['draft', 'submitted', '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;
+})
+
+module.exports = mongoose.model("InternshipRequest", formA1);
\ No newline at end of file
diff --git a/server/routes/formRoutes.js b/server/routes/formRoutes.js
new file mode 100644
index 000000000..a85b47876
--- /dev/null
+++ b/server/routes/formRoutes.js
@@ -0,0 +1,56 @@
+const express = require("express");
+const router = express.Router();
+const { insertFormData } = require("../services/insertData");
+
+// Utility: Validate required fields
+function validateFormData(formData) {
+ const requiredFields = [
+ "workplaceName",
+ "website",
+ "phone",
+ "advisorName",
+ "advisorJobTitle",
+ "advisorEmail",
+ "creditHour",
+ "startDate",
+ "endDate",
+ "tasks"
+ ];
+
+ for (const field of requiredFields) {
+ if (!formData[field] || formData[field] === "") {
+ return `Missing or empty required field: ${field}`;
+ }
+ }
+
+ if (!Array.isArray(formData.tasks) || formData.tasks.length === 0) {
+ return "Tasks must be a non-empty array";
+ }
+
+ for (const [index, task] of formData.tasks.entries()) {
+ if (!task.description || !task.outcomes) {
+ return `Task at index ${index} is missing description or outcomes`;
+ }
+ }
+
+ return null; // No errors
+}
+
+router.post("/submit", async (req, res) => {
+ const formData = req.body;
+
+ const validationError = validateFormData(formData);
+ if (validationError) {
+ return res.status(400).json({ message: validationError });
+ }
+
+ try {
+ await insertFormData(formData);
+ res.status(200).json({ message: "Form received and handled!" });
+ } catch (error) {
+ console.error("Error handling form data:", error);
+ res.status(500).json({ message: "Something went wrong" });
+ }
+});
+
+module.exports = router;
diff --git a/server/services/insertData.js b/server/services/insertData.js
new file mode 100644
index 000000000..801af75f3
--- /dev/null
+++ b/server/services/insertData.js
@@ -0,0 +1,47 @@
+const mongoose = require("mongoose");
+const InternshipRequest = require("../models/InternshipRequest");
+
+async function insertFormData(formData) {
+ try {
+ console.log("Received Form Data:\n", JSON.stringify(formData, null, 2));
+
+ // Assumes global mongoose connection is already established elsewhere in app
+
+ const formattedData = {
+ student: new mongoose.Types.ObjectId(), // TODO: Replace with actual signed-in student ID
+ workplace: {
+ name: formData.workplaceName,
+ website: formData.website,
+ phone: formData.phone,
+ },
+ internshipAdvisor: {
+ name: formData.advisorName,
+ jobTitle: formData.advisorJobTitle,
+ email: formData.advisorEmail,
+ },
+ creditHours: parseInt(formData.creditHour),
+ startDate: new Date(formData.startDate),
+ endDate: new Date(formData.endDate),
+ tasks: formData.tasks.map(task => ({
+ description: task.description,
+ outcomes: task.outcomes,
+ })),
+ status: "submitted", // Default status — adjust as needed
+ approvals: ["advisor", "coordinator"], // TODO: Might be dynamic later
+ reminders: [], // Placeholder for future reminder logic
+ completedHours: parseInt(formData.creditHour) * 60, // Assuming 1 credit = 60 hours
+ };
+
+ const savedForm = await InternshipRequest.create(formattedData);
+ console.log("Form saved successfully with ID:", savedForm._id);
+ return savedForm;
+
+ } catch (error) {
+ console.error("Error saving form:", error.message);
+ throw error;
+ }
+}
+
+module.exports = {
+ insertFormData,
+};