diff --git a/client/package.json b/client/package.json index 75898d4e..f8e56288 100644 --- a/client/package.json +++ b/client/package.json @@ -9,6 +9,7 @@ "@testing-library/user-event": "^13.5.0", "axios": "^1.8.2", "bootstrap": "^5.3.5", + "client": "file:", "react": "^19.0.0", "react-bootstrap": "^2.10.9", "react-dom": "^19.0.0", @@ -18,7 +19,9 @@ "react-signature-canvas": "^1.1.0-alpha.2", "react-toastify": "^11.0.5", "sweetalert2": "^11.17.2", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "date-fns": "^4.1.0", + "react-datepicker": "^8.3.0" }, "scripts": { "start": "react-scripts start", @@ -42,6 +45,6 @@ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" - ] + ] } } diff --git a/client/src/pages/A1InternshipRequestForm.js b/client/src/pages/A1InternshipRequestForm.js index 350e0d4c..d5e08f69 100644 --- a/client/src/pages/A1InternshipRequestForm.js +++ b/client/src/pages/A1InternshipRequestForm.js @@ -1,11 +1,12 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import "../styles/A1InternshipRequestForm.css"; + const outcomeLabels = [ "Problem Solving", "Solution Development", "Communication", - "Decision-Making", + "Decision Making", "Collaboration", "Application", ]; @@ -19,7 +20,75 @@ const outcomeDescriptions = [ "Apply computer science algorithms to create practical solutions", ]; -const A1InternshipRequestForm = () => { +// Signature font options +const signatureFonts = [ + { name: "Dancing Script", class: "font-dancing-script" }, + { name: "Great Vibes", class: "font-great-vibes" }, + { name: "Pacifico", class: "font-pacifico" }, + { name: "Satisfy", class: "font-satisfy" }, + { name: "Caveat", class: "font-caveat" } +]; + +// Signature Font Picker Component +const SignatureInput = ({ id, value, onChange, disabled, placeholder }) => { + const [showFonts, setShowFonts] = useState(false); + const [selectedFont, setSelectedFont] = useState(signatureFonts[0].class); + const [nameInput, setNameInput] = useState(value); + + useEffect(() => { + setNameInput(value); + }, [value]); + + const handleNameChange = (e) => { + setNameInput(e.target.value); + onChange({ target: { id, value: e.target.value } }); + }; + + const selectFont = (fontClass) => { + setSelectedFont(fontClass); + setShowFonts(false); + }; + + return ( +
+ !disabled && setShowFonts(true)} + /> + {showFonts && nameInput && ( +
+
Select a signature style:
+ {signatureFonts.map((font) => ( +
selectFont(font.class)} + > + {nameInput} +
+ ))} +
+ )} + {nameInput && ( +
+ {nameInput} + +
+ )} +
+ ); +}; + +const A1InternshipRequestForm = ({ userRole = "student" }) => { const initialState = { interneeName: "", soonerId: "", @@ -36,7 +105,9 @@ const A1InternshipRequestForm = () => { advisorSignature: "", coordinatorApproval: "", creditHours: "", - tasks: Array(5).fill({ description: "" }), + tasks: Array(5).fill({ description: "", outcomes: [] }), // Updated for outcomes + supervisorComments: "", + coordinatorComments: "", }; const [formData, setFormData] = useState(initialState); @@ -44,10 +115,22 @@ const A1InternshipRequestForm = () => { const [errors, setErrors] = useState({}); const [dateError, setDateError] = useState(""); + const isFieldEditable = (fieldType) => { + switch (userRole) { + case "student": + return !["advisorSignature", "coordinatorApproval", "supervisorComments", "coordinatorComments"].includes(fieldType); + case "supervisor": + return ["advisor", "supervisorComments"].includes(fieldType); + case "coordinator": + return ["coordinator", "coordinatorComments", "advisor"].includes(fieldType); + default: + return true; + } + }; + const handleInputChange = (e) => { const { id, value } = e.target; setFormData((prev) => ({ ...prev, [id]: value })); - if (id === "startDate" || id === "endDate") { setDateError(""); if (formData.startDate && formData.endDate) { @@ -57,20 +140,24 @@ const A1InternshipRequestForm = () => { ); } } + if (errors[id]) { + setErrors(prev => { + const newErrors = { ...prev }; + delete newErrors[id]; + return newErrors; + }); + } }; 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(""); - } + if (endDate <= startDate) setDateError("End date must be after start date"); + else setDateError(""); }; - const handleCreditHourChange = (value) => { - setFormData((prev) => ({ ...prev, creditHours: value })); + const handleCreditHourChange = (e) => { + setFormData((prev) => ({ ...prev, creditHours: e.target.value })); }; const handleTaskChange = (index, value) => { @@ -79,56 +166,83 @@ const A1InternshipRequestForm = () => { setFormData((prev) => ({ ...prev, tasks: updatedTasks })); }; + useEffect(() => { + const timeout = setTimeout(() => { + const descriptions = formData.tasks.map((task) => task.description.trim()).filter(Boolean); + if (descriptions.length > 0) { + fetch(`${process.env.REACT_APP_API_URL}/api/align-outcomes`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ tasks: descriptions }), + }) + .then((res) => res.json()) + .then((data) => { + const updatedTasks = formData.tasks.map((task) => { + const match = data.results.find((r) => r.task === task.description); + return match ? { ...task, outcomes: match.matched_outcomes } : { ...task, outcomes: [] }; + }); + setFormData((prev) => ({ ...prev, tasks: updatedTasks })); + }) + .catch((err) => console.error("Outcome alignment error:", err)); + } + }, 500); + return () => clearTimeout(timeout); + }, [formData.tasks]); + + const renderOutcomeCell = (task, outcome, key) => { + const normalizedOutcome = outcome.charAt(0).toLowerCase() + outcome.replace(/\s+/g, "").slice(1); + const isMatched = task.outcomes.includes(normalizedOutcome); + return ( + + + + ); + }; + 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 newErrors = {}; if (!formData.interneeName) newErrors.interneeName = "Internee name is required"; else if (!namePattern.test(formData.interneeName)) newErrors.interneeName = "Name should contain only letters and spaces"; - if (!formData.soonerId) newErrors.soonerId = "Sooner ID is required"; else if (!numberPattern.test(formData.soonerId)) newErrors.soonerId = "Sooner ID should be numeric"; - if (!formData.interneeEmail) newErrors.interneeEmail = "Email is required"; else if (!emailPattern.test(formData.interneeEmail)) newErrors.interneeEmail = "Invalid email format"; - if (!formData.workplaceName) newErrors.workplaceName = "Workplace name is required"; else if (!namePattern.test(formData.workplaceName)) newErrors.workplaceName = "Workplace name should contain only letters and spaces"; - + if (formData.website && !formData.website.includes('.')) newErrors.website = "Please enter a valid website address"; if (!formData.phone) newErrors.phone = "Phone is required"; else if (!phonePattern.test(formData.phone)) newErrors.phone = "Phone must be 10 digits"; - if (!formData.startDate) newErrors.startDate = "Start date is required"; if (!formData.endDate) newErrors.endDate = "End date is required"; - else if (formData.startDate && formData.endDate) { - const start = new Date(formData.startDate); - const end = new Date(formData.endDate); - if (end <= start) newErrors.endDate = "End date must be after start date"; - } - - if (!formData.advisorName) newErrors.advisorName = "Advisor name is required"; - else if (!namePattern.test(formData.advisorName)) newErrors.advisorName = "Advisor name should contain only letters and spaces"; - - if (!formData.advisorEmail) newErrors.advisorEmail = "Advisor email is required"; - else if (!emailPattern.test(formData.advisorEmail)) newErrors.advisorEmail = "Invalid advisor email format"; - + if (!formData.advisorName) newErrors.advisorName = "Supervisor name is required"; + else if (!namePattern.test(formData.advisorName)) newErrors.advisorName = "Supervisor name should contain only letters and spaces"; + if (!formData.advisorEmail) newErrors.advisorEmail = "Supervisor email is required"; + else if (!emailPattern.test(formData.advisorEmail)) newErrors.advisorEmail = "Invalid supervisor email format"; if (!formData.interneeSignature) newErrors.interneeSignature = "Internee signature is required"; else if (!namePattern.test(formData.interneeSignature)) newErrors.interneeSignature = "Signature should contain only letters and spaces"; - if (formData.advisorSignature && !namePattern.test(formData.advisorSignature)) { newErrors.advisorSignature = "Signature should contain only letters and spaces"; } - if (formData.coordinatorApproval && !namePattern.test(formData.coordinatorApproval)) { newErrors.coordinatorApproval = "Approval should contain only letters and spaces"; } - if (!formData.creditHours) newErrors.creditHours = "Please select credit hours"; - const tasksFilled = formData.tasks.filter((task) => task.description.trim() !== "").length >= 3; if (!tasksFilled) newErrors.tasks = "At least 3 tasks are required"; @@ -173,7 +287,6 @@ const A1InternshipRequestForm = () => { throw new Error("Failed to submit form", {cause: response}); } const data = await response.json(); - console.log("Form submitted successfully:", data); return data; } catch (error) { console.error("Error submitting form:", error); @@ -181,86 +294,67 @@ const A1InternshipRequestForm = () => { } }; - // const handleSubmit = (e) => { - // e.preventDefault(); - // if (validateForm()) { - // // sending descriptions to backend to check if they align with CS outcomes - // const taskDescriptions = formData.tasks - // .map(task => task.description.trim()) - // .filter(Boolean); - // sendTaskDescriptions(taskDescriptions); - // //ending here - // submitFormData().then(data => { - // const recipient = data.manual ? "coordinator for manual review!" : "advisor!"; - // setSuccessMsg("Form submitted successfully and sent to " + recipient); - // setTimeout(() => setSuccessMsg(""), 15000); - // }).catch(err => setErrors("Form submission failed! " + err)) - // .finally(() => setFormData(initialState)); - // } - // }; - + const sendTaskDescriptions = async (descriptions) => { + try { + const response = await fetch(`${process.env.REACT_APP_API_URL}/api/align-outcomes`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ tasks: descriptions }), + }); + if (!response.ok) throw new Error("Failed to send task descriptions"); + const data = await response.json(); + return data.results.map(({ task, matched_outcomes }) => ({ + description: task, + outcomes: matched_outcomes, + })); + } catch (error) { + console.error("Error:", error); + return null; + } + }; const handleSubmit = async (e) => { e.preventDefault(); - if (!validateForm()) return; - - const taskDescriptions = formData.tasks - .map(task => task.description.trim()) - .filter(Boolean); - + const taskDescriptions = formData.tasks.map((task) => task.description.trim()).filter(Boolean); try { const aligned = await sendTaskDescriptions(taskDescriptions); - if (aligned && aligned.length > 0) { - setFormData(prev => ({ - ...prev, - tasks: aligned - })); - + setFormData((prev) => ({ ...prev, tasks: aligned })); const submissionResponse = await submitFormData(); - const recipient = submissionResponse.manual ? "coordinator for manual review!" : "supervisor!"; - setSuccessMsg("Form submitted successfully and sent to " + recipient); + setSuccessMsg(`Form submitted successfully and sent to ${recipient}`); setTimeout(() => setSuccessMsg(""), 15000); setFormData(initialState); } else { setErrors({ tasks: "Outcome alignment failed or returned no tasks." }); } } catch (err) { - console.error("Error during submission:", err); - setErrors({ submit: "Form submission failed! " + err.message }); + setErrors({ submit: `Form submission failed! ${err.message}` }); } }; - - //function to send description to backend - const sendTaskDescriptions = async (descriptions) => { - try { - const response = await fetch(`${process.env.REACT_APP_API_URL}/api/align-outcomes`, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ tasks: descriptions }) - }); - - if (!response.ok) { - throw new Error("Failed to send task descriptions"); - } - - const data = await response.json(); - console.log("Alignment result:", data); - formData.tasks = data.results.map(({ task, matched_outcomes }) => ({ - description: task, - outcomes: matched_outcomes - })); - return formData.tasks; + useEffect(() => { + const fonts = [ + 'https://fonts.googleapis.com/css2?family=Dancing+Script:wght@500&display=swap', + 'https://fonts.googleapis.com/css2?family=Great+Vibes&display=swap', + 'https://fonts.googleapis.com/css2?family=Pacifico&display=swap', + 'https://fonts.googleapis.com/css2?family=Satisfy&display=swap', + 'https://fonts.googleapis.com/css2?family=Caveat:wght@500&display=swap' + ]; + const links = []; + fonts.forEach(font => { + const link = document.createElement('link'); + link.href = font; + link.rel = 'stylesheet'; + document.head.appendChild(link); + links.push(link); + }); + return () => { + links.forEach(link => document.head.removeChild(link)); + }; + }, []); - } catch (error) { - console.error("Error:", error); - } - }; return (

A.1 - Internship Request Form

@@ -271,84 +365,164 @@ const A1InternshipRequestForm = () => { Internee Details Workplace Details - Internship Advisor Details + Internship Supervisor Details Name*:
- + + {errors.interneeName &&
{errors.interneeName}
} Name*:
- + + {errors.workplaceName &&
{errors.workplaceName}
} Name*:
- + + {errors.advisorName &&
{errors.advisorName}
} Sooner ID*:
- + + {errors.soonerId &&
{errors.soonerId}
} Website:
- + + {errors.website &&
{errors.website}
} Job Title:
- + Email*:
- + + {errors.interneeEmail &&
{errors.interneeEmail}
} Phone*:
- + + {errors.phone &&
{errors.phone}
} Email*:
- + + {errors.advisorEmail &&
{errors.advisorEmail}
} - - Select the Number of Credit Hours* - - Start Date*:
- + Credit Hours*:
+ + {errors.creditHours &&
{errors.creditHours}
} - - End Date*:
- - {dateError &&
{dateError}
} - - - - {[1, 2, 3].map((val) => ( - - {val}
- handleCreditHourChange(val.toString())} - /> - - ))} - + +
+
+ +
+ {errors.startDate &&
{errors.startDate}
} + + + +
+
+ +
+ {dateError &&
{dateError}
} + {errors.endDate &&
{errors.endDate}
} + + @@ -358,7 +532,7 @@ const A1InternshipRequestForm = () => {
Job Description Details:
    -
  1. Tasks need to be filled by the Internship Advisor.
  2. +
  3. Tasks need to be filled by the Internship Supervisor.
  4. Only task description fields are editable.
  5. All tasks should cover a minimum of three outcomes.
@@ -367,11 +541,10 @@ const A1InternshipRequestForm = () => { Task - {outcomeLabels.map((label, i) => ( - - {label} -
- ({outcomeDescriptions[i]}) + {outcomeLabels.map((label, j) => ( + + {label}
+ ({outcomeDescriptions[j]}) ))} @@ -386,17 +559,15 @@ const A1InternshipRequestForm = () => { value={task.description} onChange={(e) => handleTaskChange(i, e.target.value)} style={{ width: "100%", padding: "4px", boxSizing: "border-box" }} + disabled={!isFieldEditable("task")} /> - {outcomeLabels.map((_, j) => ( - - - - ))} + {outcomeLabels.map((label, j) => renderOutcomeCell(task, label, `${i}-${j}`))} ))} + {errors.tasks &&
{errors.tasks}
}

Signatures:

@@ -405,49 +576,90 @@ const A1InternshipRequestForm = () => { Internee Signature*:
- +
+ +
+ {errors.interneeSignature &&
{errors.interneeSignature}
} - Internship Advisor Signature:
- + Internship Supervisor Signature:
+
+ +
+ {errors.advisorSignature &&
{errors.advisorSignature}
} Internship Coordinator Approval:
- + +
+ {errors.coordinatorApproval &&
{errors.coordinatorApproval}
} + + + {/* + +
+