diff --git a/client/src/management/ApplicationsOverview/ApplicationOverview.tsx b/client/src/management/ApplicationsOverview/ApplicationOverview.tsx index 3c77a910..41c999db 100644 --- a/client/src/management/ApplicationsOverview/ApplicationOverview.tsx +++ b/client/src/management/ApplicationsOverview/ApplicationOverview.tsx @@ -1,21 +1,27 @@ -import { Group, TextInput, Checkbox, Stack, Button, Menu, Tooltip } from '@mantine/core' -import { IconAdjustments, IconSearch } from '@tabler/icons-react' +import { Group, TextInput, Stack, Button, Menu, Tooltip, Tabs, Flex } from '@mantine/core' +import { IconPlus, IconSearch, IconUpload } from '@tabler/icons-react' import { useState, useMemo, useEffect } from 'react' import { TechnicalChallengeAssessmentModal } from './components/TechnicalChallengeAssessmentModal' import { ApplicationDatatable } from './components/ApplicationDatatable' import { MatchingResultsUploadModal } from './components/MatchingResultsUploadModal' import { useApplicationStore } from '../../state/zustand/useApplicationStore' import { useQuery } from '@tanstack/react-query' -import { Application, ApplicationType } from '../../interface/application' +import { Application, ApplicationType, Gender } from '../../interface/application' import { Query } from '../../state/query' import { getApplications } from '../../network/application' import { useCourseIterationStore } from '../../state/zustand/useCourseIterationStore' +import { FilterChips } from './components/FilterChips' +import { FilterMenu } from './components/FilterMenu' export interface Filters { - male: boolean - female: boolean + gender: Gender[] status: string[] - applicationType: ApplicationType[] + assessment: { + noScore: boolean + minScore: number + maxScore: number + } + applicationType: ApplicationType } export const StudentApplicationOverview = (): JSX.Element => { @@ -33,10 +39,14 @@ export const StudentApplicationOverview = (): JSX.Element => { const [matchingResultsUploadModalOpened, setMatchingResultsUploadModalOpened] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [filters, setFilters] = useState({ - male: false, - female: false, + gender: [], status: [], - applicationType: [ApplicationType.DEVELOPER], + assessment: { + noScore: false, + minScore: 0, + maxScore: 100, + }, + applicationType: ApplicationType.DEVELOPER, }) const { data: fetchedDeveloperApplications, isLoading: isLoadingDeveloperApplications } = @@ -93,19 +103,19 @@ export const StudentApplicationOverview = (): JSX.Element => { const tumIdToApplicationMap = useMemo(() => { const map = new Map() - if (filters.applicationType.includes(ApplicationType.DEVELOPER)) { + if (filters.applicationType === ApplicationType.DEVELOPER) { developerApplications.forEach((application) => { if (application.student.tumId) { map.set(application.student.tumId, application.id) } }) - } else if (filters.applicationType.includes(ApplicationType.COACH)) { + } else if (filters.applicationType === ApplicationType.COACH) { coachApplications.forEach((application) => { if (application.student.tumId) { map.set(application.student.tumId, application.id) } }) - } else if (filters.applicationType.includes(ApplicationType.TUTOR)) { + } else if (filters.applicationType === ApplicationType.TUTOR) { tutorApplications.forEach((application) => { if (application.student.tumId) { map.set(application.student.tumId, application.id) @@ -117,19 +127,19 @@ export const StudentApplicationOverview = (): JSX.Element => { const matriculationNumberToApplicationMap = useMemo(() => { const map = new Map() - if (filters.applicationType.includes(ApplicationType.DEVELOPER)) { + if (filters.applicationType === ApplicationType.DEVELOPER) { developerApplications.forEach((application) => { if (application.student.matriculationNumber) { map.set(application.student.matriculationNumber, application.id) } }) - } else if (filters.applicationType.includes(ApplicationType.COACH)) { + } else if (filters.applicationType === ApplicationType.COACH) { coachApplications.forEach((application) => { if (application.student.matriculationNumber) { map.set(application.student.matriculationNumber, application.id) } }) - } else if (filters.applicationType.includes(ApplicationType.TUTOR)) { + } else if (filters.applicationType === ApplicationType.TUTOR) { tutorApplications.forEach((application) => { if (application.student.matriculationNumber) { map.set(application.student.matriculationNumber, application.id) @@ -142,74 +152,8 @@ export const StudentApplicationOverview = (): JSX.Element => { return ( - } - value={searchQuery} - onChange={(e) => { - setSearchQuery(e.currentTarget.value) - }} - /> - - - - - - - { - setFilters({ ...filters, male: e.currentTarget.checked }) - }} - /> - - - { - setFilters({ ...filters, female: e.currentTarget.checked }) - }} - /> - - - - - - - +

Student Applications

- - { - setMatchingResultsUploadModalOpened(true) - }} - > - Upload Matching Results - - - 1 || - !filters.applicationType.includes(ApplicationType.DEVELOPER) - } - onClick={() => { - setTechnicalChallengeAssessmentModalOpened(true) - }} - > - Technical Challenge Assessment - - - -
{ @@ -217,18 +161,93 @@ export const StudentApplicationOverview = (): JSX.Element => { }} tumIdToDeveloperApplicationMap={tumIdToApplicationMap} /> - {filters.applicationType.length === 1 && ( - { - setMatchingResultsUploadModalOpened(false) - }} - tumIdToApplicationMap={tumIdToApplicationMap} - matriculationNumberToApplicationMap={matriculationNumberToApplicationMap} - applicationType={filters.applicationType[0]} - /> - )} + { + setMatchingResultsUploadModalOpened(false) + }} + tumIdToApplicationMap={tumIdToApplicationMap} + matriculationNumberToApplicationMap={matriculationNumberToApplicationMap} + applicationType={filters.applicationType} + />
+ + { + if (selectedApplication === 'button') { + return // ignore button clicks in the tabs + } + setFilters((oldFilters) => { + return { + ...oldFilters, + applicationType: ApplicationType[selectedApplication ?? 'DEVELOPER'], + } + }) + }} + > + + {Object.keys(ApplicationType).map((applicationKey) => ( + + {ApplicationType[applicationKey].charAt(0).toUpperCase() + + ApplicationType[applicationKey].slice(1).toLowerCase()} + + ))} + + + } + value={searchQuery} + onChange={(e) => { + setSearchQuery(e.currentTarget.value) + }} + /> + + {/**This menu will be re-designed in the very near future */} + + + + + + + { + setMatchingResultsUploadModalOpened(true) + }} + > + Upload Matching Results + + + { + setTechnicalChallengeAssessmentModalOpened(true) + }} + > + Technical Challenge Assessment + + + + + + + + + + + - filters.female && application.student.gender - ? Gender[application.student.gender] === Gender.FEMALE - : true, - ) - .filter((application) => - filters.male && application.student.gender - ? Gender[application.student.gender] === Gender.MALE - : true, - ), + .filter((application) => { + const studentGender = application.student.gender + ? Gender[application.student.gender] + : null + + // If no gender filter is selected, don't filter by gender + if (filters.gender.length === 0) return true + + // Filter logic for each gender filter + const isFemaleSelected = filters.gender.includes(Gender.FEMALE) + const isMaleSelected = filters.gender.includes(Gender.MALE) + const isOtherSelected = filters.gender.includes(Gender.OTHER) + + // Check the current student's gender against the selected filters + if (studentGender) { + if (isFemaleSelected && studentGender === Gender.FEMALE) { + return true + } else if (isMaleSelected && studentGender === Gender.MALE) { + return true + } else if ( + isOtherSelected && + (studentGender === Gender.OTHER || studentGender === Gender.PREFER_NOT_TO_SAY) + ) { + return true + } + } + // If no gender is set, return false + return false + }) + .filter((application) => { + const assessmentScore = application.assessment?.assessmentScore + + // if notEvaluated is selected, the range does have no effect + if (filters.assessment.noScore) { + if (assessmentScore == null) { + return true + } else { + return false + } + } + + const minScore = filters.assessment.minScore ?? 0 + const maxScore = filters.assessment.maxScore ?? 100 + + if (assessmentScore !== undefined) { + return assessmentScore >= minScore && assessmentScore <= maxScore + } + + return false + }), sortStatus.columnAccessor === 'fullName' ? ['student.firstName', 'student.lastName'] : sortStatus.columnAccessor, @@ -394,68 +434,19 @@ export const ApplicationDatatable = ({ }} columns={[ { - accessor: 'type', - textAlign: 'center', - filter: ( - { - setFilters({ - ...filters, - applicationType: value as ApplicationType[], - }) - }} - leftSection={} - clearable - searchable - comboboxProps={{ withinPortal: false }} - /> - ), - filtering: filters.applicationType.length > 0, - render: ({ type }) => { - return `${type.charAt(0)}${type.toLowerCase().slice(1)}` - }, + accessor: 'fullName', + title: 'Full name', + sortable: true, + render: (developerApplication) => + `${developerApplication.student.firstName ?? ''} ${ + developerApplication.student.lastName ?? '' + }`, }, { accessor: 'assessment.status', title: 'Status', textAlign: 'center', - filter: ( - { - return { - label: ApplicationStatus[key as keyof typeof ApplicationStatus], - value: key, - } - })} - value={filters.status} - placeholder='Search status...' - onChange={(value) => { - setFilters({ - ...filters, - status: value, - }) - }} - leftSection={} - clearable - searchable - comboboxProps={{ withinPortal: false }} - /> - ), - filtering: filters.applicationType.length > 0, + sortable: true, render: (application) => getAssessmentBadge(application), }, { @@ -479,15 +470,6 @@ export const ApplicationDatatable = ({ title: 'Email', sortable: true, }, - { - accessor: 'fullName', - title: 'Full name', - sortable: true, - render: (developerApplication) => - `${developerApplication.student.firstName ?? ''} ${ - developerApplication.student.lastName ?? '' - }`, - }, { accessor: 'actions', title: 'Actions', diff --git a/client/src/management/ApplicationsOverview/components/FilterChips.tsx b/client/src/management/ApplicationsOverview/components/FilterChips.tsx new file mode 100644 index 00000000..109b72c9 --- /dev/null +++ b/client/src/management/ApplicationsOverview/components/FilterChips.tsx @@ -0,0 +1,125 @@ +import { Chip, Group, rem } from '@mantine/core' +import { Filters } from '../ApplicationOverview' +import { IconX } from '@tabler/icons-react' +import { ApplicationStatus, Gender } from '../../../interface/application' + +interface FilterChipsProps { + filters: Filters + setFilters: (updateFn: (prevFilters: Filters) => Filters) => void +} + +export const FilterChips = ({ filters, setFilters }: FilterChipsProps): JSX.Element => { + return ( + + + {filters.gender.map((gender) => ( + } + color='blue' + variant='light' + defaultChecked + onClick={() => { + setFilters((oldFilters: Filters) => { + return { + ...oldFilters, + gender: oldFilters.gender.filter((g) => g !== gender), + } + }) + }} + > + Gender: {gender === Gender.OTHER ? 'Unknown Gender' : gender} + + ))} + {filters.status.map((status) => ( + } + color='blue' + variant='light' + defaultChecked + onClick={() => { + setFilters((oldFilters: Filters) => { + return { + ...oldFilters, + status: oldFilters.status.filter((s) => s !== status), + } + }) + }} + > + Status: {ApplicationStatus[status]} + + ))} + {filters.assessment.maxScore < 100 && ( + } + color='blue' + variant='light' + defaultChecked + onClick={() => { + setFilters((oldFilters: Filters) => { + return { + ...oldFilters, + assessment: { + ...oldFilters.assessment, + maxScore: 100, + noScore: false, + }, + } + }) + }} + > + Max Score: {filters.assessment.maxScore} + + )} + {filters.assessment.minScore > 0 && ( + } + color='blue' + variant='light' + defaultChecked + onClick={() => { + setFilters((oldFilters: Filters) => { + return { + ...oldFilters, + assessment: { + ...oldFilters.assessment, + minScore: 0, + }, + } + }) + }} + > + Min Score: {filters.assessment.minScore} + + )} + {filters.assessment.noScore && ( + } + color='blue' + variant='light' + defaultChecked + onClick={() => { + setFilters((oldFilters: Filters) => { + return { + ...oldFilters, + assessment: { + ...oldFilters.assessment, + noScore: false, + minScore: 0, + maxScore: 100, + }, + } + }) + }} + > + No Score + + )} + + + ) +} diff --git a/client/src/management/ApplicationsOverview/components/FilterMenu.tsx b/client/src/management/ApplicationsOverview/components/FilterMenu.tsx new file mode 100644 index 00000000..05101eab --- /dev/null +++ b/client/src/management/ApplicationsOverview/components/FilterMenu.tsx @@ -0,0 +1,206 @@ +import { + Button, + Checkbox, + Menu, + NumberInput, + useMantineColorScheme, + useMantineTheme, +} from '@mantine/core' +import { IconFilter } from '@tabler/icons-react' +import { Filters } from '../ApplicationOverview' +import { ApplicationStatus, Gender } from '../../../interface/application' +import { MenuItemCheckbox } from './MenuItemCheckbox' + +interface FilterMenuProps { + filters: Filters + setFilters: (updateFn: (prevFilters: Filters) => Filters) => void +} + +export const FilterMenu = ({ filters, setFilters }: FilterMenuProps): JSX.Element => { + const handleStatusFilterChange = (status: string, checked: boolean) => { + setFilters((currFilters) => ({ + ...currFilters, + status: checked + ? [...currFilters.status, status] + : currFilters.status.filter((existingStatus) => existingStatus !== status.toString()), + })) + } + + const { colorScheme } = useMantineColorScheme() + const theme = useMantineTheme() + + function getTextColor(isTextActive: boolean) { + return isTextActive ? 'darkgray' : colorScheme === 'dark' ? theme.colors.dark[0] : theme.black + } + + return ( + + + + + + Gender + { + setFilters((currFilters) => ({ + ...currFilters, + gender: e.currentTarget.checked + ? [...currFilters.gender, Gender.MALE] + : currFilters.gender.filter((gender) => gender !== Gender.MALE), + })) + }} + /> + { + setFilters((currFilters) => ({ + ...currFilters, + gender: e.currentTarget.checked + ? [...currFilters.gender, Gender.FEMALE] + : currFilters.gender.filter((gender) => gender !== Gender.FEMALE), + })) + }} + /> + { + setFilters((currFilters) => ({ + ...currFilters, + gender: e.currentTarget.checked + ? [...currFilters.gender, Gender.OTHER] + : currFilters.gender.filter((gender) => gender !== Gender.OTHER), + })) + }} + /> + + + Assessment + { + setFilters((oldFilters: Filters) => { + return { + ...oldFilters, + assessment: { + minScore: 0, + maxScore: 100, + noScore: e.currentTarget.checked, + }, + } + }) + }} + /> + +
+ { + setFilters((oldFilters: Filters) => { + return { + ...oldFilters, + assessment: { + minScore: +value, + noScore: +value > 0 ? false : oldFilters.assessment.noScore, + maxScore: oldFilters.assessment.noScore + ? 100 + : oldFilters.assessment.maxScore, + }, + } + }) + }} + style={{ + width: '6rem', + }} + styles={{ + input: { + color: getTextColor(filters.assessment.minScore === 0), + opacity: filters.assessment.minScore === 0 ? 0.8 : 1, + }, + }} + /> + { + setFilters((oldFilters: Filters) => { + return { + ...oldFilters, + assessment: { + minScore: oldFilters.assessment.minScore, + maxScore: +value, + noScore: +value > 0 ? false : oldFilters.assessment.noScore, + }, + } + }) + }} + style={{ width: '6rem' }} + styles={{ + input: { + color: + filters.assessment.maxScore === 100 + ? 'darkgray' + : colorScheme === 'dark' + ? theme.colors.dark[0] + : theme.black, + opacity: filters.assessment.maxScore === 100 ? 0.8 : 1, + }, + }} + /> +
+
+ + + Assessment + {Object.keys(ApplicationStatus).map((status) => ( + handleStatusFilterChange(status, e.currentTarget.checked)} + /> + ))} + + +
+ +
+
+
+ ) +} diff --git a/client/src/management/ApplicationsOverview/components/MenuItemCheckbox.tsx b/client/src/management/ApplicationsOverview/components/MenuItemCheckbox.tsx new file mode 100644 index 00000000..21f003fa --- /dev/null +++ b/client/src/management/ApplicationsOverview/components/MenuItemCheckbox.tsx @@ -0,0 +1,33 @@ +import { Checkbox, Menu } from '@mantine/core' + +interface MenuItemCheckboxProps { + label: string + checked: boolean + onChange: React.ChangeEventHandler | undefined +} + +export const MenuItemCheckbox = ({ + label, + checked, + onChange, +}: MenuItemCheckboxProps): JSX.Element => { + return ( + + + + ) +}