diff --git a/client/src/App.jsx b/client/src/App.jsx index 5375bba..8af6a8e 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -62,7 +62,6 @@ function Main() { const [applications, setApplications] = useState([]); const [notifications, setNotifications] = useState([]); const [requests, setRequests] = useState([]); - const [requestSent, setRequestSent] = useState(false); // Message to be shown to the user after an API has been called const [alert, setAlert] = useState({ @@ -183,9 +182,9 @@ function Main() { {/* prettier-ignore */} : }> - : } /> + : } /> : } /> - : } /> + : } /> : } /> : } /> : } /> diff --git a/client/src/components/ApplicationDetails.jsx b/client/src/components/ApplicationDetails.jsx index ffad7e7..0d01f59 100644 --- a/client/src/components/ApplicationDetails.jsx +++ b/client/src/components/ApplicationDetails.jsx @@ -17,12 +17,13 @@ import ConfirmationDialog from "./ConfirmationDialog"; import { useThemeContext } from "../theme/ThemeContextProvider"; import API from "../utils/API"; import StudentCareerTable from "./StudentCareerTable"; +import { NavLink } from "react-router-dom"; function ApplicationDetails(props) { const user = useContext(UserContext); const handleErrors = useContext(ErrorContext); const { theme } = useThemeContext(); - const { application, evaluateApplication, applications } = props; + const { application, evaluateApplication, applications, proposal } = props; const [decision, setDecision] = useState(null); const [openDialog, setOpenDialog] = useState(false); @@ -227,7 +228,19 @@ function ApplicationDetails(props) { Title: - {application.title} + {isApplicationAccepted() && user.role === "student" ? ( + + {application.title} + + ) : ( + application.title + )} {renderActionButton()} @@ -239,7 +252,8 @@ function ApplicationDetails(props) { ApplicationDetails.propTypes = { application: PropTypes.object, evaluateApplication: PropTypes.func, - applications: PropTypes.array + applications: PropTypes.array, + proposal: PropTypes.object }; export default ApplicationDetails; diff --git a/client/src/components/ApplicationTable.jsx b/client/src/components/ApplicationTable.jsx index d541737..7ef8512 100644 --- a/client/src/components/ApplicationTable.jsx +++ b/client/src/components/ApplicationTable.jsx @@ -12,7 +12,7 @@ import ApplicationRow from "./ApplicationRow"; import UserContext from "../contexts/UserContext"; const TEACHER_HEADERS = ["Student", "Proposal", "Status", "Open"]; -const STUDENT_HEADERS = ["Teacher", "Proposal", "Status", "Open"]; +const STUDENT_HEADERS = ["Supervisor", "Proposal", "Status", "Open"]; function generateTableHeaders(headers, align) { return headers.map((headCell) => ( diff --git a/client/src/components/RequestDetails.jsx b/client/src/components/RequestDetails.jsx index 47c1bba..8bfa9ab 100644 --- a/client/src/components/RequestDetails.jsx +++ b/client/src/components/RequestDetails.jsx @@ -8,7 +8,10 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import AccessTimeIcon from "@mui/icons-material/AccessTime"; import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; +import EditNotificationsIcon from "@mui/icons-material/EditNotifications"; import HighlightOffIcon from "@mui/icons-material/HighlightOff"; +import ModeEditIcon from "@mui/icons-material/ModeEdit"; +import ScheduleSendIcon from "@mui/icons-material/ScheduleSend"; import ConfirmationDialog from "./ConfirmationDialog"; import UserContext from "../contexts/UserContext"; import { useThemeContext } from "../theme/ThemeContextProvider"; @@ -62,7 +65,7 @@ function RequestDetails(props) { return ( - + REQUEST APPROVED @@ -95,42 +98,62 @@ function RequestDetails(props) { ); + } else if (user.role === "student") { + return ( + + + + WAITING FOR SUPERVISOR APPROVAL + + + ); } break; case "requested": - return ( - - - - - ); + if (user.role === "student") { + return ( + + + + WAITING FOR SECRETARY APPROVAL + + + ); + } else { + return ( + + + + + ); + } case "rejected": return ( - - REQUEST REJECTED + + {user.role === "student" ? "REQUEST REJECTED. YOU CAN NOW SUBMIT A NEW REQUEST." : "REQUEST REJECTED"} ); @@ -138,11 +161,27 @@ function RequestDetails(props) { return ( - - REQUEST STARTED + + THESIS STARTED ); + case "changes_requested": + return ( + user.role === "student" && ( + + + + CHANGES REQUESTED BY SUPERVISOR + + + ) + ); default: break; } @@ -150,23 +189,27 @@ function RequestDetails(props) { return ( <> - - - Student information - - - - Student ID: - {request.student_id} - + {user.role !== "student" && ( + <> + + + Student information + + + + Student ID: + {request.student_id} + + + )} Supervisors @@ -184,24 +227,36 @@ function RequestDetails(props) { )} - Thesis Request + Thesis Details Title: {request.title} - - Description: - {showMore ? `${request.description} ` : `${request.description.substring(0, 250)}... `} - setShowMore(!showMore)}> - {showMore ? "Show less" : "Show more"} - - - - + {request.description.length > 250 ? ( + + Description: + {showMore ? `${request.description} ` : `${request.description.substring(0, 250)}... `} + setShowMore(!showMore)}> + {showMore ? "Show less" : "Show more"} + + + ) : ( + + Description: + {request.description} + + )} + {renderActions()} + {user.role === "student" && request.status === "changes_requested" && ( + + Message from supervisor: + {request.changes_requested} + + )} ); } diff --git a/client/src/components/RequestForm.jsx b/client/src/components/RequestForm.jsx index 7d8e6d6..3aea19f 100644 --- a/client/src/components/RequestForm.jsx +++ b/client/src/components/RequestForm.jsx @@ -10,13 +10,15 @@ import TextField from "@mui/material/TextField"; import CustomPaper from "./CustomPaper"; function RequestForm(props) { - const { createRequest, teachers } = props; + const { createRequest, teachers, getTeacherById, proposal } = props; + + const supervisor = proposal ? getTeacherById(proposal.supervisor) : null; const [formData, setFormData] = useState({ - title: "", - description: "", - supervisor: null, - coSupervisors: [] + title: proposal ? proposal.title : "", + description: proposal ? proposal.description : "", + supervisor: supervisor ? supervisor.email : null, + coSupervisors: proposal ? proposal.co_supervisors.split(", ") : [] }); const [formErrors, setFormErrors] = useState({ @@ -191,7 +193,9 @@ function RequestForm(props) { RequestForm.propTypes = { createRequest: PropTypes.func, - teachers: PropTypes.array + teachers: PropTypes.array, + getTeacherById: PropTypes.func, + proposal: PropTypes.object }; export default RequestForm; diff --git a/client/src/components/RequestRow.jsx b/client/src/components/RequestRow.jsx index 4259633..d116666 100644 --- a/client/src/components/RequestRow.jsx +++ b/client/src/components/RequestRow.jsx @@ -38,8 +38,8 @@ function RequestRow(props) { return ( - {request.student_id} - {user.role === "secretary_clerk" && {renderSupervisor()}} + {user.role !== "student" && {request.student_id}} + {user.role !== "teacher" && {renderSupervisor()}} { if (user.role === "teacher") { return HEADERS.filter((header) => header !== "Supervisor"); + } else if (user.role === "student") { + return HEADERS.filter((header) => header !== "Student"); } else { return HEADERS; } diff --git a/client/src/components/Sidebar.jsx b/client/src/components/Sidebar.jsx index d1e7cad..6866b49 100644 --- a/client/src/components/Sidebar.jsx +++ b/client/src/components/Sidebar.jsx @@ -34,7 +34,7 @@ const sidebarTabs = [ }, { id: "requests", - label: "Requests", + label: "Start Requests", icon: , path: "/requests" }, @@ -63,7 +63,7 @@ function Sidebar(props) { let tabs = []; switch (user.role) { case "student": - tabs = sidebarTabs.filter((tab) => tab.id !== "requests"); + tabs = sidebarTabs.map((tab) => (tab.id === "requests" ? { ...tab, label: "Start Request" } : tab)); break; case "teacher": tabs = sidebarTabs; diff --git a/client/src/routes/CreateRequestPage.jsx b/client/src/routes/CreateRequestPage.jsx index 982445b..a5611d9 100644 --- a/client/src/routes/CreateRequestPage.jsx +++ b/client/src/routes/CreateRequestPage.jsx @@ -1,5 +1,5 @@ import { useContext } from "react"; -import { Link, useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import Box from "@mui/material/Box"; @@ -12,9 +12,12 @@ import ErrorContext from "../contexts/ErrorContext"; import API from "../utils/API"; function CreateRequestPage(props) { - const { teachers, setAlert, setRequestSent } = props; + const { fetchRequests, teachers, getTeacherById, setAlert } = props; const handleErrors = useContext(ErrorContext); const navigate = useNavigate(); + const location = useLocation(); + + const proposal = location.state?.proposal; const createRequest = (request) => { API.createRequest(request) @@ -23,8 +26,8 @@ function CreateRequestPage(props) { message: "Request submitted successfully", severity: "success" }); - setRequestSent(true); - navigate("/proposals"); + fetchRequests(); + navigate(-1); }) .catch((err) => handleErrors(err)); }; @@ -39,8 +42,7 @@ function CreateRequestPage(props) { justifyContent="space-between" > - Request Start Proposal + Thesis Start Request - + @@ -62,9 +69,10 @@ function CreateRequestPage(props) { } CreateRequestPage.propTypes = { + fetchRequests: PropTypes.func, teachers: PropTypes.array, - setAlert: PropTypes.func, - setRequestSent: PropTypes.func + getTeacherById: PropTypes.func, + setAlert: PropTypes.func }; export default CreateRequestPage; diff --git a/client/src/routes/ProposalsPage.jsx b/client/src/routes/ProposalsPage.jsx index 984cdcf..ddc3ac0 100644 --- a/client/src/routes/ProposalsPage.jsx +++ b/client/src/routes/ProposalsPage.jsx @@ -12,7 +12,6 @@ import Fab from "@mui/material/Fab"; import Hidden from "@mui/material/Hidden"; import InputAdornment from "@mui/material/InputAdornment"; import OutlinedInput from "@mui/material/OutlinedInput"; -import ScheduleSendIcon from "@mui/icons-material/ScheduleSend"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import Toolbar from "@mui/material/Toolbar"; @@ -24,8 +23,7 @@ import { TEACHER_PROPOSALS_FILTERS, TEACHER_HEADERS_ACTIVE, TEACHER_HEADERS_EXPI import API from "../utils/API"; function ProposalsPage(props) { - const { requestSent, setAlert, setDirty, currentDate, proposals, applications, teachers, groups, getTeacherById } = - props; + const { setAlert, setDirty, currentDate, proposals, applications, teachers, groups, getTeacherById } = props; const user = useContext(UserContext); const handleErrors = useContext(ErrorContext); const [isDrawerOpen, setIsDrawerOpen] = useState(false); @@ -120,18 +118,6 @@ function ProposalsPage(props) { Theses Proposals - - - - - - - - - - ); @@ -330,7 +304,6 @@ function ProposalsPage(props) { } ProposalsPage.propTypes = { - requestSent: PropTypes.bool, setAlert: PropTypes.func, setDirty: PropTypes.func, currentDate: PropTypes.string, diff --git a/client/src/routes/RequestsPage.jsx b/client/src/routes/RequestsPage.jsx index 140d2f4..4518b41 100644 --- a/client/src/routes/RequestsPage.jsx +++ b/client/src/routes/RequestsPage.jsx @@ -1,18 +1,131 @@ import PropTypes from "prop-types"; -import { Box, Stack, Typography } from "@mui/material"; +import { useContext, useState } from "react"; +import { Link } from "react-router-dom"; +import { Alert, Box, Button, Fab, Hidden, Paper, Stack, Step, StepLabel, Stepper, Typography } from "@mui/material"; +import AddIcon from "@mui/icons-material/Add"; +import ScheduleSendIcon from "@mui/icons-material/ScheduleSend"; import RequestTable from "../components/RequestTable"; +import UserContext from "../contexts/UserContext"; +import RequestDetails from "../components/RequestDetails"; + +const infoMessage = + "In order to start your thesis activity, you must submit a formal request. You can either create a thesis start request from this page after having discussed the topic with your supervisors, or you can create one from an accepted application in the application page."; + +const steps = ["Request submitted", "Approved by a secretary clerk", "Approved by your supervisor"]; function RequestsPage(props) { const { requests, teachers } = props; + const user = useContext(UserContext); + + const [lastActiveStep, setLastActiveStep] = useState(-1); + + const getActiveRequest = () => { + const lastRequest = requests[requests.length - 1]; + return lastRequest; + }; + + const getActiveStep = () => { + const request = getActiveRequest(); + if (!request) { + return -1; + } + let newActiveStep = -1; + switch (request.status) { + case "requested": + newActiveStep = 1; + break; + case "secretary_accepted": + newActiveStep = 2; + break; + case "started": + newActiveStep = 3; + break; + case "changes_requested": + return 2; + default: + return lastActiveStep; + } + if (newActiveStep !== lastActiveStep) { + setLastActiveStep(newActiveStep); + } + return newActiveStep; + }; + + const isStartRequestButtonDisabled = () => { + const activeRequest = getActiveRequest(); + return activeRequest && activeRequest.status !== "rejected"; + }; + return (
- Thesis Requests + {user.role === "student" ? "My Thesis Request" : "Thesis Requests"} + {user.role === "student" && ( + + + + )} - + {user.role === "student" && ( + <> + + {infoMessage} + + + {steps.map((label) => ( + + {label} + + ))} + + {getActiveRequest() ? ( + + + + + + ) : ( + + + You have no active thesis start request at the moment. Start a new request to see it here. + + + )} + + )} + {user.role !== "student" && } + {user.role === "student" && ( + + + + {isStartRequestButtonDisabled() ? : } + + + + )}
); } diff --git a/client/src/routes/ViewApplicationPage.jsx b/client/src/routes/ViewApplicationPage.jsx index d4577c6..00df13e 100644 --- a/client/src/routes/ViewApplicationPage.jsx +++ b/client/src/routes/ViewApplicationPage.jsx @@ -1,4 +1,4 @@ -import { useContext } from "react"; +import { useContext, useEffect, useState } from "react"; import { Link, useLocation } from "react-router-dom"; import PropTypes from "prop-types"; import Box from "@mui/material/Box"; @@ -6,17 +6,21 @@ import Button from "@mui/material/Button"; import Paper from "@mui/material/Paper"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; +import AddIcon from "@mui/icons-material/Add"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ApplicationDetails from "../components/ApplicationDetails"; import ErrorContext from "../contexts/ErrorContext"; +import UserContext from "../contexts/UserContext"; import API from "../utils/API"; function ViewApplicationPage(props) { const location = useLocation(); const { fetchApplications, fetchNotifications, setAlert, applications } = props; + const user = useContext(UserContext); const handleErrors = useContext(ErrorContext); const application = location.state?.application; + const [proposal, setProposal] = useState(null); const evaluateApplication = (application) => { API.evaluateApplication(application) @@ -31,6 +35,14 @@ function ViewApplicationPage(props) { .catch((err) => handleErrors(err)); }; + useEffect(() => { + if (application.state === "accepted") { + API.getProposalById(application.proposal_id) + .then((proposal) => setProposal(proposal)) + .catch((err) => handleErrors(err)); + } + }, []); + return (
Back + {user.role === "student" && application.state === "accepted" && ( + + )} Application Details @@ -59,6 +83,7 @@ function ViewApplicationPage(props) { application={application} evaluateApplication={evaluateApplication} applications={applications} + proposal={proposal} /> diff --git a/client/src/routes/ViewRequestPage.jsx b/client/src/routes/ViewRequestPage.jsx index c056374..17d25d9 100644 --- a/client/src/routes/ViewRequestPage.jsx +++ b/client/src/routes/ViewRequestPage.jsx @@ -1,5 +1,5 @@ import { useContext, useEffect, useState } from "react"; -import { Link, useLocation } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; @@ -17,9 +17,10 @@ import API from "../utils/API"; function ViewRequestPage(props) { const { fetchRequests, setAlert, requests } = props; + const handleErrors = useContext(ErrorContext); const user = useContext(UserContext); + const navigate = useNavigate(); const location = useLocation(); - const handleErrors = useContext(ErrorContext); const [changesMessage, setChangesMessage] = useState(""); const [changesError, setChangesError] = useState(""); @@ -84,8 +85,7 @@ function ViewRequestPage(props) { justifyContent="space-between" >