diff --git a/client/src/pages/A3JobEvaluationForm.jsx b/client/src/pages/A3JobEvaluationForm.jsx index cb63ddf3..8f8797d6 100644 --- a/client/src/pages/A3JobEvaluationForm.jsx +++ b/client/src/pages/A3JobEvaluationForm.jsx @@ -46,8 +46,9 @@ const A3JobEvaluationForm = () => { coordinatorSignature: "", coordinatorAgreement: false, }); + // const [advisorDetails, setAdvisorDetails] = useState(null); const [errors, setErrors] = useState({}); - + // Ratings and comments const [ratings, setRatings] = useState({}); const [comments, setComments] = useState({}); @@ -172,6 +173,9 @@ const A3JobEvaluationForm = () => { interneeName: "", interneeID: "", interneeEmail: "", + advisorName: "", + advisorJobTitle: "", + advisorEmail: "", advisorSignature: "", advisorAgreement: false, coordinatorSignature: "", @@ -231,7 +235,7 @@ const A3JobEvaluationForm = () => { style={{ backgroundColor: "#fff", maxWidth: "900px", width: "100%" }} >
- +
Internee Details
@@ -239,7 +243,6 @@ const A3JobEvaluationForm = () => { Name handleChange("interneeName", e.target.value)} isInvalid={!!errors.interneeName} placeholder="Enter full name" style={{ maxWidth: "300px" }}/> {errors.interneeName} - Sooner ID @@ -252,8 +255,24 @@ const A3JobEvaluationForm = () => { {errors.interneeEmail}
+
+
Internship Advisor Details
+ + Name + + + + Job Title + + + + Email + + +
+ diff --git a/server/index.js b/server/index.js index 80b7fe97..8352e8a0 100644 --- a/server/index.js +++ b/server/index.js @@ -22,6 +22,12 @@ const fourWeekReportRoutes = require("./routes/fourWeekReportRoutes"); const path = require("path"); + +const cronJobRoutes = require("./routes/cronJobRoutes"); + +// Author Subhash Chandra: Form A3 Reminder Job Logic +const { registerReminderA3Job } = require("./utils/reminderA3Utils"); + const app = express(); app.use(express.json()); app.use(cors()); @@ -94,9 +100,7 @@ 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 }); } }); @@ -139,9 +143,6 @@ app.post("/api/evaluation", async (req, res) => { } }); - - - //Form A.4 const presentationRoutes = require("./routes/presentationRoutes"); app.use("/api/presentation", presentationRoutes); @@ -159,4 +160,4 @@ process.on("SIGINT", async () => { }); const PORT = process.env.PORT || 5001; -app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); \ No newline at end of file +app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); diff --git a/server/jobs/cronJobsConfig.js b/server/jobs/cronJobsConfig.js index 69829511..5222f6ee 100644 --- a/server/jobs/cronJobsConfig.js +++ b/server/jobs/cronJobsConfig.js @@ -1,5 +1,12 @@ const CronJob = require("../models/CronJob"); -const { coordinatorReminder, supervisorReminder } = require("./reminderEmail"); + +// Import ALL required reminder jobs from all branches +const { + coordinatorReminder, + supervisorReminder, + evaluationReminder, +} = require("./reminderEmail"); + const { checkAndSendReminders } = require("./tokenExpiryCheck"); const autoDeactivateCronjobs = require("./autoDeactivateCronjobs"); @@ -26,6 +33,10 @@ async function getCronJobs() { schedule: job.schedule, job: async () => { try { + await CronJob.findByIdAndUpdate(job._id, { + lastRun: new Date(), + }); + await jobFunctions[job.name](); // Update last execution time await CronJob.findByIdAndUpdate(job._id, { lastRun: new Date(), diff --git a/server/jobs/reminderEmail.js b/server/jobs/reminderEmail.js index c0435d25..cf81e622 100644 --- a/server/jobs/reminderEmail.js +++ b/server/jobs/reminderEmail.js @@ -152,7 +152,43 @@ const supervisorReminder = async () => { } }; +const Evaluation = require("../models/Evaluation"); + +const evaluationReminder = async () => { + try { + const pendingEvals = await Evaluation.find({ + evaluations: { $exists: false }, + advisorAgreement: true, + }); + + for (const evalDoc of pendingEvals) { + const emailHtml = ` +

Dear Supervisor,

+

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

+ +

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

+

Complete A.3 Evaluation

+

Thanks,
IPMS Team

+ `; + + await emailService.sendEmail({ + to: evalDoc.interneeEmail, // or supervisor's email if you have it + subject: "Reminder: Pending A.3 Evaluation Submission", + html: emailHtml, + }); + + console.log(`✅ Reminder sent for: ${evalDoc.interneeName}`); + } + } catch (error) { + console.error("❌ Error sending A.3 reminders:", error); + } +}; + module.exports = { - coordinatorReminder, - supervisorReminder, + coordinatorReminder, + supervisorReminder, + evaluationReminder, }; diff --git a/server/models/Evaluation.js b/server/models/Evaluation.js index 4f838107..e37e8cce 100644 --- a/server/models/Evaluation.js +++ b/server/models/Evaluation.js @@ -1,29 +1,54 @@ const mongoose = require('mongoose'); const formMetadata = require('./FormMetadata'); +// Signature schema for both advisor and coordinator const signatureSchema = new mongoose.Schema({ - type: { type: String, enum: ['text', 'draw'], required: true }, - value: { type: String, required: true }, - font: { type: String } + type: { + type: String, + enum: ['text', 'draw'], + required: true + }, + value: { + type: String, + required: true + }, + font: { + type: String // used only if type is 'text' + } }, { _id: false }); +// Individual evaluation item schema const evaluationItemSchema = new mongoose.Schema({ category: { type: String, - required: true, + required: true }, rating: { type: String, enum: ['Satisfactory', 'Unsatisfactory'], required: true }, - comment: { type: String, maxlength: 500 } + comment: { + type: String, + maxlength: 500 + } }, { _id: false }); +// Main evaluation schema const evaluationSchema = new mongoose.Schema({ ...formMetadata, - interneeId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: false }, - internshipId: { type: mongoose.Schema.Types.ObjectId, ref: 'Internship', required: false }, + + interneeId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: false + }, + + internshipId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Internship', + required: false + }, interneeName: { type: String, @@ -36,13 +61,13 @@ const evaluationSchema = new mongoose.Schema({ interneeID: { type: String, required: true, - match: [/^\d{9}$/, 'Sooner ID must be a 9-digit number'] // Sooner ID validation + match: [/^\d{9}$/, 'Sooner ID must be a 9-digit number'] }, interneeEmail: { type: String, required: true, - match: [/\S+@\S+\.\S+/, 'Invalid email format'], // Email format validation + match: [/\S+@\S+\.\S+/, 'Invalid email format'], lowercase: true, trim: true }, @@ -52,13 +77,39 @@ const evaluationSchema = new mongoose.Schema({ validate: [arr => arr.length > 0, 'At least one evaluation item is required'] }, - advisorSignature: { type: signatureSchema, required: true }, - advisorAgreement: { type: Boolean, required: true }, - coordinatorSignature: { type: signatureSchema, required: true }, - coordinatorAgreement: { type: Boolean, required: true } + advisorSignature: { + type: signatureSchema, + required: true + }, + + advisorAgreement: { + type: Boolean, + required: true + }, + + coordinatorSignature: { + type: signatureSchema, + required: true + }, + + coordinatorAgreement: { + type: Boolean, + required: true + }, + + status: { + type: String, + enum: ['draft', 'submitted'], + default: 'draft' + }, + + submittedAt: { + type: Date + } }, { timestamps: true }); +// Unique index to prevent duplicate submissions evaluationSchema.index({ interneeID: 1, internshipId: 1 }); -module.exports = mongoose.model('Evaluation', evaluationSchema); \ No newline at end of file +module.exports = mongoose.model('Evaluation', evaluationSchema); diff --git a/server/routes/evaluationRoutes.js b/server/routes/evaluationRoutes.js new file mode 100644 index 00000000..aaae5463 --- /dev/null +++ b/server/routes/evaluationRoutes.js @@ -0,0 +1,43 @@ +const express = require('express'); +const router = express.Router(); +const Evaluation = require('../models/Evaluation'); + +// POST: Submit Evaluation Form A.3 +router.post('/submit', async (req, res) => { + try { + const data = req.body; + + // Basic validation: required fields check + if ( + !data.interneeName || + !data.interneeID || + !data.interneeEmail || + !data.evaluations || + data.evaluations.length !== 3 + ) { + return res.status(400).json({ message: 'Missing required fields or invalid number of evaluations' }); + } + + const evaluation = new Evaluation({ + interneeName: data.interneeName, + interneeID: data.interneeID, + interneeEmail: data.interneeEmail, + evaluations: data.evaluations, + advisorSignature: data.advisorSignature, + advisorAgreement: data.advisorAgreement, + coordinatorSignature: data.coordinatorSignature, + coordinatorAgreement: data.coordinatorAgreement, + status: 'submitted', + submittedAt: new Date() + }); + + await evaluation.save(); + res.status(201).json({ message: 'Evaluation submitted successfully' }); + + } catch (err) { + console.error('Error submitting evaluation:', err); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +module.exports = router; diff --git a/server/utils/reminderA3Utils.js b/server/utils/reminderA3Utils.js new file mode 100644 index 00000000..7853a387 --- /dev/null +++ b/server/utils/reminderA3Utils.js @@ -0,0 +1,79 @@ +// Author: Subhash Chandra +// server/utils/reminderA3Utils.js + +const cronJobManager = require("./cronUtils"); +const Evaluation = require("../models/Evaluation"); +const emailService = require("../services/emailService"); +const logger = require("./logger"); +const dayjs = require("dayjs"); + +// Register the Reminder Job for Form A.3 +async function registerReminderA3Job() { + cronJobManager.registerJob( + "A3Reminder", + "0 9 * * *", // ⏰ Every day at 9:00 AM (Production) + async () => { + try { + const threeDaysAgo = dayjs().subtract(3, "day").toDate(); + const oneDayAgo = dayjs().subtract(1, "day").toDate(); + + // Find draft evaluations older than 3 days and not reminded in last 1 day + const draftEvaluations = await Evaluation.find({ + status: "draft", + createdAt: { $lt: threeDaysAgo }, + $or: [ + { lastReminderSentAt: { $exists: false } }, + { lastReminderSentAt: { $lt: oneDayAgo } } + ] + }); + + logger.info(`Found ${draftEvaluations.length} draft Form A.3 evaluations older than 3 days.`); + + for (const evaluation of draftEvaluations) { + const toEmail = evaluation.interneeEmail; + + if (!toEmail) { + logger.warn(`Skipping reminder: missing email for evaluation ID: ${evaluation._id}`); + continue; + } + + const subject = `Reminder: Final Job Evaluation Form A.3 Pending Submission`; + const link = "https://yourdomain.com/form/A3"; // 🔒 Replace with production link + + const html = ` +

Dear ${evaluation.interneeName || "Internee"},

+

This is a reminder to submit your Final Job Performance Evaluation (Form A.3).

+

Please complete it as soon as possible by visiting the form:

+ ${link} +

Regards,
IPMS Team

+ `; + + await emailService.sendEmail({ + to: toEmail, + subject, + html, + text: `Please complete your A.3 form: ${link}`, + }); + + logger.info(`Reminder email sent to: ${toEmail}`); + + // ✅ Update reminder timestamp only (safe update, no validation triggered) + await Evaluation.updateOne( + { _id: evaluation._id }, + { $set: { lastReminderSentAt: new Date() } } + ); + } + } catch (err) { + logger.error("Reminder job for A.3 failed: " + err.message); + } + }, + { + timezone: "America/Chicago", + runOnInit: false // 🚫 Do not trigger immediately in production + } + ); +} + +module.exports = { + registerReminderA3Job +};