diff --git a/client/src/pages/A3JobEvaluationForm.jsx b/client/src/pages/A3JobEvaluationForm.jsx index 72f3c26d0..582ac6db0 100644 --- a/client/src/pages/A3JobEvaluationForm.jsx +++ b/client/src/pages/A3JobEvaluationForm.jsx @@ -1,79 +1,103 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { Form, Button, Container, Row, Col, Table, Modal, Tab, Nav } from 'react-bootstrap'; -import SignatureCanvas from 'react-signature-canvas'; -import '../styles/A3JobEvaluationForm.css'; +import React, { useState, useRef, useEffect } from "react"; +import { + Form, + Button, + Container, + Row, + Col, + Table, + Modal, + Tab, + Nav, +} from "react-bootstrap"; +import SignatureCanvas from "react-signature-canvas"; +import "../styles/A3JobEvaluationForm.css"; // Fonts used for styled signature typing -const fonts = ['Pacifico', 'Indie Flower', 'Dancing Script', 'Great Vibes', 'Satisfy']; +const fonts = [ + "Pacifico", + "Indie Flower", + "Dancing Script", + "Great Vibes", + "Satisfy", +]; // Evaluation criteria items const evaluationItems = [ - 'Task Execution and Quality', - 'Initiative and Proactiveness', - 'Communication and Collaboration', - 'Time Management and Dependability', - 'Problem Solving and Critical Thinking', - 'Creativity and Innovation', - 'Technical and Industry Specific Skills', - 'Work Ethic and Cultural Fit', - 'Feedback Reception and Implementation' + "Task Execution and Quality", + "Initiative and Proactiveness", + "Communication and Collaboration", + "Time Management and Dependability", + "Problem Solving and Critical Thinking", + "Creativity and Innovation", + "Technical and Industry Specific Skills", + "Work Ethic and Cultural Fit", + "Feedback Reception and Implementation", ]; - const A3JobEvaluationForm = () => { // Form state management const [formData, setFormData] = useState({ - advisorSignature: '', + advisorSignature: "", advisorAgreement: false, - coordinatorSignature: '', + coordinatorSignature: "", coordinatorAgreement: false, }); - // Ratings and comments + // Ratings and comments const [ratings, setRatings] = useState({}); const [comments, setComments] = useState({}); // Modal state const [showModal, setShowModal] = useState(false); - const [activeSignatureTarget, setActiveSignatureTarget] = useState('advisor'); - const [typedSignatures, setTypedSignatures] = useState({ advisor: '', coordinator: '' }); + const [activeSignatureTarget, setActiveSignatureTarget] = useState("advisor"); + const [typedSignatures, setTypedSignatures] = useState({ + advisor: "", + coordinator: "", + }); const [selectedFont, setSelectedFont] = useState(fonts[0]); - const [activeTab, setActiveTab] = useState('type'); + const [activeTab, setActiveTab] = useState("type"); // Signature canvas ref const sigCanvasRef = useRef(null); - // Clear typed signature if tab switches to "type" useEffect(() => { - if (activeTab === 'type') { - setTypedSignatures(prev => ({ ...prev, [activeSignatureTarget]: '' })); + if (activeTab === "type") { + setTypedSignatures((prev) => ({ ...prev, [activeSignatureTarget]: "" })); } }, [activeSignatureTarget, showModal, activeTab]); // Handle form input changes const handleChange = (field, value) => { - setFormData(prev => ({ ...prev, [field]: value })); + setFormData((prev) => ({ ...prev, [field]: value })); }; // Rating selection const handleRatingChange = (item, value) => { - setRatings(prev => ({ ...prev, [item]: value })); + setRatings((prev) => ({ ...prev, [item]: value })); }; // Comment box const handleCommentChange = (item, value) => { - setComments(prev => ({ ...prev, [item]: value })); + setComments((prev) => ({ ...prev, [item]: value })); }; // Handle inserting signature from modal const handleSignatureInsert = () => { - const targetField = activeSignatureTarget === 'advisor' ? 'advisorSignature' : 'coordinatorSignature'; - if (activeTab === 'type' && typedSignatures[activeSignatureTarget].trim()) { + const targetField = + activeSignatureTarget === "advisor" + ? "advisorSignature" + : "coordinatorSignature"; + if (activeTab === "type" && typedSignatures[activeSignatureTarget].trim()) { //handleChange(targetField, JSON.stringify({ type: 'text', value: typedSignatures[activeSignatureTarget], font: selectedFont })); - handleChange(targetField, { type: 'text', value: typedSignatures[activeSignatureTarget], font: selectedFont }); + handleChange(targetField, { + type: "text", + value: typedSignatures[activeSignatureTarget], + font: selectedFont, + }); setShowModal(false); - } else if (activeTab === 'draw') { + } else if (activeTab === "draw") { const canvas = sigCanvasRef.current; if (canvas && !canvas.isEmpty()) { let trimmedCanvas; @@ -83,9 +107,9 @@ const A3JobEvaluationForm = () => { console.warn("getTrimmedCanvas() failed, using full canvas instead."); trimmedCanvas = canvas.getCanvas(); } - const signatureData = trimmedCanvas.toDataURL('image/png'); + const signatureData = trimmedCanvas.toDataURL("image/png"); //handleChange(targetField, JSON.stringify({ type: 'draw', value: signatureData })); - handleChange(targetField, { type: 'draw', value: signatureData }) + handleChange(targetField, { type: "draw", value: signatureData }); setShowModal(false); } else { alert("Please draw your signature before inserting."); @@ -97,143 +121,319 @@ const A3JobEvaluationForm = () => { const handleSubmit = async (e) => { e.preventDefault(); if (!formData.advisorAgreement || !formData.coordinatorAgreement) { - alert('Please confirm both signature agreements before submitting.'); + alert("Please confirm both signature agreements before submitting."); return; } try { - const response = await fetch('http://localhost:5001/api/evaluation', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ formData, ratings, comments }), - }); + const response = await fetch( + `${process.env.REACT_APP_API_URL}/api/evaluation`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ formData, ratings, comments }), + } + ); if (response.ok) { - alert('Evaluation submitted successfully!'); - setFormData({ advisorSignature: '', advisorAgreement: false, coordinatorSignature: '', coordinatorAgreement: false }); + alert("Evaluation submitted successfully!"); + setFormData({ + advisorSignature: "", + advisorAgreement: false, + coordinatorSignature: "", + coordinatorAgreement: false, + }); setRatings({}); setComments({}); - setTypedSignatures({ advisor: '', coordinator: '' }); + setTypedSignatures({ advisor: "", coordinator: "" }); sigCanvasRef.current?.clear(); } else { const err = await response.json(); console.error("Backend returned error:", err); alert(`Submission failed: ${err.error}`); - } + } } catch (err) { - alert('Server error. Please try again.'); + alert("Server error. Please try again."); console.error(err); } }; -// Show preview of signature (text or image) -const renderSignaturePreview = (field) => { + // Show preview of signature (text or image) + const renderSignaturePreview = (field) => { if (!formData[field]) { - return Click to sign; + return Click to sign; } - + let sig = formData[field]; - if (typeof sig === 'string') { + if (typeof sig === "string") { try { sig = JSON.parse(sig); } catch (err) { - return Invalid signature format; + return Invalid signature format; } } - - if (sig.type === 'draw') { - return Signature; + + if (sig.type === "draw") { + return ( + Signature + ); } - if (sig.type === 'text') { - return {sig.value}; + if (sig.type === "text") { + return ( + + {sig.value} + + ); } - - return Unknown signature type; + + return Unknown signature type; }; - + return (
-

A.3 – Job Performance Evaluation

- +

A.3 – Job Performance Evaluation

+
- + + + + + + {evaluationItems.map((item, index) => ( - - - + + + ))}
ItemSatisfactoryUnsatisfactoryComments
ItemSatisfactoryUnsatisfactoryComments
{item} handleRatingChange(item, 'Satisfactory')} required /> handleRatingChange(item, 'Unsatisfactory')} /> handleCommentChange(item, e.target.value)} placeholder="Enter comments" style={{ minWidth: '250px' }} /> + handleRatingChange(item, "Satisfactory")} + required + /> + + + handleRatingChange(item, "Unsatisfactory") + } + /> + + + handleCommentChange(item, e.target.value) + } + placeholder="Enter comments" + style={{ minWidth: "250px" }} + /> +
- + {/* Signature section */} Internship Advisor Signature -
{ setActiveSignatureTarget('advisor'); setShowModal(true); }}> - {renderSignaturePreview('advisorSignature')} +
{ + setActiveSignatureTarget("advisor"); + setShowModal(true); + }} + > + {renderSignaturePreview("advisorSignature")}
- handleChange('advisorAgreement', e.target.checked)} required /> + + handleChange("advisorAgreement", e.target.checked) + } + required + /> Internship Coordinator Signature -
{ setActiveSignatureTarget('coordinator'); setShowModal(true); }}> - {renderSignaturePreview('coordinatorSignature')} +
{ + setActiveSignatureTarget("coordinator"); + setShowModal(true); + }} + > + {renderSignaturePreview("coordinatorSignature")}
- handleChange('coordinatorAgreement', e.target.checked)} required /> + + handleChange("coordinatorAgreement", e.target.checked) + } + required + /> - + {/* Submit button */}
- +
{/* Signature Modal */} - setShowModal(false)} centered dialogClassName="custom-signature-modal"> - + setShowModal(false)} + centered + dialogClassName="custom-signature-modal" + > +
Sign Here
- +
- setTypedSignatures(prev => ({ ...prev, [activeSignatureTarget]: e.target.value }))} className="mb-3" /> + + setTypedSignatures((prev) => ({ + ...prev, + [activeSignatureTarget]: e.target.value, + })) + } + className="mb-3" + />
- {fonts.map(font => ( -
setSelectedFont(font)} style={{ cursor: 'pointer', fontFamily: font, padding: '10px 15px', border: font === selectedFont ? '2px solid #9d2235' : '1px solid #ccc', borderRadius: '10px', fontSize: '24px', backgroundColor: '#fff' }}> - {typedSignatures[activeSignatureTarget] || 'Your name'} + {fonts.map((font) => ( +
setSelectedFont(font)} + style={{ + cursor: "pointer", + fontFamily: font, + padding: "10px 15px", + border: + font === selectedFont + ? "2px solid #9d2235" + : "1px solid #ccc", + borderRadius: "10px", + fontSize: "24px", + backgroundColor: "#fff", + }} + > + {typedSignatures[activeSignatureTarget] || "Your name"}
))}
- +
{/* Draw tab */}
-
Draw here
- +
+ Draw here +
+
- - + +
@@ -246,5 +446,3 @@ const renderSignaturePreview = (field) => { }; export default A3JobEvaluationForm; - - diff --git a/client/src/pages/CoordinatorDashboard.js b/client/src/pages/CoordinatorDashboard.js new file mode 100644 index 000000000..082ff2478 --- /dev/null +++ b/client/src/pages/CoordinatorDashboard.js @@ -0,0 +1,12 @@ +import React from "react"; + +const CoordinatorDashboard = () => { + return ( +
+

Coordinator Dashboard

+

Welcome, Coordinator!

+
+ ); +}; + +export default CoordinatorDashboard; \ No newline at end of file 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/router.js b/client/src/router.js index 3c0852d50..69479cdfd 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -1,5 +1,4 @@ -import React from 'react'; - +import React from "react"; import { createBrowserRouter } from "react-router-dom"; @@ -11,6 +10,8 @@ import Home from "./pages/Home"; import SignUp from "./pages/SignUp"; import NotFound from "./pages/NotFound"; 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,11 +28,19 @@ const router = createBrowserRouter([ path: "signup", element: , }, - // Add more routes as needed { - path: "evaluation", + path: "evaluation", element: , }, + { + path: "supervisor-dashboard", + element: , + }, + { + path: "coordinator-dashboard", + element: , + }, + // Add more routes as needed ], }, ]); 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/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/index.js b/server/index.js index 5ac523f71..cddf72aa6 100644 --- a/server/index.js +++ b/server/index.js @@ -6,10 +6,12 @@ require("dotenv").config(); const emailRoutes = require("./routes/emailRoutes"); const tokenRoutes = require("./routes/token"); +const approvalRoutes = require("./routes/approvalRoutes"); // Import cron job manager and register jobs const cronJobManager = require("./utils/cronUtils"); const { registerAllJobs } = require("./jobs/registerCronJobs"); +const Evaluation = require("./models/Evaluation"); const app = express(); app.use(express.json()); @@ -64,6 +66,7 @@ app.get("/api/message", (req, res) => { app.use("/api/email", emailRoutes); app.use("/api/token", tokenRoutes); +app.use("/api", approvalRoutes); app.post("/api/createUser", async (req, res) => { try { 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/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/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