From 5b76a740d1c0e65cfeaf3ea336ba7e86a34f1afa Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Mon, 18 Nov 2024 18:10:54 +0200 Subject: [PATCH] VKT(Frontend): Implement exam event listing across all examiners for OPH clerk user [deploy] --- .../packages/vkt/public/i18n/fi-FI/clerk.json | 11 +++ .../ClerkExaminerExamEventListing.tsx | 71 +++++++++++++++++ .../ClerkExaminerExamEventListingHeader.tsx | 22 ++++++ .../ClerkExaminerExamEventListingRow.tsx | 77 +++++++++++++++++++ .../ClerkExaminerExamEventToggleFilters.tsx | 45 +++++++++++ .../clerkExaminer/ClerkExaminerListing.tsx | 3 +- .../vkt/src/interfaces/clerkListExaminer.ts | 19 ++++- .../ClerkGoodAndSatisfactoryLevelPage.tsx | 6 +- .../src/redux/reducers/clerkListExaminer.ts | 28 ++++++- .../src/redux/selectors/clerkListExaminer.ts | 43 ++++++++++- frontend/packages/vkt/src/utils/examEvent.ts | 17 ++++ 11 files changed, 329 insertions(+), 13 deletions(-) create mode 100644 frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerExamEventListing.tsx create mode 100644 frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerExamEventListingHeader.tsx create mode 100644 frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerExamEventListingRow.tsx create mode 100644 frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerExamEventToggleFilters.tsx diff --git a/frontend/packages/vkt/public/i18n/fi-FI/clerk.json b/frontend/packages/vkt/public/i18n/fi-FI/clerk.json index 9683db661..60061e3f9 100644 --- a/frontend/packages/vkt/public/i18n/fi-FI/clerk.json +++ b/frontend/packages/vkt/public/i18n/fi-FI/clerk.json @@ -181,6 +181,17 @@ "copyToClipboardFailed": "Kopiointi leikepöydälle epäonnistui" } }, + "clerkExaminerExamEventListing": { + "header": { + "examDate": "Tutkintopäivä", + "examiner": "Tutkinnon vastaanottaja", + "municipality": "Paikkakunta", + "isPublic": "Nähtävillä julkisesti", + "language": "Tutkinnon kieli" + }, + "more": "Lisätiedot", + "title": "Tutkintotilaisuudet" + }, "clerkExaminerListing": { "buttons": { "viewDetails": "Katso tiedot" diff --git a/frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerExamEventListing.tsx b/frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerExamEventListing.tsx new file mode 100644 index 000000000..2df9ae4ac --- /dev/null +++ b/frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerExamEventListing.tsx @@ -0,0 +1,71 @@ +import { Divider, SelectChangeEvent } from '@mui/material'; +import { H2, PaginatedTable } from 'shared/components'; + +import { ClerkExaminerExamEventListingHeader } from 'components/clerkExaminer/ClerkExaminerExamEventListingHeader'; +import { ClerkExaminerExamEventListingRow } from 'components/clerkExaminer/ClerkExaminerExamEventListingRow'; +import { ClerkExaminerExamEventToggleFilters } from 'components/clerkExaminer/ClerkExaminerExamEventToggleFilters'; +import { LanguageFilter } from 'components/common/LanguageFilter'; +import { useClerkTranslation, useCommonTranslation } from 'configs/i18n'; +import { useAppDispatch, useAppSelector } from 'configs/redux'; +import { ExamLanguage } from 'enums/app'; +import { ClerkExaminerExamEventListingEntry } from 'interfaces/clerkListExaminer'; +import { setClerkListExaminerExamEventFilters } from 'redux/reducers/clerkListExaminer'; +import { + clerkListExaminerSelector, + selectFilteredClerkExaminerExamEvents, +} from 'redux/selectors/clerkListExaminer'; + +const getRowDetails = (entry: ClerkExaminerExamEventListingEntry) => { + return ; +}; + +export const ClerkExaminerExamEventListing = () => { + const { t } = useClerkTranslation({ + keyPrefix: 'vkt.component.clerkExamEventListing', + }); + const translateCommon = useCommonTranslation(); + const dispatch = useAppDispatch(); + + const { examLanguage } = useAppSelector(clerkListExaminerSelector).filters + .examEvents; + + const handleLanguageFilterChange = (event: SelectChangeEvent) => { + dispatch( + setClerkListExaminerExamEventFilters({ + examLanguage: event.target.value as ExamLanguage, + }), + ); + }; + + const entries = useAppSelector(selectFilteredClerkExaminerExamEvents); + + // TODO Table sorting not implemented yet! + + return ( + <> +
+
+

{t('title')}

+
+
+ + + + } + className="table-layout-auto" + data={entries} + header={} + getRowDetails={getRowDetails} + initialRowsPerPage={10} + rowsPerPageOptions={[10, 20, 50]} + rowsPerPageLabel={translateCommon('rowsPerPageLabel')} + stickyHeader + /> + + ); +}; diff --git a/frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerExamEventListingHeader.tsx b/frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerExamEventListingHeader.tsx new file mode 100644 index 000000000..eac7b885f --- /dev/null +++ b/frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerExamEventListingHeader.tsx @@ -0,0 +1,22 @@ +import { TableCell, TableHead, TableRow } from '@mui/material'; + +import { useClerkTranslation } from 'configs/i18n'; + +export const ClerkExaminerExamEventListingHeader = () => { + const { t } = useClerkTranslation({ + keyPrefix: 'vkt.component.clerkExaminerExamEventListing.header', + }); + + return ( + + + {t('examiner')} + {t('language')} + {t('municipality')} + {t('examDate')} + {t('isPublic')} + + + + ); +}; diff --git a/frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerExamEventListingRow.tsx b/frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerExamEventListingRow.tsx new file mode 100644 index 000000000..d24ae7b49 --- /dev/null +++ b/frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerExamEventListingRow.tsx @@ -0,0 +1,77 @@ +import { ChevronRight } from '@mui/icons-material'; +import { TableCell, TableRow } from '@mui/material'; +import { Link } from 'react-router-dom'; +import { CustomButtonLink, Text } from 'shared/components'; +import { Color, Variant } from 'shared/enums'; + +import { + useClerkTranslation, + useCommonTranslation, + useKoodistoMunicipalitiesTranslation, +} from 'configs/i18n'; +import { AppRoutes } from 'enums/app'; +import { ClerkExaminerExamEventListingEntry } from 'interfaces/clerkListExaminer'; +import { DateTimeUtils } from 'utils/dateTime'; + +export const ClerkExaminerExamEventListingRow = ({ + entry, +}: { + entry: ClerkExaminerExamEventListingEntry; +}) => { + const translateMunicipality = useKoodistoMunicipalitiesTranslation(); + const translateCommon = useCommonTranslation(); + const { t } = useClerkTranslation({ + keyPrefix: 'vkt.component.clerkExaminerExamEventListing', + }); + const { examiner, examEvent } = entry; + + const examEventUrl = AppRoutes.ExaminerExamEventPage.replace( + /:oid/, + examiner.oid, + ).replace(/:examEventId$/, `${examEvent.id}`); + + return ( + <> + + + + {`${examiner.firstName} ${examiner.lastName}`} + + + + {translateCommon(`examLanguage.${examEvent.language}`)} + + + {translateMunicipality(examEvent.municipality.code)} + + + {DateTimeUtils.renderDate(examEvent.date)} + + + + {examEvent.isHidden + ? translateCommon('no') + : translateCommon('yes')} + + + + } + to={examEventUrl} + > + {t('more')} + + + + + ); +}; diff --git a/frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerExamEventToggleFilters.tsx b/frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerExamEventToggleFilters.tsx new file mode 100644 index 000000000..dbcba62f5 --- /dev/null +++ b/frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerExamEventToggleFilters.tsx @@ -0,0 +1,45 @@ +import { ToggleFilterGroup } from 'shared/components'; + +import { useClerkTranslation } from 'configs/i18n'; +import { useAppDispatch, useAppSelector } from 'configs/redux'; +import { ExamEventToggleFilter } from 'enums/app'; +import { setClerkListExaminerExamEventFilters } from 'redux/reducers/clerkListExaminer'; +import { clerkListExaminerSelector } from 'redux/selectors/clerkListExaminer'; +import { ExamEventUtils } from 'utils/examEvent'; + +export const ClerkExaminerExamEventToggleFilters = () => { + const { t } = useClerkTranslation({ + keyPrefix: 'vkt.component.clerkExamEventListing.toggleFilters', + }); + + const { examiners, filters } = useAppSelector(clerkListExaminerSelector); + const examEvents = examiners.flatMap(({ examEvents }) => examEvents); + const dispatch = useAppDispatch(); + + const setToggleFilter = (status: ExamEventToggleFilter) => { + dispatch(setClerkListExaminerExamEventFilters({ toggleFilters: status })); + }; + + const filterData = [ + { + status: ExamEventToggleFilter.Upcoming, + label: t(ExamEventToggleFilter.Upcoming), + count: ExamEventUtils.getUpcomingExamEvents(examEvents).length, + testId: `clerk-exam-event-toggle-filters__${ExamEventToggleFilter.Upcoming}-btn`, + }, + { + status: ExamEventToggleFilter.Passed, + label: t(ExamEventToggleFilter.Passed), + count: ExamEventUtils.getPassedExamEvents(examEvents).length, + testId: `clerk-exam-event-toggle-filters__${ExamEventToggleFilter.Passed}-btn`, + }, + ]; + + return ( + + ); +}; diff --git a/frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerListing.tsx b/frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerListing.tsx index 2f00579fd..458e75fca 100644 --- a/frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerListing.tsx +++ b/frontend/packages/vkt/src/components/clerkExaminer/ClerkExaminerListing.tsx @@ -112,7 +112,8 @@ const ExaminerFilter = () => { const { t } = useExaminerTranslation({ keyPrefix: 'vkt.component.examinerFilter', }); - const { examLanguage } = useAppSelector(clerkListExaminerSelector).filters; + const { examLanguage } = useAppSelector(clerkListExaminerSelector).filters + .examiners; const dispatch = useAppDispatch(); return ( diff --git a/frontend/packages/vkt/src/interfaces/clerkListExaminer.ts b/frontend/packages/vkt/src/interfaces/clerkListExaminer.ts index b425d2218..dae9e4177 100644 --- a/frontend/packages/vkt/src/interfaces/clerkListExaminer.ts +++ b/frontend/packages/vkt/src/interfaces/clerkListExaminer.ts @@ -1,14 +1,29 @@ import { APIResponseStatus } from 'shared/enums'; +import { WithId } from 'shared/interfaces'; -import { ExamLanguage } from 'enums/app'; +import { ExamEventToggleFilter, ExamLanguage } from 'enums/app'; import { ExaminerDetails } from 'interfaces/examinerDetails'; +import { ExaminerExamEvent } from 'interfaces/examinerExamEvent'; export interface ClerkListExaminerFilters { examLanguage: ExamLanguage; } +export interface ClerkListExaminerExamEventFilters { + examLanguage: ExamLanguage; + toggleFilters: ExamEventToggleFilter; +} + export interface ClerkListExaminerState { status: APIResponseStatus; examiners: Array; - filters: ClerkListExaminerFilters; + filters: { + examiners: ClerkListExaminerFilters + examEvents: ClerkListExaminerExamEventFilters; + } } + +export interface ClerkExaminerExamEventListingEntry extends WithId { + examiner: Omit; + examEvent: ExaminerExamEvent; +} \ No newline at end of file diff --git a/frontend/packages/vkt/src/pages/ClerkGoodAndSatisfactoryLevelPage.tsx b/frontend/packages/vkt/src/pages/ClerkGoodAndSatisfactoryLevelPage.tsx index 174bdb0fd..d60315223 100644 --- a/frontend/packages/vkt/src/pages/ClerkGoodAndSatisfactoryLevelPage.tsx +++ b/frontend/packages/vkt/src/pages/ClerkGoodAndSatisfactoryLevelPage.tsx @@ -3,7 +3,7 @@ import { FC, useEffect } from 'react'; import { H1 } from 'shared/components'; import { APIResponseStatus } from 'shared/enums'; -import { ClerkExamEventListing } from 'components/clerkExamEvent/listing/ClerkExamEventListing'; +import { ClerkExaminerExamEventListing } from 'components/clerkExaminer/ClerkExaminerExamEventListing'; import { ClerkExaminerListing } from 'components/clerkExaminer/ClerkExaminerListing'; import { PublicExamEventGridSkeleton } from 'components/skeletons/PublicExamEventGridSkeleton'; import { useClerkTranslation } from 'configs/i18n'; @@ -45,8 +45,6 @@ export const ClerkGoodAndSatisfactoryLevelPage: FC = () => { useEffect(() => { dispatch(resetClerkExamEventOverview()); }, [dispatch]); - // TODO Listing of examiners - // TODO Listing of exam events of good and satisfactory level return ( @@ -74,7 +72,7 @@ export const ClerkGoodAndSatisfactoryLevelPage: FC = () => { {examEventsLoading ? ( ) : ( - + )} {' '} diff --git a/frontend/packages/vkt/src/redux/reducers/clerkListExaminer.ts b/frontend/packages/vkt/src/redux/reducers/clerkListExaminer.ts index de04637d6..bf0cce97b 100644 --- a/frontend/packages/vkt/src/redux/reducers/clerkListExaminer.ts +++ b/frontend/packages/vkt/src/redux/reducers/clerkListExaminer.ts @@ -1,8 +1,9 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { APIResponseStatus } from 'shared/enums'; -import { ExamLanguage } from 'enums/app'; +import { ExamEventToggleFilter, ExamLanguage } from 'enums/app'; import { + ClerkListExaminerExamEventFilters, ClerkListExaminerFilters, ClerkListExaminerState, } from 'interfaces/clerkListExaminer'; @@ -12,7 +13,13 @@ const initialState: ClerkListExaminerState = { status: APIResponseStatus.NotStarted, examiners: [], filters: { - examLanguage: ExamLanguage.ALL, + examiners: { + examLanguage: ExamLanguage.ALL, + }, + examEvents: { + examLanguage: ExamLanguage.ALL, + toggleFilters: ExamEventToggleFilter.Upcoming, + }, }, }; @@ -35,9 +42,21 @@ const clerkListExaminerSlice = createSlice({ }, setClerkListExaminerFilters( state, - action: PayloadAction, + action: PayloadAction>, + ) { + state.filters.examiners = { + ...state.filters.examiners, + ...action.payload, + }; + }, + setClerkListExaminerExamEventFilters( + state, + action: PayloadAction>, ) { - state.filters = action.payload; + state.filters.examEvents = { + ...state.filters.examEvents, + ...action.payload, + }; }, }, }); @@ -47,5 +66,6 @@ export const { loadClerkListExaminers, rejectClerkListExaminers, setClerkListExaminerFilters, + setClerkListExaminerExamEventFilters, } = clerkListExaminerSlice.actions; export const clerkListExaminerReducer = clerkListExaminerSlice.reducer; diff --git a/frontend/packages/vkt/src/redux/selectors/clerkListExaminer.ts b/frontend/packages/vkt/src/redux/selectors/clerkListExaminer.ts index d08e69884..9cb8a6818 100644 --- a/frontend/packages/vkt/src/redux/selectors/clerkListExaminer.ts +++ b/frontend/packages/vkt/src/redux/selectors/clerkListExaminer.ts @@ -1,12 +1,15 @@ import { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'configs/redux'; -import { ExamLanguage } from 'enums/app'; +import { ExamEventToggleFilter, ExamLanguage } from 'enums/app'; import { + ClerkExaminerExamEventListingEntry, + ClerkListExaminerExamEventFilters, ClerkListExaminerFilters, ClerkListExaminerState, } from 'interfaces/clerkListExaminer'; import { ExaminerDetails } from 'interfaces/examinerDetails'; +import { ExamEventUtils } from 'utils/examEvent'; export const clerkListExaminerSelector = ( state: RootState, @@ -14,7 +17,7 @@ export const clerkListExaminerSelector = ( export const selectFilteredExaminers = createSelector( (state: RootState) => state.clerkListExaminer.examiners, - (state: RootState) => state.clerkListExaminer.filters, + (state: RootState) => state.clerkListExaminer.filters.examiners, ( examiners: Array, filters: ClerkListExaminerFilters, @@ -30,3 +33,39 @@ export const selectFilteredExaminers = createSelector( } }, ); + +export const selectFilteredClerkExaminerExamEvents = createSelector( + (state: RootState) => state.clerkListExaminer.examiners, + (state: RootState) => state.clerkListExaminer.filters.examEvents, + ( + examiners: Array, + filters: ClerkListExaminerExamEventFilters, + ): Array => { + let results: Array = examiners + .flatMap((examiner) => { + const { + examEvents, + contactRequests: _contactRequests, + ...rest + } = examiner; + + return examEvents.map((examEvent) => ({ + examEvent, + examiner: rest, + })); + }) + .map((v, i) => ({ ...v, id: i })); + + if (filters.examLanguage !== ExamLanguage.ALL) { + results = results.filter( + ({ examEvent }) => examEvent.language === filters.examLanguage, + ); + } + + if (filters.toggleFilters === ExamEventToggleFilter.Upcoming) { + return ExamEventUtils.getUpcomingClerkExaminerExamEventEntries(results); + } else { + return ExamEventUtils.getPassedClerkExaminerExamEventEntries(results); + } + }, +); diff --git a/frontend/packages/vkt/src/utils/examEvent.ts b/frontend/packages/vkt/src/utils/examEvent.ts index 08fd787b8..7de5ceb56 100644 --- a/frontend/packages/vkt/src/utils/examEvent.ts +++ b/frontend/packages/vkt/src/utils/examEvent.ts @@ -5,6 +5,7 @@ import { EnrollmentStatus, ExamLanguage, ExamLevel } from 'enums/app'; import { ClerkEnrollment } from 'interfaces/clerkEnrollment'; import { ClerkExamEvent } from 'interfaces/clerkExamEvent'; import { ClerkListExamEvent } from 'interfaces/clerkListExamEvent'; +import { ClerkExaminerExamEventListingEntry } from 'interfaces/clerkListExaminer'; import { ExaminerExamEvent } from 'interfaces/examinerExamEvent'; import { PublicExamEvent } from 'interfaces/publicExamEvent'; @@ -47,6 +48,22 @@ export class ExamEventUtils { ); } + static getUpcomingClerkExaminerExamEventEntries( + entries: Array, + ) { + return entries.filter( + ({ examEvent }) => !DateUtils.isDatePartBefore(examEvent.date, dayjs()), + ); + } + + static getPassedClerkExaminerExamEventEntries( + entries: Array, + ) { + return entries.filter(({ examEvent }) => + DateUtils.isDatePartBefore(examEvent.date, dayjs()), + ); + } + static hasOpenings(examEvent: PublicExamEvent) { return examEvent.openings > 0; }