diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 24d2bb970..309db3621 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,11 +29,17 @@ jobs: server/package.json client/package.json - # Backend build + # Backend build and test - name: Install backend dependencies working-directory: ./server run: npm install + - name: Run backend tests + working-directory: ./server + run: npm test + env: + CI: true + # Frontend build - name: Install frontend dependencies working-directory: ./client @@ -46,6 +52,7 @@ jobs: - name: Verify build succeeded run: | echo "✅ Backend dependencies installed successfully" + echo "✅ Backend tests passed successfully" echo "✅ Frontend built successfully" ## CD diff --git a/.gitignore b/.gitignore index 27e4fb78b..7f5b38cc7 100644 --- a/.gitignore +++ b/.gitignore @@ -135,4 +135,9 @@ dist .yarn/install-state.gz .pnp.* -package-lock.json +# Ignore package-lock.json in all folders +**/package-lock.json +/package-lock.json +/client/package-lock.json +/server/package-lock.json + diff --git a/client/package.json b/client/package.json index 1e86de512..1eeae380b 100644 --- a/client/package.json +++ b/client/package.json @@ -2,6 +2,7 @@ "name": "client", "version": "0.1.0", "private": true, + "proxy": "http://localhost:5001", "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", @@ -12,13 +13,14 @@ "react": "^19.0.0", "react-bootstrap": "^2.10.9", "react-dom": "^19.0.0", + "react-icons": "^5.5.0", "react-router-dom": "^7.4.1", "react-scripts": "5.0.1", "react-signature-canvas": "^1.1.0-alpha.2", "web-vitals": "^2.1.4" }, "scripts": { - "start": "react-scripts start", + "start": "set NODE_OPTIONS=--openssl-legacy-provider && react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" diff --git a/client/src/App.js b/client/src/App.js index aec9d81c4..67991b7cb 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,5 +1,6 @@ import React from 'react'; import { RouterProvider } from "react-router-dom"; + import router from "./router"; import "./styles/App.css"; diff --git a/client/src/Icons/CoordinatorIcon.jsx b/client/src/Icons/CoordinatorIcon.jsx new file mode 100644 index 000000000..0b415ac4c --- /dev/null +++ b/client/src/Icons/CoordinatorIcon.jsx @@ -0,0 +1,34 @@ +import React from 'react'; + +const styles = { + Icon: { + color: '#9d2235', + fill: '#9d2235', + fontSize: '16px', + top: '322px', + left: '929px', + width: '16px', + height: '28px', + }, +}; + +const IconComponent = () => ( + + + + +); + +const defaultProps = { + IconComponent, +}; + +const CoordinatorIcon = (props) => { + return ( + props.IconComponent + ? + : + ); +}; + +export default CoordinatorIcon; \ No newline at end of file diff --git a/client/src/Icons/StudentIcon.jsx b/client/src/Icons/StudentIcon.jsx new file mode 100644 index 000000000..33c7ce506 --- /dev/null +++ b/client/src/Icons/StudentIcon.jsx @@ -0,0 +1,34 @@ +import React from 'react'; + +const styles = { + Icon: { + color: '#9d2235', + fill: '#9d2235', + fontSize: '16px', + top: '322px', + left: '494px', + width: '16px', + height: '28px', + }, +}; + +const IconComponent = () => ( + + + + +); + +const defaultProps = { + IconComponent, +}; + +const StudentIcon = (props) => { + return ( + props.IconComponent + ? + : + ); +}; + +export default StudentIcon; \ No newline at end of file diff --git a/client/src/Icons/SupervisorIcon.jsx b/client/src/Icons/SupervisorIcon.jsx new file mode 100644 index 000000000..21588898e --- /dev/null +++ b/client/src/Icons/SupervisorIcon.jsx @@ -0,0 +1,34 @@ +import React from 'react'; + +const styles = { + Icon: { + color: '#9d2235', + fill: '#9d2235', + fontSize: '23px', + top: '322px', + left: '708px', + width: '23px', + height: '28px', + }, +}; + +const IconComponent = () => ( + + + + +); + +const defaultProps = { + IconComponent, +}; + +const SupervisorIcon = (props) => { + return ( + props.IconComponent + ? + : + ); +}; + +export default SupervisorIcon; \ No newline at end of file diff --git a/client/src/components/Layout.js b/client/src/components/Layout.js index 2ff6d794f..670ad1c96 100644 --- a/client/src/components/Layout.js +++ b/client/src/components/Layout.js @@ -20,6 +20,9 @@ function Layout() {
  • Contact
  • +
  • + Weekly Report +
  • diff --git a/client/src/img/IPMS.webp b/client/src/img/IPMS.webp new file mode 100644 index 000000000..1d02f88a2 Binary files /dev/null and b/client/src/img/IPMS.webp differ diff --git a/client/src/pages/A1InternshipRequestForm.js b/client/src/pages/A1InternshipRequestForm.js new file mode 100644 index 000000000..a7b719ce9 --- /dev/null +++ b/client/src/pages/A1InternshipRequestForm.js @@ -0,0 +1,356 @@ +import React, { useState } from 'react'; +import '../styles/A1InternshipRequestForm.css'; + +const A1InternshipRequestForm = () => { + const initialState = { + interneeName: '', + soonerId: '', + interneeEmail: '', + workplaceName: '', + website: '', + phone: '', + startDate: '', + endDate: '', + advisorName: '', + advisorJobTitle: '', + advisorEmail: '', + interneeSignature: '', + advisorSignature: '', + coordinatorApproval: '', + creditHours: '', + tasks: ['', '', '', '', ''], + outcomes: Array(5).fill(Array(6).fill(false)), + }; + + const [formData, setFormData] = useState(initialState); + const [successMsg, setSuccessMsg] = useState(''); + const [errorMsg, setErrorMsg] = useState(''); + const [dateError, setDateError] = useState(''); + + const handleInputChange = (e) => { + const { id, value } = e.target; + setFormData((prev) => ({ ...prev, [id]: value })); + + // Clear date error when either date field changes + if (id === 'startDate' || id === 'endDate') { + setDateError(''); + + // Validate dates when both are filled + if (formData.startDate && formData.endDate) { + validateDates(id === 'startDate' ? value : formData.startDate, + id === 'endDate' ? value : formData.endDate); + } + } + }; + + const validateDates = (start, end) => { + const startDate = new Date(start); + const endDate = new Date(end); + + if (endDate <= startDate) { + setDateError('End date must be after start date'); + } else { + setDateError(''); + } + }; + + const handleCreditHourChange = (value) => { + setFormData((prev) => ({ ...prev, creditHours: value })); + }; + + const handleTaskChange = (index, value) => { + const updatedTasks = [...formData.tasks]; + updatedTasks[index] = value; + setFormData((prev) => ({ ...prev, tasks: updatedTasks })); + }; + + const handleOutcomeChange = (taskIndex, outcomeIndex) => { + const updatedOutcomes = formData.outcomes.map((row, i) => + i === taskIndex + ? row.map((val, j) => (j === outcomeIndex ? !val : val)) + : row + ); + setFormData((prev) => ({ ...prev, outcomes: updatedOutcomes })); + }; + + const validateForm = () => { + const namePattern = /^[A-Za-z\s]+$/; + const numberPattern = /^[0-9]+$/; + const phonePattern = /^[0-9]{10}$/; + const emailPattern = /^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$/; + + const { + interneeName, soonerId, interneeEmail, workplaceName, phone, + startDate, endDate, advisorName, advisorEmail, + interneeSignature, advisorSignature, coordinatorApproval, + creditHours, tasks, outcomes + } = formData; + + const requiredFieldsFilled = interneeName && soonerId && interneeEmail && + workplaceName && phone && startDate && endDate && + advisorName && advisorEmail && interneeSignature && + advisorSignature && coordinatorApproval && creditHours; + + const patternsValid = namePattern.test(interneeName) && + numberPattern.test(soonerId) && + emailPattern.test(interneeEmail) && + namePattern.test(workplaceName) && + phonePattern.test(phone) && + namePattern.test(advisorName) && + emailPattern.test(advisorEmail) && + namePattern.test(interneeSignature) && + namePattern.test(advisorSignature) && + namePattern.test(coordinatorApproval); + + const tasksFilled = tasks.every(task => task.trim() !== ''); + + const start = new Date(startDate); + const end = new Date(endDate); + const datesValid = end > start; + + if (!datesValid) { + setDateError('End date must be after start date'); + return false; + } + + const outcomesValid = outcomes.every(taskOutcomes => + taskOutcomes.filter(val => val).length >= 4 + ); + + return requiredFieldsFilled && patternsValid && tasksFilled && datesValid && outcomesValid; + }; + + const handleSubmit = (e) => { + e.preventDefault(); + const isValid = validateForm(); + + if (isValid) { + setSuccessMsg('Form submitted successfully!'); + setErrorMsg(''); + submitFormData(formData); + setTimeout(() => setSuccessMsg(''), 3000); + setFormData(initialState); + } else { + setErrorMsg('Please fill all required fields with valid data. Each task must have at least 4 outcomes selected.'); + setSuccessMsg(''); + } + }; + const submitFormData = async () => { + const outcomeMap = { + 0: 'problemSolving', + 1: 'solutionDevelopment', + 2: 'communication', + 3: 'decisionMaking', + 4: 'collaboration', + 5: 'application' + }; + + const tasksWithOutcomes = formData.tasks.map((taskDesc, i) => { + const selectedOutcomes = formData.outcomes[i] + .map((checked, j) => (checked ? outcomeMap[j] : null)) + .filter(Boolean); + return { + description: taskDesc.trim(), + outcomes: selectedOutcomes + }; + }); + + const payload = { + interneeName: formData.interneeName.trim(), + soonerId: formData.soonerId.trim(), + interneeEmail: formData.interneeEmail.trim(), + workplaceName: formData.workplaceName.trim(), + website: formData.website.trim(), + phone: formData.phone.trim(), + startDate: formData.startDate, + endDate: formData.endDate, + advisorName: formData.advisorName.trim(), + advisorJobTitle: formData.advisorJobTitle.trim(), + advisorEmail: formData.advisorEmail.trim(), + interneeSignature: formData.interneeSignature.trim(), + advisorSignature: formData.advisorSignature.trim(), + coordinatorApproval: formData.coordinatorApproval.trim(), + creditHour: formData.creditHours, + tasks: tasksWithOutcomes + }; + + try { + const response = await fetch("http://localhost:5001/api/form/submit", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + await response.json(); + } catch (err) { + console.error(err); + } +}; + + + return ( +
    +

    A.1 - Internship Request Form

    +

    Internee & Workplace Information:

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {[1, 2, 3].map((val) => ( + + ))} + + + +
    Internee DetailsWorkplace DetailsInternship Advisor Details
    Name:
    Name:
    Name:
    Sooner ID:
    Website:
    Job Title:
    Email:
    Phone:
    Email:
    Select the Number of Credit Hours + Start Date:
    + +
    + End Date:
    + + {dateError &&
    {dateError}
    } +
    + {val}
    + handleCreditHourChange(val.toString())} + /> +
    + + {/* Tasks and Outcomes Section */} +

    Task Details & Program Outcomes:

    + + + + + + + + + + + + + + + + + + {formData.tasks.map((task, i) => ( + + + {formData.outcomes[i].map((outcome, j) => ( + + ))} + + ))} + +
    Job Description DetailsProgram Outcome
    +
      +
    1. Tasks need to be filled by the Internship Advisor.
    2. +
    3. Select one or more outcomes per task.
    4. +
    5. All tasks must cover at least 4 outcomes.
    6. +
    +
    Problem SolvingSolution DevelopmentCommunicationDecision-MakingCollaborationApplication
    + Task {i + 1}:
    + handleTaskChange(i, e.target.value)} + className="task" + /> +
    + handleOutcomeChange(i, j)} + className="outcome" + /> +
    + + {/* Signatures */} +

    Signatures:

    + + + + + + + + +
    + Internee Signature
    + +
    + Internship Advisor Signature
    + +
    + Internship Coordinator Approval
    + +
    + +
    + +
    + {successMsg &&
    {successMsg}
    } + {errorMsg &&
    {errorMsg}
    } +
    +
    + ); +}; + +export default A1InternshipRequestForm; \ No newline at end of file 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..1ce88c4e5 --- /dev/null +++ b/client/src/pages/CoordinatorDashboard.js @@ -0,0 +1,97 @@ +import React, { useEffect, useState } from "react"; +import "../styles/dashboard.css"; + +function CoordinatorDashboard() { + const [requests, setRequests] = useState([]); + + useEffect(() => { + const fetchRequests = async () => { + try { + const res = await fetch("/api/coordinator/requests"); + const data = await res.json(); + setRequests(data); + } catch (err) { + console.error("Failed to fetch requests:", err); + } + }; + + fetchRequests(); + }, []); + + const handleApprove = async (_id) => { + try { + const res = await fetch(`/api/coordinator/requests/${_id}/approve`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + }); + + const result = await res.json(); + alert(result.message); + } catch (err) { + console.error("Approval failed:", err); + alert("Error approving request."); + } + }; + + const handleReject = async (_id) => { + const reason = prompt("Enter rejection reason:"); + if (!reason) return; + + try { + const res = await fetch(`/api/coordinator/requests/${_id}/reject`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ reason }), + }); + + const result = await res.json(); + alert(result.message); + } catch (err) { + console.error("Rejection failed:", err); + alert("Error rejecting request."); + } + }; + + // 🔢 Calculate remaining days until expiration + const daysRemaining = (expiresAt) => { + const now = new Date(); + const due = new Date(expiresAt); + const diff = Math.ceil((due - now) / (1000 * 60 * 60 * 24)); + return diff; + }; + + return ( +
    +

    Coordinator Dashboard

    +

    Review and manage internship requests.

    + +
    + {requests.map((req) => ( +
    +

    {req.fullName}

    +

    Email: {req.ouEmail}

    +

    Advisor: {req.academicAdvisor}

    +

    Status: {req.status}

    +

    Requested At: {new Date(req.requestedAt).toLocaleDateString()}

    +

    Expires In: + + {daysRemaining(req.expiresAt)} days + +

    + +
    + + +
    +
    + ))} +
    +
    + ); +} + +export default CoordinatorDashboard; diff --git a/client/src/pages/Home.js b/client/src/pages/Home.js index 6068e1027..fd105af53 100644 --- a/client/src/pages/Home.js +++ b/client/src/pages/Home.js @@ -1,15 +1,26 @@ -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"; +import "../styles/login.css"; +import StudentIcon from "../Icons/StudentIcon"; +import CoordinatorIcon from "../Icons/CoordinatorIcon"; +import SupervisorIcon from "../Icons/SupervisorIcon"; function Home() { const navigate = useNavigate(); const [formData, setFormData] = useState({ email: "", password: "", - role: "Student", + role: "student", }); + const [showPassword, setShowPassword] = useState(false); + const [role, setRole] = useState("student"); + + // Sync role into formData.role + useEffect(() => { + setFormData((prev) => ({ ...prev, role })); + }, [role]); const handleInputChange = (e) => { const { name, value } = e.target; @@ -22,6 +33,17 @@ function Home() { const handleSubmit = (e) => { e.preventDefault(); console.log(`${formData.role} sign in attempted`, formData); + + // Redirect user based on role + if (formData.role === "coordinator") { + navigate("/coordinator/dashboard"); + } else if (formData.role === "student") { + navigate("/student/dashboard"); + } else if (formData.role === "supervisor") { + navigate("/supervisor/dashboard"); + } else { + alert("Please select a valid role."); + } }; return ( @@ -32,48 +54,81 @@ function Home() {
    -

    Sign in to continue

    +

    Welcome back

    - - + +
    + {[ + { role: "student", Icon: StudentIcon }, + { role: "supervisor", Icon: SupervisorIcon }, + { role: "coordinator", Icon: CoordinatorIcon }, + ].map(({ role: r, Icon }) => ( +
    setRole(r)} + > + +

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

    +
    + ))} +
    -
    - +
    +
    -
    - - +
    + +
    + + setShowPassword(!showPassword)} + > + {showPassword ? : } + +
    +
    + +
    + + + Forgot password? +
    -
    +
    Don't have an account? { e.preventDefault(); navigate("/signup"); }} + style={{ + color: "#7f1d1d", + fontWeight: "600", + marginLeft: "4px", + textDecoration: "underline", + }} > - {" "} - Sign Up + Sign up for free
    @@ -99,4 +159,4 @@ function Home() { ); } -export default Home; +export default Home; \ No newline at end of file diff --git a/client/src/pages/SignUp.js b/client/src/pages/SignUp.js index de2af4541..23a4a282e 100644 --- a/client/src/pages/SignUp.js +++ b/client/src/pages/SignUp.js @@ -1,31 +1,85 @@ import React, { useState } from "react"; import axios from "axios"; import { useNavigate } from "react-router-dom"; +import "../styles/signup.css"; +import StudentIcon from "../Icons/StudentIcon"; +import CoordinatorIcon from "../Icons/CoordinatorIcon"; +import SupervisorIcon from "../Icons/SupervisorIcon"; +import { FaEye, FaEyeSlash } from "react-icons/fa"; function SignUp() { const navigate = useNavigate(); - const [userName, setUserName] = useState(""); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); const [role, setRole] = useState("-"); const [responseMessage, setResponseMessage] = useState(""); + const [step, setStep] = useState(1); + + const [fullName, setFullName] = useState(""); + const [ouEmail, setOuEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [passwordStrength, setPasswordStrength] = useState(""); + const [semester, setSemester] = useState(""); + const [academicAdvisor, setAcademicAdvisor] = useState(""); + const [agreed, setAgreed] = useState(false); + + const togglePasswordVisibility = () => { + setShowPassword(!showPassword); + }; + + const validatePassword = (value) => { + setPassword(value); + + const hasNumber = /\d/.test(value); + const hasSymbol = /[!@#$%^&*(),.?":{}|<>]/.test(value); + const hasUpper = /[A-Z]/.test(value); + const hasLower = /[a-z]/.test(value); + + if (value.length >= 8 && hasNumber && hasSymbol && hasUpper && hasLower) { + setPasswordStrength("Strong"); + } else if (value.length >= 6 && (hasNumber || hasSymbol)) { + setPasswordStrength("Medium"); + } else { + setPasswordStrength("Weak"); + } + }; + + const passwordsMatch = password === confirmPassword; const createUser = async (e) => { + console.log("createUser() called"); + e.preventDefault(); try { const response = await axios.post( - `${process.env.REACT_APP_API_URL}/api/createUser`, + `${process.env.REACT_APP_API_URL}/api/token/request`, { - userName, - email, + fullName, + ouEmail, password, + semester, + academicAdvisor: role === "student" ? academicAdvisor : "", role, } ); - setResponseMessage(response.data.message); + console.log("Signup response:", response.data); + // if (response.data && response.data.user) { + // localStorage.setItem("user", JSON.stringify({ user: response.data.user })); + // } + + if (role === "student") { + setResponseMessage("Token requested and email sent."); + } else { + setResponseMessage("Account created successfully."); + } + // Reset form after successful submission - setEmail(""); + setFullName(""); + setOuEmail(""); setPassword(""); + setConfirmPassword(""); + setSemester(""); + setAcademicAdvisor(""); // Redirect to home after successful signup setTimeout(() => { @@ -41,67 +95,250 @@ function SignUp() { return (
    -

    Sign Up for IPMS

    -

    - Create your account to access the Internship Program Management System -

    - {responseMessage && (
    {responseMessage}
    )}
    -
    - - -
    - -
    - - setUserName(e.target.value)} - required - /> -
    - -
    - - setEmail(e.target.value)} - required - /> -
    - -
    - - setPassword(e.target.value)} - required - /> -
    - - + {step === 1 && ( +
    +

    Create Your Account

    +

    + Who are you? Select your role to continue +

    + +
    + {[ + { role: "student", Icon: StudentIcon }, + { role: "supervisor", Icon: SupervisorIcon }, + { role: "coordinator", Icon: CoordinatorIcon }, + ].map(({ role: r, Icon }) => ( +
    setRole(r)} + > + +

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

    + +
    + ))} +
    + + +
    + )} + + {step === 2 && ( + <> +
    +

    + Welcome +

    +

    + To proceed, enter your details below +

    +
    +
    + + +
    + You selected: {role.charAt(0).toUpperCase() + role.slice(1)} +
    +
    + +
    + + setFullName(e.target.value)} + placeholder="Enter your full name" + required + /> +
    + +
    + + setOuEmail(e.target.value)} + placeholder="Enter your university email" + required + /> +
    +
    +
    + +
    + validatePassword(e.target.value)} + placeholder="Enter your password" + required + className="form-control" + /> + + {showPassword ? : } + +
    + + {password && ( + <> +
    + {[1, 2, 3].map((index) => ( +
    + ))} +
    +

    + {passwordStrength === "Strong" + ? "Strong" + : passwordStrength === "Medium" + ? "Weak" + : "Very Weak"} +

    + + )} +
    + +
    + + setConfirmPassword(e.target.value)} + placeholder="Re-enter your password" + required + className="form-control" + /> + {!passwordsMatch && confirmPassword.length > 0 && ( +

    Passwords do not match!

    + )} +
    +
    + +
    + + +
    + + {role === "student" && ( +
    + + setAcademicAdvisor(e.target.value)} + placeholder="Enter your academic advisor's name" + required + /> +
    + )} + +
    + +
    + + + + )}
    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

    +
    +
    + + +
    + +
    + +