+ {error &&
{error}
}
+
+
@@ -63,4 +105,4 @@ const ViewFormModal = ({ formData, onClose, onAction }) => {
);
};
-export default ViewFormModal;
+export default ViewFormModal;
\ No newline at end of file
diff --git a/client/src/styles/A1InternshipRequestForm.css b/client/src/styles/A1InternshipRequestForm.css
index c35ea522..b53a78b9 100644
--- a/client/src/styles/A1InternshipRequestForm.css
+++ b/client/src/styles/A1InternshipRequestForm.css
@@ -1,116 +1,193 @@
+/* Layout Container */
.form-container {
- font-family: 'Roboto', sans-serif;
- margin: 30px;
- background-color: #f5f5f5;
- padding: 20px;
-}
-
-h2 {
+ max-width: 1100px;
+ margin: 30px auto;
+ padding: 30px;
+ background-color: #ffffff;
+ border-radius: 12px;
+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.08);
+ font-family: 'Segoe UI', sans-serif;
+ }
+
+ /* Headings */
+ .form-container h2 {
text-align: center;
- color: #841617;
-}
-
-.section-title {
- font-size: 16px;
- color: #841617;
+ color: #333;
+ margin-bottom: 25px;
+ }
+
+ .section-title {
margin-top: 30px;
margin-bottom: 10px;
+ font-size: 1.2rem;
font-weight: bold;
-}
-
-table {
+ color: #444;
+ border-bottom: 1px solid #ddd;
+ padding-bottom: 6px;
+ }
+
+ /* Table Styling */
+ table {
width: 100%;
border-collapse: collapse;
- margin-bottom: 20px;
- background-color: #fff;
-}
-
-th, td {
- padding: 8px; /* Adjusted padding */
- border: 1px solid #999;
+ margin-bottom: 25px;
+ }
+
+ table th, table td {
+ padding: 10px;
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"] {
+ }
+
+ table th {
+ background-color: #f8f9fa;
+ text-align: left;
+ font-weight: 600;
+ }
+
+ /* Inputs */
+ input[type="text"],
+ input[type="email"],
+ input[type="date"],
+ select,
+ textarea {
+ width: 100%;
+ padding: 8px 10px;
+ border: 1px solid #ccc;
+ border-radius: 6px;
+ box-sizing: border-box;
+ font-size: 0.95rem;
+ transition: border-color 0.3s;
+ }
+
+ input:focus,
+ select:focus,
+ textarea:focus {
+ border-color: #4a90e2;
+ outline: none;
+ }
+
+ /* Dropdowns */
+ select {
appearance: none;
- width: 16px;
- height: 16px;
- border: 2px solid #333;
- background-color: white;
+ background-color: #fff;
+ background-image: url("data:image/svg+xml;utf8,
");
+ background-repeat: no-repeat;
+ background-position-x: 95%;
+ background-position-y: center;
+ }
+
+ /* Error Messages */
+ .error-text,
+ .error-msg {
+ color: #d93025;
+ font-size: 0.85rem;
+ margin-top: 5px;
+ }
+
+ /* Success Message */
+ .success-msg {
+ color: #0f9d58;
+ font-size: 1rem;
+ margin-top: 20px;
+ text-align: center;
+ }
+
+ /* Signature Section */
+ .signature-input-container {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .signature-font-options {
+ position: absolute;
+ background-color: #fff;
+ border: 1px solid #ccc;
+ z-index: 10;
+ max-height: 180px;
+ overflow-y: auto;
+ width: 100%;
+ top: 100%;
+ left: 0;
+ border-radius: 4px;
+ margin-top: 2px;
+ }
+
+ .signature-font-preview-header {
+ font-size: 0.85rem;
+ padding: 5px 10px;
+ background-color: #f0f0f0;
+ font-weight: bold;
+ }
+
+ .signature-font-preview {
+ padding: 6px 10px;
cursor: pointer;
-}
-
-input[type="checkbox"]:checked {
- background-color: #841617;
-}
-
-.signature-cell {
+ transition: background 0.2s ease;
+ }
+
+ .signature-font-preview:hover {
+ background-color: #e6f2ff;
+ }
+
+ .signature-preview {
+ margin-top: 5px;
+ font-size: 1.1rem;
+ padding: 6px 10px;
+ border: 1px dashed #aaa;
+ border-radius: 4px;
+ background-color: #fafafa;
+ }
+
+ /* Date Picker Customization */
+ .custom-date-picker {
+ width: 100%;
+ padding: 8px 10px;
+ font-size: 0.95rem;
+ border: 1px solid #ccc;
+ border-radius: 6px;
+ box-sizing: border-box;
+ }
+
+ /* Buttons */
+ .submit-section {
text-align: center;
- background-color: #841617;
- color: #fff;
-}
-
-.signature-cell input {
- margin-top: 6px;
- padding: 6px;
- width: 90%;
-}
-
-span.description {
- font-size: 11.5px;
+ margin-top: 30px;
+ }
+
+ button[type="submit"] {
+ background-color: #007bff;
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;
+ padding: 10px 22px;
+ font-size: 1rem;
border: none;
- font-size: 14px;
- border-radius: 5px;
+ border-radius: 6px;
cursor: pointer;
-}
-
-button:hover {
- background-color: #6e1212;
-}
-
-.success-msg {
- color: green;
- font-weight: bold;
- text-align: center;
- margin-top: 10px;
-}
-
-.error-msg {
+ transition: background 0.3s ease;
+ }
+
+ button[type="submit"]:hover {
+ background-color: #0056b3;
+ }
+ .font-dancing-script {
+ font-family: 'Dancing Script', cursive;
+ }
+ .font-great-vibes {
+ font-family: 'Great Vibes', cursive;
+ }
+ .font-pacifico {
+ font-family: 'Pacifico', cursive;
+ }
+ .font-satisfy {
+ font-family: 'Satisfy', cursive;
+ }
+ .font-caveat {
+ font-family: 'Caveat', cursive;
+ }
+
+ /* Asterisk */
+ .required-asterisk {
color: red;
- font-weight: bold;
- text-align: center;
- margin-top: 10px;
-}
-
-.required-asterisk {
- color: #ff6666; /* Light red */
-}
+ margin-left: 2px;
+ }
+
\ No newline at end of file
diff --git a/server/controllers/approvalController.js b/server/controllers/approvalController.js
index 32582466..77149b68 100644
--- a/server/controllers/approvalController.js
+++ b/server/controllers/approvalController.js
@@ -55,7 +55,7 @@ exports.rejectSubmission = async (req, res) => {
exports.getCoordinatorRequests = async (req, res) => {
try {
const requests = await InternshipRequest.find({
- status: "submitted",
+ coordinator_status: "pending",
}).populate("student", "userName email");
res.status(200).json(requests);
} catch (err) {
@@ -80,7 +80,7 @@ exports.coordinatorApproveRequest = async (req, res) => {
try {
const request = await InternshipRequest.findByIdAndUpdate(
req.params.id,
- { status: "approved" },
+ { coordinator_status: "approved" },
{ new: true }
);
@@ -105,7 +105,7 @@ exports.coordinatorRejectRequest = async (req, res) => {
try {
const request = await InternshipRequest.findByIdAndUpdate(
req.params.id,
- { status: "rejected" },
+ { coordinator_status: "rejected" },
{ new: true }
);
diff --git a/server/models/InternshipRequest.js b/server/models/InternshipRequest.js
index 3732f04d..14b7bdc4 100644
--- a/server/models/InternshipRequest.js
+++ b/server/models/InternshipRequest.js
@@ -13,10 +13,10 @@ const Task = new mongoose.Schema({
}
});
const formA1 = new mongoose.Schema({
- student: { // get student's name, email, id from User
+ student: {
type: ObjectId,
required: true,
- ref: 'User'
+ ref: 'UserTokenRequest'
},
workplace: {
name: {
@@ -51,11 +51,11 @@ const formA1 = new mongoose.Schema({
type: [Task],
required: true
},
- status: {
- type: String,
- required: true,
- enum: ['draft', 'submitted','pending manual review' ,'approved']
- },
+ // status: {
+ // type: String,
+ // required: true,
+ // enum: ['draft', 'submitted','pending manual review' ,'approved']
+ // },
approvals: {
type: [String],
enum: ['advisor', 'coordinator']
@@ -63,7 +63,16 @@ const formA1 = new mongoose.Schema({
reminders: [Date],
// requiredHours is an easily derived attribute
// TODO needs to be a virtual getter that checks this student's WeeklyReports
- completedHours: Number
+ completedHours: Number,
+
+ supervisor_status: {
+ type: String,
+ default: "pending"
+ },
+ coordinator_status: {
+ type: String,
+ default: "pending"
+ }
}, { timestamps: true });
formA1.virtual("requiredHours").get(function() {
return this.creditHours * 60;
diff --git a/server/routes/formRoutes.js b/server/routes/formRoutes.js
index c80f3ebe..1da5ffca 100644
--- a/server/routes/formRoutes.js
+++ b/server/routes/formRoutes.js
@@ -1,12 +1,36 @@
-const express = require('express');
+const express = require("express");
const router = express.Router();
-const { insertFormData } = require('../services/insertData');
+const InternshipRequest = require("../models/InternshipRequest");
+const { insertFormData } = require("../services/insertData");
+const {
+ getPendingSubmissions,
+ approveSubmission,
+ rejectSubmission
+} = require("../controllers/approvalController");
-let status = '';
+router.post("/internshiprequests/:id/approve", approveSubmission);
+router.post("/internshiprequests/:id/reject", rejectSubmission);
+
+// UPDATED: GET route to fetch internship requests pending supervisor action
+router.get("/internshiprequests", async (req, res) => {
+ try {
+ const requests = await InternshipRequest.find({
+ supervisor_status: "pending",
+ // approvals: "advisor", // advisor has approved
+ supervisor_status: { $in: [null, "pending"] } // not yet reviewed by supervisor
+ }).sort({ createdAt: 1 }) .populate("student", "userName") // oldest first
+
+ res.status(200).json(requests);
+ } catch (err) {
+ console.error("Error fetching internship requests:", err);
+ res.status(500).json({ message: "Server error while fetching internship requests" });
+ }
+});
// Validate required fields
function validateFormData(formData) {
const requiredFields = [
+ 'soonerId',
'workplaceName',
'website',
'phone',
@@ -25,6 +49,9 @@ function validateFormData(formData) {
}
}
+ if (!/^[0-9]{9}$/.test(formData.soonerId))
+ return `Sooner ID must be a 9-digit number, not ${formData.soonerId}`;
+
if (!Array.isArray(formData.tasks) || formData.tasks.length === 0) {
return 'Tasks must be a non-empty array';
}
@@ -41,7 +68,7 @@ function validateFormData(formData) {
const tasks = formData.tasks;
console.log(tasks);
- if (tasks.filter((task) => task.description).length < 3)
+ if (tasks.filter((task) => task.description && task.description.trim() !== '').length < 3)
return 'At least 3 tasks must be provided';
const uniqueOutcomes = new Set();
tasks.forEach((task) => {
@@ -53,6 +80,7 @@ function validateFormData(formData) {
return null;
}
+
router.post('/submit', async (req, res) => {
const formData = req.body;
const validationError = validateFormData(formData);
@@ -62,11 +90,11 @@ router.post('/submit', async (req, res) => {
try {
await insertFormData(formData);
- res.status(200).json({ message: 'Form received and handled!', status, manual: formData.status !== 'submitted'});
+ res.status(200).json({ message: 'Form received and handled!', manual: formData.status !== 'submitted'});
} catch (error) {
console.error('Error handling form data:', error);
res.status(500).json({ message: 'Something went wrong' });
}
});
-module.exports = router;
+module.exports = router;
\ No newline at end of file
diff --git a/server/services/insertData.js b/server/services/insertData.js
index 7668f7c6..27eded26 100644
--- a/server/services/insertData.js
+++ b/server/services/insertData.js
@@ -7,6 +7,17 @@ async function insertFormData(formData) {
console.log("Received Form Data:\n", JSON.stringify(formData, null, 2));
// 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
+ 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.");
+ }
const formattedData = {
student: new mongoose.Types.ObjectId(), // TODO: Replace with actual signed-in student ID
@@ -28,8 +39,11 @@ async function insertFormData(formData) {
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
+ // status: "submitted", // Default status — adjust as needed
+ // status: formData.status, // Default status — adjust as needed
+
+ 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
@@ -37,24 +51,7 @@ async function insertFormData(formData) {
const savedForm = await InternshipRequest.create(formattedData);
console.log("Form saved successfully with ID:", savedForm._id);
-
- if (formData.status === "submitted") {
- // const submission = {
- // name:`Internship at ${formData.workplaceName}`,
- // student_name: formData.interneeName,
- // details: formData.website,
- // supervisor_status: "pending",
- // coordinator_status: "pending",
- // };
- // await Submission.create(submission);
- console.log("Submission sent to Supervisor Dashboard.");
- } else if (formData.status === "pending manual review") {
- // const instance={
- // // group a schema attributes
- // };
- // await groupaschema.create(instance) // group A schema
- console.log("Task not aligned with CS Outcomes. Sent to coordinator for manual review.");
- }
+ console.log("saved form",savedForm)
return savedForm;
} catch (error) {
@@ -65,4 +62,4 @@ async function insertFormData(formData) {
module.exports = {
insertFormData,
-};
+};
\ No newline at end of file