From 2c9ee4a550c03811fd7b28d744b68279546754f7 Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Mon, 23 Sep 2024 13:46:56 +0200 Subject: [PATCH 1/8] redesigning filter and adding the functionality to filter for score --- .../ApplicationOverview.tsx | 171 +++++++++++++++++- .../components/ApplicationDatatable.tsx | 55 +++++- 2 files changed, 206 insertions(+), 20 deletions(-) diff --git a/client/src/management/ApplicationsOverview/ApplicationOverview.tsx b/client/src/management/ApplicationsOverview/ApplicationOverview.tsx index 3c77a910..0b2e3c20 100644 --- a/client/src/management/ApplicationsOverview/ApplicationOverview.tsx +++ b/client/src/management/ApplicationsOverview/ApplicationOverview.tsx @@ -1,4 +1,4 @@ -import { Group, TextInput, Checkbox, Stack, Button, Menu, Tooltip } from '@mantine/core' +import { Group, TextInput, Checkbox, Stack, Button, Menu, Tooltip, NumberInput } from '@mantine/core' import { IconAdjustments, IconSearch } from '@tabler/icons-react' import { useState, useMemo, useEffect } from 'react' import { TechnicalChallengeAssessmentModal } from './components/TechnicalChallengeAssessmentModal' @@ -6,15 +6,19 @@ 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' export interface Filters { - male: boolean - female: boolean + gender: Gender[] status: string[] + assessment: { + includeNotAssessed: boolean + minScore: number + maxScore: number + } applicationType: ApplicationType[] } @@ -33,9 +37,13 @@ export const StudentApplicationOverview = (): JSX.Element => { const [matchingResultsUploadModalOpened, setMatchingResultsUploadModalOpened] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [filters, setFilters] = useState({ - male: false, - female: false, + gender: [], status: [], + assessment: { + includeNotAssessed: false, + minScore: 0, + maxScore: 100, + }, applicationType: [ApplicationType.DEVELOPER], }) @@ -156,24 +164,167 @@ export const StudentApplicationOverview = (): JSX.Element => { + Gender { - setFilters({ ...filters, male: e.currentTarget.checked }) + setFilters((currFilters) => ({ + ...currFilters, + gender: e.currentTarget.checked + ? [...currFilters.gender, Gender.MALE] + : currFilters.gender.filter((gender) => gender !== Gender.MALE), + })) }} /> { - setFilters({ ...filters, female: e.currentTarget.checked }) + 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.PREFER_NOT_TO_SAY, Gender.OTHER] + : currFilters.gender.filter( + (gender) => + gender !== Gender.PREFER_NOT_TO_SAY && gender !== Gender.OTHER, + ), + })) + }} + /> + + + + Assessment + + { + setFilters((oldFilters: Filters) => { + return { + ...oldFilters, + assessment: { + minScore: e.currentTarget.checked ? 0 : 0, + maxScore: e.currentTarget.checked ? 0 : 100, + includeNotAssessed: e.currentTarget.checked, + }, + } + }) + }} + /> + + +
+ { + setFilters((oldFilters: Filters) => { + return { + ...oldFilters, + assessment: { + minScore: +value, + includeNotAssessed: + +value > 0 ? false : oldFilters.assessment.includeNotAssessed, + maxScore: oldFilters.assessment.includeNotAssessed + ? 100 + : oldFilters.assessment.maxScore, + }, + } + }) + }} + style={{ + width: '6rem', + }} + styles={{ + input: { + color: filters.assessment.minScore === 0 ? 'darkgray' : 'black', + opacity: filters.assessment.minScore === 0 ? 0.8 : 1, + }, + description: { + color: 'black', + }, + }} + /> + { + setFilters((oldFilters: Filters) => { + return { + ...oldFilters, + assessment: { + ...oldFilters.assessment, + maxScore: +value, + includeNotAssessed: + +value > 0 ? false : oldFilters.assessment.includeNotAssessed, + }, + } + }) + }} + style={{ width: '6rem' }} + styles={{ + input: { + color: filters.assessment.maxScore === 100 ? 'darkgray' : 'black', + opacity: filters.assessment.maxScore === 100 ? 0.8 : 1, + }, + description: { + color: 'black', + }, + }} + /> +
+
+ + + + +
diff --git a/client/src/management/ApplicationsOverview/components/ApplicationDatatable.tsx b/client/src/management/ApplicationsOverview/components/ApplicationDatatable.tsx index 985b07e2..18c2616a 100644 --- a/client/src/management/ApplicationsOverview/components/ApplicationDatatable.tsx +++ b/client/src/management/ApplicationsOverview/components/ApplicationDatatable.tsx @@ -145,16 +145,51 @@ export const ApplicationDatatable = ({ filters.status.length === 0 || filters.status.includes(application.assessment?.status ?? 'NOT_ASSESSED'), ) - .filter((application) => - 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 isPreferNotToSaySelected = filters.gender.includes(Gender.PREFER_NOT_TO_SAY) + 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) { + return true + } else if (isPreferNotToSaySelected && 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 (!assessmentScore && filters.assessment.includeNotAssessed) { + return true + } + + 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, From 7cb4d3c9b349ba1181e28ec2388bb999541e4939 Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Mon, 23 Sep 2024 14:56:36 +0200 Subject: [PATCH 2/8] fixing noEvaluated logic and adding filter chips --- .../ApplicationOverview.tsx | 44 +++++--- .../components/ApplicationDatatable.tsx | 17 ++- .../components/FilterChips.tsx | 105 ++++++++++++++++++ 3 files changed, 142 insertions(+), 24 deletions(-) create mode 100644 client/src/management/ApplicationsOverview/components/FilterChips.tsx diff --git a/client/src/management/ApplicationsOverview/ApplicationOverview.tsx b/client/src/management/ApplicationsOverview/ApplicationOverview.tsx index 0b2e3c20..d79666e8 100644 --- a/client/src/management/ApplicationsOverview/ApplicationOverview.tsx +++ b/client/src/management/ApplicationsOverview/ApplicationOverview.tsx @@ -1,4 +1,13 @@ -import { Group, TextInput, Checkbox, Stack, Button, Menu, Tooltip, NumberInput } from '@mantine/core' +import { + Group, + TextInput, + Checkbox, + Stack, + Button, + Menu, + Tooltip, + NumberInput, +} from '@mantine/core' import { IconAdjustments, IconSearch } from '@tabler/icons-react' import { useState, useMemo, useEffect } from 'react' import { TechnicalChallengeAssessmentModal } from './components/TechnicalChallengeAssessmentModal' @@ -10,12 +19,13 @@ import { Application, ApplicationType, Gender } from '../../interface/applicatio import { Query } from '../../state/query' import { getApplications } from '../../network/application' import { useCourseIterationStore } from '../../state/zustand/useCourseIterationStore' +import { FilterChips } from './components/FilterChips' export interface Filters { gender: Gender[] status: string[] assessment: { - includeNotAssessed: boolean + notEvaluated: boolean minScore: number maxScore: number } @@ -40,7 +50,7 @@ export const StudentApplicationOverview = (): JSX.Element => { gender: [], status: [], assessment: { - includeNotAssessed: false, + notEvaluated: false, minScore: 0, maxScore: 100, }, @@ -204,11 +214,8 @@ export const StudentApplicationOverview = (): JSX.Element => { setFilters((currFilters) => ({ ...currFilters, gender: e.currentTarget.checked - ? [...currFilters.gender, Gender.PREFER_NOT_TO_SAY, Gender.OTHER] - : currFilters.gender.filter( - (gender) => - gender !== Gender.PREFER_NOT_TO_SAY && gender !== Gender.OTHER, - ), + ? [...currFilters.gender, Gender.OTHER] + : currFilters.gender.filter((gender) => gender !== Gender.OTHER), })) }} /> @@ -219,15 +226,15 @@ export const StudentApplicationOverview = (): JSX.Element => { { setFilters((oldFilters: Filters) => { return { ...oldFilters, assessment: { - minScore: e.currentTarget.checked ? 0 : 0, - maxScore: e.currentTarget.checked ? 0 : 100, - includeNotAssessed: e.currentTarget.checked, + minScore: 0, + maxScore: 100, + notEvaluated: e.currentTarget.checked, }, } }) @@ -248,9 +255,8 @@ export const StudentApplicationOverview = (): JSX.Element => { ...oldFilters, assessment: { minScore: +value, - includeNotAssessed: - +value > 0 ? false : oldFilters.assessment.includeNotAssessed, - maxScore: oldFilters.assessment.includeNotAssessed + notEvaluated: +value > 0 ? false : oldFilters.assessment.notEvaluated, + maxScore: oldFilters.assessment.notEvaluated ? 100 : oldFilters.assessment.maxScore, }, @@ -283,8 +289,7 @@ export const StudentApplicationOverview = (): JSX.Element => { assessment: { ...oldFilters.assessment, maxScore: +value, - includeNotAssessed: - +value > 0 ? false : oldFilters.assessment.includeNotAssessed, + notEvaluated: +value > 0 ? false : oldFilters.assessment.notEvaluated, }, } }) @@ -314,7 +319,7 @@ export const StudentApplicationOverview = (): JSX.Element => { ...oldFilters, gender: [], assessment: { - includeNotAssessed: false, + notEvaluated: false, minScore: 0, maxScore: 100, }, @@ -380,6 +385,9 @@ export const StudentApplicationOverview = (): JSX.Element => { /> )} + + + { const assessmentScore = application.assessment?.assessmentScore - if (!assessmentScore && filters.assessment.includeNotAssessed) { - return true + // if notEvaluated is selected, the range does have no effect + if (filters.assessment.notEvaluated) { + if (assessmentScore == null) { + return true + } else { + return false + } } const minScore = filters.assessment.minScore ?? 0 diff --git a/client/src/management/ApplicationsOverview/components/FilterChips.tsx b/client/src/management/ApplicationsOverview/components/FilterChips.tsx new file mode 100644 index 00000000..5410ef99 --- /dev/null +++ b/client/src/management/ApplicationsOverview/components/FilterChips.tsx @@ -0,0 +1,105 @@ +import { Chip, Group, rem } from '@mantine/core' +import { Filters } from '../ApplicationOverview' +import { IconX } from '@tabler/icons-react' +import { 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='filled' + defaultChecked + onClick={() => { + setFilters((oldFilters: Filters) => { + return { + ...oldFilters, + gender: oldFilters.gender.filter((g) => g !== gender), + } + }) + }} + > + {gender === Gender.OTHER ? 'Unknown Gender' : gender} + + ))} + {filters.assessment.maxScore < 100 && ( + } + color='blue' + variant='filled' + defaultChecked + onClick={() => { + setFilters((oldFilters: Filters) => { + return { + ...oldFilters, + assessment: { + ...oldFilters.assessment, + maxScore: 100, + notEvaluated: false, + }, + } + }) + }} + > + Max Score: {filters.assessment.maxScore} + + )} + {filters.assessment.minScore > 0 && ( + } + color='blue' + variant='filled' + defaultChecked + onClick={() => { + setFilters((oldFilters: Filters) => { + return { + ...oldFilters, + assessment: { + ...oldFilters.assessment, + minScore: 0, + }, + } + }) + }} + > + Min Score: {filters.assessment.minScore} + + )} + {filters.assessment.notEvaluated && ( + } + color='blue' + variant='filled' + defaultChecked + onClick={() => { + setFilters((oldFilters: Filters) => { + return { + ...oldFilters, + assessment: { + notEvaluated: false, + minScore: 0, + maxScore: 100, + }, + } + }) + }} + > + Not Evaluated + + )} + + + ) +} From b0f39da0c015c0794ee85bfb7f100373677d6c21 Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Mon, 23 Sep 2024 19:02:09 +0200 Subject: [PATCH 3/8] renaming to noScore and including assessment in filter --- .../ApplicationOverview.tsx | 22 +++++++++------ .../components/ApplicationDatatable.tsx | 2 +- .../components/FilterChips.tsx | 28 ++++++++++++++++--- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/client/src/management/ApplicationsOverview/ApplicationOverview.tsx b/client/src/management/ApplicationsOverview/ApplicationOverview.tsx index d79666e8..7f936e5e 100644 --- a/client/src/management/ApplicationsOverview/ApplicationOverview.tsx +++ b/client/src/management/ApplicationsOverview/ApplicationOverview.tsx @@ -25,7 +25,7 @@ export interface Filters { gender: Gender[] status: string[] assessment: { - notEvaluated: boolean + noScore: boolean minScore: number maxScore: number } @@ -50,7 +50,7 @@ export const StudentApplicationOverview = (): JSX.Element => { gender: [], status: [], assessment: { - notEvaluated: false, + noScore: false, minScore: 0, maxScore: 100, }, @@ -225,8 +225,8 @@ export const StudentApplicationOverview = (): JSX.Element => { Assessment { setFilters((oldFilters: Filters) => { return { @@ -234,7 +234,7 @@ export const StudentApplicationOverview = (): JSX.Element => { assessment: { minScore: 0, maxScore: 100, - notEvaluated: e.currentTarget.checked, + noScore: e.currentTarget.checked, }, } }) @@ -255,8 +255,8 @@ export const StudentApplicationOverview = (): JSX.Element => { ...oldFilters, assessment: { minScore: +value, - notEvaluated: +value > 0 ? false : oldFilters.assessment.notEvaluated, - maxScore: oldFilters.assessment.notEvaluated + noScore: +value > 0 ? false : oldFilters.assessment.noScore, + maxScore: oldFilters.assessment.noScore ? 100 : oldFilters.assessment.maxScore, }, @@ -289,7 +289,7 @@ export const StudentApplicationOverview = (): JSX.Element => { assessment: { ...oldFilters.assessment, maxScore: +value, - notEvaluated: +value > 0 ? false : oldFilters.assessment.notEvaluated, + noScore: +value > 0 ? false : oldFilters.assessment.noScore, }, } }) @@ -319,7 +319,7 @@ export const StudentApplicationOverview = (): JSX.Element => { ...oldFilters, gender: [], assessment: { - notEvaluated: false, + noScore: false, minScore: 0, maxScore: 100, }, @@ -332,6 +332,10 @@ export const StudentApplicationOverview = (): JSX.Element => { + + + + diff --git a/client/src/management/ApplicationsOverview/components/ApplicationDatatable.tsx b/client/src/management/ApplicationsOverview/components/ApplicationDatatable.tsx index d2cf6648..fae72bbc 100644 --- a/client/src/management/ApplicationsOverview/components/ApplicationDatatable.tsx +++ b/client/src/management/ApplicationsOverview/components/ApplicationDatatable.tsx @@ -178,7 +178,7 @@ export const ApplicationDatatable = ({ const assessmentScore = application.assessment?.assessmentScore // if notEvaluated is selected, the range does have no effect - if (filters.assessment.notEvaluated) { + if (filters.assessment.noScore) { if (assessmentScore == null) { return true } else { diff --git a/client/src/management/ApplicationsOverview/components/FilterChips.tsx b/client/src/management/ApplicationsOverview/components/FilterChips.tsx index 5410ef99..15570925 100644 --- a/client/src/management/ApplicationsOverview/components/FilterChips.tsx +++ b/client/src/management/ApplicationsOverview/components/FilterChips.tsx @@ -31,6 +31,25 @@ export const FilterChips = ({ filters, setFilters }: FilterChipsProps): JSX.Elem {gender === Gender.OTHER ? 'Unknown Gender' : gender} ))} + {filters.status.map((status) => ( + } + color='blue' + variant='filled' + defaultChecked + onClick={() => { + setFilters((oldFilters: Filters) => { + return { + ...oldFilters, + status: oldFilters.status.filter((s) => s !== status), + } + }) + }} + > + {status} + + ))} {filters.assessment.maxScore < 100 && ( )} - {filters.assessment.notEvaluated && ( + {filters.assessment.noScore && ( } @@ -88,7 +107,8 @@ export const FilterChips = ({ filters, setFilters }: FilterChipsProps): JSX.Elem return { ...oldFilters, assessment: { - notEvaluated: false, + ...oldFilters.assessment, + noScore: false, minScore: 0, maxScore: 100, }, @@ -96,7 +116,7 @@ export const FilterChips = ({ filters, setFilters }: FilterChipsProps): JSX.Elem }) }} > - Not Evaluated + No Score )} From cc772b0c90656b2c34e7ad5368eee52cf43d9a8a Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Mon, 23 Sep 2024 19:31:11 +0200 Subject: [PATCH 4/8] Adding Status to filter --- .../ApplicationOverview.tsx | 34 ++++++++++++++++--- .../components/FilterChips.tsx | 8 ++--- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/client/src/management/ApplicationsOverview/ApplicationOverview.tsx b/client/src/management/ApplicationsOverview/ApplicationOverview.tsx index 7f936e5e..9f633361 100644 --- a/client/src/management/ApplicationsOverview/ApplicationOverview.tsx +++ b/client/src/management/ApplicationsOverview/ApplicationOverview.tsx @@ -15,7 +15,12 @@ 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, Gender } from '../../interface/application' +import { + Application, + ApplicationStatus, + ApplicationType, + Gender, +} from '../../interface/application' import { Query } from '../../state/query' import { getApplications } from '../../network/application' import { useCourseIterationStore } from '../../state/zustand/useCourseIterationStore' @@ -57,6 +62,17 @@ export const StudentApplicationOverview = (): JSX.Element => { applicationType: [ApplicationType.DEVELOPER], }) + const applicationStatusOptions = Object.values(ApplicationStatus) + + const handleStatusFilterChange = (status: string, checked: boolean) => { + setFilters((currFilters) => ({ + ...currFilters, + status: checked + ? [...currFilters.status, status] + : currFilters.status.filter((existingStatus) => existingStatus !== status.toString()), + })) + } + const { data: fetchedDeveloperApplications, isLoading: isLoadingDeveloperApplications } = useQuery({ queryKey: [Query.DEVELOPER_APPLICATION, selectedCourseIteration?.semesterName], @@ -308,6 +324,18 @@ export const StudentApplicationOverview = (): JSX.Element => { + + Assessment + {Object.keys(ApplicationStatus).map((status) => ( + + handleStatusFilterChange(status, e.currentTarget.checked)} + /> + + ))} + - - - diff --git a/client/src/management/ApplicationsOverview/components/FilterChips.tsx b/client/src/management/ApplicationsOverview/components/FilterChips.tsx index 15570925..667068fe 100644 --- a/client/src/management/ApplicationsOverview/components/FilterChips.tsx +++ b/client/src/management/ApplicationsOverview/components/FilterChips.tsx @@ -1,7 +1,7 @@ import { Chip, Group, rem } from '@mantine/core' import { Filters } from '../ApplicationOverview' import { IconX } from '@tabler/icons-react' -import { Gender } from '../../../interface/application' +import { ApplicationStatus, Gender } from '../../../interface/application' interface FilterChipsProps { filters: Filters @@ -28,7 +28,7 @@ export const FilterChips = ({ filters, setFilters }: FilterChipsProps): JSX.Elem }) }} > - {gender === Gender.OTHER ? 'Unknown Gender' : gender} + Gender: {gender === Gender.OTHER ? 'Unknown Gender' : gender} ))} {filters.status.map((status) => ( @@ -47,7 +47,7 @@ export const FilterChips = ({ filters, setFilters }: FilterChipsProps): JSX.Elem }) }} > - {status} + Status: {ApplicationStatus[status]} ))} {filters.assessment.maxScore < 100 && ( @@ -97,7 +97,7 @@ export const FilterChips = ({ filters, setFilters }: FilterChipsProps): JSX.Elem )} {filters.assessment.noScore && ( } color='blue' variant='filled' From 88db38ced662728e7b87727641b76e829096b10d Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Mon, 23 Sep 2024 20:29:32 +0200 Subject: [PATCH 5/8] moving filter menu in own file --- .../ApplicationOverview.tsx | 208 +---------------- .../components/FilterMenu.tsx | 215 ++++++++++++++++++ 2 files changed, 220 insertions(+), 203 deletions(-) create mode 100644 client/src/management/ApplicationsOverview/components/FilterMenu.tsx diff --git a/client/src/management/ApplicationsOverview/ApplicationOverview.tsx b/client/src/management/ApplicationsOverview/ApplicationOverview.tsx index 9f633361..467295d8 100644 --- a/client/src/management/ApplicationsOverview/ApplicationOverview.tsx +++ b/client/src/management/ApplicationsOverview/ApplicationOverview.tsx @@ -1,30 +1,17 @@ -import { - Group, - TextInput, - Checkbox, - Stack, - Button, - Menu, - Tooltip, - NumberInput, -} from '@mantine/core' -import { IconAdjustments, IconSearch } from '@tabler/icons-react' +import { Group, TextInput, Stack, Button, Menu, Tooltip } from '@mantine/core' +import { IconSearch } 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, - ApplicationStatus, - ApplicationType, - Gender, -} 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 { gender: Gender[] @@ -62,17 +49,6 @@ export const StudentApplicationOverview = (): JSX.Element => { applicationType: [ApplicationType.DEVELOPER], }) - const applicationStatusOptions = Object.values(ApplicationStatus) - - const handleStatusFilterChange = (status: string, checked: boolean) => { - setFilters((currFilters) => ({ - ...currFilters, - status: checked - ? [...currFilters.status, status] - : currFilters.status.filter((existingStatus) => existingStatus !== status.toString()), - })) - } - const { data: fetchedDeveloperApplications, isLoading: isLoadingDeveloperApplications } = useQuery({ queryKey: [Query.DEVELOPER_APPLICATION, selectedCourseIteration?.semesterName], @@ -185,182 +161,8 @@ export const StudentApplicationOverview = (): JSX.Element => { setSearchQuery(e.currentTarget.value) }} /> - - - - - - 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: filters.assessment.minScore === 0 ? 'darkgray' : 'black', - opacity: filters.assessment.minScore === 0 ? 0.8 : 1, - }, - description: { - color: 'black', - }, - }} - /> - { - setFilters((oldFilters: Filters) => { - return { - ...oldFilters, - assessment: { - ...oldFilters.assessment, - maxScore: +value, - noScore: +value > 0 ? false : oldFilters.assessment.noScore, - }, - } - }) - }} - style={{ width: '6rem' }} - styles={{ - input: { - color: filters.assessment.maxScore === 100 ? 'darkgray' : 'black', - opacity: filters.assessment.maxScore === 100 ? 0.8 : 1, - }, - description: { - color: 'black', - }, - }} - /> -
-
- - - Assessment - {Object.keys(ApplicationStatus).map((status) => ( - - handleStatusFilterChange(status, e.currentTarget.checked)} - /> - - ))} - - - - - -
-
+ diff --git a/client/src/management/ApplicationsOverview/components/FilterMenu.tsx b/client/src/management/ApplicationsOverview/components/FilterMenu.tsx new file mode 100644 index 00000000..0502fbd0 --- /dev/null +++ b/client/src/management/ApplicationsOverview/components/FilterMenu.tsx @@ -0,0 +1,215 @@ +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' + +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() + + 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: + filters.assessment.minScore === 0 + ? 'darkgray' + : colorScheme === 'dark' + ? theme.colors.dark[0] + : theme.black, + 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)} + /> + + ))} + + + + + +
+
+ ) +} From 0739fc1adb1cbcfbe8f2df1cde5b5a6e3d1bff2c Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Tue, 24 Sep 2024 10:15:24 +0200 Subject: [PATCH 6/8] redesign of filtering for developer / coach / tutor --- .../ApplicationOverview.tsx | 77 ++++++++++++------ .../components/ApplicationDatatable.tsx | 80 +++---------------- 2 files changed, 63 insertions(+), 94 deletions(-) diff --git a/client/src/management/ApplicationsOverview/ApplicationOverview.tsx b/client/src/management/ApplicationsOverview/ApplicationOverview.tsx index 467295d8..045b94da 100644 --- a/client/src/management/ApplicationsOverview/ApplicationOverview.tsx +++ b/client/src/management/ApplicationsOverview/ApplicationOverview.tsx @@ -1,4 +1,13 @@ -import { Group, TextInput, Stack, Button, Menu, Tooltip } from '@mantine/core' +import { + Group, + TextInput, + Stack, + Button, + Menu, + Tooltip, + SegmentedControl, + Center, +} from '@mantine/core' import { IconSearch } from '@tabler/icons-react' import { useState, useMemo, useEffect } from 'react' import { TechnicalChallengeAssessmentModal } from './components/TechnicalChallengeAssessmentModal' @@ -21,7 +30,7 @@ export interface Filters { minScore: number maxScore: number } - applicationType: ApplicationType[] + applicationType: ApplicationType } export const StudentApplicationOverview = (): JSX.Element => { @@ -46,7 +55,7 @@ export const StudentApplicationOverview = (): JSX.Element => { minScore: 0, maxScore: 100, }, - applicationType: [ApplicationType.DEVELOPER], + applicationType: ApplicationType.DEVELOPER, }) const { data: fetchedDeveloperApplications, isLoading: isLoadingDeveloperApplications } = @@ -103,19 +112,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) @@ -127,19 +136,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) @@ -171,7 +180,7 @@ export const StudentApplicationOverview = (): JSX.Element => { { setMatchingResultsUploadModalOpened(true) }} @@ -185,10 +194,7 @@ export const StudentApplicationOverview = (): JSX.Element => { technical challenge score, please select solely 'Developer' sorting filter" > 1 || - !filters.applicationType.includes(ApplicationType.DEVELOPER) - } + disabled={filters.applicationType !== ApplicationType.DEVELOPER} onClick={() => { setTechnicalChallengeAssessmentModalOpened(true) }} @@ -205,21 +211,42 @@ 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} + /> + + setFilters((oldFilters) => { + return { + ...oldFilters, + applicationType: ApplicationType[selectedApplication], + } + }) + } + data={Object.keys(ApplicationType).map((applicationKey) => ({ + value: applicationKey.toString(), + label: ( +
+ + {ApplicationType[applicationKey].charAt(0).toUpperCase() + + ApplicationType[applicationKey].slice(1).toLowerCase()} + +
+ ), + }))} + /> + { - 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), }, { @@ -519,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', From 2e3a4fdeb3f02244b0f968fd4ee819e7f3527e1e Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Tue, 24 Sep 2024 17:58:22 +0200 Subject: [PATCH 7/8] redesign of the menu bar with help of bene --- .../ApplicationOverview.tsx | 143 +++++++++--------- .../components/FilterChips.tsx | 10 +- 2 files changed, 77 insertions(+), 76 deletions(-) diff --git a/client/src/management/ApplicationsOverview/ApplicationOverview.tsx b/client/src/management/ApplicationsOverview/ApplicationOverview.tsx index 045b94da..41c999db 100644 --- a/client/src/management/ApplicationsOverview/ApplicationOverview.tsx +++ b/client/src/management/ApplicationsOverview/ApplicationOverview.tsx @@ -1,14 +1,5 @@ -import { - Group, - TextInput, - Stack, - Button, - Menu, - Tooltip, - SegmentedControl, - Center, -} from '@mantine/core' -import { 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' @@ -161,49 +152,8 @@ export const StudentApplicationOverview = (): JSX.Element => { return ( - } - value={searchQuery} - onChange={(e) => { - setSearchQuery(e.currentTarget.value) - }} - /> - - - - - - - +

Student Applications

- - { - setMatchingResultsUploadModalOpened(true) - }} - > - Upload Matching Results - - - { - setTechnicalChallengeAssessmentModalOpened(true) - }} - > - Technical Challenge Assessment - - - -
{ @@ -222,30 +172,81 @@ export const StudentApplicationOverview = (): JSX.Element => { />
- - - + onChange={(selectedApplication) => { + if (selectedApplication === 'button') { + return // ignore button clicks in the tabs + } setFilters((oldFilters) => { return { ...oldFilters, - applicationType: ApplicationType[selectedApplication], + applicationType: ApplicationType[selectedApplication ?? 'DEVELOPER'], } }) - } - data={Object.keys(ApplicationType).map((applicationKey) => ({ - value: applicationKey.toString(), - label: ( -
- - {ApplicationType[applicationKey].charAt(0).toUpperCase() + - ApplicationType[applicationKey].slice(1).toLowerCase()} - -
- ), - }))} - /> + }} + > + + {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 + + + + + + + + + + } color='blue' - variant='filled' + variant='light' defaultChecked onClick={() => { setFilters((oldFilters: Filters) => { @@ -36,7 +36,7 @@ export const FilterChips = ({ filters, setFilters }: FilterChipsProps): JSX.Elem key={status} icon={} color='blue' - variant='filled' + variant='light' defaultChecked onClick={() => { setFilters((oldFilters: Filters) => { @@ -55,7 +55,7 @@ export const FilterChips = ({ filters, setFilters }: FilterChipsProps): JSX.Elem key='maxScore' icon={} color='blue' - variant='filled' + variant='light' defaultChecked onClick={() => { setFilters((oldFilters: Filters) => { @@ -78,7 +78,7 @@ export const FilterChips = ({ filters, setFilters }: FilterChipsProps): JSX.Elem key='minScore' icon={} color='blue' - variant='filled' + variant='light' defaultChecked onClick={() => { setFilters((oldFilters: Filters) => { @@ -100,7 +100,7 @@ export const FilterChips = ({ filters, setFilters }: FilterChipsProps): JSX.Elem key='no Score assigned' icon={} color='blue' - variant='filled' + variant='light' defaultChecked onClick={() => { setFilters((oldFilters: Filters) => { From 592b3a2e306b4c9830f3c5c5c7ab132358a0d6de Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Wed, 25 Sep 2024 21:44:54 +0200 Subject: [PATCH 8/8] Fixing dom related wanring + making checkbox clickable on whole menu width + some code cleanup --- .../components/FilterMenu.tsx | 149 ++++++++---------- .../components/MenuItemCheckbox.tsx | 33 ++++ 2 files changed, 103 insertions(+), 79 deletions(-) create mode 100644 client/src/management/ApplicationsOverview/components/MenuItemCheckbox.tsx diff --git a/client/src/management/ApplicationsOverview/components/FilterMenu.tsx b/client/src/management/ApplicationsOverview/components/FilterMenu.tsx index 0502fbd0..05101eab 100644 --- a/client/src/management/ApplicationsOverview/components/FilterMenu.tsx +++ b/client/src/management/ApplicationsOverview/components/FilterMenu.tsx @@ -9,6 +9,7 @@ import { import { IconFilter } from '@tabler/icons-react' import { Filters } from '../ApplicationOverview' import { ApplicationStatus, Gender } from '../../../interface/application' +import { MenuItemCheckbox } from './MenuItemCheckbox' interface FilterMenuProps { filters: Filters @@ -28,6 +29,10 @@ export const FilterMenu = ({ filters, setFilters }: FilterMenuProps): JSX.Elemen const { colorScheme } = useMantineColorScheme() const theme = useMantineTheme() + function getTextColor(isTextActive: boolean) { + return isTextActive ? 'darkgray' : colorScheme === 'dark' ? theme.colors.dark[0] : theme.black + } + return ( @@ -37,73 +42,65 @@ export const FilterMenu = ({ filters, setFilters }: FilterMenuProps): JSX.Elemen 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), - })) - }} - /> - + { + 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: 0, + maxScore: 100, + noScore: e.currentTarget.checked, + }, + } + }) + }} + /> +
Assessment {Object.keys(ApplicationStatus).map((status) => ( - - handleStatusFilterChange(status, e.currentTarget.checked)} - /> - + 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 ( + + + + ) +}