diff --git a/frontend/frontend/src/components/DeadlineCalendar.tsx b/frontend/frontend/src/components/DeadlineCalendar.tsx index df7c3c16..51c041a8 100644 --- a/frontend/frontend/src/components/DeadlineCalendar.tsx +++ b/frontend/frontend/src/components/DeadlineCalendar.tsx @@ -99,10 +99,24 @@ function DeadlineMenu({ assignments, selectedDay }: DeadlineMenuProps) { width={'100%'} alignItems={'center'} > - + {t('deadlines_on')}: {selectedDay?.format('DD/MM/YYYY')} - + + {assignments.filter((assignment: project) => { + return dayjs(assignment.deadline).isSame(selectedDay, 'day') + }).length === 0 && ( + + + + {t('no_deadline') + 's'} + + + + )} {assignments .filter((assignment: project) => dayjs(assignment.deadline).isSame(selectedDay, 'day') @@ -121,6 +135,7 @@ function DeadlineMenu({ assignments, selectedDay }: DeadlineMenuProps) { } > + ))} diff --git a/frontend/frontend/src/components/GroupAccessComponent.tsx b/frontend/frontend/src/components/GroupAccessComponent.tsx index af831496..922896bc 100644 --- a/frontend/frontend/src/components/GroupAccessComponent.tsx +++ b/frontend/frontend/src/components/GroupAccessComponent.tsx @@ -1,8 +1,6 @@ import { Button } from './CustomComponents' -import { Box, Typography } from '@mui/material' +import { Box } from '@mui/material' import { t } from 'i18next' -import Switch from '@mui/material/Switch' -import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' interface GroupAccessComponentProps { @@ -20,17 +18,12 @@ export function GroupAccessComponent({ courseid, }: GroupAccessComponentProps) { const navigate = useNavigate() - const [allowGroups, setAllowGroups] = useState(false) // Handle click event to navigate to the groups page const handleClick = () => { navigate(`/course/${courseid}/assignment/${assignmentid}/groups`) } - useEffect(() => { - //set max group size to 1 if groups are not allowed and register all students to a group of their own - }, [allowGroups]) - return ( <> {/* Button to navigate to groups page */} - {allowGroups ? ( - - ) : ( - // Show text indicating groups are not allowed - - {t('groups')} - - )} - {/* Switch to toggle group access */} - setAllowGroups(!allowGroups)} - color={'primary'} - /> + ) diff --git a/frontend/frontend/src/components/Header.tsx b/frontend/frontend/src/components/Header.tsx index ac4e4f11..750212c3 100644 --- a/frontend/frontend/src/components/Header.tsx +++ b/frontend/frontend/src/components/Header.tsx @@ -120,7 +120,8 @@ export const Header = ({ variant, title }: Props) => { {/* Back Button (if variant is not default) */} - {!(variant === 'default' || variant === 'main') && ( + {(variant === 'not_main' || + variant === 'editable') && ( { - let filename = 'indiening.zip' - if (newSubmission.filename) { - filename = newSubmission.filename - } - const blob = new Blob([res.data], { - type: res.headers['content-type'], + //Get the submission file + const newSubmission: Submission = + lastSubmissionResponse.data + newSubmission.filename = + lastSubmissionResponse.data.bestand.replace( + /^.*[\\/]/, + '' + ) + newSubmission.bestand = await instance + .get( + `/indieningen/${lastSubmission.indiening_id}/indiening_bestand`, + { + responseType: 'blob', + } + ) + .then((res) => { + let filename = 'indiening.zip' + if (newSubmission.filename) { + filename = newSubmission.filename + } + const blob = new Blob([res.data], { + type: res.headers['content-type'], + }) + const file: File = new File([blob], filename, { + type: res.headers['content-type'], + }) + return file }) - const file: File = new File([blob], filename, { - type: res.headers['content-type'], - }) - return file - }) - setSubmitted(newSubmission) + setSubmitted(newSubmission) + } if (lastSubmission) { const scoreResponse = await instance.get( `/scores/?indiening=${lastSubmission.indiening_id}` diff --git a/frontend/frontend/src/i18n/en.ts b/frontend/frontend/src/i18n/en.ts index cff29fe0..61370cea 100644 --- a/frontend/frontend/src/i18n/en.ts +++ b/frontend/frontend/src/i18n/en.ts @@ -105,9 +105,14 @@ const english = { join_group: 'Join', leave: 'Leave', acces: 'This gives you access to the course.', + copy_invite: 'Copy invitation link', deadlines_on: 'Deadlines on', + no_assignmentfile: 'No assignment file', email: 'email', - copy_invite: 'Copy invitation link', + n_of_members: 'Max Amount Of Members', + existing_submissions: + 'This could impact existing submissions. Are you sure?', + divide_groups: 'Divide Groups', noGroup: "You don't seem to be in a group yet.", contactTeacher: 'Please contact your teacher.', chooseGroup: 'Join a group before submitting.', diff --git a/frontend/frontend/src/i18n/nl.ts b/frontend/frontend/src/i18n/nl.ts index d615d021..6799bd67 100644 --- a/frontend/frontend/src/i18n/nl.ts +++ b/frontend/frontend/src/i18n/nl.ts @@ -105,9 +105,14 @@ const dutch = { join_group: 'Wordt lid', leave: 'Verlaat', acces: 'Dit geeft je toegang tot het vak.', + copy_invite: 'Kopieer uitnodigingslink', + no_assignmentfile: 'Geen opdrachtbestand', deadlines_on: 'Deadlines op', email: 'email', - copy_invite: 'Kopieer uitnodigingslink', + n_of_members: 'Max Aantal Leden', + existing_submissions: + 'Dit kan invloed hebben op bestaande indieningen. Weet je het zeker?', + divide_groups: 'Verdeel Groepen', noGroup: 'Je lijkt nog geen groep te hebben.', chooseGroup: 'Kies een groep voor je indient', contactTeacher: 'Gelieve contact op te nemen met je lesgever.', diff --git a/frontend/frontend/src/pages/FourOFourPage.tsx b/frontend/frontend/src/pages/FourOFourPage.tsx new file mode 100644 index 00000000..b4e2098d --- /dev/null +++ b/frontend/frontend/src/pages/FourOFourPage.tsx @@ -0,0 +1,106 @@ +import { Box, Typography } from '@mui/material' +import { t } from 'i18next' + +export default function FourOFourPage() { + return ( + <> + + + + + + + + {"404"} + + + {"Error: Page not found"} + + + + + + + ) +} \ No newline at end of file diff --git a/frontend/frontend/src/pages/addChangeAssignmentPage/AddChangeAssignmentPage.tsx b/frontend/frontend/src/pages/addChangeAssignmentPage/AddChangeAssignmentPage.tsx index 5ed43e36..71ee8679 100644 --- a/frontend/frontend/src/pages/addChangeAssignmentPage/AddChangeAssignmentPage.tsx +++ b/frontend/frontend/src/pages/addChangeAssignmentPage/AddChangeAssignmentPage.tsx @@ -101,6 +101,7 @@ export function AddChangeAssignmentPage() { const [maxScore, SetMaxScore] = useState(20) const [cleared, setCleared] = useState(false) const [filename, setFilename] = useState('indiening.zip') + const [groupSize, setGroupSize] = useState(1) const [user, setUser] = useState() @@ -162,14 +163,15 @@ export function AddChangeAssignmentPage() { //set the initial values of the assignment if it is an edit useEffect(() => { //get the data - const fetchData = async () => { - //begin loading -> set loading to true + const fetchUser = async () => { setUserLoading(true) - setLoading(true) const userResponse = await instance.get('/gebruikers/me/') setUser(userResponse.data) setUserLoading(false) - + } + const fetchData = async () => { + //begin loading -> set loading to true + setLoading(true) //get the assignment await instance .get(`/projecten/${assignmentId}`) @@ -261,6 +263,9 @@ export function AddChangeAssignmentPage() { } //if there is an assignmentId, get the data else use the default values + fetchUser().catch((error) => { + console.error(error) + }) if (assignmentId !== undefined) { fetchData().catch((error) => { console.error(error) @@ -384,6 +389,7 @@ export function AddChangeAssignmentPage() { if (extraDueDate !== null) { formData.append('extra_deadline', extraDueDate.format()) } + formData.append('max_groep_grootte', groupSize.toString()) const config = { headers: { @@ -932,6 +938,48 @@ export function AddChangeAssignmentPage() { + {/* change group size allowed, no need for extra group switch*/} + {!assignmentId && ( + + + {t('n_of_members')} + + {loading ? ( + + ) : ( + + setGroupSize( + parseInt( + event + .target + .value + ) + ) + } + /> + )} + + )} {/* This section allows the teacher to set the maximum score for the assignment.*/} () const [submissions, setSubmissions] = useState([]) const [groups, setGroups] = useState([]) + const [singleStudents, setSingleStudents] = useState([]) const [submissionFile, setSubmissionFile] = useState() const [submit, setSubmit] = useState(false) + const [students, setStudents] = useState([]) + //state for loading the page const [loading, setLoading] = useState(true) const [userLoading, setUserLoading] = useState(true) + const [studentsLoading, setStudentsLoading] = useState(true) // state for the warning popup const [openNoGroup, setOpenNoGroup] = useState(false) @@ -86,51 +93,75 @@ export function AssignmentPage() { } useEffect(() => { + async function fetchUser() { + setUserLoading(true) + const userResponse = await instance.get('/gebruikers/me/') + setUser(userResponse.data) + setUserLoading(false) + } + async function fetchData() { try { - setUserLoading(true) setLoading(true) - const userResponse = await instance.get('/gebruikers/me/') - setUser(userResponse.data) - setUserLoading(false) const assignmentResponse = await instance.get( `/projecten/${assignmentId}/` ) const newAssignment: Project = assignmentResponse.data - newAssignment.filename = - assignmentResponse.data.opgave_bestand.replace( - /^.*[\\/]/, - '' - ) - newAssignment.opgave_bestand = await instance - .get(`/projecten/${assignmentId}/opgave_bestand/`, { - responseType: 'blob', - }) - .then((res) => { - let filename = 'indiening.zip' - if (newAssignment.filename) { - filename = newAssignment.filename - } - const blob = new Blob([res.data], { - type: res.headers['content-type'], + if (assignmentResponse.data.opgave_bestand) { + newAssignment.filename = + assignmentResponse.data.opgave_bestand.replace( + /^.*[\\/]/, + '' + ) + newAssignment.opgave_bestand = await instance + .get(`/projecten/${assignmentId}/opgave_bestand/`, { + responseType: 'blob', }) - const file: File = new File([blob], filename, { - type: res.headers['content-type'], + .then((res) => { + let filename = 'indiening.zip' + if (newAssignment.filename) { + filename = newAssignment.filename + } + const blob = new Blob([res.data], { + type: res.headers['content-type'], + }) + const file: File = new File([blob], filename, { + type: res.headers['content-type'], + }) + return file }) - return file - }) + } setAssignment(newAssignment) - if (userResponse.data) { + if (user) { if (user.is_lesgever) { const groupsResponse = await instance.get( `/groepen/?project=${assignmentId}` ) setGroups(groupsResponse.data) + if (newAssignment.max_groep_grootte == 1) { + const userPromises: Promise>[] = + groupsResponse.data.map((group: Group) => + instance.get( + '/gebruikers/' + group.studenten[0] + ) + ) + + const temp_students = await axios.all(userPromises) + + setSingleStudents( + temp_students.map((res) => res.data) + ) + } } else { - const submissionsResponse = await instance.get( - `/indieningen/?vak=${courseId}` + const groupResponse = await instance.get<[Group]>( + `/groepen/?student=${user.user}&project=${assignmentId}` ) - setSubmissions(submissionsResponse.data) + if (groupResponse.data.length > 0) { + const submissionsResponse = await instance.get( + `/indieningen/?project=${assignmentId}&groep=${groupResponse.data[0].groep_id}` + ) + setSubmissions(submissionsResponse.data) + } } } } catch (error) { @@ -141,8 +172,40 @@ export function AssignmentPage() { } } - fetchData().catch((err) => console.error(err)) - }, [assignmentId, courseId, user.is_lesgever, submit]) + // Ensure fetchUser completes before fetchData starts + ;(async () => { + if (user.user === 0) { + await fetchUser() + } + await fetchData() // Use await here to ensure fetchData waits for fetchUser to complete + })() + }, [assignmentId, courseId, user.is_lesgever, submit, user]) + + useEffect(() => { + async function fetchStudents() { + setStudentsLoading(true) + const groupResponse = await instance.get( + `groepen/?student=${user.user}&project=${assignmentId}` + ) + const group: Group = groupResponse.data[0] + + const studentPromises: Promise>[] = + group.studenten.map((id: number) => + instance.get('/gebruikers/' + id) + ) + + const temp_students = await axios.all(studentPromises) + setStudents(temp_students.map((res) => res.data)) + + setStudentsLoading(false) + } + // Fetch students + if (!user.is_lesgever && user.user !== 0) { + fetchStudents().catch((error) => + console.error('Error fetching students data:', error) + ) + } + }, [user, assignment, groups, assignmentId]) // Function to download all submissions as a zip file const downloadAllSubmissions = () => { @@ -242,12 +305,10 @@ export function AssignmentPage() { }, } const groupResponse = await instance.get( - `/groepen/?student=${user.user}` + `/groepen/?student=${user.user}&project=${assignmentId}` ) if (groupResponse.data) { - const group = groupResponse.data.find( - (group: Group) => String(group.project) === assignmentId - ) + const group = groupResponse.data[0] if (group) { const formData = new FormData() formData.append('groep', group.groep_id) @@ -366,12 +427,20 @@ export function AssignmentPage() { }} /> ) : ( - + {(assignment?.max_groep_grootte + ? assignment.max_groep_grootte + : 1) > 1 && ( + )} - courseid={parseInt(courseId)} - /> + )} @@ -599,7 +668,10 @@ export function AssignmentPage() { backgroundColor: 'background.default', }} > - + ) : ( - - {assignment - ? dayjs( - assignment.deadline - ).format( - 'DD/MM/YYYY-HH:MM' - ) - : 'no deadline'} - + <> + {assignment !== undefined && + assignment.deadline !== + null && ( + + {assignment + ? dayjs( + assignment.deadline + ).format( + 'DD/MM/YYYY-HH:MM' + ) + : 'no deadline'} + + )} + )} @@ -647,7 +727,7 @@ export function AssignmentPage() { /> ) : ( <> - {assignment?.student_groep && ( + {assignment?.student_groep ? ( + ) : ( + + {assignment?.max_groep_grootte === + 1 ? ( + + {user.first_name + + ' ' + + user.last_name} + + ) : ( + <> + {console.log( + students + )} + + + )} + )} )} + {/*extra deadline*/} + {assignment?.extra_deadline && ( + + + Extra Deadline: + + {loading ? ( + + ) : ( + + {assignment + ? dayjs( + assignment.extra_deadline + ).format( + 'DD/MM/YYYY-HH:MM' + ) + : 'no deadline'} + + )} + + )} {/*download opgave*/} } onClick={downloadAssignment} + disabled={ + assignment === undefined || + assignment.filename === undefined + } > {loading ? ( {assignment ? assignment.filename - : 'error'} + ? assignment.filename + : t('no_assignmentfile') + : t('no_assignmentfile')} )} diff --git a/frontend/frontend/src/pages/groupsPage/GroupsPage.tsx b/frontend/frontend/src/pages/groupsPage/GroupsPage.tsx index ff0bdcb7..a0d53b84 100644 --- a/frontend/frontend/src/pages/groupsPage/GroupsPage.tsx +++ b/frontend/frontend/src/pages/groupsPage/GroupsPage.tsx @@ -3,6 +3,7 @@ import { Button, Card } from '../../components/CustomComponents.tsx' import { Autocomplete, Box, + CircularProgress, Grid, IconButton, MenuItem, @@ -15,7 +16,6 @@ import { TableCell, TableContainer, TableRow, - TextField, Tooltip, Typography, } from '@mui/material' @@ -40,6 +40,8 @@ export interface Group { project: number } +//FIXME: groupsize should not be signed after creation of project, refactor! + // Typescript typing for hashmap type Hashmap = Map @@ -57,7 +59,6 @@ export function GroupsPage() { ) //state for new groups and new groupSize, don't change the old groups and groupSize until the user clicks save const [newGroups, setNewGroups] = useState([]) - const [newGroupSize, setNewGroupSize] = useState(1) const [currentGroup, setCurrentGroup] = useState('') const [availableStudents, setAvailableStudents] = useState([]) const [projectName, setProjectName] = useState('') @@ -65,6 +66,9 @@ export function GroupsPage() { // confirmation dialog state const [confirmOpen, setConfirmOpen] = useState(false) + //random groups dialog state + const [randomOpen, setRandomOpen] = useState(false) + // state for correct loading of the page const [loading, setLoading] = useState(true) @@ -102,17 +106,15 @@ export function GroupsPage() { } else { // update the old groups with the new groups for (const group of newGroups) { - if (group.studenten.length !== 0) { - instance - .put('/groepen/' + group.groep_id + '/', { - groep_id: group.groep_id, - studenten: group.studenten, - project: parseInt(assignmentId), - }) - .catch((error) => { - console.log(error) - }) - } + instance + .put('/groepen/' + group.groep_id + '/', { + groep_id: group.groep_id, + studenten: group.studenten, + project: parseInt(assignmentId), + }) + .catch((error) => { + console.log(error) + }) } } navigate('/course/' + courseId + '/assignment/' + assignmentId) @@ -213,7 +215,8 @@ export function GroupsPage() { .get('/projecten/' + assignmentId) .then((response) => { setProjectName(response.data.titel) - setNewGroupSize(response.data.max_groep_grootte) + + setMaxGroupSize(response.data.max_groep_grootte) }) .catch((error) => { console.log(error) @@ -228,21 +231,6 @@ export function GroupsPage() { .get(`/groepen/?project=${assignmentId}`) .then((response) => { const newgroups: Group[] = response.data - if ( - newgroups.length < - Math.ceil(studentNames.size / newGroupSize) - ) { - for ( - let i = newgroups.length; - i < Math.ceil(studentNames.size / newGroupSize); - i++ - ) { - newgroups.push({ - studenten: [], - project: parseInt(assignmentId), - }) - } - } setNewGroups(newgroups) }) .catch((error) => { @@ -311,22 +299,18 @@ export function GroupsPage() { // Randomise groups const randomGroups = () => { const students = Array.from(studentNames.keys()) - const newGroups: Group[] = [] - for (let i = 0; i < Math.ceil(students.length / newGroupSize); i++) { - newGroups.push({ - studenten: [], - project: parseInt(assignmentId), + const shuffledStudents = students.sort(() => Math.random() - 0.5) + const randomisedGroups: Group[] = [] + for (const group of newGroups) { + const sliceSize = Math.min(max_group_size, shuffledStudents.length) + randomisedGroups.push({ + ...group, + studenten: shuffledStudents.slice(0, sliceSize), }) + shuffledStudents.splice(0, sliceSize) + if (shuffledStudents.length === 0) break } - for (let i = 0; i < students.length; i++) { - let randomGroup = Math.floor(Math.random() * newGroups.length) - while (newGroups[randomGroup].studenten.length >= newGroupSize) { - randomGroup = Math.floor(Math.random() * newGroups.length) - } - newGroups[randomGroup].studenten.push(students[i]) - } - - setNewGroups(newGroups) + setNewGroups(randomisedGroups) } // assign a student to a group @@ -853,6 +837,7 @@ export function GroupsPage() { } onClick={() => { assignStudent( + student, parseInt( currentGroup @@ -862,6 +847,7 @@ export function GroupsPage() { }} > + diff --git a/frontend/frontend/src/pages/subjectsPage/AddChangeSubjectPage.tsx b/frontend/frontend/src/pages/subjectsPage/AddChangeSubjectPage.tsx index 01c515b4..7328adaf 100644 --- a/frontend/frontend/src/pages/subjectsPage/AddChangeSubjectPage.tsx +++ b/frontend/frontend/src/pages/subjectsPage/AddChangeSubjectPage.tsx @@ -4,8 +4,10 @@ import { SecondaryButton, EvenlySpacedRow, } from '../../components/CustomComponents.tsx' + import { Box, + CircularProgress, IconButton, ListItem, ListItemText, @@ -16,7 +18,6 @@ import { CircularProgress, } from '@mui/material' import { Header } from '../../components/Header' -import { Button } from '../../components/CustomComponents.tsx' import React, { ChangeEvent, useEffect, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import List from '@mui/material/List' @@ -734,6 +735,7 @@ export function AddChangeSubjectPage() { + diff --git a/frontend/frontend/src/pages/subjectsPage/SubjectsPage.tsx b/frontend/frontend/src/pages/subjectsPage/SubjectsPage.tsx index 1b1715c5..83f6e456 100644 --- a/frontend/frontend/src/pages/subjectsPage/SubjectsPage.tsx +++ b/frontend/frontend/src/pages/subjectsPage/SubjectsPage.tsx @@ -375,7 +375,7 @@ export function SubjectsPage() { }} >
{ + match.replace(':', '\n') + return match + }) + } + } newSubmission.filename = submissionResponse.data.bestand.replace(/^.*[\\/]/, '') newSubmission.bestand = await instance @@ -290,21 +301,40 @@ export function SubmissionPage() { /> )} - - - Deadline - {project?.deadline - ? dayjs(project.deadline).format( - 'DD/MM/YYYY HH:mm' - ) - : 'error'} - - + {project?.deadline && ( + + + Deadline + {project?.deadline + ? dayjs(project.deadline).format( + 'DD/MM/YYYY HH:mm' + ) + : 'error'} + + + )} + {project?.extra_deadline && ( + + + Extra Deadline + {project?.extra_deadline + ? dayjs(project.extra_deadline).format( + 'DD/MM/YYYY HH:mm' + ) + : 'error'} + + + )} , errorElement: , }, - { - path: '*', - element: , // todo: change to 404-page - }, { path: '/error', element: , }, + { + path: '*', + element: , + }, ]) export default router