- Create your account to access the Internship Program Management System
-
diff --git a/client/src/pages/SupervisorDashboard.js b/client/src/pages/SupervisorDashboard.js
new file mode 100644
index 000000000..88accfab4
--- /dev/null
+++ b/client/src/pages/SupervisorDashboard.js
@@ -0,0 +1,59 @@
+import React, { useEffect, useState, useCallback } from "react";
+import axios from "axios";
+import '../styles/SupervisorDashboard.css';
+
+const SupervisorDashboard = () => {
+ const [submissions, setSubmissions] = useState([]);
+ const url = process.env.REACT_APP_API_URL
+
+ const fetchPendingSubmissions = useCallback(async () => {
+ try {
+ const response = await axios.get(url + "/api/submissions/pending");
+ setSubmissions(response.data);
+ } catch (err) {
+ console.error("Error fetching submissions:", err);
+ }
+ }, [url]);
+
+ useEffect(() => {
+ fetchPendingSubmissions();
+ }, [fetchPendingSubmissions]);
+
+
+ const handleDecision = async (id, action) => {
+ try {
+ const endpoint = url + `/api/submissions/${id}/${action}`;
+ await axios.post(endpoint);
+ alert(`Submission ${action}d successfully!`);
+ fetchPendingSubmissions(); // refresh list
+ } catch (err) {
+ console.error("Error updating submission:", err);
+ }
+ };
+
+ return (
+
+
Supervisor Dashboard
+
Pending Approvals
+
+ {submissions.length === 0 ? (
+
+
No pending approvals at this time.
+
+ ) : (
+ submissions.map(item => (
+ -
+ {item.name} - Details: {item.details} - Status: {item.supervisor_status}
+
+
+
+
+
+ ))
+ )}
+
+
+ );
+};
+
+export default SupervisorDashboard;
diff --git a/client/src/pages/WeeklyProgressReportForm.css b/client/src/pages/WeeklyProgressReportForm.css
new file mode 100644
index 000000000..b8b8989fa
--- /dev/null
+++ b/client/src/pages/WeeklyProgressReportForm.css
@@ -0,0 +1,45 @@
+/* WeeklyProgressReportForm.css */
+
+.a2-form-container {
+ max-width: 700px;
+ margin: auto;
+ padding: 2rem;
+ border: 1px solid #ccc;
+ background: #f9f9f9;
+ border-radius: 8px;
+ }
+
+ .form-row {
+ display: flex;
+ gap: 1rem;
+ margin-bottom: 1rem;
+ justify-content: space-between;
+ }
+
+ .form-row label {
+ flex: 1;
+ }
+
+ .form-group {
+ margin-bottom: 1rem;
+ }
+
+ textarea {
+ width: 100%;
+ height: 100px;
+ }
+
+ .submit-button {
+ background: maroon;
+ color: white;
+ padding: 0.7rem 1.5rem;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ }
+
+ .form-message {
+ margin-top: 1rem;
+ color: green;
+ }
+
\ No newline at end of file
diff --git a/client/src/pages/WeeklyProgressReportForm.js b/client/src/pages/WeeklyProgressReportForm.js
new file mode 100644
index 000000000..2e9b8c54e
--- /dev/null
+++ b/client/src/pages/WeeklyProgressReportForm.js
@@ -0,0 +1,132 @@
+
+
+import React, { useState } from "react";
+import axios from "axios";
+import "./WeeklyProgressReportForm.css"; // optional: for clean styling
+
+const WeeklyProgressReportForm = () => {
+ const [formData, setFormData] = useState({
+ week: "Week 1",
+ hours: "",
+ tasks: "",
+ lessons: "",
+ supervisorComments: "",
+ });
+
+ const [message, setMessage] = useState("");
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData((prev) => ({ ...prev, [name]: value }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ // Get the user ID from localStorage (ensure it exists)
+ // const user = JSON.parse(localStorage.getItem("user"));
+ // const studentId = user?.user?._id;
+
+ // // Check if studentId exists in localStorage
+ // if (!studentId) {
+ // setMessage("Student ID not found. Please log in again.");
+ // return;
+ // }
+
+ // Check that all required fields are filled
+ if (!formData.week || !formData.hours || !formData.tasks || !formData.lessons) {
+ setMessage("Please fill in all the fields.");
+ return;
+ }
+
+ //const payload = { studentId, ...formData };
+ const payload = { ...formData };
+
+ try {
+ // Sending the form data to the backend
+ const res = await axios.post(`${process.env.REACT_APP_API_URL}/api/reports`, payload);
+
+ // Display success message
+ setMessage(res.data.message || "Report submitted!");
+ setFormData({
+ week: "Week 1",
+ hours: "",
+ tasks: "",
+ lessons: "",
+ supervisorComments: "",
+ });
+ } catch (err) {
+ console.error(err);
+ setMessage("Submission failed. Try again.");
+ }
+ };
+
+
+ return (
+
+
A.2 - Weekly Progress Report
+
+ {message &&
{message}
}
+
+ );
+};
+
+export default WeeklyProgressReportForm;
diff --git a/client/src/router.js b/client/src/router.js
index 3c0852d50..351dbaff5 100644
--- a/client/src/router.js
+++ b/client/src/router.js
@@ -1,7 +1,8 @@
-import React from 'react';
-
+import React from "react";
import { createBrowserRouter } from "react-router-dom";
+import A1InternshipRequestForm from "./pages/A1InternshipRequestForm";
+
// Layout
import Layout from "./components/Layout";
@@ -10,7 +11,10 @@ import Layout from "./components/Layout";
import Home from "./pages/Home";
import SignUp from "./pages/SignUp";
import NotFound from "./pages/NotFound";
+import WeeklyProgressReportForm from "./pages/WeeklyProgressReportForm";
import A3JobEvaluationForm from "./pages/A3JobEvaluationForm";
+import SupervisorDashboard from "./pages/SupervisorDashboard";
+import CoordinatorDashboard from "./pages/CoordinatorDashboard";
// Create and export the router configuration
const router = createBrowserRouter([
@@ -27,13 +31,33 @@ const router = createBrowserRouter([
path: "signup",
element:
,
},
- // Add more routes as needed
+ {
+ path: "weekly-report",
+ element:
,
+ },
+ {
+ path: "a1-form",
+ element:
,
+ },
+ {
+ path: "coordinator/dashboard",
+ element:
,
+ },
{
path: "evaluation",
element:
,
},
+ {
+ path: "supervisor-dashboard",
+ element:
,
+ },
+ {
+ path: "coordinator-dashboard",
+ element:
,
+ },
],
},
]);
+
export default router;
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/client/src/styles/App.css b/client/src/styles/App.css
index 39bfb6a24..d542be7f7 100644
--- a/client/src/styles/App.css
+++ b/client/src/styles/App.css
@@ -466,8 +466,17 @@ select.form-control {
flex-direction: column;
}
- .illustration {
- padding: 30px;
+ .illustration.full-height {
+ flex: 1;
+ height: 100%;
+ overflow: hidden;
+ }
+
+ .full-image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
}
.login-options {
@@ -514,3 +523,10 @@ select.form-control {
padding: 25px 20px;
}
}
+
+/* Hide the illustration only on medium screens (768px to 992px) */
+@media (min-width: 768px) and (max-width: 992px) {
+ .illustration {
+ display: none;
+ }
+}
diff --git a/client/src/styles/SupervisorDashboard.css b/client/src/styles/SupervisorDashboard.css
new file mode 100644
index 000000000..0e2acfa75
--- /dev/null
+++ b/client/src/styles/SupervisorDashboard.css
@@ -0,0 +1,106 @@
+.dashboard-container {
+ padding: 20px;
+ background-color: #f9f9f9;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+}
+
+.dashboard-container h2 {
+ font-size: 20px;
+ margin-bottom: 20px;
+ color: #333;
+}
+
+.dashboard-title {
+ font-size: 24px;
+ margin-bottom: 20px;
+ color: #333;
+}
+
+.pending-approvals {
+ list-style-type: none;
+ padding: 0;
+}
+
+.pending-approvals li {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px;
+ margin: 40px 0;
+ background-color: #fff;
+ border-radius: 5px;
+ box-shadow: 0 1px 5px rgba(0, 0, 0, 0.1);
+ font-size: 16px;
+}
+
+.pending-approvals li button {
+ margin-left: 10px;
+ padding: 5px 10px;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ transition: background-color 0.3s;
+}
+
+.approve{
+ background-color: #28a745; /* Green background for approve button */
+ color: white;
+}
+
+.reject{
+ background-color: #dc3545; /* Red background for reject button */
+ color: white;
+}
+
+.approve:hover {
+ background-color: #218838;
+}
+
+.reject:hover{
+ background-color: #c82333;
+}
+
+.pending-approvals li button:focus {
+ outline: none;
+}
+
+.empty-message-container{
+ display: flex; /* Use flexbox */
+ justify-content: center; /* Center horizontally */
+ align-items: center; /* Center vertically */
+ height: 20vh;
+}
+.empty-message {
+ font-size: 28px; /* Adjust font size */
+ color: #000000; /* Change text color for better visibility */
+ text-align: center; /*Center the message*/
+ margin: 20px 0; /* Add some margin for spacing */
+ font-weight: bold; /* Make the message bold */
+}
+
+form {
+ margin-bottom: 5px;
+}
+
+form input,
+form textarea {
+ width: 100%;
+ padding: 10px;
+ margin: 5px 0;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+}
+
+form button {
+ padding: 10px 15px;
+ background-color: #007bff;
+ color: white;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+}
+
+form button:hover {
+ background-color: #0056b3;
+}
diff --git a/client/src/styles/dashboard.css b/client/src/styles/dashboard.css
new file mode 100644
index 000000000..9cc8a208e
--- /dev/null
+++ b/client/src/styles/dashboard.css
@@ -0,0 +1,31 @@
+.dashboard-container {
+ padding: 2rem;
+ font-family: Arial, sans-serif;
+ }
+
+ .request-card {
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ padding: 1rem;
+ margin-bottom: 1rem;
+ box-shadow: 2px 2px 6px rgba(0,0,0,0.1);
+ }
+
+ .action-buttons button {
+ margin-right: 10px;
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ }
+
+ .approve-btn {
+ background-color: #15803d;
+ color: white;
+ }
+
+ .reject-btn {
+ background-color: #b91c1c;
+ color: white;
+ }
+
\ No newline at end of file
diff --git a/client/src/styles/login.css b/client/src/styles/login.css
new file mode 100644
index 000000000..cbbb35d10
--- /dev/null
+++ b/client/src/styles/login.css
@@ -0,0 +1,44 @@
+.clean-input label {
+ display: flex;
+ align-items: center;
+ font-weight: 500;
+ margin-bottom: 6px;
+ font-size: 0.95rem;
+ color: #333;
+ }
+
+ .clean-input input {
+ width: 100%;
+ padding: 12px 14px;
+ border-radius: 8px;
+ border: 1.5px solid #ccc;
+ background-color: #f9f9f9;
+ font-size: 1rem;
+ transition: border-color 0.3s ease;
+ }
+
+ .clean-input input:focus {
+ border-color: #7f1d1d;
+ outline: none;
+ background-color: #fff;
+ }
+
+ .password-wrapper {
+ position: relative;
+ }
+
+ .password-wrapper input {
+ width: 100%;
+ padding-right: 40px;
+ }
+
+ .eye-toggle {
+ position: absolute;
+ right: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ cursor: pointer;
+ color: #888;
+ font-size: 1rem;
+ }
+
\ No newline at end of file
diff --git a/client/src/styles/signup.css b/client/src/styles/signup.css
new file mode 100644
index 000000000..98a9c0a9c
--- /dev/null
+++ b/client/src/styles/signup.css
@@ -0,0 +1,147 @@
+.role-cards {
+ display: flex;
+ justify-content: center;
+ gap: 1.5rem;
+ margin-top: 1rem;
+}
+.role-card {
+ padding: 1rem;
+ border-radius: 10px;
+ background-color: #f9f9f9;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.2s;
+ width: 150px;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
+}
+.role-card.selected {
+ border: 2px solid #800000; /* OU Crimson */
+ background-color: #fff4f4;
+}
+.role-icon {
+ height: 40px;
+ margin-bottom: 0.5rem;
+}
+.info-icon {
+ display: block;
+ font-size: 0.8rem;
+ color: #3d3d3d;
+ margin-top: 0.5rem;
+}
+
+.row {
+ display: flex;
+ gap: 20px;
+ flex-wrap: wrap;
+}
+
+.col {
+ flex: 1;
+ min-width: 250px;
+}
+
+.step-two-header {
+ display: flex;
+ justify-content: space-between;
+}
+
+.password-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 20px;
+}
+
+.password-col {
+ flex: 1 1 300px;
+ min-width: 280px;
+}
+
+.password-wrapper {
+ position: relative;
+}
+
+.eye-icon {
+ position: absolute;
+ right: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ cursor: pointer;
+}
+
+.strength-bar {
+ display: flex;
+ gap: 4px;
+ margin-top: 6px;
+}
+
+.bar-segment {
+ flex: 1;
+ height: 5px;
+ background-color: #ccc;
+}
+
+.bar-segment.strong {
+ background-color: green;
+}
+
+.bar-segment.medium {
+ background-color: orange;
+}
+
+.bar-segment.weak {
+ background-color: #800000;
+}
+
+.strength-text {
+ font-size: 0.85rem;
+ margin: 0;
+}
+
+.strength-text.strong {
+ color: green;
+}
+
+.strength-text.medium {
+ color: orange;
+}
+
+.strength-text.weak {
+ color: #800000;
+}
+
+.match-warning {
+ color: red;
+ font-size: 0.85rem;
+ margin-top: 5px;
+}
+
+.form-control {
+ width: 100%;
+ padding: 10px;
+ box-sizing: border-box;
+}
+
+#semester {
+ width: 100%;
+ padding: 14px 16px;
+ border: 1px solid #e0e0e0;
+ border-radius: 8px;
+ font-size: 16px;
+ background-color: #f9fafb;
+ appearance: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20viewBox%3D%220%200%2020%2020%22%20fill%3D%22none%22%20xmlns%3D%22http://www.w3.org/2000/svg%22%3E%3Cpath%20d%3D%22M5%207L10%2012L15%207%22%20stroke%3D%22%237f1d1d%22%20stroke-width%3D%222%22/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 10px center;
+ background-size: 16px;
+ cursor: pointer;
+ transition: border-color 0.3s ease;
+}
+
+#semester:focus {
+ border-color: #7f1d1d;
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(127, 29, 29, 0.15);
+ background-color: #fff;
+}
diff --git a/client/src/utils/emailUtils.js b/client/src/utils/emailUtils.js
index 9ba56af3b..64efd679e 100644
--- a/client/src/utils/emailUtils.js
+++ b/client/src/utils/emailUtils.js
@@ -3,7 +3,7 @@
*/
// Define the API base URL
-const API_BASE_URL = process.env.REACT_APP_API_URL || "http://localhost:5000";
+const API_BASE_URL = process.env.REACT_APP_API_URL;
/**
* Send an email with custom content
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..435d69818
--- /dev/null
+++ b/package.json
@@ -0,0 +1,5 @@
+{
+ "dependencies": {
+ "jsonwebtoken": "^9.0.2"
+ }
+}
diff --git a/server/.env b/server/.env
index 70e7e5d81..a7af28870 100644
--- a/server/.env
+++ b/server/.env
@@ -9,3 +9,6 @@ EMAIL_SECURE=false
EMAIL_USER=sep.ipms.spring2025@gmail.com
EMAIL_PASSWORD=rmrl msnq kflk uimr
EMAIL_DEFAULT_SENDER=sep.ipms.spring2025@gmail.com
+
+# Secret
+JWT_SECRET=supersecretkey123
diff --git a/server/app.js b/server/app.js
new file mode 100644
index 000000000..43377c338
--- /dev/null
+++ b/server/app.js
@@ -0,0 +1,4 @@
+const cronJobRoutes = require("./routes/cronJobRoutes");
+
+// Add cron job routes
+app.use("/api/cron-jobs", cronJobRoutes);
diff --git a/server/controllers/approvalController.js b/server/controllers/approvalController.js
new file mode 100644
index 000000000..8b3357a5e
--- /dev/null
+++ b/server/controllers/approvalController.js
@@ -0,0 +1,61 @@
+const Submission = require("../models/Submission");
+
+// ✅ Get pending submissions for supervisor
+exports.getPendingSubmissions = async (req, res) => {
+ try {
+ const submissions = await Submission.find({ supervisor_status: "pending" });
+ res.json(submissions);
+ } catch (err) {
+ res.status(500).json({ message: "Failed to fetch pending submissions", error: err });
+ }
+};
+
+// ✅ Supervisor Approves
+exports.approveSubmission = async (req, res) => {
+ const { id } = req.params;
+
+ try {
+ const submission = await Submission.findByIdAndUpdate(
+ id,
+ { supervisor_status: "Approved" },
+ { new: true }
+ );
+
+ if (!submission) {
+ return res.status(404).json({ message: "Submission not found" });
+ }
+
+ res.json({
+ message: "Submission approved and forwarded to Coordinator",
+ updatedSubmission: submission
+ });
+
+ } catch (err) {
+ res.status(500).json({ message: "Approval failed", error: err });
+ }
+};
+
+// ❌ Supervisor Rejects
+exports.rejectSubmission = async (req, res) => {
+ const { id } = req.params;
+
+ try {
+ const submission = await Submission.findByIdAndUpdate(
+ id,
+ { supervisor_status: "Rejected" },
+ { new: true }
+ );
+
+ if (!submission) {
+ return res.status(404).json({ message: "Submission not found" });
+ }
+
+ res.json({
+ message: "Submission rejected",
+ updatedSubmission: submission
+ });
+
+ } catch (err) {
+ res.status(500).json({ message: "Rejection failed", error: err });
+ }
+};
diff --git a/server/controllers/cronJobController.js b/server/controllers/cronJobController.js
new file mode 100644
index 000000000..a3843f1f6
--- /dev/null
+++ b/server/controllers/cronJobController.js
@@ -0,0 +1,118 @@
+const CronJob = require("../models/CronJob");
+const cronParser = require("cron-parser");
+const jobFunctions = require("../jobs/cronJobsConfig").jobFunctions;
+
+// Validate cron schedule
+function isValidCronSchedule(schedule) {
+ try {
+ cronParser.parseExpression(schedule);
+ return true;
+ } catch (error) {
+ return false;
+ }
+}
+
+// Get all cron jobs
+exports.getAllJobs = async (req, res) => {
+ try {
+ const jobs = await CronJob.find();
+ res.json(jobs);
+ } catch (error) {
+ res
+ .status(500)
+ .json({ message: "Error fetching cron jobs", error: error.message });
+ }
+};
+
+// Create a new cron job
+exports.createJob = async (req, res) => {
+ try {
+ const { name, schedule, options } = req.body;
+
+ // Validate required fields
+ if (!name || !schedule) {
+ return res
+ .status(400)
+ .json({ message: "Name and schedule are required" });
+ }
+
+ // Validate job function exists
+ if (!jobFunctions[name]) {
+ return res
+ .status(400)
+ .json({
+ message: "Invalid job name - no corresponding function found",
+ });
+ }
+
+ // Validate cron schedule
+ if (!isValidCronSchedule(schedule)) {
+ return res.status(400).json({ message: "Invalid cron schedule format" });
+ }
+
+ const job = new CronJob({
+ name,
+ schedule,
+ options: options || {},
+ isActive: true,
+ });
+
+ await job.save();
+ res.status(201).json(job);
+ } catch (error) {
+ if (error.code === 11000) {
+ res.status(400).json({ message: "A job with this name already exists" });
+ } else {
+ res
+ .status(500)
+ .json({ message: "Error creating cron job", error: error.message });
+ }
+ }
+};
+
+// Update a cron job
+exports.updateJob = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { schedule, isActive, options } = req.body;
+
+ // Validate cron schedule if provided
+ if (schedule && !isValidCronSchedule(schedule)) {
+ return res.status(400).json({ message: "Invalid cron schedule format" });
+ }
+
+ const job = await CronJob.findByIdAndUpdate(
+ id,
+ { schedule, isActive, options },
+ { new: true }
+ );
+
+ if (!job) {
+ return res.status(404).json({ message: "Cron job not found" });
+ }
+
+ res.json(job);
+ } catch (error) {
+ res
+ .status(500)
+ .json({ message: "Error updating cron job", error: error.message });
+ }
+};
+
+// Delete a cron job
+exports.deleteJob = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const job = await CronJob.findByIdAndDelete(id);
+
+ if (!job) {
+ return res.status(404).json({ message: "Cron job not found" });
+ }
+
+ res.json({ message: "Cron job deleted successfully" });
+ } catch (error) {
+ res
+ .status(500)
+ .json({ message: "Error deleting cron job", error: error.message });
+ }
+};
diff --git a/server/controllers/emailController.js b/server/controllers/emailController.js
index 8f5b4e4cc..012b7f0b6 100644
--- a/server/controllers/emailController.js
+++ b/server/controllers/emailController.js
@@ -21,7 +21,7 @@ const emailController = {
});
}
- // Send the email with all provided options
+
const result = await emailService.sendEmail({
to,
subject,
@@ -58,3 +58,4 @@ const emailController = {
};
module.exports = emailController;
+
diff --git a/server/controllers/reportController.js b/server/controllers/reportController.js
new file mode 100644
index 000000000..d14e0ff18
--- /dev/null
+++ b/server/controllers/reportController.js
@@ -0,0 +1,88 @@
+const User = require("../models/User");
+const WeeklyReport = require("../models/WeeklyReport");
+
+/**
+ * Report Controller – handles weekly report submissions and retrieval
+ */
+
+const reportController = {
+ // POST /api/reports
+ createReport: async (req, res) => {
+ try {
+ const {
+ studentId,
+ week,
+ hours,
+ tasks,
+ lessons,
+ supervisorComments
+ } = req.body;
+
+
+ // Role-check: Only students can submit (based on their ID)
+ // const user = await User.findById(studentId);
+ // if (!user || user.role.toLowerCase() !== "student") {
+ // return res.status(403).json({
+ // success: false,
+ // message: "Only students can submit reports.",
+ // });
+ // }
+
+ // Basic field validation
+ if (!week || hours === undefined || isNaN(hours) || !tasks || !lessons) {
+ return res.status(400).json({
+ success: false,
+ message: "All required fields must be valid.",
+ });
+ }
+
+
+ // Save the report
+ const newReport = new WeeklyReport({
+ //studentId,
+ week,
+ hours,
+ tasks,
+ lessons,
+ supervisorComments: supervisorComments || "",
+ });
+
+
+ await newReport.save();
+
+ res.status(201).json({
+ success: true,
+ message: "Report submitted successfully.",
+ });
+ } catch (error) {
+ console.error("Error in createReport:", error);
+ res.status(500).json({
+ success: false,
+ message: "Internal server error.",
+ });
+ }
+ },
+
+ // GET /api/reports/:userId
+ getReportsByStudent: async (req, res) => {
+ try {
+ const { userId } = req.params;
+
+ const reports = await WeeklyReport.find({ studentId: userId }).sort({
+ createdAt: -1,
+ });
+
+ res.status(200).json({
+ success: true,
+ reports,
+ });
+ } catch (error) {
+ console.error("Error in getReportsByStudent:", error);
+ res
+ .status(500)
+ .json({ success: false, message: "Failed to retrieve reports." });
+ }
+ },
+};
+
+module.exports = reportController;
diff --git a/server/index.js b/server/index.js
index be86a1e58..1393d17d6 100644
--- a/server/index.js
+++ b/server/index.js
@@ -1,21 +1,30 @@
+const weeklyReportRoutes = require("./routes/weeklyReportRoutes");
+
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();
-// Import routes
const emailRoutes = require("./routes/emailRoutes");
+const tokenRoutes = require("./routes/token");
+const approvalRoutes = require("./routes/approvalRoutes");
+const coordinatorRoutes = require("./routes/coordinator");
+
// Import cron job manager and register jobs
const cronJobManager = require("./utils/cronUtils");
-require("./jobs/registerCronJobs");
+const { registerAllJobs } = require("./jobs/registerCronJobs");
+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
-// MongoDB Configuration
const mongoConfig = {
serverSelectionTimeoutMS: 5000,
autoIndex: true,
@@ -26,8 +35,15 @@ const mongoConfig = {
mongoose
.connect(process.env.MONGO_URI, mongoConfig)
- .then(() => {
+ .then(async () => {
console.log("Connected to Local MongoDB");
+ // Initialize cron jobs after database connection is established
+ try {
+ await registerAllJobs();
+ console.log("✅ Cron jobs initialized successfully");
+ } catch (error) {
+ console.error("❌ Failed to initialize cron jobs:", error);
+ }
})
.catch((err) => {
console.error("MongoDB Connection Error:", err);
@@ -57,7 +73,12 @@ app.get("/api/message", (req, res) => {
});
app.use("/api/email", emailRoutes);
+app.use("/api/token", tokenRoutes);
+app.use("/api", approvalRoutes);
+app.use("/api/coordinator", coordinatorRoutes);
+
+app.use("/api/reports", weeklyReportRoutes);
app.post("/api/createUser", async (req, res) => {
try {
const { userName, email, password, role } = req.body;
@@ -67,17 +88,19 @@ app.post("/api/createUser", async (req, res) => {
res.status(201).json({ message: "User created successfully", user });
} catch (error) {
console.error("Error creating user:", error);
- res.status(500).json({ message: "Failed to create user", error: error.message });
+ res
+ .status(500)
+ .json({ message: "Failed to create user", error: error.message });
}
});
app.post("/api/evaluation", async (req, res) => {
try {
const { formData, ratings, comments } = req.body;
- const evaluations = Object.keys(ratings).map(category => ({
+ const evaluations = Object.keys(ratings).map((category) => ({
category,
rating: ratings[category],
- comment: comments[category] || ''
+ comment: comments[category] || "",
}));
const newEvaluation = new Evaluation({
@@ -85,7 +108,7 @@ app.post("/api/evaluation", async (req, res) => {
advisorAgreement: formData.advisorAgreement,
coordinatorSignature: formData.coordinatorSignature,
coordinatorAgreement: formData.coordinatorAgreement,
- evaluations
+ evaluations,
});
await newEvaluation.save();
@@ -108,5 +131,5 @@ process.on("SIGINT", async () => {
}
});
-const PORT = process.env.PORT || 5000;
+const PORT = process.env.PORT || 5001;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
diff --git a/server/jobs/cronJobsConfig.js b/server/jobs/cronJobsConfig.js
new file mode 100644
index 000000000..9aef87af5
--- /dev/null
+++ b/server/jobs/cronJobsConfig.js
@@ -0,0 +1,46 @@
+const CronJob = require("../models/CronJob");
+const coordinatorReminder = require("./reminderEmail");
+
+// Map of job names to their corresponding functions
+const jobFunctions = {
+ coordinatorApprovalReminder: coordinatorReminder,
+ // Add more job functions here as needed
+};
+
+async function getCronJobs() {
+ try {
+ const jobs = await CronJob.find({ isActive: true });
+
+ // Transform database records into the expected format
+ return jobs.reduce((acc, job) => {
+ if (jobFunctions[job.name]) {
+ acc[job.name] = {
+ schedule: job.schedule,
+ job: async () => {
+ try {
+ // Update last run time
+ await CronJob.findByIdAndUpdate(job._id, {
+ lastRun: new Date(),
+ });
+
+ // Execute the job
+ await jobFunctions[job.name]();
+ } catch (error) {
+ console.error(`Error executing job ${job.name}:`, error);
+ }
+ },
+ options: job.options,
+ };
+ }
+ return acc;
+ }, {});
+ } catch (error) {
+ console.error("Error fetching cron jobs:", error);
+ return {};
+ }
+}
+
+module.exports = {
+ getCronJobs,
+ jobFunctions,
+};
diff --git a/server/jobs/registerCronJobs.js b/server/jobs/registerCronJobs.js
index 1fb82cfd3..67d934438 100644
--- a/server/jobs/registerCronJobs.js
+++ b/server/jobs/registerCronJobs.js
@@ -1,12 +1,64 @@
const cronJobManager = require("../utils/cronUtils");
-const coordinatorReminder = require("./reminderEmail");
-
-cronJobManager.registerJob(
- "coordinatorApprovalReminder",
- "*/2 * * * *",
- coordinatorReminder,
- {
- runOnInit: true,
- timezone: "Asia/Kolkata",
+const { getCronJobs } = require("./cronJobsConfig");
+
+async function registerAllJobs() {
+ try {
+ console.log("🔄 Fetching and registering cron jobs...");
+ const cronJobs = await getCronJobs();
+
+ if (Object.keys(cronJobs).length === 0) {
+ console.warn("⚠️ No active cron jobs found in the database");
+ return;
+ }
+
+ // Register all cron jobs from the configuration
+ Object.entries(cronJobs).forEach(([jobName, config]) => {
+ console.log(
+ `📅 Registering job: ${jobName} with schedule: ${config.schedule}`
+ );
+ const success = cronJobManager.registerJob(
+ jobName,
+ config.schedule,
+ config.job,
+ {
+ ...config.options,
+ runOnInit: false, // This prevents immediate execution
+ }
+ );
+
+ if (!success) {
+ console.error(`❌ Failed to register job: ${jobName}`);
+ }
+ });
+ console.log("✅ All cron jobs registered successfully");
+ } catch (error) {
+ console.error("❌ Error registering cron jobs:", error);
+ throw error; // Re-throw to ensure the error is not silently caught
}
-);
+}
+
+// Export the function
+module.exports = { registerAllJobs };
+
+// Only run the auto-registration if this file is being run directly
+if (require.main === module) {
+ console.log("🚀 Initializing cron job registration...");
+ // Register jobs immediately
+ registerAllJobs()
+ .then(() => {
+ console.log("✅ Initial cron job registration completed");
+ })
+ .catch((error) => {
+ console.error("❌ Failed to initialize cron jobs:", error);
+ process.exit(1);
+ });
+
+ // Optionally, set up a periodic refresh of job configurations
+ setInterval(() => {
+ registerAllJobs()
+ .then(() => console.log("✅ Cron jobs refreshed"))
+ .catch((error) =>
+ console.error("❌ Failed to refresh cron jobs:", error)
+ );
+ }, 5 * 60 * 1000); // Refresh every 5 minutes
+}
diff --git a/server/jobs/registerCronJobs.test.js b/server/jobs/registerCronJobs.test.js
index 3b17123dd..ca6f98210 100644
--- a/server/jobs/registerCronJobs.test.js
+++ b/server/jobs/registerCronJobs.test.js
@@ -1,23 +1,40 @@
const cronJobManager = require("../utils/cronUtils");
-const coordinatorReminder = require("./reminderEmail");
+const { getCronJobs } = require("./cronJobsConfig");
+const { registerAllJobs } = require("./registerCronJobs");
jest.mock("../utils/cronUtils");
+jest.mock("./cronJobsConfig");
describe("registerCronJobs", () => {
- beforeEach(() => {
- cronJobManager.registerJob.mockClear();
- });
-
- it("registers coordinator cron job", () => {
- require("./registerCronJobs");
- // Check registerJob was called
- expect(cronJobManager.registerJob).toHaveBeenCalledTimes(1);
- expect(cronJobManager.registerJob).toHaveBeenCalledWith("coordinatorApprovalReminder",
- "*/2 * * * *",
- coordinatorReminder,
- {
- runOnInit: true,
- timezone: "Asia/Kolkata",
- });
+ beforeEach(() => {
+ cronJobManager.registerJob.mockClear();
+ getCronJobs.mockClear();
+ });
+
+ it("registers coordinator cron job", async () => {
+ // Mock the getCronJobs function to return our test configuration
+ getCronJobs.mockResolvedValue({
+ coordinatorApprovalReminder: {
+ schedule: "*/2 * * * *",
+ job: jest.fn(),
+ options: {
+ timezone: "Asia/Kolkata",
+ },
+ },
});
-})
\ No newline at end of file
+
+ await registerAllJobs();
+
+ // Check registerJob was called with correct parameters
+ expect(cronJobManager.registerJob).toHaveBeenCalledTimes(1);
+ expect(cronJobManager.registerJob).toHaveBeenCalledWith(
+ "coordinatorApprovalReminder",
+ "*/2 * * * *",
+ expect.any(Function),
+ {
+ timezone: "Asia/Kolkata",
+ runOnInit: false,
+ }
+ );
+ });
+});
diff --git a/server/middleware/authMiddleware.js b/server/middleware/authMiddleware.js
new file mode 100644
index 000000000..adef8663d
--- /dev/null
+++ b/server/middleware/authMiddleware.js
@@ -0,0 +1,11 @@
+exports.isSupervisor = (req, res, next) => {
+ // const supervisor = Sup.find({$id: username})
+
+
+ req.user = { role: 'supervisor' }; // Mocking user role for demo
+ if (req.user.role === "supervisor") {
+ next();
+ } else {
+ res.status(403).json({ message: "Access denied. Not a supervisor." });
+ }
+};
\ No newline at end of file
diff --git a/server/models/CronJob.js b/server/models/CronJob.js
new file mode 100644
index 000000000..caac5f825
--- /dev/null
+++ b/server/models/CronJob.js
@@ -0,0 +1,27 @@
+const mongoose = require("mongoose");
+
+const cronJobSchema = new mongoose.Schema({
+ name: {
+ type: String,
+ required: true,
+ unique: true,
+ },
+ schedule: {
+ type: String,
+ required: true,
+ },
+ isActive: {
+ type: Boolean,
+ default: false,
+ },
+ options: {
+ type: Object,
+ default: {},
+ },
+ lastRun: {
+ type: Date,
+ default: null,
+ },
+});
+
+module.exports = mongoose.model("CronJob", cronJobSchema);
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/models/Submission.js b/server/models/Submission.js
new file mode 100644
index 000000000..c8bd10d76
--- /dev/null
+++ b/server/models/Submission.js
@@ -0,0 +1,13 @@
+const mongoose = require("mongoose");
+
+const submissionSchema = new mongoose.Schema({
+ name: { type: String, required: true },
+ student_name: { type: String, required: true },
+ details: { type: String, required: true },
+ supervisor_status: { type: String, default: "pending" },
+ supervisor_comment: { type: String },
+ coordinator_status: { type: String, default: "pending" },
+ coordinator_comment: { type: String },
+}, { timestamps: true });
+
+module.exports = mongoose.model("Submission", submissionSchema);
diff --git a/server/models/TokenRequest.js b/server/models/TokenRequest.js
new file mode 100644
index 000000000..ebc518008
--- /dev/null
+++ b/server/models/TokenRequest.js
@@ -0,0 +1,122 @@
+// models/UserTokenRequest.js
+
+const mongoose = require('mongoose');
+
+/**
+ * UserTokenRequest Schema
+ * ---------------------------------------------
+ * This schema handles the access token lifecycle for students
+ * in the Internship Program Management System.
+ *
+ * Fields:
+ * - fullName: Student's full name.
+ * - password: Encrypted password for login authentication.
+ * - ouEmail: Unique OU email for login.
+ * - semester: The semester in which the internship is active.
+ * - academicAdvisor: Reference to the academic advisor (if using a separate collection).
+ * - token: Unique access token used for login.
+ * - isActivated: Whether the token has been activated.
+ * - requestedAt: When the request was made.
+ * - activatedAt: When the token was activated.
+ * - expiresAt: Auto-calculated 6 months from requestedAt.
+ * - deletedAt: Marks soft deletion if the student cancels.
+ * - status: Optional string enum for tracking token state.
+ * - activationLinkSentAt: Timestamp when the activation email was sent.
+ * - password: Encrypted password for login authentication.
+ *
+ * Additional Features:
+ * - Automatically sets `expiresAt` to 6 months from `requestedAt`.
+ * - Uses `timestamps` to auto-generate `createdAt` and `updatedAt`.
+ * - `ouEmail` and `token` are unique.
+ * - Partial TTL index for auto-deletion of inactive token requests
+ * 5 days (432000 seconds) after `requestedAt` if not activated.
+ */
+
+const userTokenRequestSchema = new mongoose.Schema(
+ {
+ fullName: {
+ type: String,
+ required: [true, 'Full name is required'],
+ trim: true,
+ },
+ password: {
+ type: String,
+ required: [true, 'Password is required'],
+ trim: true,
+ },
+ ouEmail: {
+ type: String,
+ required: [true, 'OU email is required'],
+ unique: true,
+ lowercase: true,
+ match: [/^[\w-.]+@ou\.edu$/, 'Email must be a valid OU address'],
+ },
+ semester: {
+ type: String,
+ required: [true, 'Semester is required'],
+ },
+ academicAdvisor: {
+ type: String,
+ required: function () {
+ return this.role === "student";
+ }
+ },
+ isStudent: {
+ type: Boolean,
+ default: false,
+ },
+ token: {
+ type: String,
+ required: [true, 'Token is required'],
+ unique: true,
+ },
+ isActivated: {
+ type: Boolean,
+ default: false,
+ },
+ requestedAt: {
+ type: Date,
+ default: Date.now,
+ },
+ activatedAt: {
+ type: Date,
+ },
+ expiresAt: {
+ type: Date,
+ },
+ deletedAt: {
+ type: Date,
+ },
+ activationLinkSentAt: {
+ type: Date,
+ },
+ status: {
+ type: String,
+ enum: ['pending', 'activated', 'expired', 'deleted'],
+ default: 'pending',
+ },
+ },
+ {
+ timestamps: true, // adds createdAt and updatedAt fields automatically
+ }
+);
+
+// Automatically set expiresAt to 6 months after requestedAt
+userTokenRequestSchema.pre('save', function (next) {
+ if (!this.expiresAt) {
+ const sixMonthsLater = new Date(this.requestedAt);
+ sixMonthsLater.setMonth(sixMonthsLater.getMonth() + 6);
+ this.expiresAt = sixMonthsLater;
+ }
+ next();
+});
+
+userTokenRequestSchema.index(
+ { requestedAt: 1 },
+ {
+ expireAfterSeconds: 432000,
+ partialFilterExpression: { isActivated: false },
+ }
+);
+
+module.exports = mongoose.model('UserTokenRequest', userTokenRequestSchema);
\ No newline at end of file
diff --git a/server/models/User.js b/server/models/User.js
index 387fe614c..59aecfdd8 100644
--- a/server/models/User.js
+++ b/server/models/User.js
@@ -25,3 +25,4 @@ const userSchema = new mongoose.Schema({
});
module.exports = mongoose.model("User", userSchema);
+
diff --git a/server/models/WeeklyReport.js b/server/models/WeeklyReport.js
new file mode 100644
index 000000000..4c60e2aff
--- /dev/null
+++ b/server/models/WeeklyReport.js
@@ -0,0 +1,41 @@
+const mongoose = require("mongoose");
+
+const weeklyReportSchema = new mongoose.Schema({
+ // studentId: {
+ // type: mongoose.Schema.Types.ObjectId,
+ // ref: "User",
+ // required: true,
+ // },
+
+ week: {
+ type: String,
+ required: true,
+ },
+
+ hours: {
+ type: Number,
+ required: true,
+ },
+
+ tasks: {
+ type: String,
+ required: true,
+ },
+
+ lessons: {
+ type: String,
+ required: true,
+ },
+
+ supervisorComments: {
+ type: String,
+ default: "",
+ },
+
+ submittedAt: {
+ type: Date,
+ default: Date.now,
+ },
+});
+
+module.exports = mongoose.model("WeeklyReport", weeklyReportSchema);
diff --git a/server/package.json b/server/package.json
index 7258c54ff..124ec89bb 100644
--- a/server/package.json
+++ b/server/package.json
@@ -14,8 +14,10 @@
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
+ "cron-parser": "^5.1.1",
"dotenv": "^16.4.7",
"express": "^4.21.2",
+ "jsonwebtoken": "^9.0.2",
"mongodb": "^6.14.2",
"mongoose": "^8.13.2",
"node-cron": "^3.0.3",
@@ -27,11 +29,14 @@
"jest": {
"collectCoverage": true,
"collectCoverageFrom": [
- "**/*.js",
+ "**/*.js",
"!node_modules/**",
- "!**/test/**"
+ "!**/test/**"
],
"coverageDirectory": "coverage",
- "coverageReporters": ["text", "lcov"]
+ "coverageReporters": [
+ "text",
+ "lcov"
+ ]
}
}
diff --git a/server/routes/approvalRoutes.js b/server/routes/approvalRoutes.js
new file mode 100644
index 000000000..0d8211b83
--- /dev/null
+++ b/server/routes/approvalRoutes.js
@@ -0,0 +1,10 @@
+const express = require("express");
+const router = express.Router();
+const { getPendingSubmissions, approveSubmission, rejectSubmission } = require("../controllers/approvalController");
+const { isSupervisor } = require("../middleware/authMiddleware");
+
+router.get("/submissions/pending", isSupervisor, getPendingSubmissions);
+router.post("/submissions/:id/approve", isSupervisor, approveSubmission);
+router.post("/submissions/:id/reject", isSupervisor, rejectSubmission);
+
+module.exports = router;
\ No newline at end of file
diff --git a/server/routes/coordinator.js b/server/routes/coordinator.js
new file mode 100644
index 000000000..109c093b6
--- /dev/null
+++ b/server/routes/coordinator.js
@@ -0,0 +1,99 @@
+const express = require("express");
+const router = express.Router();
+const mongoose = require("mongoose");
+const emailService = require("../services/emailService");
+const fs = require("fs");
+const path = require("path");
+
+// Mongoose model for usertokenrequests
+const Request = mongoose.model("usertokenrequests", new mongoose.Schema({
+ fullName: String,
+ ouEmail: String,
+ academicAdvisor: String,
+ status: String,
+ requestedAt: Date
+}));
+
+// === LOGGING FUNCTION ===
+const logPath = path.join(__dirname, "../logs/coordinatorActions.log");
+
+const logAction = (entry) => {
+ const timestamp = new Date().toISOString();
+ const logLine = `[${timestamp}] ${entry}\n`;
+ fs.appendFileSync(logPath, logLine, "utf-8");
+};
+
+// === ROUTES ===
+
+// GET all pending requests
+router.get("/requests", async (req, res) => {
+ try {
+ const requests = await Request.find({ status: "pending" });
+ res.json(requests);
+ } catch (err) {
+ console.error("Error fetching requests:", err);
+ res.status(500).json({ message: "Failed to fetch requests" });
+ }
+});
+
+// APPROVE request
+router.post("/requests/:id/approve", async (req, res) => {
+ try {
+ const request = await Request.findByIdAndUpdate(
+ req.params.id,
+ { status: "approved" },
+ { new: true }
+ );
+
+ if (!request) return res.status(404).json({ message: "Request not found" });
+
+ // Send email to student, advisor, coordinator
+ await emailService.sendEmail({
+ to: [request.ouEmail, request.academicAdvisor, "coordinator@ipms.edu"],
+ subject: "Internship Request Approved",
+ html: `
Hello ${request.fullName},
Your internship request has been approved by the coordinator.
`
+ });
+
+ // Log approval
+ logAction(`[APPROVE] Request ID ${request._id} approved for ${request.ouEmail}`);
+
+ res.json({ message: "Request approved and email sent." });
+ } catch (err) {
+ console.error("Approval error:", err);
+ res.status(500).json({ message: "Approval failed" });
+ }
+});
+
+// REJECT request
+router.post("/requests/:id/reject", async (req, res) => {
+ const { reason } = req.body;
+
+ if (!reason) return res.status(400).json({ message: "Rejection reason required" });
+
+ try {
+ const request = await Request.findByIdAndUpdate(
+ req.params.id,
+ { status: "rejected" },
+ { new: true }
+ );
+
+ if (!request) return res.status(404).json({ message: "Request not found" });
+
+ // Send email to student, advisor, coordinator
+ await emailService.sendEmail({
+ to: [request.ouEmail, request.academicAdvisor, "coordinator@ipms.edu"],
+ subject: "Internship Request Rejected",
+ html: `
Hello ${request.fullName},
Your internship request has been rejected.
Reason: ${reason}
`
+ });
+
+ // Log rejection
+ logAction(`[REJECT] Request ID ${request._id} rejected for ${request.ouEmail} (Reason: ${reason})`);
+
+ res.json({ message: "Request rejected and email sent." });
+ } catch (err) {
+ console.error("Rejection error:", err);
+ res.status(500).json({ message: "Rejection failed" });
+ }
+});
+
+module.exports = router;
diff --git a/server/routes/cronJobRoutes.js b/server/routes/cronJobRoutes.js
new file mode 100644
index 000000000..3e1c6d2c0
--- /dev/null
+++ b/server/routes/cronJobRoutes.js
@@ -0,0 +1,17 @@
+const express = require("express");
+const router = express.Router();
+const cronJobController = require("../controllers/cronJobController");
+
+// Get all cron jobs
+router.get("/", cronJobController.getAllJobs);
+
+// Create a new cron job
+router.post("/", cronJobController.createJob);
+
+// Update a cron job
+router.put("/:id", cronJobController.updateJob);
+
+// Delete a cron job
+router.delete("/:id", cronJobController.deleteJob);
+
+module.exports = router;
diff --git a/server/routes/emailRoutes.js b/server/routes/emailRoutes.js
index 34aa969bd..813726e60 100644
--- a/server/routes/emailRoutes.js
+++ b/server/routes/emailRoutes.js
@@ -2,7 +2,8 @@ const express = require("express");
const router = express.Router();
const emailController = require("../controllers/emailController");
-// Route to send custom emails
+
router.post("/send", emailController.sendEmail);
module.exports = router;
+
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/routes/token.js b/server/routes/token.js
new file mode 100644
index 000000000..d50d2ee5d
--- /dev/null
+++ b/server/routes/token.js
@@ -0,0 +1,111 @@
+const express = require("express");
+const router = express.Router();
+const jwt = require("jsonwebtoken");
+const TokenRequest = require("../models/TokenRequest");
+const emailService = require("../services/emailService");
+
+const JWT_SECRET = process.env.JWT_SECRET;
+const FRONTEND_URL = process.env.FRONTEND_URL;
+
+
+router.post("/request", async (req, res) => {
+ try {
+ const { fullName, ouEmail, password, semester, academicAdvisor,role } = req.body;
+
+ if (!fullName || !ouEmail || !password || !semester) {
+ return res.status(400).json({ error: "All fields are required." });
+ }
+
+
+ const token = jwt.sign({ ouEmail }, JWT_SECRET, { expiresIn: "180d" });
+
+ const request = new TokenRequest({
+ fullName,
+ ouEmail,
+ password,
+ semester,
+ academicAdvisor: role === "student" ? academicAdvisor : "",
+ isStudent: role === "student",
+ token,
+ });
+
+ await request.save();
+
+ const activationLink = `${FRONTEND_URL}/activate/${token}`;
+
+ const emailBody = `
+
Hi ${fullName},
+
Thank you for requesting access to the Internship Program Management System (IPMS).
+
Your activation link:
+
${activationLink}
+
Note: This token will expire in 5 days if not activated.
+
Regards,
IPMS Team
+ `;
+
+ await emailService.sendEmail({
+ to: ouEmail,
+ subject: "Your IPMS Token Activation Link",
+ html: emailBody,
+ });
+
+ res.status(201).json({ message: "Token requested and email sent." });
+ } catch (err) {
+ console.error("Token Request Error:", err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+
+router.get("/activate/:token", async (req, res) => {
+ try {
+ const { token } = req.params;
+ const user = await TokenRequest.findOne({ token });
+
+ if (!user) return res.status(404).json({ error: "Token not found." });
+ if (user.isActivated) return res.status(400).json({ error: "Token already activated." });
+
+ user.isActivated = true;
+ user.activatedAt = new Date();
+ user.status = "Activated";
+
+ await user.save();
+
+ res.json({ message: "Token activated successfully." });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+
+router.post("/login", async (req, res) => {
+ try {
+ const { token } = req.body;
+ const user = await TokenRequest.findOne({ token });
+
+ if (!user) return res.status(404).json({ error: "Invalid token." });
+ if (!user.isActivated) return res.status(403).json({ error: "Token not activated." });
+
+ res.json({ message: "Login successful", user });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+
+router.delete("/deactivate", async (req, res) => {
+ try {
+ const { token } = req.body;
+ const user = await TokenRequest.findOneAndDelete({ token });
+
+ if (!user) {
+ return res.status(404).json({ error: "Token not found." });
+ }
+
+ res.json({ message: "Token deleted successfully." });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+module.exports = router;
+
diff --git a/server/routes/weeklyReportRoutes.js b/server/routes/weeklyReportRoutes.js
new file mode 100644
index 000000000..ecc133dfa
--- /dev/null
+++ b/server/routes/weeklyReportRoutes.js
@@ -0,0 +1,13 @@
+const express = require("express");
+const router = express.Router();
+const reportController = require("../controllers/reportController");
+
+// Routing weekly report actions
+
+// POST - Submit a new weekly report
+router.post("/", reportController.createReport);
+
+// GET - Fetch all reports by a specific student
+router.get("/:userId", reportController.getReportsByStudent);
+
+module.exports = router;
diff --git a/server/services/emailService.js b/server/services/emailService.js
index f777d11b4..86dea5c36 100644
--- a/server/services/emailService.js
+++ b/server/services/emailService.js
@@ -1,12 +1,14 @@
const nodemailer = require("nodemailer");
+const fs = require("fs");
+const path = require("path");
require("dotenv").config();
- /**
- * Simple Email Service for the Internship Management System
- */
+/**
+ * Simple Email Service for the Internship Management System
+ */
class EmailService {
constructor() {
- // Create transporter using SMTP transport
+ // Create transporter using SMTP transport
this.transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST || "smtp.gmail.com",
port: process.env.EMAIL_PORT || 587,
@@ -19,20 +21,44 @@ class EmailService {
this.defaultSender =
process.env.EMAIL_DEFAULT_SENDER || "IPMS
";
+
+ // Define the path for the email log file
+ this.logPath = path.join(__dirname, "../logs/emailLogs.json");
+ }
+
+ // Append email result to log file
+ appendToLog(entry) {
+ let logData = [];
+ try {
+ if (fs.existsSync(this.logPath)) {
+ const existing = fs.readFileSync(this.logPath, "utf8");
+ logData = JSON.parse(existing || "[]");
+ }
+ } catch (err) {
+ console.error("⚠️ Failed to read email log:", err.message);
+ }
+
+ logData.push(entry);
+ try {
+ fs.writeFileSync(this.logPath, JSON.stringify(logData, null, 2), "utf-8");
+ } catch (err) {
+ console.error("⚠️ Failed to write to email log:", err.message);
+ }
}
+
/**
- * Send an email with custom content
- * @param {Object} options - Email options
- * @param {string} options.to - Recipient email(s)
- * @param {string} options.subject - Email subject
- * @param {string} options.html - HTML content of the email
- * @param {string} [options.text] - Plain text version (optional)
- * @param {string} [options.from] - Sender email (defaults to system default)
- * @param {Array} [options.attachments] - Array of attachment objects
- * @param {Array} [options.cc] - Carbon copy recipients
- * @param {Array} [options.bcc] - Blind carbon copy recipients
- * @returns {Promise