diff --git a/client/src/pages/A3JobEvaluationForm.jsx b/client/src/pages/A3JobEvaluationForm.jsx index cc510f71..beb8c6fa 100644 --- a/client/src/pages/A3JobEvaluationForm.jsx +++ b/client/src/pages/A3JobEvaluationForm.jsx @@ -146,10 +146,24 @@ const A3JobEvaluationForm = () => { // Submit the form to the backend const handleSubmit = async (e) => { e.preventDefault(); + if (!validateForm() || !formData.supervisorAgreement || !formData.coordinatorAgreement) { alert("Please confirm internee details and both signature agreements before submitting."); return; } + + // ✅ Build evaluations array with ALL 9 ITEMS + const evaluations = evaluationItems.slice(0, 3).map((item) => ({ + category: item, + rating: ratings[item], + comment: comments[item] || "" + })); + + if (evaluations.length !== 3 || evaluations.some(ev => !ev.rating)) { + alert("Please complete ratings for the required 3 evaluation items before submitting."); + return; + } + try { const response = await fetch( `${process.env.REACT_APP_API_URL}/api/evaluation`, @@ -161,28 +175,27 @@ const A3JobEvaluationForm = () => { interneeID: formData.interneeID, interneeEmail: formData.interneeEmail, supervisorSignature: formData.supervisorSignature, - coordinatorSignature: formData.coordinatorSignature, supervisorAgreement: formData.supervisorAgreement, + coordinatorSignature: formData.coordinatorSignature, coordinatorAgreement: formData.coordinatorAgreement, - ratings, - comments, + evaluations, + locked: true, // 🔥 ADD THIS LINE }), - } + } ); + if (response.ok) { alert("Evaluation submitted successfully!"); + // Reset form fields setFormData({ interneeName: "", interneeID: "", interneeEmail: "", - supervisorName: "", - supervisorJobTitle: "", - supervisorEmail: "", supervisorSignature: "", supervisorAgreement: false, coordinatorSignature: "", coordinatorAgreement: false, - locked: true, //locked when properly approved + locked: true, }); setRatings({}); setComments({}); @@ -198,6 +211,7 @@ const A3JobEvaluationForm = () => { console.error(err); } }; + // Show preview of signature (text or image) const renderSignaturePreview = (field) => { diff --git a/server/controllers/internshipRequestController.js b/server/controllers/internshipRequestController.js index e03c0c07..f6929ea9 100644 --- a/server/controllers/internshipRequestController.js +++ b/server/controllers/internshipRequestController.js @@ -1,4 +1,4 @@ -const InternshipRequest = require("../models/internshiprequest"); +const InternshipRequest = require("../models/InternshipRequest"); const WeeklyReport = require("../models/WeeklyReport"); exports.getA1ByEmail = async (req, res) => { diff --git a/server/controllers/reportController.js b/server/controllers/reportController.js index c5179546..86985cba 100644 --- a/server/controllers/reportController.js +++ b/server/controllers/reportController.js @@ -1,7 +1,7 @@ const WeeklyReport = require("../models/WeeklyReport"); const SupervisorReview = require("../models/SupervisorReview"); const CoordinatorReview = require("../models/CoordinatorReview"); -const InternshipRequest = require("../models/internshiprequest"); +const InternshipRequest = require("../models/InternshipRequest"); const { sendStudentProgressEmail } = require("../jobs/reminderEmail"); const STATIC_USER_ID = "vikash123"; diff --git a/server/index.js b/server/index.js index 0a94c037..2dfa4b3f 100644 --- a/server/index.js +++ b/server/index.js @@ -14,6 +14,10 @@ const presentationRoutes = require("./routes/presentationRoutes"); const User = require("./models/User"); const Evaluation = require("./models/Evaluation"); + +const fourWeekReportRoutes = require("./routes/fourWeekReportRoutes"); +const path = require("path"); + // Import cron job manager and register jobs const cronJobManager = require("./utils/cronUtils"); const { registerAllJobs } = require("./jobs/registerCronJobs"); @@ -47,6 +51,11 @@ mongoose try { await registerAllJobs(); // Register cronjobs console.log("Cron jobs initialized successfully"); + + //Register your coordinator reminder job here + const { registerCoordinatorReminderJob } = require("./utils/reminderCoordinatorUtils"); + registerCoordinatorReminderJob(); + //await manualCoordinatorReminderTest(); // This runs immediately for testing } catch (error) { console.error("Failed to initialize cron jobs:", error); } @@ -97,30 +106,27 @@ app.post("/api/createUser", async (req, res) => { }); // Temporary API for saving an evaluation -app.post("/api/evaluation", async (req, res) => { +app.post('/api/evaluation', async (req, res) => { try { - const { interneeName, interneeID, interneeEmail, supervisorSignature, supervisorAgreement, coordinatorSignature, coordinatorAgreement, ratings, comments } = req.body; - - //check if there's an existing evaluation for the given interneeID and email - const existingEvaluation = await Evaluation.findOne({ interneeID, interneeEmail }); - - if (existingEvaluation) { - //If evaluation is locked, prevent update - if (existingEvaluation.locked) { - return res.status(400).json({ error: "Evaluation is locked and cannot be modified." }); - } + // ✅ Extract fields directly from req.body + const { + interneeName, + interneeID, + interneeEmail, + supervisorSignature, + supervisorAgreement, + coordinatorSignature, + coordinatorAgreement, + evaluations, + locked, + } = req.body; - //If evaluation is not in 'draft' status, prevent update - if (existingEvaluation.status !== 'draft') { - return res.status(400).json({ error: "This evaluation has already been finalized and cannot be modified." }); - } - } - const evaluations = Object.keys(ratings).map((category) => ({ - category, - rating: ratings[category], - comment: comments[category] || "", - })); + + // ✅ Optional: Validate evaluations exist and have exactly 3 items + if (!evaluations || evaluations.length !== 3) { + return res.status(400).json({ error: 'Exactly 3 evaluation items must be provided' }); + } const newEvaluation = new Evaluation({ interneeName, @@ -131,8 +137,8 @@ app.post("/api/evaluation", async (req, res) => { coordinatorSignature, coordinatorAgreement, evaluations, - locked: false, - }); + locked, + }); await newEvaluation.save(); res.status(201).json({ message: "Evaluation saved successfully!" }); @@ -142,6 +148,7 @@ app.post("/api/evaluation", async (req, res) => { } }); + // Graceful shutdown process.on("SIGINT", async () => { try { diff --git a/server/models/Evaluation.js b/server/models/Evaluation.js index cbf5692e..933b9e5a 100644 --- a/server/models/Evaluation.js +++ b/server/models/Evaluation.js @@ -67,7 +67,8 @@ const evaluationSchema = new mongoose.Schema({ evaluations: { type: [evaluationItemSchema], validate: [arr => arr.length === 3, 'Exactly 3 evaluation items are required'] - }, + } + , supervisorSignature: { type: signatureSchema, diff --git a/server/utils/reminderCoordinatorUtils.js b/server/utils/reminderCoordinatorUtils.js new file mode 100644 index 00000000..f8b8dbb5 --- /dev/null +++ b/server/utils/reminderCoordinatorUtils.js @@ -0,0 +1,72 @@ +// server/utils/reminderCoordinatorUtils.js +// Author: Subhash Chandra +// Reminder system for Coordinator approval on Form A.3 (Sprint 4) +const cronJobManager = require("./cronUtils"); +const Evaluation = require("../models/Evaluation"); +const emailService = require("../services/emailService"); +const logger = require("./logger"); +const dayjs = require("dayjs"); + +function registerCoordinatorReminderJob() { + cronJobManager.registerJob( + "CoordinatorReminder", + "0 9 * * *", // Every day at 9:00 AM (Production cron expression) + async () => { + try { + const threeDaysAgo = dayjs().subtract(3, "day").toDate(); + + // Find evaluations submitted but not yet approved + const pendingEvaluations = await Evaluation.find({ + status: "submitted", + updatedAt: { $lt: threeDaysAgo }, + coordinatorSignature: null // Coordinator hasn't signed yet + }); + + logger.info(`CoordinatorReminder: Found ${pendingEvaluations.length} pending submissions.`); + + for (const evaluation of pendingEvaluations) { + const toEmail = evaluation.coordinatorEmail; // Assuming you have coordinatorEmail in the schema + + if (!toEmail) { + logger.warn(`Skipping reminder: missing coordinator email for evaluation ID ${evaluation._id}`); + continue; + } + + const subject = "Approval Pending: Form A.3 Evaluation Reminder"; + const link = `https://yourdomain.com/coordinator/approval/${evaluation._id}`; // Change link properly if needed + + const html = ` +

Dear Coordinator,

+

This is a reminder to approve the Form A.3 evaluation for student ${evaluation.interneeName}.

+

Please review and approve it at the following link:

+

${link}

+

Thank you,
IPMS Team

+ `; + + try { + await emailService.sendEmail({ + to: toEmail, + subject, + html, + text: `Reminder to approve Form A.3 for ${evaluation.interneeName}. Link: ${link}`, + }); + + logger.info(`CoordinatorReminder: Email sent successfully to ${toEmail} for evaluation ${evaluation._id}`); + } catch (emailError) { + logger.error(`CoordinatorReminder: Failed to send email to ${toEmail}: ${emailError.message}`); + } + } + } catch (err) { + logger.error("CoordinatorReminder job failed: " + err.message); + } + }, + { + timezone: "America/Chicago", + runOnInit: false // Only run daily; don't trigger immediately + } + ); +} + +module.exports = { + registerCoordinatorReminderJob +};