diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerEnrollmentController.java b/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerEnrollmentController.java index 5f4465833..a59c2536b 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerEnrollmentController.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerEnrollmentController.java @@ -36,12 +36,21 @@ public ExaminerEnrollmentAppointmentDTO updateEnrollmentAppointment( @GetMapping(path = "/contact/{enrollmentContactId:\\d+}", consumes = ALL_VALUE) @Operation(tags = TAG_ENROLLMENT, summary = "Get enrollment contact request") public ClerkEnrollmentContactRequestDTO getEnrollmentContactRequest( - @PathVariable String oid, + @PathVariable final String oid, @PathVariable final long enrollmentContactId ) { return examinerEnrollmentService.getEnrollmentContactRequest(oid, enrollmentContactId); } + @DeleteMapping(path = "/contact/{enrollmentContactId:\\d+}", consumes = ALL_VALUE) + @Operation(tags = TAG_ENROLLMENT, summary = "Delete enrollment contact request") + public void deleteEnrollmentContactRequest( + @PathVariable final String oid, + @PathVariable final long enrollmentContactId + ) { + examinerEnrollmentService.deleteEnrollmentContactRequest(oid, enrollmentContactId); + } + @PostMapping(path = "/contact/{enrollmentContactId:\\d+}/convertToAppointment", consumes = ALL_VALUE) @Operation(tags = TAG_ENROLLMENT, summary = "Convert enrollment contact request to enrollment appointment") public ExaminerEnrollmentAppointmentDTO enrollmentContactRequestToAppointment( diff --git a/backend/vkt/src/main/java/fi/oph/vkt/repository/EnrollmentAppointmentRepository.java b/backend/vkt/src/main/java/fi/oph/vkt/repository/EnrollmentAppointmentRepository.java index ba377f77c..351017b22 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/repository/EnrollmentAppointmentRepository.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/repository/EnrollmentAppointmentRepository.java @@ -1,22 +1,16 @@ package fi.oph.vkt.repository; -import fi.oph.vkt.api.dto.FreeEnrollmentDetails; -import fi.oph.vkt.model.Enrollment; import fi.oph.vkt.model.EnrollmentAppointment; -import fi.oph.vkt.model.ExamEvent; import fi.oph.vkt.model.Examiner; -import fi.oph.vkt.model.Person; import fi.oph.vkt.model.type.EnrollmentAppointmentStatus; -import fi.oph.vkt.model.type.EnrollmentStatus; import java.util.List; import java.util.Optional; -import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository public interface EnrollmentAppointmentRepository extends BaseRepository { - Optional findByIdAndAuthHash(final long id, final String paymentLinkHash); - List findByExaminerAndStatus( + Optional findByIdAndAuthHashAndDeletedAtIsNull(final long id, final String paymentLinkHash); + List findByExaminerAndStatusAndDeletedAtIsNull( final Examiner examiner, final EnrollmentAppointmentStatus status ); 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 3dd5968d6..aac850477 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 @@ -101,7 +101,7 @@ public ExaminerDetailsDTO getExaminer(final String oid) { throw new APIException(APIExceptionType.EXAMINER_NOT_FOUND); } final String baseUrlAPI = environment.getRequiredProperty("app.base-url.api"); - final List enrollmentAppointments = enrollmentAppointmentRepository.findByExaminerAndStatus( + final List enrollmentAppointments = enrollmentAppointmentRepository.findByExaminerAndStatusAndDeletedAtIsNull( examiner, EnrollmentAppointmentStatus.CONTACT_CREATED ); diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentService.java index 2ce1271be..c75b785a7 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentService.java @@ -243,4 +243,17 @@ public ExaminerEnrollmentAppointmentDTO sendEnrollmentAppointmentLink( return ClerkEnrollmentUtil.createClerkEnrollmentAppointmentDTO(enrollmentAppointment, baseUrlAPI); } + + @Transactional + public void deleteEnrollmentContactRequest(final String oid, final long enrollmentContactId) { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById( + enrollmentContactId + ); + + checkExaminerOid(enrollmentAppointment, oid); + + enrollmentAppointment.setDeletedAt(LocalDateTime.now()); + + enrollmentAppointmentRepository.flush(); + } } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentAppointmentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentAppointmentService.java index 9e194c1c9..760357424 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentAppointmentService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentAppointmentService.java @@ -18,7 +18,9 @@ public EnrollmentAppointment getEnrollmentAppointmentByHash( final long enrollmentAppointmentId, final String authHash ) { - return enrollmentAppointmentRepository.findByIdAndAuthHash(enrollmentAppointmentId, authHash).orElseThrow(); + return enrollmentAppointmentRepository + .findByIdAndAuthHashAndDeletedAtIsNull(enrollmentAppointmentId, authHash) + .orElseThrow(); } public void savePersonInfo(final long targetId, final Long appointmentId, final Person person) { diff --git a/frontend/packages/vkt/public/i18n/fi-FI/clerk.json b/frontend/packages/vkt/public/i18n/fi-FI/clerk.json index 255db8c14..6d730d44a 100644 --- a/frontend/packages/vkt/public/i18n/fi-FI/clerk.json +++ b/frontend/packages/vkt/public/i18n/fi-FI/clerk.json @@ -102,6 +102,16 @@ "getAllKoskiEducations": "Hae kaikki KOSKI-koulutustiedot" } }, + "clerkcontactRequest": { + "createEnrollment": "Luo ilmoittautuminen", + "contactDetails": "Yhteydenoton tiedot", + "message": "Viesti", + "wantFullExam": "Haluan suorittaa koko tutkinnon?", + "deleteAreYouSure": "Haluatko varmasti poistaa yhteydenoton?", + "deleteDescription": "Toimintoa ei voida peruuttaa.", + "deleteContactRequest": "Poista yhteydenotto", + "deleteContactRequestSuccess": "Yhteydenotto poistettu" + }, "clerkEnrollmentListing": { "header": { "examEventCoverage": "Tutkintokokonaisuus", diff --git a/frontend/packages/vkt/src/pages/ClerkEnrollmentContactRequestPage.tsx b/frontend/packages/vkt/src/pages/ClerkEnrollmentContactRequestPage.tsx index a5c79feaf..03ff7d4c4 100644 --- a/frontend/packages/vkt/src/pages/ClerkEnrollmentContactRequestPage.tsx +++ b/frontend/packages/vkt/src/pages/ClerkEnrollmentContactRequestPage.tsx @@ -9,31 +9,65 @@ import { LoadingProgressIndicator, Text, } from 'shared/components'; -import { APIResponseStatus, Color, Variant } from 'shared/enums'; +import { + APIResponseStatus, + Color, + Duration, + Severity, + Variant, +} from 'shared/enums'; +import { useDialog, useToast } from 'shared/hooks'; import { TopControls } from 'components/clerkExamEvent/overview/TopControls'; -import { useCommonTranslation } from 'configs/i18n'; +import { useClerkTranslation, useCommonTranslation } from 'configs/i18n'; import { useAppDispatch, useAppSelector } from 'configs/redux'; import { AppRoutes } from 'enums/app'; import { PartialExamsAndSkills } from 'interfaces/common/enrollment'; import { createClerkEnrollmentAppointment, + deleteClerkEnrollmentContactRequest, loadClerkEnrollmentContactRequest, resetClerkEnrollmentContactRequestToInitialState, } from 'redux/reducers/clerkEnrollmentContactRequest'; +import { resetExaminerDetailsToInitialState } from 'redux/reducers/examinerDetails'; import { clerkEnrollmentContactRequestSelector } from 'redux/selectors/clerkEnrollmentContactRequest'; import { EnrollmentUtils } from 'utils/enrollment'; export const ClerkEnrollmentContactRequestPage: FC = () => { - const { status, createStatus, enrollment } = useAppSelector( + const { status, deleteStatus, createStatus, enrollment } = useAppSelector( clerkEnrollmentContactRequestSelector, ); + const { t } = useClerkTranslation({ + keyPrefix: 'vkt.component.clerkcontactRequest', + }); const translateCommon = useCommonTranslation(); const params = useParams(); const navigate = useNavigate(); + const { showDialog } = useDialog(); + const { showToast } = useToast(); const dispatch = useAppDispatch(); + useEffect(() => { + if (deleteStatus === APIResponseStatus.Success && params.oid) { + dispatch(resetExaminerDetailsToInitialState()); + navigate(AppRoutes.ExaminerHomePage.replace(':oid', params.oid)); + showToast({ + severity: Severity.Success, + description: t('deleteContactRequestSuccess'), + timeOut: Duration.Short, + }); + } + }, [dispatch, params.oid, deleteStatus, navigate, t, showToast]); + + // Cleanup on unmount + useEffect( + () => () => { + dispatch(resetClerkEnrollmentContactRequestToInitialState()); + }, + [dispatch], + ); + useEffect(() => { if ( enrollment?.id && @@ -84,7 +118,8 @@ export const ClerkEnrollmentContactRequestPage: FC = () => { ]); const isLoading = status === APIResponseStatus.InProgress; - const isSavingDisabled = isLoading; + const isDeleteLoading = deleteStatus === APIResponseStatus.InProgress; + const isSavingDisabled = isDeleteLoading || isLoading; if (!enrollment) { return <>; @@ -123,6 +158,32 @@ export const ClerkEnrollmentContactRequestPage: FC = () => { }), ); }; + + const openDeleteDialog = () => { + showDialog({ + title: t('deleteAreYouSure'), + severity: Severity.Warning, + description: t('deleteDescription'), + actions: [ + { + title: translateCommon('back'), + variant: Variant.Outlined, + }, + { + title: translateCommon('yes'), + variant: Variant.Contained, + action: () => + dispatch( + deleteClerkEnrollmentContactRequest({ + id: enrollment.id, + oid: params.oid || '', + }), + ), + }, + ], + }); + }; + const backTo = AppRoutes.ExaminerHomePage.replace(':oid', params.oid || ''); return ( @@ -164,9 +225,9 @@ export const ClerkEnrollmentContactRequestPage: FC = () => { -

Yhteydenoton tiedot

+

{t('contactDetails')}

-

Haluan suorittaa koko tutkinnon?

+

{t('wantFullExam')}

{EnrollmentUtils.isFullExam(enrollment) ? translateCommon('yes') @@ -186,10 +247,21 @@ export const ClerkEnrollmentContactRequestPage: FC = () => {
-

Viesti

+

{t('message')}

{enrollment.message}
-
+
+ + + {t('deleteContactRequest')} + + { disabled={isSavingDisabled} onClick={onSubmit} > - {translateCommon('save')} + {t('createEnrollment')}
diff --git a/frontend/packages/vkt/src/redux/reducers/clerkEnrollmentContactRequest.ts b/frontend/packages/vkt/src/redux/reducers/clerkEnrollmentContactRequest.ts index 06be1037f..1211e8b14 100644 --- a/frontend/packages/vkt/src/redux/reducers/clerkEnrollmentContactRequest.ts +++ b/frontend/packages/vkt/src/redux/reducers/clerkEnrollmentContactRequest.ts @@ -5,6 +5,7 @@ import { ClerkEnrollmentContact } from 'interfaces/clerkEnrollment'; interface ClerkEnrollmentContactRequestState { status: APIResponseStatus; + deleteStatus: APIResponseStatus; enrollment?: ClerkEnrollmentContact; createStatus: APIResponseStatus; } @@ -12,6 +13,7 @@ interface ClerkEnrollmentContactRequestState { const initialState: ClerkEnrollmentContactRequestState = { status: APIResponseStatus.NotStarted, createStatus: APIResponseStatus.NotStarted, + deleteStatus: APIResponseStatus.NotStarted, }; const clerkEnrollmentContactRequestSlice = createSlice({ @@ -58,6 +60,18 @@ const clerkEnrollmentContactRequestSlice = createSlice({ resetClerkEnrollmentContactRequestToInitialState(_state) { return initialState; }, + deleteClerkEnrollmentContactRequest( + state, + _action: PayloadAction<{ + id: number; + oid: string; + }>, + ) { + state.deleteStatus = APIResponseStatus.InProgress; + }, + storeDeleteClerkEnrollmentContactRequest(state) { + state.deleteStatus = APIResponseStatus.Success; + }, }, }); @@ -71,4 +85,6 @@ export const { storeCreateClerkEnrollmentAppointment, rejectCreateClerkEnrollmentAppointment, resetClerkEnrollmentContactRequestToInitialState, + deleteClerkEnrollmentContactRequest, + storeDeleteClerkEnrollmentContactRequest, } = clerkEnrollmentContactRequestSlice.actions; diff --git a/frontend/packages/vkt/src/redux/reducers/examinerDetails.ts b/frontend/packages/vkt/src/redux/reducers/examinerDetails.ts index ee4cefbed..61c6da389 100644 --- a/frontend/packages/vkt/src/redux/reducers/examinerDetails.ts +++ b/frontend/packages/vkt/src/redux/reducers/examinerDetails.ts @@ -47,6 +47,9 @@ const examinerDetailsSlice = createSlice({ ) { state.examEventFilters.toggleFilter = action.payload; }, + resetExaminerDetailsToInitialState(_state) { + return initialState; + }, }, }); @@ -58,4 +61,5 @@ export const { setExaminerOid, setExaminerExamEventLanguageFilter, setExaminerExamEventToggleFilter, + resetExaminerDetailsToInitialState, } = examinerDetailsSlice.actions; diff --git a/frontend/packages/vkt/src/redux/sagas/clerkEnrollmentContactRequest.ts b/frontend/packages/vkt/src/redux/sagas/clerkEnrollmentContactRequest.ts index b4522fd1c..8b9169381 100644 --- a/frontend/packages/vkt/src/redux/sagas/clerkEnrollmentContactRequest.ts +++ b/frontend/packages/vkt/src/redux/sagas/clerkEnrollmentContactRequest.ts @@ -7,11 +7,13 @@ import { APIEndpoints } from 'enums/api'; import { ClerkEnrollmentContactResponse } from 'interfaces/clerkEnrollment'; import { createClerkEnrollmentAppointment, + deleteClerkEnrollmentContactRequest, loadClerkEnrollmentContactRequest, rejectClerkEnrollmentContactRequest, rejectCreateClerkEnrollmentAppointment, storeClerkEnrollmentContactRequest, storeCreateClerkEnrollmentAppointment, + storeDeleteClerkEnrollmentContactRequest, } from 'redux/reducers/clerkEnrollmentContactRequest'; import { SerializationUtils } from 'utils/serialization'; @@ -43,6 +45,27 @@ function* createClerkEnrollmentAppointmentSaga( } } +function* deleteClerkEnrollmentContactRequestSaga( + action: PayloadAction<{ + id: number; + oid: string; + }>, +) { + try { + const { id, oid } = action.payload; + const deleteUrl = `${APIEndpoints.ExaminerEnrollmentContactRequest.replace( + /:oid/, + oid, + )}/${id}`; + + yield call(axiosInstance.delete, deleteUrl); + + yield put(storeDeleteClerkEnrollmentContactRequest()); + } catch (error) { + yield put(rejectClerkEnrollmentContactRequest()); + } +} + function* loadClerkEnrollmentContactRequestSaga( action: PayloadAction<{ id: number; @@ -76,6 +99,10 @@ export function* watchClerkEnrollmentContactRequest() { loadClerkEnrollmentContactRequest.type, loadClerkEnrollmentContactRequestSaga, ); + yield takeLatest( + deleteClerkEnrollmentContactRequest.type, + deleteClerkEnrollmentContactRequestSaga, + ); yield takeLatest( createClerkEnrollmentAppointment.type, createClerkEnrollmentAppointmentSaga,