From 5ee0bd4ea0c024e4378ce447882d0983250cf6b9 Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Mon, 4 Nov 2024 17:09:39 +0200 Subject: [PATCH] VKT(Backend&Frontend): Load all examiner details for OPH clerk --- .../api/clerk/ClerkExaminerController.java | 27 ++++++++++++++++ .../vkt/repository/ExaminerRepository.java | 1 + .../oph/vkt/service/ClerkExaminerService.java | 29 +++++++++++++++++ .../vkt/service/ExaminerDetailsService.java | 28 ++--------------- .../java/fi/oph/vkt/util/ExaminerUtil.java | 30 ++++++++++++++++++ frontend/packages/vkt/src/enums/api.ts | 1 + .../vkt/src/interfaces/clerkListExaminer.ts | 8 +++++ .../ClerkGoodAndSatisfactoryLevelPage.tsx | 12 ++++++- .../src/pages/examiner/ExaminerHomePage.tsx | 2 +- .../src/redux/reducers/clerkListExaminer.ts | 31 +++++++++++++++++++ .../vkt/src/redux/sagas/clerkListExaminer.ts | 31 +++++++++++++++++++ .../packages/vkt/src/redux/sagas/index.ts | 2 ++ .../src/redux/selectors/clerkListExaminer.ts | 6 ++++ .../packages/vkt/src/redux/store/index.ts | 2 ++ 14 files changed, 183 insertions(+), 27 deletions(-) create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkExaminerController.java create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExaminerService.java create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/util/ExaminerUtil.java create mode 100644 frontend/packages/vkt/src/interfaces/clerkListExaminer.ts create mode 100644 frontend/packages/vkt/src/redux/reducers/clerkListExaminer.ts create mode 100644 frontend/packages/vkt/src/redux/sagas/clerkListExaminer.ts create mode 100644 frontend/packages/vkt/src/redux/selectors/clerkListExaminer.ts diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkExaminerController.java b/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkExaminerController.java new file mode 100644 index 000000000..628606c00 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkExaminerController.java @@ -0,0 +1,27 @@ +package fi.oph.vkt.api.clerk; + +import fi.oph.vkt.api.dto.examiner.ExaminerDetailsDTO; +import fi.oph.vkt.service.ClerkExaminerService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.annotation.Resource; +import java.util.List; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(value = "/api/v1/clerk/examiner", produces = MediaType.APPLICATION_JSON_VALUE) +public class ClerkExaminerController { + + private static final String TAG_CLERK_EXAMINER = "Examiner API for OPH clerk users"; + + @Resource + private ClerkExaminerService clerkExaminerService; + + @GetMapping + @Operation(tags = TAG_CLERK_EXAMINER, summary = "List examiner details") + public List listExaminers() { + return clerkExaminerService.listExaminers(); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/repository/ExaminerRepository.java b/backend/vkt/src/main/java/fi/oph/vkt/repository/ExaminerRepository.java index 0950160d1..4130b1ce2 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/repository/ExaminerRepository.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/repository/ExaminerRepository.java @@ -9,6 +9,7 @@ @Repository public interface ExaminerRepository extends BaseRepository { List getAllByDeletedAtIsNullAndIsPublicIsTrue(); + List getAllByDeletedAtIsNull(); Examiner getByOid(String oid); Optional findByOid(String oid); diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExaminerService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExaminerService.java new file mode 100644 index 000000000..81429b02d --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExaminerService.java @@ -0,0 +1,29 @@ +package fi.oph.vkt.service; + +import fi.oph.vkt.api.dto.examiner.ExaminerDetailsDTO; +import fi.oph.vkt.audit.AuditService; +import fi.oph.vkt.repository.ExaminerRepository; +import fi.oph.vkt.util.ExaminerUtil; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ClerkExaminerService { + + private final ExaminerRepository examinerRepository; + private final AuditService auditService; + + @Transactional(readOnly = true) + public List listExaminers() { + // TODO Audit log entry + return examinerRepository + .getAllByDeletedAtIsNull() + .stream() + .map(ExaminerUtil::toExaminerDetailsDTO) + .collect(Collectors.toList()); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerDetailsService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerDetailsService.java index 655fff41a..12ea3f42d 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerDetailsService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerDetailsService.java @@ -1,15 +1,14 @@ package fi.oph.vkt.service; -import fi.oph.vkt.api.dto.MunicipalityDTO; import fi.oph.vkt.api.dto.examiner.ExaminerDetailsDTO; import fi.oph.vkt.api.dto.examiner.ExaminerDetailsInitDTO; import fi.oph.vkt.api.dto.examiner.ExaminerDetailsUpsertDTO; import fi.oph.vkt.audit.AuditService; import fi.oph.vkt.model.Examiner; -import fi.oph.vkt.model.Municipality; import fi.oph.vkt.repository.ExaminerRepository; import fi.oph.vkt.service.onr.OnrService; import fi.oph.vkt.service.onr.PersonalData; +import fi.oph.vkt.util.ExaminerUtil; import fi.oph.vkt.util.exception.APIException; import fi.oph.vkt.util.exception.APIExceptionType; import java.util.List; @@ -34,27 +33,6 @@ private PersonalData getOnrPersonalData(final String oid) { return oidToData.get(oid); } - private static MunicipalityDTO toMunicipalityDTO(final Municipality municipality) { - return MunicipalityDTO.builder().code(municipality.getCode()).build(); - } - - private static ExaminerDetailsDTO toExaminerDetailsDTO(final Examiner examiner) { - return ExaminerDetailsDTO - .builder() - .id(examiner.getId()) - .version(examiner.getVersion()) - .oid(examiner.getOid()) - .lastName(examiner.getLastName()) - .firstName(examiner.getFirstName()) - .email(examiner.getEmail()) - .phoneNumber(examiner.getPhoneNumber()) - .municipalities(examiner.getMunicipalities().stream().map(ExaminerDetailsService::toMunicipalityDTO).toList()) - .isPublic(examiner.isPublic()) - .examLanguageFinnish(examiner.isExamLanguageFinnish()) - .examLanguageSwedish(examiner.isExamLanguageSwedish()) - .build(); - } - @Transactional(readOnly = true) public ExaminerDetailsInitDTO getInitialExaminerPersonalData(final String oid) { // TODO Audit log entry @@ -105,7 +83,7 @@ public ExaminerDetailsDTO upsertExaminer(final String oid, ExaminerDetailsUpsert examiner.setPublic(examinerDetailsUpsertDTO.isPublic()); examinerRepository.saveAndFlush(examiner); - return toExaminerDetailsDTO(examiner); + return ExaminerUtil.toExaminerDetailsDTO(examiner); } @Transactional(readOnly = true) @@ -115,7 +93,7 @@ public ExaminerDetailsDTO getExaminer(final String oid) { if (examiner == null) { throw new APIException(APIExceptionType.EXAMINER_NOT_FOUND); } - return toExaminerDetailsDTO(examiner); + return ExaminerUtil.toExaminerDetailsDTO(examiner); } @Transactional diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/ExaminerUtil.java b/backend/vkt/src/main/java/fi/oph/vkt/util/ExaminerUtil.java new file mode 100644 index 000000000..b6db2cac4 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/ExaminerUtil.java @@ -0,0 +1,30 @@ +package fi.oph.vkt.util; + +import fi.oph.vkt.api.dto.MunicipalityDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerDetailsDTO; +import fi.oph.vkt.model.Examiner; +import fi.oph.vkt.model.Municipality; + +public class ExaminerUtil { + + public static MunicipalityDTO toMunicipalityDTO(final Municipality municipality) { + return MunicipalityDTO.builder().code(municipality.getCode()).build(); + } + + public static ExaminerDetailsDTO toExaminerDetailsDTO(final Examiner examiner) { + return ExaminerDetailsDTO + .builder() + .id(examiner.getId()) + .version(examiner.getVersion()) + .oid(examiner.getOid()) + .lastName(examiner.getLastName()) + .firstName(examiner.getFirstName()) + .email(examiner.getEmail()) + .phoneNumber(examiner.getPhoneNumber()) + .municipalities(examiner.getMunicipalities().stream().map(ExaminerUtil::toMunicipalityDTO).toList()) + .isPublic(examiner.isPublic()) + .examLanguageFinnish(examiner.isExamLanguageFinnish()) + .examLanguageSwedish(examiner.isExamLanguageSwedish()) + .build(); + } +} diff --git a/frontend/packages/vkt/src/enums/api.ts b/frontend/packages/vkt/src/enums/api.ts index ba5098321..8c7be37b2 100644 --- a/frontend/packages/vkt/src/enums/api.ts +++ b/frontend/packages/vkt/src/enums/api.ts @@ -15,6 +15,7 @@ export enum APIEndpoints { FeatureFlags = '/vkt/api/v1/featureFlags', UploadPostPolicy = '/vkt/api/v1/uploadPostPolicy/:examEventId', ClerkRefreshKoskiEducationDetails = '/vkt/api/v1/clerk/enrollment/:enrollmentId/refreshKoskiEducationDetails', + ClerkExaminer = '/vkt/api/v1/clerk/examiner', // TODO Consider using prefix /examiner instead of /tv ExaminerDetails = '/vkt/api/v1/tv/:oid', ExaminerDetailsInit = '/vkt/api/v1/tv/:oid/init', diff --git a/frontend/packages/vkt/src/interfaces/clerkListExaminer.ts b/frontend/packages/vkt/src/interfaces/clerkListExaminer.ts new file mode 100644 index 000000000..913ddb721 --- /dev/null +++ b/frontend/packages/vkt/src/interfaces/clerkListExaminer.ts @@ -0,0 +1,8 @@ +import { APIResponseStatus } from 'shared/enums'; + +import { ExaminerDetails } from 'interfaces/examinerDetails'; + +export interface ClerkListExaminerState { + status: APIResponseStatus; + examiners: Array; +} diff --git a/frontend/packages/vkt/src/pages/ClerkGoodAndSatisfactoryLevelPage.tsx b/frontend/packages/vkt/src/pages/ClerkGoodAndSatisfactoryLevelPage.tsx index 684070f88..dcbe3627d 100644 --- a/frontend/packages/vkt/src/pages/ClerkGoodAndSatisfactoryLevelPage.tsx +++ b/frontend/packages/vkt/src/pages/ClerkGoodAndSatisfactoryLevelPage.tsx @@ -9,7 +9,9 @@ import { useClerkTranslation } from 'configs/i18n'; import { useAppDispatch, useAppSelector } from 'configs/redux'; import { resetClerkExamEventOverview } from 'redux/reducers/clerkExamEventOverview'; import { loadExamEvents } from 'redux/reducers/clerkListExamEvent'; +import { loadExaminers } from 'redux/reducers/clerkListExaminer'; import { clerkListExamEventsSelector } from 'redux/selectors/clerkListExamEvent'; +import { clerkListExaminerSelector } from 'redux/selectors/clerkListExaminer'; export const ClerkGoodAndSatisfactoryLevelPage: FC = () => { // I18 @@ -19,14 +21,22 @@ export const ClerkGoodAndSatisfactoryLevelPage: FC = () => { const dispatch = useAppDispatch(); const { status } = useAppSelector(clerkListExamEventsSelector); + const { status: examinerListStatus } = useAppSelector( + clerkListExaminerSelector, + ); - const examinersLoading = false; + const examinersLoading = examinerListStatus === APIResponseStatus.InProgress; useEffect(() => { if (status === APIResponseStatus.NotStarted) { dispatch(loadExamEvents()); } }, [dispatch, status]); + useEffect(() => { + if (examinerListStatus === APIResponseStatus.NotStarted) { + dispatch(loadExaminers()); + } + }, [dispatch, examinerListStatus]); useEffect(() => { dispatch(resetClerkExamEventOverview()); diff --git a/frontend/packages/vkt/src/pages/examiner/ExaminerHomePage.tsx b/frontend/packages/vkt/src/pages/examiner/ExaminerHomePage.tsx index 0064aeff8..783b34aa5 100644 --- a/frontend/packages/vkt/src/pages/examiner/ExaminerHomePage.tsx +++ b/frontend/packages/vkt/src/pages/examiner/ExaminerHomePage.tsx @@ -39,7 +39,7 @@ const PublicInformation = () => { const examDates: Array = [ { examDate: dayjs('2024-10-10'), isFull: false }, - { examDate: dayjs('2024-10-24'), isFull: true }, + { examDate: dayjs('2024-10-12'), isFull: true }, { examDate: dayjs('2024-10-15'), isFull: false }, { examDate: dayjs('2024-10-21'), isFull: false }, ]; diff --git a/frontend/packages/vkt/src/redux/reducers/clerkListExaminer.ts b/frontend/packages/vkt/src/redux/reducers/clerkListExaminer.ts new file mode 100644 index 000000000..15b5da1e1 --- /dev/null +++ b/frontend/packages/vkt/src/redux/reducers/clerkListExaminer.ts @@ -0,0 +1,31 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { APIResponseStatus } from 'shared/enums'; + +import { ClerkListExaminerState } from 'interfaces/clerkListExaminer'; +import { ExaminerDetails } from 'interfaces/examinerDetails'; + +const initialState: ClerkListExaminerState = { + status: APIResponseStatus.NotStarted, + examiners: [], +}; + +const clerkListExaminerSlice = createSlice({ + name: 'clerkListExaminer', + initialState, + reducers: { + acceptExaminers(state, action: PayloadAction>) { + state.status = APIResponseStatus.Success; + state.examiners = action.payload; + }, + loadExaminers(state) { + state.status = APIResponseStatus.InProgress; + }, + rejectExaminers(state) { + state.status = APIResponseStatus.Error; + }, + }, +}); + +export const { acceptExaminers, loadExaminers, rejectExaminers } = + clerkListExaminerSlice.actions; +export const clerkListExaminerReducer = clerkListExaminerSlice.reducer; diff --git a/frontend/packages/vkt/src/redux/sagas/clerkListExaminer.ts b/frontend/packages/vkt/src/redux/sagas/clerkListExaminer.ts new file mode 100644 index 000000000..c2803fb7e --- /dev/null +++ b/frontend/packages/vkt/src/redux/sagas/clerkListExaminer.ts @@ -0,0 +1,31 @@ +import { AxiosError, AxiosResponse } from 'axios'; +import { call, put, takeLatest } from 'redux-saga/effects'; + +import axiosInstance from 'configs/axios'; +import { APIEndpoints } from 'enums/api'; +import { ExaminerDetails } from 'interfaces/examinerDetails'; +import { setAPIError } from 'redux/reducers/APIError'; +import { + acceptExaminers, + loadExaminers, + rejectExaminers, +} from 'redux/reducers/clerkListExaminer'; +import { NotifierUtils } from 'utils/notifier'; + +function* loadExaminersSaga() { + try { + const response: AxiosResponse> = yield call( + axiosInstance.get, + APIEndpoints.ClerkExaminer, + ); + yield put(acceptExaminers(response.data)); + } catch (error) { + const errorMessage = NotifierUtils.getAPIErrorMessage(error as AxiosError); + yield put(setAPIError(errorMessage)); + yield put(rejectExaminers()); + } +} + +export function* watchListExaminers() { + yield takeLatest(loadExaminers.type, loadExaminersSaga); +} diff --git a/frontend/packages/vkt/src/redux/sagas/index.ts b/frontend/packages/vkt/src/redux/sagas/index.ts index 00d2f0f11..fc692777b 100644 --- a/frontend/packages/vkt/src/redux/sagas/index.ts +++ b/frontend/packages/vkt/src/redux/sagas/index.ts @@ -3,6 +3,7 @@ import { all } from 'redux-saga/effects'; import { watchClerkEnrollmentDetails } from 'redux/sagas/clerkEnrollmentDetails'; import { watchClerkExamEventOverview } from 'redux/sagas/clerkExamEventOverview'; import { watchListExamEvents } from 'redux/sagas/clerkListExamEvent'; +import { watchListExaminers } from 'redux/sagas/clerkListExaminer'; import { watchClerkNewExamDate } from 'redux/sagas/clerkNewExamDate'; import { watchClerkUser } from 'redux/sagas/clerkUser'; import { watchExaminerDetails } from 'redux/sagas/examinerDetails'; @@ -33,5 +34,6 @@ export default function* rootSaga() { watchExaminerDetails(), watchExaminerDetailsInit(), watchExaminerDetailsUpsert(), + watchListExaminers(), ]); } diff --git a/frontend/packages/vkt/src/redux/selectors/clerkListExaminer.ts b/frontend/packages/vkt/src/redux/selectors/clerkListExaminer.ts new file mode 100644 index 000000000..c151d239f --- /dev/null +++ b/frontend/packages/vkt/src/redux/selectors/clerkListExaminer.ts @@ -0,0 +1,6 @@ +import { RootState } from 'configs/redux'; +import { ClerkListExaminerState } from 'interfaces/clerkListExaminer'; + +export const clerkListExaminerSelector = ( + state: RootState, +): ClerkListExaminerState => state.clerkListExaminer; diff --git a/frontend/packages/vkt/src/redux/store/index.ts b/frontend/packages/vkt/src/redux/store/index.ts index 8fe85e051..f61f9728f 100644 --- a/frontend/packages/vkt/src/redux/store/index.ts +++ b/frontend/packages/vkt/src/redux/store/index.ts @@ -8,6 +8,7 @@ import { APIErrorReducer } from 'redux/reducers/APIError'; import { clerkEnrollmentDetailsReducer } from 'redux/reducers/clerkEnrollmentDetails'; import { clerkExamEventOverviewReducer } from 'redux/reducers/clerkExamEventOverview'; import { clerkListExamEventReducer } from 'redux/reducers/clerkListExamEvent'; +import { clerkListExaminerReducer } from 'redux/reducers/clerkListExaminer'; import { clerkNewExamDateReducer } from 'redux/reducers/clerkNewExamDate'; import { clerkUserReducer } from 'redux/reducers/clerkUser'; import { examinerDetailsReducer } from 'redux/reducers/examinerDetails'; @@ -46,6 +47,7 @@ const reducer = combineReducers({ examinerDetails: examinerDetailsReducer, examinerDetailsInit: examinerDetailsInitReducer, examinerDetailsUpsert: examinerDetailsUpsertReducer, + clerkListExaminer: clerkListExaminerReducer, }); const persistedReducer = persistReducer(persistConfig, reducer);