From 6a952743cc1834c21702651d1aa5ae886e8ab951 Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Thu, 3 Oct 2024 15:10:18 +0300 Subject: [PATCH] VKT(Backend&Frontend): Get (mocked, fixed) list of examiners to display on frontend from backend API [deploy] --- .../java/fi/oph/vkt/api/PublicController.java | 10 +++ .../fi/oph/vkt/api/dto/PublicExaminerDTO.java | 18 ++++ .../vkt/api/dto/PublicMunicipalityDTO.java | 8 ++ .../vkt/service/PublicExaminerService.java | 35 ++++++++ .../PublicExaminerListing.tsx | 87 +++++++++++++------ frontend/packages/vkt/src/enums/api.ts | 1 + .../vkt/src/interfaces/publicExaminer.ts | 16 +++- ...licGoodAndSatisfactoryLevelLandingPage.tsx | 8 +- .../vkt/src/redux/reducers/publicExaminer.ts | 58 ++++--------- .../packages/vkt/src/redux/sagas/index.ts | 2 + .../vkt/src/redux/sagas/publicExaminer.ts | 31 +++++++ .../packages/vkt/src/utils/serialization.ts | 27 ++++++ 12 files changed, 234 insertions(+), 67 deletions(-) create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExaminerDTO.java create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicMunicipalityDTO.java create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/service/PublicExaminerService.java create mode 100644 frontend/packages/vkt/src/redux/sagas/publicExaminer.ts diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/PublicController.java b/backend/vkt/src/main/java/fi/oph/vkt/api/PublicController.java index bd04cf760..b9798b4ab 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/PublicController.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/PublicController.java @@ -6,6 +6,7 @@ import fi.oph.vkt.api.dto.PublicEnrollmentDTO; import fi.oph.vkt.api.dto.PublicEnrollmentInitialisationDTO; import fi.oph.vkt.api.dto.PublicExamEventDTO; +import fi.oph.vkt.api.dto.PublicExaminerDTO; import fi.oph.vkt.api.dto.PublicPersonDTO; import fi.oph.vkt.api.dto.PublicReservationDTO; import fi.oph.vkt.model.Enrollment; @@ -20,6 +21,7 @@ import fi.oph.vkt.service.PublicAuthService; import fi.oph.vkt.service.PublicEnrollmentService; import fi.oph.vkt.service.PublicExamEventService; +import fi.oph.vkt.service.PublicExaminerService; import fi.oph.vkt.service.PublicPersonService; import fi.oph.vkt.service.PublicReservationService; import fi.oph.vkt.service.koski.KoskiService; @@ -87,11 +89,19 @@ public class PublicController { @Resource private FeatureFlagService featureFlagService; + @Resource + private PublicExaminerService publicExaminerService; + @GetMapping(path = "/examEvent") public List list() { return publicExamEventService.listExamEvents(ExamLevel.EXCELLENT); } + @GetMapping(path = "/examiner") + public List listExaminers() { + return publicExaminerService.listExaminers(); + } + @PostMapping(path = "/enrollment/reservation/{reservationId:\\d+}") @ResponseStatus(HttpStatus.CREATED) public PublicEnrollmentDTO createEnrollment( diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExaminerDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExaminerDTO.java new file mode 100644 index 000000000..64656baeb --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExaminerDTO.java @@ -0,0 +1,18 @@ +package fi.oph.vkt.api.dto; + +import fi.oph.vkt.model.type.ExamLanguage; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; +import java.util.List; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record PublicExaminerDTO( + @NonNull @NotNull Long id, + @NonNull @NotNull String lastName, + @NonNull @NotNull String firstName, + @NonNull @NotNull List languages, + @NonNull @NotNull List municipalities, + @NonNull @NotNull List examDates +) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicMunicipalityDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicMunicipalityDTO.java new file mode 100644 index 000000000..14d40cb70 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicMunicipalityDTO.java @@ -0,0 +1,8 @@ +package fi.oph.vkt.api.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record PublicMunicipalityDTO(@NonNull @NotNull String fi, @NonNull @NotNull String sv) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExaminerService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExaminerService.java new file mode 100644 index 000000000..143ef0182 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExaminerService.java @@ -0,0 +1,35 @@ +package fi.oph.vkt.service; + +import fi.oph.vkt.api.dto.PublicExaminerDTO; +import fi.oph.vkt.api.dto.PublicMunicipalityDTO; +import fi.oph.vkt.model.type.ExamLanguage; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PublicExaminerService { + + public List listExaminers() { + final PublicMunicipalityDTO helsinki = PublicMunicipalityDTO.builder().fi("Helsinki").sv("Helsingfors").build(); + final PublicMunicipalityDTO espoo = PublicMunicipalityDTO.builder().fi("Espoo").sv("Esbo").build(); + final PublicMunicipalityDTO vantaa = PublicMunicipalityDTO.builder().fi("Vantaa").sv("Vanda").build(); + final PublicMunicipalityDTO kauniainen = PublicMunicipalityDTO.builder().fi("Kauniainen").sv("Grankulla").build(); + final List examiners = new ArrayList<>(); + examiners.add( + PublicExaminerDTO + .builder() + .id(1L) + .lastName("Laine") + .firstName("Eemeli") + .languages(List.of(ExamLanguage.FI)) + .municipalities(List.of(helsinki, espoo, vantaa, kauniainen)) + .examDates(List.of()) + .build() + ); + + return examiners; + } +} diff --git a/frontend/packages/vkt/src/components/publicExaminerListing/PublicExaminerListing.tsx b/frontend/packages/vkt/src/components/publicExaminerListing/PublicExaminerListing.tsx index 7f1c8ce56..1488bb76f 100644 --- a/frontend/packages/vkt/src/components/publicExaminerListing/PublicExaminerListing.tsx +++ b/frontend/packages/vkt/src/components/publicExaminerListing/PublicExaminerListing.tsx @@ -1,4 +1,5 @@ import { + Box, Paper, SelectChangeEvent, TableCell, @@ -6,13 +7,23 @@ import { TableRow, } from '@mui/material'; import { Fragment } from 'react'; -import { CustomButtonLink, CustomTable, H2, Text } from 'shared/components'; -import { Color, Variant } from 'shared/enums'; +import { + CustomButtonLink, + CustomCircularProgress, + CustomTable, + H2, + Text, +} from 'shared/components'; +import { APIResponseStatus, AppLanguage, Color, Variant } from 'shared/enums'; import { useWindowProperties } from 'shared/hooks'; import { DateUtils } from 'shared/utils'; import { LanguageFilter } from 'components/common/LanguageFilter'; -import { usePublicTranslation } from 'configs/i18n'; +import { + getCurrentLang, + useCommonTranslation, + usePublicTranslation, +} from 'configs/i18n'; import { useAppDispatch, useAppSelector } from 'configs/redux'; import { ExamLanguage } from 'enums/app'; import { PublicExaminer } from 'interfaces/publicExaminer'; @@ -50,6 +61,7 @@ const DesktopExaminerRow = ({ const { t } = usePublicTranslation({ keyPrefix: 'vkt.component.publicExaminerListing', }); + const appLanguage = getCurrentLang(); return ( @@ -61,7 +73,13 @@ const DesktopExaminerRow = ({ - {municipalities.length > 0 ? municipalities.join(', ') : ''} + {municipalities.length > 0 + ? municipalities + .map(({ fi, sv }) => + appLanguage === AppLanguage.Swedish ? sv : fi, + ) + .join(', ') + : ''} @@ -106,8 +124,7 @@ const getRowDetails = ({ }; export const PublicExaminerListing = () => { - // TODO Kick off API request & display content based on request status - const { languageFilter } = useAppSelector(publicExaminerSelector); + const { languageFilter, status } = useAppSelector(publicExaminerSelector); const filteredExaminers = useAppSelector(selectFilteredPublicExaminers); const dispatch = useAppDispatch(); @@ -117,23 +134,43 @@ export const PublicExaminerListing = () => { ); }; - return ( - -
-
-

Ota yhteyttä tutkinnon vastaanottajiin

-
-
- - } - /> -
- ); + const translateCommon = useCommonTranslation(); + + switch (status) { + case APIResponseStatus.NotStarted: + case APIResponseStatus.InProgress: + return ; + case APIResponseStatus.Cancelled: + case APIResponseStatus.Error: + return ( + +

{translateCommon('errors.loadingFailed')}

+
+ ); + case APIResponseStatus.Success: + return ( + +
+
+

Ota yhteyttä tutkinnon vastaanottajiin

+
+
+ + } + /> +
+ ); + } }; diff --git a/frontend/packages/vkt/src/enums/api.ts b/frontend/packages/vkt/src/enums/api.ts index 4d90b4fba..c1794057d 100644 --- a/frontend/packages/vkt/src/enums/api.ts +++ b/frontend/packages/vkt/src/enums/api.ts @@ -2,6 +2,7 @@ export enum APIEndpoints { PublicAuthLogin = '/vkt/api/v1/auth/login/:examEventId/:type?locale=:locale', PublicAuthLogout = '/vkt/api/v1/auth/logout', PublicExamEvent = '/vkt/api/v1/examEvent', + PublicExaminer = '/vkt/api/v1/examiner', PublicEnrollment = '/vkt/api/v1/enrollment', PublicReservation = '/vkt/api/v1/reservation', PublicEducation = '/vkt/api/v1/education', diff --git a/frontend/packages/vkt/src/interfaces/publicExaminer.ts b/frontend/packages/vkt/src/interfaces/publicExaminer.ts index 0d3c5cd19..8cd737c8d 100644 --- a/frontend/packages/vkt/src/interfaces/publicExaminer.ts +++ b/frontend/packages/vkt/src/interfaces/publicExaminer.ts @@ -4,14 +4,26 @@ import { WithId } from 'shared/interfaces'; import { ExamLanguage } from 'enums/app'; import { APIResponseStatus } from 'shared/enums'; +interface PublicMunicipality { + fi: string; + sv: string; +} + export interface PublicExaminer extends WithId { name: string; language: ExamLanguage; - // TODO Municipality could instead be something like { fi: 'Helsinki', sv: 'Helsingfors' } ? - municipalities: Array; + municipalities: Array; examDates: Array; } +export interface PublicExaminerResponse extends WithId { + lastName: string; + firstName: string; + languages: Array; + municipalities: Array; + examDates: Array; +} + export interface PublicExaminerState { status: APIResponseStatus; examiners: Array; diff --git a/frontend/packages/vkt/src/pages/goodAndSatisfactoryLevel/PublicGoodAndSatisfactoryLevelLandingPage.tsx b/frontend/packages/vkt/src/pages/goodAndSatisfactoryLevel/PublicGoodAndSatisfactoryLevelLandingPage.tsx index a1f5bf972..cd9fff971 100644 --- a/frontend/packages/vkt/src/pages/goodAndSatisfactoryLevel/PublicGoodAndSatisfactoryLevelLandingPage.tsx +++ b/frontend/packages/vkt/src/pages/goodAndSatisfactoryLevel/PublicGoodAndSatisfactoryLevelLandingPage.tsx @@ -1,14 +1,20 @@ import { Box, Grid } from '@mui/material'; -import { FC } from 'react'; +import { FC, useEffect } from 'react'; import { H1, HeaderSeparator, Text } from 'shared/components'; import { PublicExaminerListing } from 'components/publicExaminerListing/PublicExaminerListing'; import { usePublicTranslation } from 'configs/i18n'; +import { useAppDispatch } from 'configs/redux'; +import { loadPublicExaminers } from 'redux/reducers/publicExaminer'; export const PublicGoodAndSatisfactoryLevelLandingPage: FC = () => { const { t } = usePublicTranslation({ keyPrefix: 'vkt.component.goodAndSatisfactoryLevel', }); + const dispatch = useAppDispatch(); + useEffect(() => { + dispatch(loadPublicExaminers()); + }, [dispatch]); return ( diff --git a/frontend/packages/vkt/src/redux/reducers/publicExaminer.ts b/frontend/packages/vkt/src/redux/reducers/publicExaminer.ts index 35d98e302..59009f28e 100644 --- a/frontend/packages/vkt/src/redux/reducers/publicExaminer.ts +++ b/frontend/packages/vkt/src/redux/reducers/publicExaminer.ts @@ -1,47 +1,12 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import dayjs from 'dayjs'; import { APIResponseStatus } from 'shared/enums'; import { ExamLanguage } from 'enums/app'; -import { PublicExaminerState } from 'interfaces/publicExaminer'; +import { PublicExaminer, PublicExaminerState } from 'interfaces/publicExaminer'; const initialState: PublicExaminerState = { - status: APIResponseStatus.Success, - examiners: [ - { - id: 1, - name: 'Eemeli Laine', - language: ExamLanguage.FI, - municipalities: ['Helsinki', 'Espoo', 'Vantaa', 'Kauniainen'], - examDates: [], - }, - { - id: 2, - name: 'Kerttu Virtanen', - language: ExamLanguage.SV, - municipalities: ['Vaasa'], - examDates: [], - }, - { - id: 3, - name: 'Aapo Mäkinen', - language: ExamLanguage.ALL, - municipalities: ['Tampere'], - examDates: [dayjs('2024-10-17')], - }, - { - id: 4, - name: 'Veera Salminen', - language: ExamLanguage.FI, - municipalities: ['Kajaani'], - examDates: [ - dayjs('2024-09-12'), - dayjs('2024-09-13'), - dayjs('2024-09-19'), - dayjs('2024-09-20'), - ], - }, - ], + status: APIResponseStatus.NotStarted, + examiners: [], languageFilter: ExamLanguage.ALL, }; @@ -49,6 +14,16 @@ const publicExaminerSlice = createSlice({ name: 'publicExaminer', initialState, reducers: { + loadPublicExaminers(state) { + state.status = APIResponseStatus.InProgress; + }, + rejectPublicExaminers(state) { + state.status = APIResponseStatus.Error; + }, + storePublicExaminers(state, action: PayloadAction>) { + state.status = APIResponseStatus.Success; + state.examiners = action.payload; + }, setPublicExaminerLanguageFilter( state, action: PayloadAction, @@ -59,4 +34,9 @@ const publicExaminerSlice = createSlice({ }); export const publicExaminerReducer = publicExaminerSlice.reducer; -export const { setPublicExaminerLanguageFilter } = publicExaminerSlice.actions; +export const { + loadPublicExaminers, + rejectPublicExaminers, + storePublicExaminers, + setPublicExaminerLanguageFilter, +} = publicExaminerSlice.actions; diff --git a/frontend/packages/vkt/src/redux/sagas/index.ts b/frontend/packages/vkt/src/redux/sagas/index.ts index cfaf07a1d..fc1264fe5 100644 --- a/frontend/packages/vkt/src/redux/sagas/index.ts +++ b/frontend/packages/vkt/src/redux/sagas/index.ts @@ -9,6 +9,7 @@ import { watchFeatureFlags } from 'redux/sagas/featureFlags'; import { watchPublicEducation } from 'redux/sagas/publicEducation'; import { watchPublicEnrollments } from 'redux/sagas/publicEnrollment'; import { watchPublicExamEvents } from 'redux/sagas/publicExamEvent'; +import { watchPublicExaminers } from 'redux/sagas/publicExaminer'; import { watchFileUpload } from 'redux/sagas/publicFileUpload'; import { watchPublicUser } from 'redux/sagas/publicUser'; @@ -25,5 +26,6 @@ export default function* rootSaga() { watchFeatureFlags(), watchFileUpload(), watchPublicEducation(), + watchPublicExaminers(), ]); } diff --git a/frontend/packages/vkt/src/redux/sagas/publicExaminer.ts b/frontend/packages/vkt/src/redux/sagas/publicExaminer.ts new file mode 100644 index 000000000..117bee571 --- /dev/null +++ b/frontend/packages/vkt/src/redux/sagas/publicExaminer.ts @@ -0,0 +1,31 @@ +import { AxiosResponse } from 'axios'; +import { call, put, takeLatest } from 'redux-saga/effects'; + +import axiosInstance from 'configs/axios'; +import { APIEndpoints } from 'enums/api'; +import { PublicExaminerResponse } from 'interfaces/publicExaminer'; +import { + loadPublicExaminers, + rejectPublicExaminers, + storePublicExaminers, +} from 'redux/reducers/publicExaminer'; +import { SerializationUtils } from 'utils/serialization'; + +function* loadPublicExaminersSaga() { + try { + const response: AxiosResponse> = yield call( + axiosInstance.get, + APIEndpoints.PublicExaminer, + ); + const examiners = response.data.map( + SerializationUtils.deserializePublicExaminer, + ); + yield put(storePublicExaminers(examiners)); + } catch (error) { + yield put(rejectPublicExaminers()); + } +} + +export function* watchPublicExaminers() { + yield takeLatest(loadPublicExaminers.type, loadPublicExaminersSaga); +} diff --git a/frontend/packages/vkt/src/utils/serialization.ts b/frontend/packages/vkt/src/utils/serialization.ts index 326d53500..9049aa5b2 100644 --- a/frontend/packages/vkt/src/utils/serialization.ts +++ b/frontend/packages/vkt/src/utils/serialization.ts @@ -1,6 +1,7 @@ import dayjs from 'dayjs'; import { DateUtils } from 'shared/utils'; +import { ExamLanguage } from 'enums/app'; import { ClerkEnrollment, ClerkEnrollmentResponse, @@ -30,6 +31,10 @@ import { PublicExamEvent, PublicExamEventResponse, } from 'interfaces/publicExamEvent'; +import { + PublicExaminer, + PublicExaminerResponse, +} from 'interfaces/publicExaminer'; export class SerializationUtils { static deserializePublicExamEvent( @@ -163,4 +168,26 @@ export class SerializationUtils { ongoing: e.isActive, })); } + + static deserializePublicExaminer( + publicExaminer: PublicExaminerResponse, + ): PublicExaminer { + let examinerLanguage; + if (publicExaminer.languages.includes(ExamLanguage.SV)) { + examinerLanguage = ExamLanguage.SV; + if (publicExaminer.languages.includes(ExamLanguage.FI)) { + examinerLanguage = ExamLanguage.ALL; + } + } else { + examinerLanguage = ExamLanguage.FI; + } + + return { + id: publicExaminer.id, + name: `${publicExaminer.firstName} ${publicExaminer.lastName}`, + language: examinerLanguage, + municipalities: publicExaminer.municipalities, + examDates: publicExaminer.examDates.map(dayjs), + }; + } }