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 (
+ <>
+
+
+
+
+ }
+ 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;
}