diff --git a/client/package.json b/client/package.json index ddbbc14ce..75898d4e2 100644 --- a/client/package.json +++ b/client/package.json @@ -42,6 +42,6 @@ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" - ] + ] } } diff --git a/client/public/OU-IPMS.png b/client/public/OU-IPMS.png new file mode 100644 index 000000000..de5d894a9 Binary files /dev/null and b/client/public/OU-IPMS.png differ diff --git a/client/src/components/Layout.js b/client/src/components/Layout.js index 670ad1c96..c711e16c4 100644 --- a/client/src/components/Layout.js +++ b/client/src/components/Layout.js @@ -6,8 +6,7 @@ function Layout() {
- OU Logo -

IPMS

+ OU Logo
+ {errors.coordinatorSignature} { + if (activeTab === "type") setTypedSignature(""); + }, [activeTab]); + + const validateForm = () => { + const newErrors = {}; + if (!formData.interneeName?.trim()) newErrors.interneeName = "Name is required."; + if (!/^\d{9}$/.test(formData.interneeID || "")) newErrors.interneeID = "Enter a valid 9-digit Sooner ID."; + if (!/\S+@\S+\.\S+/.test(formData.interneeEmail || "")) newErrors.interneeEmail = "Invalid email."; + if (!formData.companyName?.trim()) newErrors.companyName = "Company name is required."; + if (!formData.companyWebsite?.trim()) { + newErrors.companyWebsite = "Company website is required."; + } else if (!/^(https?:\/\/)?(www\.)?[a-zA-Z0-9-]+\.[a-zA-Z]{2,}([/\w.-]*)*\/?$/.test(formData.companyWebsite.trim())) { + newErrors.companyWebsite = "Enter a valid website."; + } + if (!/^\d{10}$/.test(formData.companyPhone || "")) newErrors.companyPhone = "Phone must be 10 digits."; + if (!formData.advisorName?.trim()) newErrors.advisorName = "Advisor name is required."; + if (!formData.advisorTitle?.trim()) newErrors.advisorTitle = "Job title is required."; + if (!/\S+@\S+\.\S+/.test(formData.advisorEmail || "")) newErrors.advisorEmail = "Invalid advisor email."; + if (!formData.presentationDate) newErrors.presentationDate = "Presentation date is required."; + if (!formData.coordinatorSignature) newErrors.coordinatorSignature = "Signature is required."; + ["Presentation Content", "Delivery and Communication", "Answering Questions"].forEach((item) => { + if (!formData[`${item}_rating`]) { + newErrors[`${item}_rating`] = `${item} rating is required.`; + } + }); + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleChange = (field, value) => { + setFormData((prev) => ({ ...prev, [field]: value })); + setErrors((prev) => ({ ...prev, [field]: undefined })); + }; + + const handleSignatureInsert = () => { + if (activeTab === "type" && typedSignature.trim()) { + handleChange("coordinatorSignature", { + type: "text", + value: typedSignature, + font: selectedFont, + }); + setShowModal(false); + } else if (activeTab === "draw") { + const canvas = sigCanvasRef.current; + if (canvas && !canvas.isEmpty()) { + let trimmedCanvas; + try { + trimmedCanvas = canvas.getTrimmedCanvas(); + } catch { + trimmedCanvas = canvas.getCanvas(); + } + const image = trimmedCanvas.toDataURL("image/png"); + handleChange("coordinatorSignature", { type: "draw", value: image }); + setShowModal(false); + } else { + alert("Please draw your signature before inserting."); + } + } + }; + + const renderSignaturePreview = () => { + const sig = formData.coordinatorSignature; + if (!sig) return Click to sign; + if (sig.type === "draw") return Signature; + if (sig.type === "text") return {sig.value}; + return Invalid signature; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!validateForm()) return; + + // Step 1: Prepare `evaluations` array from formData + const evaluations = [ + { + category: "Presentation Content", + rating: formData["Presentation Content_rating"], + comment: formData["Presentation Content_comments"] || "" + }, + { + category: "Delivery and Communication", + rating: formData["Delivery and Communication_rating"], + comment: formData["Delivery and Communication_comments"] || "" + }, + { + category: "Answering Questions", + rating: formData["Answering Questions_rating"], + comment: formData["Answering Questions_comments"] || "" + } + ]; + + // Step 2: Create payload in the format the backend expects + const payload = { + interneeName: formData.interneeName, + interneeID: formData.interneeID, + interneeEmail: formData.interneeEmail, + companyName: formData.companyName, + companyWebsite: formData.companyWebsite, + companyPhone: formData.companyPhone, + advisorName: formData.advisorName, + advisorTitle: formData.advisorTitle, + advisorEmail: formData.advisorEmail, + presentationDate: formData.presentationDate, + evaluations, + coordinatorSignature: formData.coordinatorSignature + }; + + // Step 3: Make POST request + try { + const response = await fetch("http://localhost:5001/api/presentation/a4/submit", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(payload) + }); + + const result = await response.json(); + if (response.ok) { + alert("✅ Form A.4 submitted successfully!"); + setFormData(initialFormState); + setTypedSignature(""); + sigCanvasRef.current?.clear(); + } else { + alert("❌ Submission failed: " + result.error); + console.error(result); + } + } catch (err) { + console.error("❌ Network error:", err); + alert("❌ Failed to submit. Check console for details."); + } + }; + + return ( +
+

A.4 – Internship Coordinator Presentation Evaluation Form

+ + + + +
+
Internee Details
+ + Name + handleChange("interneeName", e.target.value)} isInvalid={!!errors.interneeName} placeholder="Enter full name" /> + {errors.interneeName} + + + Sooner ID + handleChange("interneeID", e.target.value)} isInvalid={!!errors.interneeID} placeholder="Enter 9-digit student ID"/> + {errors.interneeID} + + + Email + handleChange("interneeEmail", e.target.value)} isInvalid={!!errors.interneeEmail} placeholder="Enter student email"/> + {errors.interneeEmail} + +
+ + +
+
Workplace Details
+ + Name + handleChange("companyName", e.target.value)} isInvalid={!!errors.companyName} placeholder="Enter company name"/> + {errors.companyName} + + + Website + handleChange("companyWebsite", e.target.value)} isInvalid={!!errors.companyWebsite} placeholder="Enter company website" /> + {errors.companyWebsite} + + + Phone + handleChange("companyPhone", e.target.value)} isInvalid={!!errors.companyPhone} placeholder="Enter 10-digit phone number" /> + {errors.companyPhone} + +
+ + +
+
Internship Advisor Details
+ + Name + handleChange("advisorName", e.target.value)} isInvalid={!!errors.advisorName} placeholder="Enter advisor's name"/> + {errors.advisorName} + + + Job Title + handleChange("advisorTitle", e.target.value)} isInvalid={!!errors.advisorTitle} placeholder="Enter advisor's job title" /> + {errors.advisorTitle} + + + Email + handleChange("advisorEmail", e.target.value)} isInvalid={!!errors.advisorEmail} placeholder="Enter advisor's email"/> + {errors.advisorEmail} + +
+ +
+ + + + + Presentation Date + handleChange("presentationDate", e.target.value)} isInvalid={!!errors.presentationDate} /> + {errors.presentationDate} + + + + + + + + + + + + + + + + {["Presentation Content", "Delivery and Communication", "Answering Questions"].map((item) => ( + + + + + + + ))} + +
ItemSatisfactoryUnsatisfactoryComments
{item} + handleChange(`${item}_rating`, e.target.value)} required /> + + handleChange(`${item}_rating`, e.target.value)} /> + + handleChange(`${item}_comments`, e.target.value)} /> +
+ +
+ + + + Internship Coordinator Signature +
setShowModal(true) } + > + {renderSignaturePreview()} +
+ {errors.coordinatorSignature} + +
+ +
+ +
+ +
+ + setShowModal(false)} centered> + +
+
Sign Here
+ +
+ + + + + setTypedSignature(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" }}> + {typedSignature || "Your name"} +
+ ))} +
+
+ +
+
+ +
+
Draw here
+ +
+ + +
+
+
+
+
+
+
+
+ ); +} + +export default A4PresentationEvaluationForm; + diff --git a/client/src/pages/CoordinatorDashboard.js b/client/src/pages/CoordinatorDashboard.js index 082ff2478..fc16b60c2 100644 --- a/client/src/pages/CoordinatorDashboard.js +++ b/client/src/pages/CoordinatorDashboard.js @@ -1,12 +1,48 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import axios from "axios"; +import "../styles/dashboard.css"; + +function CoordinatorDashboard() { + const [requests, setRequests] = useState([]); + const navigate = useNavigate(); + + const fetchRequests = async () => { + try { + const res = await axios.get( + `${process.env.REACT_APP_API_URL}/api/coordinator/requests` + ); + setRequests(res.data); + } catch (err) { + console.error("Failed to fetch requests:", err); + } + }; + + useEffect(() => { + fetchRequests(); + }, []); -const CoordinatorDashboard = () => { return ( -
-

Coordinator Dashboard

-

Welcome, Coordinator!

+
+

Coordinator Dashboard

+ + {requests.length === 0 ? ( +

No Pending Requests

+ ) : ( + requests.map((req) => ( +
navigate(`/coordinator/request/${req._id}`)} + > +

{req.student.userName}

+

Email: {req.student.email}

+

Company: {req.workplace.name}

+
+ )) + )}
); -}; +} -export default CoordinatorDashboard; \ No newline at end of file +export default CoordinatorDashboard; diff --git a/client/src/pages/CoordinatorRequestDetailView.js b/client/src/pages/CoordinatorRequestDetailView.js new file mode 100644 index 000000000..8c8a12f58 --- /dev/null +++ b/client/src/pages/CoordinatorRequestDetailView.js @@ -0,0 +1,107 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import axios from "axios"; +import "../styles/CoordinatorRequestDetailView.css"; + +const CoordinatorRequestDetailView = () => { + const { id } = useParams(); + const navigate = useNavigate(); + const [data, setData] = useState(null); + + useEffect(() => { + axios + .get(`${process.env.REACT_APP_API_URL}/api/coordinator/request/${id}`) + .then((res) => setData(res.data)) + .catch((err) => console.log(err)); + }, [id]); + + const handleApprove = async () => { + try { + const res = await axios.post( + `${process.env.REACT_APP_API_URL}/api/coordinator/request/${id}/approve` + ); + alert(res.data.message); + navigate("/coordinator-dashboard"); + } catch (err) { + console.error("Approval failed:", err); + alert("Error approving request."); + } + }; + + const handleReject = async () => { + const reason = prompt("Please enter a reason for rejection:"); + if (!reason) return alert("Rejection reason required!"); + + try { + const res = await axios.post( + `${process.env.REACT_APP_API_URL}/api/coordinator/request/${id}/reject`, + { reason } + ); + alert(res.data.message); + navigate("/coordinator-dashboard"); + } catch (err) { + console.error("Rejection failed:", err); + alert("Error rejecting request."); + } + }; + + if (!data) return

Loading...

; + + const { requestData, supervisorStatus } = data; + + return ( +
+

Internship Request Details

+ +
+

+ Student: {requestData.student.userName} +

+

+ Email: {requestData.student.email} +

+

+ Company: {requestData.workplace.name} +

+

+ Supervisor Status: {supervisorStatus} +

+ +

Tasks & CS Outcomes

+ + + + + + + + + {requestData.tasks.map((task, idx) => ( + + + + + ))} + +
TaskOutcomes
{task.description}{task.outcomes.join(", ")}
+ +
+ + + +
+
+
+ ); +}; + +export default CoordinatorRequestDetailView; diff --git a/client/src/pages/Home.js b/client/src/pages/Home.js index c1fdf9e6c..c8ed3df5d 100644 --- a/client/src/pages/Home.js +++ b/client/src/pages/Home.js @@ -1,5 +1,4 @@ -import React from 'react'; -import { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Link, useNavigate } from "react-router-dom"; import "../styles/App.css"; import { FaEnvelope, FaLock, FaEye, FaEyeSlash } from "react-icons/fa"; @@ -7,17 +6,23 @@ import "../styles/login.css"; import StudentIcon from "../Icons/StudentIcon"; import CoordinatorIcon from "../Icons/CoordinatorIcon"; import SupervisorIcon from "../Icons/SupervisorIcon"; -import Swal from 'sweetalert2'; +import Swal from "sweetalert2"; function Home() { const navigate = useNavigate(); const [formData, setFormData] = useState({ email: "", password: "", - role: "", + + role: "student", }); const [showPassword, setShowPassword] = useState(false); - + const [role] = useState("student"); + + // Sync role into formData.role + useEffect(() => { + setFormData((prev) => ({ ...prev, role })); + }, [role]); const handleInputChange = (e) => { const { name, value } = e.target; @@ -29,9 +34,10 @@ function Home() { const handleSubmit = async (e) => { e.preventDefault(); - + console.log(`${formData.role} sign in attempted`, formData); + const { email: ouEmail, password, role } = formData; - + if (!ouEmail || !password || !role) { return Swal.fire({ icon: "warning", @@ -39,24 +45,36 @@ function Home() { text: "Please fill in all fields to sign in 💫", }); } - + try { - const response = await fetch(`${process.env.REACT_APP_API_URL}/api/token/user-login`, { - method: "POST", - headers: { - "Content-Type": "application/json", + const response = await fetch( + `${process.env.REACT_APP_API_URL}/api/token/user-login`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ouEmail, password, role }), }, - body: JSON.stringify({ ouEmail, password, role }), - }); - + ); + const data = await response.json(); - + if (response.ok) { Swal.fire({ icon: "success", title: "Login Successful 🌟", text: `Welcome back, ${role}!`, }); + + // Redirect user based on role + if (role === "coordinator") { + navigate("/coordinator-dashboard"); + } else if (role === "student") { + navigate("/student-dashboard"); + } else if (role === "supervisor") { + navigate("/supervisor-dashboard"); + } } else { Swal.fire({ icon: "error", @@ -73,8 +91,6 @@ function Home() { }); } }; - - return (
@@ -101,26 +117,20 @@ function Home() { ].map(({ role: r, Icon }) => (
setFormData({ - ...formData, - role: r, - })} + className={`role-card ${ + formData.role === r ? "selected" : "" + }`} + onClick={() => + setFormData({ + ...formData, + role: r, + }) + } >

{r.charAt(0).toUpperCase() + r.slice(1)}

-
))}
@@ -179,17 +189,20 @@ function Home() { marginBottom: "1rem", }} > -